mirror of
https://github.com/rls-moe/nyx
synced 2024-12-24 12:17:16 +00:00
Improved stability, improved spam algorithm, fixed some bugs
This commit is contained in:
parent
afd9ae71cf
commit
19d0e9282d
@ -1,21 +1,139 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/tidwall/buntdb"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func handleCleanup(w http.ResponseWriter, r *http.Request) {
|
||||
sess := middle.GetSession(r)
|
||||
if sess == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
if sess.CAttr("mode") != "admin" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Beginning cleanup...")
|
||||
db := middle.GetDB(r)
|
||||
err := db.Update(func(tx *buntdb.Tx) error {
|
||||
var delKeys = []string{}
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
var err error
|
||||
tx.AscendKeys("*", func(key, value string) bool {
|
||||
keyType := detectType(key)
|
||||
if keyType == "thread" {
|
||||
var host string
|
||||
host, err = resources.GetHostnameFromKey(key)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
return false
|
||||
}
|
||||
var thread = &resources.Thread{}
|
||||
err = json.Unmarshal([]byte(value), thread)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
return false
|
||||
}
|
||||
threadTime := resources.DateFromId(thread.ID)
|
||||
dur := threadTime.Sub(time.Now())
|
||||
if dur > time.Hour*24*7 {
|
||||
fmt.Printf("Sched %s for deletion: expired\n", key)
|
||||
delKeys = append(delKeys, key)
|
||||
return true
|
||||
}
|
||||
err = resources.FillReplies(tx, host, thread)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
return false
|
||||
}
|
||||
if len(thread.GetReplies()) == 0 {
|
||||
fmt.Printf("Sched %s for deletion: empty\n", key)
|
||||
delKeys = append(delKeys, key)
|
||||
return true
|
||||
}
|
||||
if _, err := resources.GetReply(tx, host, thread.Board, thread.ID, thread.StartReply); err == buntdb.ErrNotFound {
|
||||
fmt.Printf("Sched %s for delection: main reply dead\n", key)
|
||||
delKeys = append(delKeys, key)
|
||||
return true
|
||||
}
|
||||
} else if keyType == "reply" {
|
||||
var host string
|
||||
host, err = resources.GetHostnameFromKey(key)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
return false
|
||||
}
|
||||
var reply = &resources.Reply{}
|
||||
err = json.Unmarshal([]byte(value), reply)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
return false
|
||||
}
|
||||
replyTime := resources.DateFromId(reply.ID)
|
||||
dur := replyTime.Sub(time.Now())
|
||||
if dur > time.Hour*24*7 {
|
||||
fmt.Printf("Sched %s for deletion: expired\n", key)
|
||||
delKeys = append(delKeys, key)
|
||||
return true
|
||||
}
|
||||
if val, ok := reply.Metadata["deleted"]; ok && val == "yes" {
|
||||
fmt.Printf("Sched %s for deletion: deleted\n", key)
|
||||
delKeys = append(delKeys, key)
|
||||
return true
|
||||
}
|
||||
if err := resources.TestThread(tx, host, reply.Board, reply.Thread); err == buntdb.ErrNotFound {
|
||||
fmt.Printf("Sched %s for deletion: missing parent %d: %s\n", key, reply.Thread, err)
|
||||
delKeys = append(delKeys, key)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
/* Insert cleanup codes here */
|
||||
return err
|
||||
})
|
||||
fmt.Println("Removing sched' entries")
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
for _, v := range delKeys {
|
||||
fmt.Printf("Deleting %s\n", v)
|
||||
tx.Delete(v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
fmt.Println("Shrinking DB")
|
||||
err = db.Shrink()
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
fmt.Println("Finished Cleanup")
|
||||
|
||||
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func detectType(key string) string {
|
||||
if strings.Contains(key, "/jack/") {
|
||||
if strings.HasSuffix(key, "/board-data") {
|
||||
return "board"
|
||||
}
|
||||
if strings.HasSuffix(key, "/thread") {
|
||||
return "thread"
|
||||
}
|
||||
if strings.HasSuffix(key, "/reply-data") {
|
||||
return "reply"
|
||||
}
|
||||
return "system"
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
74
http/admin/delpost.go
Normal file
74
http/admin/delpost.go
Normal file
@ -0,0 +1,74 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"strconv"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
func handleDelPost(w http.ResponseWriter, r *http.Request) {
|
||||
sess := middle.GetSession(r)
|
||||
if sess == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
if sess.CAttr("mode") != "admin" && sess.CAttr("mode") != "mod" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
rid, err := strconv.Atoi(r.FormValue("reply_id"))
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
trid, err := strconv.Atoi(r.FormValue("thread_id"))
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
board := r.FormValue("board")
|
||||
|
||||
if sess.CAttr("mode") == "mod" && sess.CAttr("board") != board {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Not on this board"))
|
||||
return
|
||||
}
|
||||
|
||||
db := middle.GetDB(r)
|
||||
|
||||
err = db.Update(func(tx *buntdb.Tx) error {
|
||||
reply, err := resources.GetReply(tx, r.Host, board, trid, rid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply.Text = "[deleted]"
|
||||
reply.Metadata["deleted"] = "yes"
|
||||
reply.Image = nil
|
||||
reply.Thumbnail = nil
|
||||
err = resources.UpdateReply(tx, r.Host, board, reply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", board, trid), http.StatusSeeOther)
|
||||
}
|
@ -2,17 +2,12 @@ package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/GeertJohan/go.rice"
|
||||
"github.com/icza/session"
|
||||
"github.com/pressly/chi"
|
||||
"github.com/tidwall/buntdb"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -27,8 +22,9 @@ var riceConf = rice.Config{
|
||||
var box = riceConf.MustFindBox("http/admin/res/")
|
||||
|
||||
var (
|
||||
panelTmpl = template.New("admin/panel")
|
||||
loginTmpl = template.New("admin/login")
|
||||
panelTmpl = template.New("admin/panel")
|
||||
loginTmpl = template.New("admin/login")
|
||||
statusTmpl = template.New("admin/status")
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -41,6 +37,10 @@ func init() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
statusTmpl, err = statusTmpl.Parse(box.MustString("status.html"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Router sets up the Administration Panel
|
||||
@ -50,8 +50,12 @@ func AdminRouter(r chi.Router) {
|
||||
r.Get("/index.html", serveLogin)
|
||||
r.Get("/panel.html", servePanel)
|
||||
r.Post("/new_board.sh", handleNewBoard)
|
||||
r.Post("/cleanup.sh", handleCleanup)
|
||||
r.Post("/login.sh", handleLogin)
|
||||
r.Post("/logout.sh", handleLogout)
|
||||
r.Post("/new_admin.sh", handleNewAdmin)
|
||||
r.Post("/del_admin.sh", handleDelAdmin)
|
||||
r.Get("/status.sh", serveStatus)
|
||||
}
|
||||
|
||||
// Router sets up moderation functions
|
||||
@ -60,69 +64,6 @@ func ModRouter(r chi.Router) {
|
||||
r.Post("/del_reply.sh", handleDelPost)
|
||||
}
|
||||
|
||||
func handleDelPost(w http.ResponseWriter, r *http.Request) {
|
||||
sess := middle.GetSession(r)
|
||||
if sess == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
if sess.CAttr("mode") != "admin" && sess.CAttr("mode") != "mod" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
rid, err := strconv.Atoi(r.FormValue("reply_id"))
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
trid, err := strconv.Atoi(r.FormValue("thread_id"))
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
board := r.FormValue("board")
|
||||
|
||||
if sess.CAttr("mode") == "mod" && sess.CAttr("board") != board {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Not on this board"))
|
||||
return
|
||||
}
|
||||
|
||||
db := middle.GetDB(r)
|
||||
|
||||
err = db.Update(func(tx *buntdb.Tx) error {
|
||||
reply, err := resources.GetReply(tx, r.Host, board, trid, rid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply.Text = "[deleted]"
|
||||
reply.Metadata["deleted"] = "yes"
|
||||
reply.Image = nil
|
||||
reply.Thumbnail = nil
|
||||
err = resources.UpdateReply(tx, r.Host, board, reply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", board, trid), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func serveLogin(w http.ResponseWriter, r *http.Request) {
|
||||
dat := bytes.NewBuffer([]byte{})
|
||||
err := loginTmpl.Execute(dat, middle.GetBaseCtx(r))
|
||||
@ -150,41 +91,3 @@ func servePanel(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, "panel.html", time.Now(),
|
||||
bytes.NewReader(dat.Bytes()))
|
||||
}
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
sess := middle.GetSession(r)
|
||||
if sess == nil {
|
||||
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
|
||||
}
|
||||
session.Remove(sess, w)
|
||||
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
|
||||
}
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
}
|
||||
db := middle.GetDB(r)
|
||||
|
||||
var admin = &resources.AdminPass{}
|
||||
err = db.View(func(tx *buntdb.Tx) error {
|
||||
var err error
|
||||
admin, err = resources.GetAdmin(tx, r.FormValue("id"))
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
|
||||
errw.ErrorWriter(err, w, r)
|
||||
}
|
||||
err = admin.VerifyLogin(r.FormValue("pass"))
|
||||
if err != nil {
|
||||
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
|
||||
errw.ErrorWriter(err, w, r)
|
||||
}
|
||||
sess := session.NewSessionOptions(&session.SessOptions{
|
||||
CAttrs: map[string]interface{}{"mode": "admin"},
|
||||
})
|
||||
session.Add(sess, w)
|
||||
|
||||
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
|
||||
}
|
||||
|
50
http/admin/login.go
Normal file
50
http/admin/login.go
Normal file
@ -0,0 +1,50 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/icza/session"
|
||||
"github.com/tidwall/buntdb"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
sess := middle.GetSession(r)
|
||||
if sess == nil {
|
||||
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
|
||||
}
|
||||
session.Remove(sess, w)
|
||||
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
|
||||
}
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
}
|
||||
db := middle.GetDB(r)
|
||||
|
||||
var admin = &resources.AdminPass{}
|
||||
err = db.View(func(tx *buntdb.Tx) error {
|
||||
var err error
|
||||
admin, err = resources.GetAdmin(tx, r.FormValue("id"))
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
err = admin.VerifyLogin(r.FormValue("pass"))
|
||||
if err != nil {
|
||||
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
sess := session.NewSessionOptions(&session.SessOptions{
|
||||
CAttrs: map[string]interface{}{"mode": "admin"},
|
||||
})
|
||||
session.Add(sess, w)
|
||||
|
||||
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
|
||||
}
|
92
http/admin/newadmin.go
Normal file
92
http/admin/newadmin.go
Normal file
@ -0,0 +1,92 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/tidwall/buntdb"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handleDelAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
sess := middle.GetSession(r)
|
||||
if !middle.IsAdminSession(sess) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
db := middle.GetDB(r)
|
||||
|
||||
adminID := r.FormValue("adminid")
|
||||
if len(adminID) > 255 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The ID of the administrator is too long"), w, r)
|
||||
return
|
||||
}
|
||||
if len(adminID) < 4 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The ID of the administrator is too short"), w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err = db.Update(func(tx *buntdb.Tx) error {
|
||||
return resources.DelAdmin(tx, adminID)
|
||||
}); err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func handleNewAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
sess := middle.GetSession(r)
|
||||
if !middle.IsAdminSession(sess) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
db := middle.GetDB(r)
|
||||
|
||||
var admin = &resources.AdminPass{}
|
||||
|
||||
admin.ID = r.FormValue("adminid")
|
||||
if len(admin.ID) > 255 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The ID of the administrator is too long"), w, r)
|
||||
return
|
||||
}
|
||||
if len(admin.ID) < 4 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The ID of the administrator is too short"), w, r)
|
||||
return
|
||||
}
|
||||
if len(r.FormValue("adminpass")) > 255 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The Password of the administrator is too long"), w, r)
|
||||
return
|
||||
}
|
||||
if len(r.FormValue("adminpass")) < 12 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The Password of the administrator is too short"), w, r)
|
||||
return
|
||||
}
|
||||
admin.HashLogin(r.FormValue("adminpass"))
|
||||
|
||||
if err = db.Update(func(tx *buntdb.Tx) error {
|
||||
return resources.NewAdmin(tx, admin)
|
||||
}); err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
|
||||
}
|
@ -11,12 +11,7 @@ import (
|
||||
|
||||
func handleNewBoard(w http.ResponseWriter, r *http.Request) {
|
||||
sess := middle.GetSession(r)
|
||||
if sess == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
if sess.CAttr("mode") != "admin" {
|
||||
if !middle.IsAdminSession(sess) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
|
@ -19,20 +19,53 @@
|
||||
<input type="submit" value="Logout" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="panel boardmgr">
|
||||
<form method="POST" action="/admin/new_board.sh">
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="{{ .CSRFToken }}" />
|
||||
<input type="text" placeholder="shortname" name="shortname"/>
|
||||
<input type="text" placeholder="longname" name="longname"/>
|
||||
<input type="submit" value="Create Board" />
|
||||
<input type="reset" value="Reset" />
|
||||
<br clear="left" /><hr />
|
||||
<div class="postarea">
|
||||
<form id="postform1" action="/admin/new_board.sh" method="POST">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="postblock">
|
||||
Action
|
||||
</td>
|
||||
<td>
|
||||
New Board
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="{{ .CSRFToken }}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="postblock">
|
||||
Short Name
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" placeholder="shortname" name="shortname"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="postblock">
|
||||
Long Name
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" placeholder="longname" name="longname"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="postblock"></td>
|
||||
<td>
|
||||
<input type="submit" value="Create Board" />
|
||||
<input type="reset" value="Reset" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<br clear="left" /><hr />
|
||||
<div class="postarea">
|
||||
<form id="postform1" action="/admin/new_admin.sh" method="POST">
|
||||
<form id="postform2" action="/admin/new_admin.sh" method="POST">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
@ -53,8 +86,10 @@
|
||||
</td>
|
||||
<td>
|
||||
<input type="text"
|
||||
minlength="8"
|
||||
name="id" />
|
||||
minlength="4"
|
||||
maxlength="255"
|
||||
name="adminid"
|
||||
required />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -65,7 +100,83 @@
|
||||
<input type="password"
|
||||
minlength="12"
|
||||
maxlength="255"
|
||||
name="id" />
|
||||
name="adminpass"
|
||||
required />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="postblock"></td>
|
||||
<td>
|
||||
<input type="submit" value="Create Admin ID" />
|
||||
<input type="reset" value="Reset" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<br clear="left" /><hr />
|
||||
<div class="postarea">
|
||||
<form id="postform3" action="/admin/del_admin.sh" method="POST">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="postblock">
|
||||
Action
|
||||
</td>
|
||||
<td>
|
||||
Delete Administrator
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="{{ .CSRFToken }}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="postblock">
|
||||
ID
|
||||
</td>
|
||||
<td>
|
||||
<input type="text"
|
||||
minlength="4"
|
||||
maxlength="255"
|
||||
name="adminid"
|
||||
required />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="postblock"></td>
|
||||
<td>
|
||||
<input type="submit" value="Delete Admin ID" />
|
||||
<input type="reset" value="Reset" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<br clear="left" /><hr />
|
||||
<div class="postarea">
|
||||
<form id="postform4" action="/admin/cleanup.sh" method="POST">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="postblock">
|
||||
Action
|
||||
</td>
|
||||
<td>
|
||||
Cleanup Database
|
||||
<input
|
||||
type="hidden"
|
||||
name="csrf_token"
|
||||
value="{{ .CSRFToken }}" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="postblock"></td>
|
||||
<td>
|
||||
<input type="submit" value="Start Cleanup" />
|
||||
<input type="reset" value="Reset" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
125
http/admin/res/status.html
Normal file
125
http/admin/res/status.html
Normal file
@ -0,0 +1,125 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Config.Site.Title}} Status</title>
|
||||
<link rel="stylesheet" href="/@/style.css">
|
||||
<link rel="stylesheet" href="/@/custom.css">
|
||||
<link rel="stylesheet" href="/@/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="banner logo">
|
||||
<h2>Runtime Statistics</h2>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="postarea">
|
||||
<form id="postform1" action="/admin/new_board.sh" method="POST">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td class="postblock">
|
||||
Start Time
|
||||
</td><td>
|
||||
{{.Uptime.human}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Uptime (Precise)
|
||||
</td><td>
|
||||
{{ .Uptime.hours | printf "%.4f"}} hours<br/>
|
||||
{{ .Uptime.seconds | printf "%.4f"}} seconds
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Num. CPU
|
||||
</td><td>
|
||||
{{.GC.numcpu}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Num. GoRoutines
|
||||
</td><td>
|
||||
{{.GC.numgor}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Go Version<br/>
|
||||
Arch/OS/Compiler
|
||||
</td><td>
|
||||
{{.GC.version}}<br/>
|
||||
{{.GC.arch}} / {{.GC.os}} / {{.GC.compiler}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Current Allocs
|
||||
</td><td>
|
||||
{{.GC.memory.alloc}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Cumulative Allocs
|
||||
</td><td>
|
||||
{{.GC.memory.calloc}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Used Sys Memory
|
||||
</td><td>
|
||||
{{.GC.memory.sysmem}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Pointer Lookups
|
||||
</td><td>
|
||||
{{.GC.memory.lookups}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
MAllocs
|
||||
</td><td>
|
||||
{{.GC.memory.mallocs}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
MFrees
|
||||
</td><td>
|
||||
{{.GC.memory.frees}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Live Objects
|
||||
</td><td>
|
||||
{{.GC.memory.liveobj}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Heap Allocated
|
||||
</td><td>
|
||||
{{.GC.memory.heapalloc}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
Heap Released
|
||||
</td><td>
|
||||
{{.GC.memory.heaprelease}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
GC Metadata
|
||||
</td><td>
|
||||
{{.GC.memory.gcmeta}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
GC Pause
|
||||
</td><td>
|
||||
{{.GC.memory.pause}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
GC Invokations
|
||||
</td><td>
|
||||
{{.GC.memory.gctimes}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
GC Forced
|
||||
</td><td>
|
||||
{{.GC.memory.fgctimes}}
|
||||
</td></tr>
|
||||
<tr><td class="postblock">
|
||||
GC CPU Usage
|
||||
</td><td>
|
||||
{{.GC.memory.cpufrac}}
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<br clear="left" /><hr />
|
||||
</body>
|
||||
</html>
|
87
http/admin/status.go
Normal file
87
http/admin/status.go
Normal file
@ -0,0 +1,87 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/dustin/go-humanize"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var memStat = map[string]interface{}{}
|
||||
var memStatLock = new(sync.RWMutex)
|
||||
|
||||
var startTime = time.Now().UTC()
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
update()
|
||||
ticker := time.Tick(time.Second * 10)
|
||||
for _ = range ticker {
|
||||
update()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func update() {
|
||||
memStatLock.Lock()
|
||||
defer memStatLock.Unlock()
|
||||
memStat["Uptime"] = uptime()
|
||||
memStat["GC"] = gcStat()
|
||||
}
|
||||
func uptime() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"human": humanize.Time(startTime),
|
||||
"hours": time.Now().Sub(startTime).Hours(),
|
||||
"seconds": time.Now().Sub(startTime).Seconds(),
|
||||
}
|
||||
}
|
||||
|
||||
func gcStat() map[string]interface{} {
|
||||
m := &runtime.MemStats{}
|
||||
runtime.ReadMemStats(m)
|
||||
|
||||
mem := map[string]interface{}{}
|
||||
mem["alloc"] = fmt.Sprintf("%.5f GiB", float64(m.Alloc)/1024/1024/1024)
|
||||
mem["calloc"] = fmt.Sprintf("%.5f GiB", float64(m.TotalAlloc)/1024/1024/1024)
|
||||
mem["sysmem"] = fmt.Sprintf("%.5f MiB", float64(m.Sys)/1024/1024)
|
||||
mem["lookups"] = fmt.Sprintf("× %d", m.Lookups)
|
||||
mem["mallocs"] = fmt.Sprintf("× %d", m.Mallocs)
|
||||
mem["frees"] = fmt.Sprintf("× %d", m.Frees)
|
||||
mem["liveobj"] = fmt.Sprintf("× %d", m.Mallocs-m.Frees)
|
||||
mem["heapalloc"] = fmt.Sprintf("%.5f MiB", float64(m.HeapSys)/1024/1024)
|
||||
mem["heaprelease"] = fmt.Sprintf("%.5f MiB", float64(m.HeapReleased)/1024/1024)
|
||||
mem["gcmeta"] = fmt.Sprintf("%.5f MiB", float64(m.GCSys)/1024/1024)
|
||||
mem["pause"] = fmt.Sprintf("%.5f min", float64(m.PauseTotalNs)/1000/1000/1000/60)
|
||||
mem["gctimes"] = fmt.Sprintf("× %d", m.NumGC)
|
||||
mem["fgctimes"] = fmt.Sprintf("× %d", m.NumForcedGC)
|
||||
mem["cpufrac"] = fmt.Sprintf("%.5f %%", m.GCCPUFraction*100)
|
||||
return map[string]interface{}{
|
||||
"numcpu": runtime.NumCPU(),
|
||||
"numgor": runtime.NumGoroutine(),
|
||||
"version": runtime.Version(),
|
||||
"arch": runtime.GOARCH,
|
||||
"os": runtime.GOOS,
|
||||
"compiler": runtime.Compiler,
|
||||
"memory": mem,
|
||||
}
|
||||
}
|
||||
func serveStatus(w http.ResponseWriter, r *http.Request) {
|
||||
memStatLock.RLock()
|
||||
defer memStatLock.RUnlock()
|
||||
ctx := middle.GetBaseCtx(r)
|
||||
ctx["Uptime"] = memStat["Uptime"]
|
||||
ctx["GC"] = memStat["GC"]
|
||||
dat := bytes.NewBuffer([]byte{})
|
||||
err := statusTmpl.Execute(dat, ctx)
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, r, "panel.html", time.Now(),
|
||||
bytes.NewReader(dat.Bytes()))
|
||||
}
|
@ -18,18 +18,21 @@ func serveBoard(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := middle.GetBaseCtx(r)
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
bName := chi.URLParam(r, "board")
|
||||
log.Println("Getting board")
|
||||
b, err := resources.GetBoard(tx, r.Host, bName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx["Board"] = b
|
||||
|
||||
log.Println("Listing Threads...")
|
||||
threads, err := resources.ListThreads(tx, r.Host, bName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("Number of Thread on board: ", len(threads))
|
||||
|
||||
log.Println("Filling threads")
|
||||
for k := range threads {
|
||||
err := resources.FillReplies(tx, r.Host, threads[k])
|
||||
if err != nil {
|
||||
|
@ -27,9 +27,6 @@ var box = riceConf.MustFindBox("http/board/res/")
|
||||
|
||||
var (
|
||||
tmpls = template.New("base")
|
||||
//dirTmpl = template.New("board/dir")
|
||||
//boardTmpl = template.New("board/board")
|
||||
//threadTmpl = template.New("board/thread")
|
||||
|
||||
hdlFMap = template.FuncMap{
|
||||
"renderText": resources.OperateReplyText,
|
||||
@ -53,6 +50,10 @@ var (
|
||||
"formatDate": func(date time.Time) string {
|
||||
return date.Format("02 Jan 06 15:04:05")
|
||||
},
|
||||
"isAdminSession": middle.IsAdminSession,
|
||||
"isModSession": middle.IsModSession,
|
||||
"captchaProb": resources.CaptchaProb,
|
||||
"percentFloat": func(in float64) float64 { return in * 100 },
|
||||
}
|
||||
)
|
||||
|
||||
@ -93,6 +94,7 @@ func Router(r chi.Router) {
|
||||
|
||||
func serveThumb(w http.ResponseWriter, r *http.Request) {
|
||||
dat := bytes.NewBuffer([]byte{})
|
||||
var date time.Time
|
||||
db := middle.GetDB(r)
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
bName := chi.URLParam(r, "board")
|
||||
@ -113,17 +115,19 @@ func serveThumb(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
date = resources.DateFromId(reply.ID)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, r, "thumb.png", time.Now(), bytes.NewReader(dat.Bytes()))
|
||||
http.ServeContent(w, r, "thumb.png", date, bytes.NewReader(dat.Bytes()))
|
||||
}
|
||||
|
||||
func serveFullImage(w http.ResponseWriter, r *http.Request) {
|
||||
dat := bytes.NewBuffer([]byte{})
|
||||
var date time.Time
|
||||
db := middle.GetDB(r)
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
bName := chi.URLParam(r, "board")
|
||||
@ -144,13 +148,14 @@ func serveFullImage(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
date = resources.DateFromId(reply.ID)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
http.ServeContent(w, r, "image.png", time.Now(), bytes.NewReader(dat.Bytes()))
|
||||
http.ServeContent(w, r, "image.png", date, bytes.NewReader(dat.Bytes()))
|
||||
}
|
||||
|
||||
func serveDir(w http.ResponseWriter, r *http.Request) {
|
||||
|
61
http/board/imageparser.go
Normal file
61
http/board/imageparser.go
Normal file
@ -0,0 +1,61 @@
|
||||
package board
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/nfnt/resize"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func parseImage(reply *resources.Reply, file multipart.File, hdr *multipart.FileHeader, err error) error {
|
||||
if err != nil && err != http.ErrMissingFile {
|
||||
return err
|
||||
}
|
||||
if err == http.ErrMissingFile {
|
||||
return nil
|
||||
}
|
||||
cfg, _, err := image.DecodeConfig(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Height > 8000 || cfg.Width > 8000 {
|
||||
log.Println("Somebody tried to detonate the memory!")
|
||||
return errw.MakeErrorWithTitle("Too large", "Your upload was too large")
|
||||
}
|
||||
_, err = file.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if img.Bounds().Dx() > 8000 || img.Bounds().Dy() > 8000 {
|
||||
log.Println("Somebody tried to detonate the memory!")
|
||||
return errw.MakeErrorWithTitle("Too large", "Your upload was too large")
|
||||
}
|
||||
thumb := resize.Thumbnail(128, 128, img, resize.Lanczos3)
|
||||
imgBuf := bytes.NewBuffer([]byte{})
|
||||
err = png.Encode(imgBuf, thumb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Thumb has size %d KiB", imgBuf.Len()/1024)
|
||||
reply.Thumbnail = make([]byte, imgBuf.Len())
|
||||
copy(reply.Thumbnail, imgBuf.Bytes())
|
||||
imgBuf.Reset()
|
||||
err = png.Encode(imgBuf, img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Image has size %d KiB", imgBuf.Len()/1024)
|
||||
reply.Image = make([]byte, imgBuf.Len())
|
||||
copy(reply.Image, imgBuf.Bytes())
|
||||
return nil
|
||||
}
|
@ -1,20 +1,15 @@
|
||||
package board
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/pressly/chi"
|
||||
"github.com/tidwall/buntdb"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func handleNewReply(w http.ResponseWriter, r *http.Request) {
|
||||
@ -23,7 +18,7 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
err = r.ParseMultipartForm(10 * 1024 * 1024)
|
||||
err = r.ParseMultipartForm(4 * 1024 * 1024)
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
@ -39,66 +34,16 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var reply = &resources.Reply{}
|
||||
|
||||
reply.Board = chi.URLParam(r, "board")
|
||||
tid, err := strconv.Atoi(chi.URLParam(r, "thread"))
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
reply.Thread = tid
|
||||
reply.Text = r.FormValue("text")
|
||||
if len(reply.Text) > 10000 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r)
|
||||
return
|
||||
}
|
||||
if len(reply.Text) < 5 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if score, err := resources.SpamScore(reply.Text); err != nil || !resources.CaptchaPass(score) {
|
||||
err = parseReply(r, reply)
|
||||
if err == trollThrottle {
|
||||
http.Redirect(w, r,
|
||||
fmt.Sprintf("/%s/%s/thread.html?err=trollthrottle",
|
||||
chi.URLParam(r, "board"), chi.URLParam(r, "thread")),
|
||||
http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
file, _, err := r.FormFile("image")
|
||||
if err != nil && err != http.ErrMissingFile {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
} else if err != http.ErrMissingFile {
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
thumb := resize.Thumbnail(128, 128, img, resize.Lanczos3)
|
||||
imgBuf := bytes.NewBuffer([]byte{})
|
||||
err = png.Encode(imgBuf, img)
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
fmt.Println("Image has size ", len(imgBuf.Bytes()))
|
||||
reply.Image = imgBuf.Bytes()
|
||||
imgBuf = bytes.NewBuffer([]byte{})
|
||||
err = png.Encode(imgBuf, thumb)
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
reply.Thumbnail = imgBuf.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
reply.Metadata = map[string]string{}
|
||||
if r.FormValue("tripcode") != "" {
|
||||
reply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))
|
||||
} else {
|
||||
reply.Metadata["trip"] = "Anonymous"
|
||||
} else if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
db := middle.GetDB(r)
|
||||
|
@ -1,16 +1,12 @@
|
||||
package board
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/pressly/chi"
|
||||
"github.com/tidwall/buntdb"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"image"
|
||||
"image/png"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -37,59 +33,18 @@ func handleNewThread(w http.ResponseWriter, r *http.Request) {
|
||||
var thread = &resources.Thread{}
|
||||
var mainReply = &resources.Reply{}
|
||||
|
||||
mainReply.Board = chi.URLParam(r, "board")
|
||||
thread.Board = chi.URLParam(r, "board")
|
||||
mainReply.Text = r.FormValue("text")
|
||||
if len(mainReply.Text) > 10000 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r)
|
||||
return
|
||||
}
|
||||
if len(mainReply.Text) < 5 {
|
||||
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if score, err := resources.SpamScore(mainReply.Text); err != nil || !resources.CaptchaPass(score) {
|
||||
err = parseReply(r, mainReply)
|
||||
if err == trollThrottle {
|
||||
http.Redirect(w, r,
|
||||
fmt.Sprintf("/%s/board.html?err=trollthrottle",
|
||||
chi.URLParam(r, "board")),
|
||||
http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
file, _, err := r.FormFile("image")
|
||||
if err != nil && err != http.ErrMissingFile {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
} else if err != http.ErrMissingFile {
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
thumb := resize.Thumbnail(128, 128, img, resize.Lanczos3)
|
||||
imgBuf := bytes.NewBuffer([]byte{})
|
||||
err = png.Encode(imgBuf, img)
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
fmt.Println("Image has size ", len(imgBuf.Bytes()))
|
||||
mainReply.Image = imgBuf.Bytes()
|
||||
imgBuf = bytes.NewBuffer([]byte{})
|
||||
err = png.Encode(imgBuf, thumb)
|
||||
if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
mainReply.Thumbnail = imgBuf.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
mainReply.Metadata = map[string]string{}
|
||||
if r.FormValue("tripcode") != "" {
|
||||
mainReply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))
|
||||
} else if err != nil {
|
||||
errw.ErrorWriter(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
db := middle.GetDB(r)
|
||||
|
72
http/board/replyparser.go
Normal file
72
http/board/replyparser.go
Normal file
@ -0,0 +1,72 @@
|
||||
package board
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pressly/chi"
|
||||
"go.rls.moe/nyx/http/errw"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"go.rls.moe/nyx/resources"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var trollThrottle = errors.New("Troll throttle")
|
||||
|
||||
func parseReply(r *http.Request, reply *resources.Reply) error {
|
||||
reply.Board = chi.URLParam(r, "board")
|
||||
reply.Text = r.FormValue("text")
|
||||
if tidStr := chi.URLParam(r, "thread"); tidStr != "" {
|
||||
var err error
|
||||
reply.Thread, err = strconv.Atoi(tidStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(reply.Text) > 10000 {
|
||||
return errw.MakeErrorWithTitle(
|
||||
"I'm sorry but I can't do that",
|
||||
"There are too many characters")
|
||||
}
|
||||
if len(reply.Text) < 5 {
|
||||
return errw.MakeErrorWithTitle(
|
||||
"I'm sorry but I can't do that",
|
||||
"There are not enough characters")
|
||||
}
|
||||
|
||||
reply.Metadata = map[string]string{}
|
||||
|
||||
spamScore, err := resources.SpamScore(reply.Text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply.Metadata["spamscore"] = fmt.Sprintf("%.6f", spamScore)
|
||||
reply.Metadata["captchaprob"] = fmt.Sprintf("%.2f", resources.CaptchaProb(spamScore)*100)
|
||||
|
||||
if !resources.CaptchaPass(spamScore) {
|
||||
return trollThrottle
|
||||
}
|
||||
|
||||
file, hdr, err := r.FormFile("image")
|
||||
err = parseImage(reply, file, hdr, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.FormValue("tripcode") != "" {
|
||||
reply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))
|
||||
}
|
||||
if middle.IsModSession(middle.GetSession(r)) {
|
||||
if r.FormValue("modpost") != "" {
|
||||
reply.Metadata["modpost"] = "yes"
|
||||
}
|
||||
if middle.IsAdminSession(middle.GetSession(r)) {
|
||||
if r.FormValue("adminpost") != "" {
|
||||
reply.Metadata["adminpost"] = "yes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -78,6 +78,23 @@
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ if (isModSession .Session) }}
|
||||
<tr>
|
||||
<td class="postblock">
|
||||
Mod Post
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="modpost"/>Mark as Mod Post
|
||||
</label>
|
||||
{{ if (isAdminSession .Session) }}
|
||||
<label>
|
||||
<input type="checkbox" name="adminpost"/>Mark as Admin Post
|
||||
</label>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<td class="postblock">
|
||||
|
||||
@ -100,6 +117,12 @@
|
||||
{{ else }}
|
||||
Anonymous
|
||||
{{ end }}
|
||||
{{ if .Reply.Metadata.modpost }}
|
||||
(Mod)
|
||||
{{ end }}
|
||||
{{ if .Reply.Metadata.adminpost }}
|
||||
[Admin]
|
||||
{{ end }}
|
||||
</span></label>
|
||||
<span class="date">{{dateFromID .Reply.ID | formatDate}}</span>
|
||||
{{ if .Session }}
|
||||
@ -126,7 +149,15 @@
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<span>
|
||||
{{printf "[SpamScore: %f]" (rateSpam .Reply.Text) }}
|
||||
{{ if not .Reply.Metadata.spamscore }}
|
||||
{{ $score := (rateSpam .Reply.Text) }}
|
||||
{{printf "[SpamScore: %f]" $score }}
|
||||
{{printf "[Captcha: %.3f%%]" (percentFloat (captchaProb $score)) }}
|
||||
{{printf "[OLD]"}}
|
||||
{{ else }}
|
||||
{{ printf "[SpamScore: %s]" .Reply.Metadata.spamscore }}
|
||||
{{ printf "[Captcha: %s %%]" .Reply.Metadata.captchaprob }}
|
||||
{{ end }}
|
||||
</span>
|
||||
<span class="reflink">
|
||||
<a href="/{{.Boardlink}}/{{.ThreadID}}/thread.html">No.{{.Reply.ID}}</a>
|
||||
|
15
http/middle/limitsize.go
Normal file
15
http/middle/limitsize.go
Normal file
@ -0,0 +1,15 @@
|
||||
package middle
|
||||
|
||||
import (
|
||||
"go.rls.moe/nyx/config"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func LimitSize(c *config.Config) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 10*1024*1024)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
@ -13,3 +13,26 @@ func init() {
|
||||
func GetSession(r *http.Request) session.Session {
|
||||
return session.Get(r)
|
||||
}
|
||||
|
||||
func IsAdminSession(sess session.Session) bool {
|
||||
if sess == nil {
|
||||
return false
|
||||
}
|
||||
if sess.CAttr("mode") == "admin" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsModSession(sess session.Session) bool {
|
||||
if sess == nil {
|
||||
return false
|
||||
}
|
||||
if IsAdminSession(sess) {
|
||||
return true
|
||||
}
|
||||
if sess.CAttr("mode") == "mod" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"go.rls.moe/nyx/http/board"
|
||||
"go.rls.moe/nyx/http/middle"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var riceConf = rice.Config{
|
||||
@ -27,6 +28,7 @@ func Start(config *config.Config) {
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.CloseNotify)
|
||||
r.Use(middle.LimitSize(config))
|
||||
r.Use(middleware.DefaultCompress)
|
||||
|
||||
r.Use(middle.ConfigCtx(config))
|
||||
@ -50,5 +52,13 @@ func Start(config *config.Config) {
|
||||
r.Group(board.Router)
|
||||
|
||||
fmt.Println("Setup Complete, Starting Web Server...")
|
||||
http.ListenAndServe(config.ListenOn, r)
|
||||
srv := &http.Server{
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
Handler: r,
|
||||
Addr: config.ListenOn,
|
||||
MaxHeaderBytes: 1 * 1024 * 1024,
|
||||
}
|
||||
srv.ListenAndServe()
|
||||
}
|
||||
|
@ -19,8 +19,19 @@ func (a *AdminPass) HashLogin(pass string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *AdminPass) VerifyLogin(pass string) error {
|
||||
var err error
|
||||
func (a *AdminPass) VerifyLogin(pass string) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var ok bool
|
||||
err, ok = r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("pkg: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if a == nil {
|
||||
return errors.New("no login")
|
||||
}
|
||||
err = passlib.VerifyNoUpgrade(pass, a.Password)
|
||||
return err
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/tidwall/buntdb"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -19,6 +20,16 @@ const (
|
||||
adminPassPath = "/jack/./pass/admin/%s/admin-data"
|
||||
)
|
||||
|
||||
func GetHostnameFromKey(key string) (string, error) {
|
||||
regex := regexp.MustCompile(`^/jack/(.+)/(board|pass)`)
|
||||
res := regex.FindStringSubmatch(key)
|
||||
if len(res) != 3 {
|
||||
fmt.Printf("Found %d keys: %s", len(res), res)
|
||||
return "", errors.New("Could not find host in key")
|
||||
}
|
||||
return unescapeString(res[1]), nil
|
||||
}
|
||||
|
||||
func InitialSetup(db *buntdb.DB) error {
|
||||
return db.Update(func(tx *buntdb.Tx) error {
|
||||
if _, err := tx.Get(setup); err != nil {
|
||||
@ -112,3 +123,17 @@ func escapeString(in string) string {
|
||||
in = strings.Replace(in, "<", ".arrow-right.", -1)
|
||||
return in
|
||||
}
|
||||
|
||||
func unescapeString(in string) string {
|
||||
in = strings.Replace(in, ".arrow-right.", "<", -1)
|
||||
in = strings.Replace(in, ".arrow-left.", ">", -1)
|
||||
in = strings.Replace(in, ".quote.", ">>", -1)
|
||||
in = strings.Replace(in, ".at.", "@", -1)
|
||||
in = strings.Replace(in, ".slash.", "/", -1)
|
||||
in = strings.Replace(in, ".ask.", "?", -1)
|
||||
in = strings.Replace(in, ".star.", "*", -1)
|
||||
in = strings.Replace(in, ".backslash.", "\\", -1)
|
||||
in = strings.Replace(in, ".minus.", "-", -1)
|
||||
in = strings.Replace(in, ".dot.", ".", -1)
|
||||
return in
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ package resources
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strings"
|
||||
@ -34,6 +34,7 @@ var (
|
||||
"nazi",
|
||||
"beemovie",
|
||||
"bee movie",
|
||||
"honey",
|
||||
}
|
||||
)
|
||||
|
||||
@ -56,7 +57,13 @@ func SpamScore(spam string) (float64, error) {
|
||||
blScore += float64(strings.Count(spam, v))
|
||||
}
|
||||
|
||||
score := float64(len(spam)) / float64(counter.p)
|
||||
lines := strings.Count(spam, "\n")
|
||||
|
||||
if lines == 0 {
|
||||
lines = 1
|
||||
}
|
||||
|
||||
score := float64(len(spam)*lines) / float64(counter.p)
|
||||
|
||||
return (score * blScore) / 100, nil
|
||||
}
|
||||
@ -70,15 +77,19 @@ func (b *byteCounter) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func CaptchaPass(spamScore float64) bool {
|
||||
chance := math.Max(
|
||||
func CaptchaProb(spamScore float64) float64 {
|
||||
return math.Max(
|
||||
passScoreLimitMin,
|
||||
math.Min(
|
||||
passScoreReactive*math.Atan(
|
||||
passScoreAggressive*spamScore,
|
||||
),
|
||||
passScoreLimitMax))
|
||||
}
|
||||
|
||||
func CaptchaPass(spamScore float64) bool {
|
||||
chance := CaptchaProb(spamScore)
|
||||
take := rand.Float64()
|
||||
fmt.Printf("Chance: %f, Take %f", chance, take)
|
||||
log.Printf("Chance: %f, Take %f\n", chance, take)
|
||||
return take > chance
|
||||
}
|
||||
|
@ -91,6 +91,21 @@ func GetThread(tx *buntdb.Tx, host, board string, id int) (*Thread, error) {
|
||||
}
|
||||
|
||||
ret.intReply, err = GetReply(tx, host, board, id, ret.StartReply)
|
||||
if err != nil && err == buntdb.ErrNotFound {
|
||||
ret.intReply = &Reply{
|
||||
Board: ret.Board,
|
||||
Thread: ret.ID,
|
||||
ID: -1,
|
||||
Image: nil,
|
||||
Thumbnail: nil,
|
||||
Metadata: map[string]string{
|
||||
"deleted": "not found",
|
||||
},
|
||||
Text: "[not found]",
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@ -132,7 +147,22 @@ func ListThreads(tx *buntdb.Tx, host, board string) ([]*Thread, error) {
|
||||
}
|
||||
thread.intReply, err = GetReply(tx, host, board, thread.ID, thread.StartReply)
|
||||
if err != nil {
|
||||
return false
|
||||
if err == buntdb.ErrNotFound {
|
||||
err = nil
|
||||
thread.intReply = &Reply{
|
||||
Board: thread.Board,
|
||||
Thread: thread.ID,
|
||||
ID: -1,
|
||||
Image: nil,
|
||||
Thumbnail: nil,
|
||||
Metadata: map[string]string{
|
||||
"deleted": "not found",
|
||||
},
|
||||
Text: "[not found]",
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
threadList = append(threadList, thread)
|
||||
|
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
Normal file
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
92
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
Normal file
92
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
# Humane Units [![Build Status](https://travis-ci.org/dustin/go-humanize.svg?branch=master)](https://travis-ci.org/dustin/go-humanize) [![GoDoc](https://godoc.org/github.com/dustin/go-humanize?status.svg)](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
||||
|
||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
Normal file
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// order of magnitude (to a max order)
|
||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
if mag == maxmag && maxmag >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
||||
// total order of magnitude
|
||||
// (same as above, but with no upper limit)
|
||||
func oom(n, b *big.Int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
Normal file
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
Normal file
@ -0,0 +1,173 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bigIECExp = big.NewInt(1024)
|
||||
|
||||
// BigByte is one byte in bit.Ints
|
||||
BigByte = big.NewInt(1)
|
||||
// BigKiByte is 1,024 bytes in bit.Ints
|
||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
||||
)
|
||||
|
||||
var (
|
||||
bigSIExp = big.NewInt(1000)
|
||||
|
||||
// BigSIByte is one SI byte in big.Ints
|
||||
BigSIByte = big.NewInt(1)
|
||||
// BigKByte is 1,000 SI bytes in big.Ints
|
||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
||||
)
|
||||
|
||||
var bigBytesSizeTable = map[string]*big.Int{
|
||||
"b": BigByte,
|
||||
"kib": BigKiByte,
|
||||
"kb": BigKByte,
|
||||
"mib": BigMiByte,
|
||||
"mb": BigMByte,
|
||||
"gib": BigGiByte,
|
||||
"gb": BigGByte,
|
||||
"tib": BigTiByte,
|
||||
"tb": BigTByte,
|
||||
"pib": BigPiByte,
|
||||
"pb": BigPByte,
|
||||
"eib": BigEiByte,
|
||||
"eb": BigEByte,
|
||||
"zib": BigZiByte,
|
||||
"zb": BigZByte,
|
||||
"yib": BigYiByte,
|
||||
"yb": BigYByte,
|
||||
// Without suffix
|
||||
"": BigByte,
|
||||
"ki": BigKiByte,
|
||||
"k": BigKByte,
|
||||
"mi": BigMiByte,
|
||||
"m": BigMByte,
|
||||
"gi": BigGiByte,
|
||||
"g": BigGByte,
|
||||
"ti": BigTiByte,
|
||||
"t": BigTByte,
|
||||
"pi": BigPiByte,
|
||||
"p": BigPByte,
|
||||
"ei": BigEiByte,
|
||||
"e": BigEByte,
|
||||
"z": BigZByte,
|
||||
"zi": BigZiByte,
|
||||
"y": BigYByte,
|
||||
"yi": BigYiByte,
|
||||
}
|
||||
|
||||
var ten = big.NewInt(10)
|
||||
|
||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
||||
if s.Cmp(ten) < 0 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
c := (&big.Int{}).Set(s)
|
||||
val, mag := oomm(c, base, len(sizes)-1)
|
||||
suffix := sizes[mag]
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
|
||||
}
|
||||
|
||||
// BigBytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigBytes(82854982) -> 83 MB
|
||||
func BigBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
return humanateBigBytes(s, bigSIExp, sizes)
|
||||
}
|
||||
|
||||
// BigIBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigIBytes(82854982) -> 79 MiB
|
||||
func BigIBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
return humanateBigBytes(s, bigIECExp, sizes)
|
||||
}
|
||||
|
||||
// ParseBigBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See also: BigBytes, BigIBytes.
|
||||
//
|
||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
||||
func ParseBigBytes(s string) (*big.Int, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
val := &big.Rat{}
|
||||
_, err := fmt.Sscanf(num, "%f", val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
||||
mv := (&big.Rat{}).SetInt(m)
|
||||
val.Mul(val, mv)
|
||||
rv := &big.Int{}
|
||||
rv.Div(val.Num(), val.Denom())
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
Normal file
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
// Bytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// Bytes(82854982) -> 83 MB
|
||||
func Bytes(s uint64) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
||||
|
||||
// IBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// IBytes(82854982) -> 79 MiB
|
||||
func IBytes(s uint64) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
return humanateBytes(s, 1024, sizes)
|
||||
}
|
||||
|
||||
// ParseBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See Also: Bytes, IBytes.
|
||||
//
|
||||
// ParseBytes("42 MB") -> 42000000, nil
|
||||
// ParseBytes("42 mib") -> 44040192, nil
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bytesSizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
108
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
Normal file
108
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Comma produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Comma(834142) -> 834,142
|
||||
func Comma(v int64) string {
|
||||
sign := ""
|
||||
|
||||
// minin64 can't be negated to a usable value, so it has to be special cased.
|
||||
if v == math.MinInt64 {
|
||||
return "-9,223,372,036,854,775,808"
|
||||
}
|
||||
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
parts := []string{"", "", "", "", "", "", ""}
|
||||
j := len(parts) - 1
|
||||
|
||||
for v > 999 {
|
||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
v = v / 1000
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(v))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
||||
// Commaf produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Commaf(834142.32) -> 834,142.32
|
||||
func Commaf(v float64) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// BigComma produces a string form of the given big.Int in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigComma(b *big.Int) string {
|
||||
sign := ""
|
||||
if b.Sign() < 0 {
|
||||
sign = "-"
|
||||
b.Abs(b)
|
||||
}
|
||||
|
||||
athousand := big.NewInt(1000)
|
||||
c := (&big.Int{}).Set(b)
|
||||
_, m := oom(c, athousand)
|
||||
parts := make([]string, m+1)
|
||||
j := len(parts) - 1
|
||||
|
||||
mod := &big.Int{}
|
||||
for b.Cmp(athousand) >= 0 {
|
||||
b.DivMod(b, athousand, mod)
|
||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
Normal file
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BigCommaf produces a string form of the given big.Float in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigCommaf(v *big.Float) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v.Sign() < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v.Abs(v)
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(v.Text('f', -1), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
23
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
Normal file
23
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
func stripTrailingZeros(s string) string {
|
||||
offset := len(s) - 1
|
||||
for offset > 0 {
|
||||
if s[offset] == '.' {
|
||||
offset--
|
||||
break
|
||||
}
|
||||
if s[offset] != '0' {
|
||||
break
|
||||
}
|
||||
offset--
|
||||
}
|
||||
return s[:offset+1]
|
||||
}
|
||||
|
||||
// Ftoa converts a float to a string with no trailing zeros.
|
||||
func Ftoa(num float64) string {
|
||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
||||
}
|
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
Normal file
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
||||
|
||||
Durations can be turned into strings such as "3 days ago", numbers
|
||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
||||
"79 MiB" (whichever you prefer).
|
||||
*/
|
||||
package humanize
|
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
Normal file
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
Normal file
@ -0,0 +1,192 @@
|
||||
package humanize
|
||||
|
||||
/*
|
||||
Slightly adapted from the source to fit go-humanize.
|
||||
|
||||
Author: https://github.com/gorhill
|
||||
Source: https://gist.github.com/gorhill/5285193
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
renderFloatPrecisionMultipliers = [...]float64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
}
|
||||
|
||||
renderFloatPrecisionRounders = [...]float64{
|
||||
0.5,
|
||||
0.05,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.00005,
|
||||
0.000005,
|
||||
0.0000005,
|
||||
0.00000005,
|
||||
0.000000005,
|
||||
0.0000000005,
|
||||
}
|
||||
)
|
||||
|
||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||
// * thousands separator
|
||||
// * decimal separator
|
||||
// * decimal precision
|
||||
//
|
||||
// Usage: s := RenderFloat(format, n)
|
||||
// The format parameter tells how to render the number n.
|
||||
//
|
||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||
//
|
||||
// Examples of format strings, given n = 12345.6789:
|
||||
// "#,###.##" => "12,345.67"
|
||||
// "#,###." => "12,345"
|
||||
// "#,###" => "12345,678"
|
||||
// "#\u202F###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < -math.MaxFloat64 {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
Normal file
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Ordinal gives you the input number in a rank/ordinal format.
|
||||
//
|
||||
// Ordinal(3) -> 3rd
|
||||
func Ordinal(x int) string {
|
||||
suffix := "th"
|
||||
switch x % 10 {
|
||||
case 1:
|
||||
if x%100 != 11 {
|
||||
suffix = "st"
|
||||
}
|
||||
case 2:
|
||||
if x%100 != 12 {
|
||||
suffix = "nd"
|
||||
}
|
||||
case 3:
|
||||
if x%100 != 13 {
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(x) + suffix
|
||||
}
|
113
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
Normal file
113
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
Normal file
@ -0,0 +1,113 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var siPrefixTable = map[float64]string{
|
||||
-24: "y", // yocto
|
||||
-21: "z", // zepto
|
||||
-18: "a", // atto
|
||||
-15: "f", // femto
|
||||
-12: "p", // pico
|
||||
-9: "n", // nano
|
||||
-6: "µ", // micro
|
||||
-3: "m", // milli
|
||||
0: "",
|
||||
3: "k", // kilo
|
||||
6: "M", // mega
|
||||
9: "G", // giga
|
||||
12: "T", // tera
|
||||
15: "P", // peta
|
||||
18: "E", // exa
|
||||
21: "Z", // zetta
|
||||
24: "Y", // yotta
|
||||
}
|
||||
|
||||
var revSIPrefixTable = revfmap(siPrefixTable)
|
||||
|
||||
// revfmap reverses the map and precomputes the power multiplier
|
||||
func revfmap(in map[float64]string) map[string]float64 {
|
||||
rv := map[string]float64{}
|
||||
for k, v := range in {
|
||||
rv[v] = math.Pow(10, k)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
var riParseRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
ri := `^([\-0-9.]+)\s?([`
|
||||
for _, v := range siPrefixTable {
|
||||
ri += v
|
||||
}
|
||||
ri += `]?)(.*)`
|
||||
|
||||
riParseRegex = regexp.MustCompile(ri)
|
||||
}
|
||||
|
||||
// ComputeSI finds the most appropriate SI prefix for the given number
|
||||
// and returns the prefix along with the value adjusted to be within
|
||||
// that prefix.
|
||||
//
|
||||
// See also: SI, ParseSI.
|
||||
//
|
||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
|
||||
func ComputeSI(input float64) (float64, string) {
|
||||
if input == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
mag := math.Abs(input)
|
||||
exponent := math.Floor(logn(mag, 10))
|
||||
exponent = math.Floor(exponent/3) * 3
|
||||
|
||||
value := mag / math.Pow(10, exponent)
|
||||
|
||||
// Handle special case where value is exactly 1000.0
|
||||
// Should return 1 M instead of 1000 k
|
||||
if value == 1000.0 {
|
||||
exponent += 3
|
||||
value = mag / math.Pow(10, exponent)
|
||||
}
|
||||
|
||||
value = math.Copysign(value, input)
|
||||
|
||||
prefix := siPrefixTable[exponent]
|
||||
return value, prefix
|
||||
}
|
||||
|
||||
// SI returns a string with default formatting.
|
||||
//
|
||||
// SI uses Ftoa to format float value, removing trailing zeros.
|
||||
//
|
||||
// See also: ComputeSI, ParseSI.
|
||||
//
|
||||
// e.g. SI(1000000, "B") -> 1 MB
|
||||
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
|
||||
func SI(input float64, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return Ftoa(value) + " " + prefix + unit
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("invalid input")
|
||||
|
||||
// ParseSI parses an SI string back into the number and unit.
|
||||
//
|
||||
// See also: SI, ComputeSI.
|
||||
//
|
||||
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
|
||||
func ParseSI(input string) (float64, string, error) {
|
||||
found := riParseRegex.FindStringSubmatch(input)
|
||||
if len(found) != 4 {
|
||||
return 0, "", errInvalid
|
||||
}
|
||||
mag := revSIPrefixTable[found[2]]
|
||||
unit := found[3]
|
||||
|
||||
base, err := strconv.ParseFloat(found[1], 64)
|
||||
return base * mag, unit, err
|
||||
}
|
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
Normal file
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Day = 24 * time.Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
LongTime = 37 * Year
|
||||
)
|
||||
|
||||
// Time formats a time into a relative string.
|
||||
//
|
||||
// Time(someT) -> "3 weeks ago"
|
||||
func Time(then time.Time) string {
|
||||
return RelTime(then, time.Now(), "ago", "from now")
|
||||
}
|
||||
|
||||
// A RelTimeMagnitude struct contains a relative time point at which
|
||||
// the relative format of time will switch to a new format string. A
|
||||
// slice of these in ascending order by their "D" field is passed to
|
||||
// CustomRelTime to format durations.
|
||||
//
|
||||
// The Format field is a string that may contain a "%s" which will be
|
||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||
// now") and a "%d" that will be replaced by the quantity.
|
||||
//
|
||||
// The DivBy field is the amount of time the time difference must be
|
||||
// divided by in order to display correctly.
|
||||
//
|
||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||
// DivBy should be time.Minute so whatever the duration is will be
|
||||
// expressed in minutes.
|
||||
type RelTimeMagnitude struct {
|
||||
D time.Duration
|
||||
Format string
|
||||
DivBy time.Duration
|
||||
}
|
||||
|
||||
var defaultMagnitudes = []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{2 * time.Minute, "1 minute %s", 1},
|
||||
{time.Hour, "%d minutes %s", time.Minute},
|
||||
{2 * time.Hour, "1 hour %s", 1},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{Month, "%d weeks %s", Week},
|
||||
{2 * Month, "1 month %s", 1},
|
||||
{Year, "%d months %s", Month},
|
||||
{18 * Month, "1 year %s", 1},
|
||||
{2 * Year, "2 years %s", 1},
|
||||
{LongTime, "%d years %s", Year},
|
||||
{math.MaxInt64, "a long while %s", 1},
|
||||
}
|
||||
|
||||
// RelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times and two labels. In addition to the generic time
|
||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||
// the label corresponding to the smaller time is applied.
|
||||
//
|
||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||
}
|
||||
|
||||
// CustomRelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times two labels and a table of relative time formats.
|
||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||
// labels are used applied so that the label corresponding to the
|
||||
// smaller time is applied.
|
||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||
lbl := albl
|
||||
diff := b.Sub(a)
|
||||
|
||||
if a.After(b) {
|
||||
lbl = blbl
|
||||
diff = a.Sub(b)
|
||||
}
|
||||
|
||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||
return magnitudes[i].D >= diff
|
||||
})
|
||||
|
||||
if n >= len(magnitudes) {
|
||||
n = len(magnitudes) - 1
|
||||
}
|
||||
mag := magnitudes[n]
|
||||
args := []interface{}{}
|
||||
escaped := false
|
||||
for _, ch := range mag.Format {
|
||||
if escaped {
|
||||
switch ch {
|
||||
case 's':
|
||||
args = append(args, lbl)
|
||||
case 'd':
|
||||
args = append(args, diff/mag.DivBy)
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = ch == '%'
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(mag.Format, args...)
|
||||
}
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
@ -38,6 +38,12 @@
|
||||
"revision": "9e952142169c3cd6268c6482a3a61c121536aca2",
|
||||
"revisionTime": "2015-07-28T12:50:59Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "rhLUtXvcmouYuBwOq9X/nYKzvNg=",
|
||||
"path": "github.com/dustin/go-humanize",
|
||||
"revision": "259d2a102b871d17f30e3cd9881a642961a1e486",
|
||||
"revisionTime": "2017-02-28T07:34:54Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "6defOlYtxIqheaUEG/cWWouQnIU=",
|
||||
"path": "github.com/hlandau/passlib",
|
||||
|
Loading…
Reference in New Issue
Block a user