mirror of
https://github.com/rls-moe/nyx
synced 2024-11-22 22:12:24 +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
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
"go.rls.moe/nyx/http/errw"
|
"go.rls.moe/nyx/http/errw"
|
||||||
"go.rls.moe/nyx/http/middle"
|
"go.rls.moe/nyx/http/middle"
|
||||||
|
"go.rls.moe/nyx/resources"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleCleanup(w http.ResponseWriter, r *http.Request) {
|
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)
|
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 */
|
/* 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
|
return nil
|
||||||
})
|
})
|
||||||
|
fmt.Println("Shrinking DB")
|
||||||
err = db.Shrink()
|
err = db.Shrink()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errw.ErrorWriter(err, w, r)
|
errw.ErrorWriter(err, w, r)
|
||||||
return
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"github.com/GeertJohan/go.rice"
|
"github.com/GeertJohan/go.rice"
|
||||||
"github.com/icza/session"
|
|
||||||
"github.com/pressly/chi"
|
"github.com/pressly/chi"
|
||||||
"github.com/tidwall/buntdb"
|
|
||||||
"go.rls.moe/nyx/http/errw"
|
"go.rls.moe/nyx/http/errw"
|
||||||
"go.rls.moe/nyx/http/middle"
|
"go.rls.moe/nyx/http/middle"
|
||||||
"go.rls.moe/nyx/resources"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,8 +22,9 @@ var riceConf = rice.Config{
|
|||||||
var box = riceConf.MustFindBox("http/admin/res/")
|
var box = riceConf.MustFindBox("http/admin/res/")
|
||||||
|
|
||||||
var (
|
var (
|
||||||
panelTmpl = template.New("admin/panel")
|
panelTmpl = template.New("admin/panel")
|
||||||
loginTmpl = template.New("admin/login")
|
loginTmpl = template.New("admin/login")
|
||||||
|
statusTmpl = template.New("admin/status")
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -41,6 +37,10 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
statusTmpl, err = statusTmpl.Parse(box.MustString("status.html"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router sets up the Administration Panel
|
// Router sets up the Administration Panel
|
||||||
@ -50,8 +50,12 @@ func AdminRouter(r chi.Router) {
|
|||||||
r.Get("/index.html", serveLogin)
|
r.Get("/index.html", serveLogin)
|
||||||
r.Get("/panel.html", servePanel)
|
r.Get("/panel.html", servePanel)
|
||||||
r.Post("/new_board.sh", handleNewBoard)
|
r.Post("/new_board.sh", handleNewBoard)
|
||||||
|
r.Post("/cleanup.sh", handleCleanup)
|
||||||
r.Post("/login.sh", handleLogin)
|
r.Post("/login.sh", handleLogin)
|
||||||
r.Post("/logout.sh", handleLogout)
|
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
|
// Router sets up moderation functions
|
||||||
@ -60,69 +64,6 @@ func ModRouter(r chi.Router) {
|
|||||||
r.Post("/del_reply.sh", handleDelPost)
|
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) {
|
func serveLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
dat := bytes.NewBuffer([]byte{})
|
dat := bytes.NewBuffer([]byte{})
|
||||||
err := loginTmpl.Execute(dat, middle.GetBaseCtx(r))
|
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(),
|
http.ServeContent(w, r, "panel.html", time.Now(),
|
||||||
bytes.NewReader(dat.Bytes()))
|
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) {
|
func handleNewBoard(w http.ResponseWriter, r *http.Request) {
|
||||||
sess := middle.GetSession(r)
|
sess := middle.GetSession(r)
|
||||||
if sess == nil {
|
if !middle.IsAdminSession(sess) {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
w.Write([]byte("Unauthorized"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if sess.CAttr("mode") != "admin" {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
w.Write([]byte("Unauthorized"))
|
w.Write([]byte("Unauthorized"))
|
||||||
return
|
return
|
||||||
|
@ -19,20 +19,53 @@
|
|||||||
<input type="submit" value="Logout" />
|
<input type="submit" value="Logout" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel boardmgr">
|
<br clear="left" /><hr />
|
||||||
<form method="POST" action="/admin/new_board.sh">
|
<div class="postarea">
|
||||||
<input
|
<form id="postform1" action="/admin/new_board.sh" method="POST">
|
||||||
type="hidden"
|
<table>
|
||||||
name="csrf_token"
|
<tbody>
|
||||||
value="{{ .CSRFToken }}" />
|
<tr>
|
||||||
<input type="text" placeholder="shortname" name="shortname"/>
|
<td class="postblock">
|
||||||
<input type="text" placeholder="longname" name="longname"/>
|
Action
|
||||||
<input type="submit" value="Create Board" />
|
</td>
|
||||||
<input type="reset" value="Reset" />
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<br clear="left" /><hr />
|
||||||
<div class="postarea">
|
<div class="postarea">
|
||||||
<form id="postform1" action="/admin/new_admin.sh" method="POST">
|
<form id="postform2" action="/admin/new_admin.sh" method="POST">
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@ -53,8 +86,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
minlength="8"
|
minlength="4"
|
||||||
name="id" />
|
maxlength="255"
|
||||||
|
name="adminid"
|
||||||
|
required />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -65,7 +100,83 @@
|
|||||||
<input type="password"
|
<input type="password"
|
||||||
minlength="12"
|
minlength="12"
|
||||||
maxlength="255"
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</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)
|
ctx := middle.GetBaseCtx(r)
|
||||||
err := db.View(func(tx *buntdb.Tx) error {
|
err := db.View(func(tx *buntdb.Tx) error {
|
||||||
bName := chi.URLParam(r, "board")
|
bName := chi.URLParam(r, "board")
|
||||||
|
log.Println("Getting board")
|
||||||
b, err := resources.GetBoard(tx, r.Host, bName)
|
b, err := resources.GetBoard(tx, r.Host, bName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx["Board"] = b
|
ctx["Board"] = b
|
||||||
|
|
||||||
|
log.Println("Listing Threads...")
|
||||||
threads, err := resources.ListThreads(tx, r.Host, bName)
|
threads, err := resources.ListThreads(tx, r.Host, bName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println("Number of Thread on board: ", len(threads))
|
log.Println("Number of Thread on board: ", len(threads))
|
||||||
|
|
||||||
|
log.Println("Filling threads")
|
||||||
for k := range threads {
|
for k := range threads {
|
||||||
err := resources.FillReplies(tx, r.Host, threads[k])
|
err := resources.FillReplies(tx, r.Host, threads[k])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -27,9 +27,6 @@ var box = riceConf.MustFindBox("http/board/res/")
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
tmpls = template.New("base")
|
tmpls = template.New("base")
|
||||||
//dirTmpl = template.New("board/dir")
|
|
||||||
//boardTmpl = template.New("board/board")
|
|
||||||
//threadTmpl = template.New("board/thread")
|
|
||||||
|
|
||||||
hdlFMap = template.FuncMap{
|
hdlFMap = template.FuncMap{
|
||||||
"renderText": resources.OperateReplyText,
|
"renderText": resources.OperateReplyText,
|
||||||
@ -53,6 +50,10 @@ var (
|
|||||||
"formatDate": func(date time.Time) string {
|
"formatDate": func(date time.Time) string {
|
||||||
return date.Format("02 Jan 06 15:04:05")
|
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) {
|
func serveThumb(w http.ResponseWriter, r *http.Request) {
|
||||||
dat := bytes.NewBuffer([]byte{})
|
dat := bytes.NewBuffer([]byte{})
|
||||||
|
var date time.Time
|
||||||
db := middle.GetDB(r)
|
db := middle.GetDB(r)
|
||||||
err := db.View(func(tx *buntdb.Tx) error {
|
err := db.View(func(tx *buntdb.Tx) error {
|
||||||
bName := chi.URLParam(r, "board")
|
bName := chi.URLParam(r, "board")
|
||||||
@ -113,17 +115,19 @@ func serveThumb(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
date = resources.DateFromId(reply.ID)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errw.ErrorWriter(err, w, r)
|
errw.ErrorWriter(err, w, r)
|
||||||
return
|
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) {
|
func serveFullImage(w http.ResponseWriter, r *http.Request) {
|
||||||
dat := bytes.NewBuffer([]byte{})
|
dat := bytes.NewBuffer([]byte{})
|
||||||
|
var date time.Time
|
||||||
db := middle.GetDB(r)
|
db := middle.GetDB(r)
|
||||||
err := db.View(func(tx *buntdb.Tx) error {
|
err := db.View(func(tx *buntdb.Tx) error {
|
||||||
bName := chi.URLParam(r, "board")
|
bName := chi.URLParam(r, "board")
|
||||||
@ -144,13 +148,14 @@ func serveFullImage(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
date = resources.DateFromId(reply.ID)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errw.ErrorWriter(err, w, r)
|
errw.ErrorWriter(err, w, r)
|
||||||
return
|
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) {
|
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
|
package board
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/nfnt/resize"
|
|
||||||
"github.com/pressly/chi"
|
"github.com/pressly/chi"
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
"go.rls.moe/nyx/http/errw"
|
"go.rls.moe/nyx/http/errw"
|
||||||
"go.rls.moe/nyx/http/middle"
|
"go.rls.moe/nyx/http/middle"
|
||||||
"go.rls.moe/nyx/resources"
|
"go.rls.moe/nyx/resources"
|
||||||
"image"
|
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
"image/png"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleNewReply(w http.ResponseWriter, r *http.Request) {
|
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)
|
errw.ErrorWriter(err, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = r.ParseMultipartForm(10 * 1024 * 1024)
|
err = r.ParseMultipartForm(4 * 1024 * 1024)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errw.ErrorWriter(err, w, r)
|
errw.ErrorWriter(err, w, r)
|
||||||
return
|
return
|
||||||
@ -39,66 +34,16 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var reply = &resources.Reply{}
|
var reply = &resources.Reply{}
|
||||||
|
|
||||||
reply.Board = chi.URLParam(r, "board")
|
err = parseReply(r, reply)
|
||||||
tid, err := strconv.Atoi(chi.URLParam(r, "thread"))
|
if err == trollThrottle {
|
||||||
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) {
|
|
||||||
http.Redirect(w, r,
|
http.Redirect(w, r,
|
||||||
fmt.Sprintf("/%s/%s/thread.html?err=trollthrottle",
|
fmt.Sprintf("/%s/%s/thread.html?err=trollthrottle",
|
||||||
chi.URLParam(r, "board"), chi.URLParam(r, "thread")),
|
chi.URLParam(r, "board"), chi.URLParam(r, "thread")),
|
||||||
http.StatusSeeOther)
|
http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
} else if err != nil {
|
||||||
|
errw.ErrorWriter(err, w, r)
|
||||||
{
|
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db := middle.GetDB(r)
|
db := middle.GetDB(r)
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
package board
|
package board
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/nfnt/resize"
|
|
||||||
"github.com/pressly/chi"
|
"github.com/pressly/chi"
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
"go.rls.moe/nyx/http/errw"
|
"go.rls.moe/nyx/http/errw"
|
||||||
"go.rls.moe/nyx/http/middle"
|
"go.rls.moe/nyx/http/middle"
|
||||||
"go.rls.moe/nyx/resources"
|
"go.rls.moe/nyx/resources"
|
||||||
"image"
|
|
||||||
"image/png"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -37,59 +33,18 @@ func handleNewThread(w http.ResponseWriter, r *http.Request) {
|
|||||||
var thread = &resources.Thread{}
|
var thread = &resources.Thread{}
|
||||||
var mainReply = &resources.Reply{}
|
var mainReply = &resources.Reply{}
|
||||||
|
|
||||||
mainReply.Board = chi.URLParam(r, "board")
|
|
||||||
thread.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,
|
http.Redirect(w, r,
|
||||||
fmt.Sprintf("/%s/board.html?err=trollthrottle",
|
fmt.Sprintf("/%s/board.html?err=trollthrottle",
|
||||||
chi.URLParam(r, "board")),
|
chi.URLParam(r, "board")),
|
||||||
http.StatusSeeOther)
|
http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
} else if err != nil {
|
||||||
|
errw.ErrorWriter(err, w, r)
|
||||||
{
|
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"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db := middle.GetDB(r)
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ 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>
|
<tr>
|
||||||
<td class="postblock">
|
<td class="postblock">
|
||||||
|
|
||||||
@ -100,6 +117,12 @@
|
|||||||
{{ else }}
|
{{ else }}
|
||||||
Anonymous
|
Anonymous
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .Reply.Metadata.modpost }}
|
||||||
|
(Mod)
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Reply.Metadata.adminpost }}
|
||||||
|
[Admin]
|
||||||
|
{{ end }}
|
||||||
</span></label>
|
</span></label>
|
||||||
<span class="date">{{dateFromID .Reply.ID | formatDate}}</span>
|
<span class="date">{{dateFromID .Reply.ID | formatDate}}</span>
|
||||||
{{ if .Session }}
|
{{ if .Session }}
|
||||||
@ -126,7 +149,15 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<span>
|
<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>
|
||||||
<span class="reflink">
|
<span class="reflink">
|
||||||
<a href="/{{.Boardlink}}/{{.ThreadID}}/thread.html">No.{{.Reply.ID}}</a>
|
<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 {
|
func GetSession(r *http.Request) session.Session {
|
||||||
return session.Get(r)
|
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/board"
|
||||||
"go.rls.moe/nyx/http/middle"
|
"go.rls.moe/nyx/http/middle"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var riceConf = rice.Config{
|
var riceConf = rice.Config{
|
||||||
@ -27,6 +28,7 @@ func Start(config *config.Config) {
|
|||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
r.Use(middleware.CloseNotify)
|
r.Use(middleware.CloseNotify)
|
||||||
|
r.Use(middle.LimitSize(config))
|
||||||
r.Use(middleware.DefaultCompress)
|
r.Use(middleware.DefaultCompress)
|
||||||
|
|
||||||
r.Use(middle.ConfigCtx(config))
|
r.Use(middle.ConfigCtx(config))
|
||||||
@ -50,5 +52,13 @@ func Start(config *config.Config) {
|
|||||||
r.Group(board.Router)
|
r.Group(board.Router)
|
||||||
|
|
||||||
fmt.Println("Setup Complete, Starting Web Server...")
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AdminPass) VerifyLogin(pass string) error {
|
func (a *AdminPass) VerifyLogin(pass string) (err error) {
|
||||||
var 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)
|
err = passlib.VerifyNoUpgrade(pass, a.Password)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,6 +20,16 @@ const (
|
|||||||
adminPassPath = "/jack/./pass/admin/%s/admin-data"
|
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 {
|
func InitialSetup(db *buntdb.DB) error {
|
||||||
return db.Update(func(tx *buntdb.Tx) error {
|
return db.Update(func(tx *buntdb.Tx) error {
|
||||||
if _, err := tx.Get(setup); err != nil {
|
if _, err := tx.Get(setup); err != nil {
|
||||||
@ -112,3 +123,17 @@ func escapeString(in string) string {
|
|||||||
in = strings.Replace(in, "<", ".arrow-right.", -1)
|
in = strings.Replace(in, "<", ".arrow-right.", -1)
|
||||||
return in
|
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 (
|
import (
|
||||||
"compress/flate"
|
"compress/flate"
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
@ -34,6 +34,7 @@ var (
|
|||||||
"nazi",
|
"nazi",
|
||||||
"beemovie",
|
"beemovie",
|
||||||
"bee movie",
|
"bee movie",
|
||||||
|
"honey",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,7 +57,13 @@ func SpamScore(spam string) (float64, error) {
|
|||||||
blScore += float64(strings.Count(spam, v))
|
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
|
return (score * blScore) / 100, nil
|
||||||
}
|
}
|
||||||
@ -70,15 +77,19 @@ func (b *byteCounter) Write(p []byte) (n int, err error) {
|
|||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CaptchaPass(spamScore float64) bool {
|
func CaptchaProb(spamScore float64) float64 {
|
||||||
chance := math.Max(
|
return math.Max(
|
||||||
passScoreLimitMin,
|
passScoreLimitMin,
|
||||||
math.Min(
|
math.Min(
|
||||||
passScoreReactive*math.Atan(
|
passScoreReactive*math.Atan(
|
||||||
passScoreAggressive*spamScore,
|
passScoreAggressive*spamScore,
|
||||||
),
|
),
|
||||||
passScoreLimitMax))
|
passScoreLimitMax))
|
||||||
|
}
|
||||||
|
|
||||||
|
func CaptchaPass(spamScore float64) bool {
|
||||||
|
chance := CaptchaProb(spamScore)
|
||||||
take := rand.Float64()
|
take := rand.Float64()
|
||||||
fmt.Printf("Chance: %f, Take %f", chance, take)
|
log.Printf("Chance: %f, Take %f\n", chance, take)
|
||||||
return take > chance
|
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)
|
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
|
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)
|
thread.intReply, err = GetReply(tx, host, board, thread.ID, thread.StartReply)
|
||||||
if err != nil {
|
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)
|
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",
|
"revision": "9e952142169c3cd6268c6482a3a61c121536aca2",
|
||||||
"revisionTime": "2015-07-28T12:50:59Z"
|
"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=",
|
"checksumSHA1": "6defOlYtxIqheaUEG/cWWouQnIU=",
|
||||||
"path": "github.com/hlandau/passlib",
|
"path": "github.com/hlandau/passlib",
|
||||||
|
Loading…
Reference in New Issue
Block a user