diff --git a/http/admin/cleanup.go b/http/admin/cleanup.go index a5a120a..8c1be67 100644 --- a/http/admin/cleanup.go +++ b/http/admin/cleanup.go @@ -1,21 +1,139 @@ package admin import ( + "encoding/json" + "fmt" "github.com/tidwall/buntdb" "go.rls.moe/nyx/http/errw" "go.rls.moe/nyx/http/middle" + "go.rls.moe/nyx/resources" "net/http" + "strings" + "time" ) func handleCleanup(w http.ResponseWriter, r *http.Request) { + sess := middle.GetSession(r) + if sess == nil { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized")) + return + } + if sess.CAttr("mode") != "admin" { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized")) + return + } + + fmt.Println("Beginning cleanup...") db := middle.GetDB(r) - err := db.Update(func(tx *buntdb.Tx) error { + var delKeys = []string{} + err := db.View(func(tx *buntdb.Tx) error { + var err error + tx.AscendKeys("*", func(key, value string) bool { + keyType := detectType(key) + if keyType == "thread" { + var host string + host, err = resources.GetHostnameFromKey(key) + if err != nil { + fmt.Printf("Error: %s", err) + return false + } + var thread = &resources.Thread{} + err = json.Unmarshal([]byte(value), thread) + if err != nil { + fmt.Printf("Error: %s", err) + return false + } + threadTime := resources.DateFromId(thread.ID) + dur := threadTime.Sub(time.Now()) + if dur > time.Hour*24*7 { + fmt.Printf("Sched %s for deletion: expired\n", key) + delKeys = append(delKeys, key) + return true + } + err = resources.FillReplies(tx, host, thread) + if err != nil { + fmt.Printf("Error: %s", err) + return false + } + if len(thread.GetReplies()) == 0 { + fmt.Printf("Sched %s for deletion: empty\n", key) + delKeys = append(delKeys, key) + return true + } + if _, err := resources.GetReply(tx, host, thread.Board, thread.ID, thread.StartReply); err == buntdb.ErrNotFound { + fmt.Printf("Sched %s for delection: main reply dead\n", key) + delKeys = append(delKeys, key) + return true + } + } else if keyType == "reply" { + var host string + host, err = resources.GetHostnameFromKey(key) + if err != nil { + fmt.Printf("Error: %s", err) + return false + } + var reply = &resources.Reply{} + err = json.Unmarshal([]byte(value), reply) + if err != nil { + fmt.Printf("Error: %s", err) + return false + } + replyTime := resources.DateFromId(reply.ID) + dur := replyTime.Sub(time.Now()) + if dur > time.Hour*24*7 { + fmt.Printf("Sched %s for deletion: expired\n", key) + delKeys = append(delKeys, key) + return true + } + if val, ok := reply.Metadata["deleted"]; ok && val == "yes" { + fmt.Printf("Sched %s for deletion: deleted\n", key) + delKeys = append(delKeys, key) + return true + } + if err := resources.TestThread(tx, host, reply.Board, reply.Thread); err == buntdb.ErrNotFound { + fmt.Printf("Sched %s for deletion: missing parent %d: %s\n", key, reply.Thread, err) + delKeys = append(delKeys, key) + return true + } + } + return true + }) /* Insert cleanup codes here */ + return err + }) + fmt.Println("Removing sched' entries") + db.Update(func(tx *buntdb.Tx) error { + for _, v := range delKeys { + fmt.Printf("Deleting %s\n", v) + tx.Delete(v) + } return nil }) + fmt.Println("Shrinking DB") err = db.Shrink() if err != nil { errw.ErrorWriter(err, w, r) return } + fmt.Println("Finished Cleanup") + + http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther) +} + +func detectType(key string) string { + if strings.Contains(key, "/jack/") { + if strings.HasSuffix(key, "/board-data") { + return "board" + } + if strings.HasSuffix(key, "/thread") { + return "thread" + } + if strings.HasSuffix(key, "/reply-data") { + return "reply" + } + return "system" + } + return "none" } diff --git a/http/admin/delpost.go b/http/admin/delpost.go new file mode 100644 index 0000000..2256435 --- /dev/null +++ b/http/admin/delpost.go @@ -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) +} diff --git a/http/admin/handler.go b/http/admin/handler.go index 24007ea..6555551 100644 --- a/http/admin/handler.go +++ b/http/admin/handler.go @@ -2,17 +2,12 @@ package admin import ( "bytes" - "fmt" "github.com/GeertJohan/go.rice" - "github.com/icza/session" "github.com/pressly/chi" - "github.com/tidwall/buntdb" "go.rls.moe/nyx/http/errw" "go.rls.moe/nyx/http/middle" - "go.rls.moe/nyx/resources" "html/template" "net/http" - "strconv" "time" ) @@ -27,8 +22,9 @@ var riceConf = rice.Config{ var box = riceConf.MustFindBox("http/admin/res/") var ( - panelTmpl = template.New("admin/panel") - loginTmpl = template.New("admin/login") + panelTmpl = template.New("admin/panel") + loginTmpl = template.New("admin/login") + statusTmpl = template.New("admin/status") ) func init() { @@ -41,6 +37,10 @@ func init() { if err != nil { panic(err) } + statusTmpl, err = statusTmpl.Parse(box.MustString("status.html")) + if err != nil { + panic(err) + } } // Router sets up the Administration Panel @@ -50,8 +50,12 @@ func AdminRouter(r chi.Router) { r.Get("/index.html", serveLogin) r.Get("/panel.html", servePanel) r.Post("/new_board.sh", handleNewBoard) + r.Post("/cleanup.sh", handleCleanup) r.Post("/login.sh", handleLogin) r.Post("/logout.sh", handleLogout) + r.Post("/new_admin.sh", handleNewAdmin) + r.Post("/del_admin.sh", handleDelAdmin) + r.Get("/status.sh", serveStatus) } // Router sets up moderation functions @@ -60,69 +64,6 @@ func ModRouter(r chi.Router) { r.Post("/del_reply.sh", handleDelPost) } -func handleDelPost(w http.ResponseWriter, r *http.Request) { - sess := middle.GetSession(r) - if sess == nil { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Unauthorized")) - return - } - if sess.CAttr("mode") != "admin" && sess.CAttr("mode") != "mod" { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Unauthorized")) - return - } - - err := r.ParseForm() - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - - rid, err := strconv.Atoi(r.FormValue("reply_id")) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - trid, err := strconv.Atoi(r.FormValue("thread_id")) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - board := r.FormValue("board") - - if sess.CAttr("mode") == "mod" && sess.CAttr("board") != board { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Not on this board")) - return - } - - db := middle.GetDB(r) - - err = db.Update(func(tx *buntdb.Tx) error { - reply, err := resources.GetReply(tx, r.Host, board, trid, rid) - if err != nil { - return err - } - reply.Text = "[deleted]" - reply.Metadata["deleted"] = "yes" - reply.Image = nil - reply.Thumbnail = nil - err = resources.UpdateReply(tx, r.Host, board, reply) - if err != nil { - return err - } - return nil - }) - - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - - http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", board, trid), http.StatusSeeOther) -} - func serveLogin(w http.ResponseWriter, r *http.Request) { dat := bytes.NewBuffer([]byte{}) err := loginTmpl.Execute(dat, middle.GetBaseCtx(r)) @@ -150,41 +91,3 @@ func servePanel(w http.ResponseWriter, r *http.Request) { http.ServeContent(w, r, "panel.html", time.Now(), bytes.NewReader(dat.Bytes())) } - -func handleLogout(w http.ResponseWriter, r *http.Request) { - sess := middle.GetSession(r) - if sess == nil { - http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther) - } - session.Remove(sess, w) - http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther) -} -func handleLogin(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - errw.ErrorWriter(err, w, r) - } - db := middle.GetDB(r) - - var admin = &resources.AdminPass{} - err = db.View(func(tx *buntdb.Tx) error { - var err error - admin, err = resources.GetAdmin(tx, r.FormValue("id")) - return err - }) - if err != nil { - err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid") - errw.ErrorWriter(err, w, r) - } - err = admin.VerifyLogin(r.FormValue("pass")) - if err != nil { - err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid") - errw.ErrorWriter(err, w, r) - } - sess := session.NewSessionOptions(&session.SessOptions{ - CAttrs: map[string]interface{}{"mode": "admin"}, - }) - session.Add(sess, w) - - http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther) -} diff --git a/http/admin/login.go b/http/admin/login.go new file mode 100644 index 0000000..12a9d28 --- /dev/null +++ b/http/admin/login.go @@ -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) +} diff --git a/http/admin/newadmin.go b/http/admin/newadmin.go new file mode 100644 index 0000000..15def3a --- /dev/null +++ b/http/admin/newadmin.go @@ -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) +} diff --git a/http/admin/newboard.go b/http/admin/newboard.go index 58e7513..ef707bd 100644 --- a/http/admin/newboard.go +++ b/http/admin/newboard.go @@ -11,12 +11,7 @@ import ( func handleNewBoard(w http.ResponseWriter, r *http.Request) { sess := middle.GetSession(r) - if sess == nil { - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Unauthorized")) - return - } - if sess.CAttr("mode") != "admin" { + if !middle.IsAdminSession(sess) { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Unauthorized")) return diff --git a/http/admin/res/panel.html b/http/admin/res/panel.html index 1bf0804..4721d13 100644 --- a/http/admin/res/panel.html +++ b/http/admin/res/panel.html @@ -19,20 +19,53 @@ -
-
- - - - - +

+
+ + + + + + + + + + + + + + + + + + + + +
+ Action + + New Board + +
+ Short Name + + +
+ Long Name + + +
+ + +
+

-
+ @@ -53,8 +86,10 @@ @@ -65,7 +100,83 @@ + name="adminpass" + required /> + + + + + + + +
+ minlength="4" + maxlength="255" + name="adminid" + required />
+ + +
+
+
+

+
+
+ + + + + + + + + + + + + + + +
+ Action + + Delete Administrator + +
+ ID + + +
+ + +
+
+
+

+
+
+ + + + + + + + + diff --git a/http/admin/res/status.html b/http/admin/res/status.html new file mode 100644 index 0000000..378015c --- /dev/null +++ b/http/admin/res/status.html @@ -0,0 +1,125 @@ + + + + + + {{.Config.Site.Title}} Status + + + + + + +
+
+
+ +
+ Action + + Cleanup Database + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ Start Time + + {{.Uptime.human}} +
+ Uptime (Precise) + + {{ .Uptime.hours | printf "%.4f"}} hours
+ {{ .Uptime.seconds | printf "%.4f"}} seconds +
+ Num. CPU + + {{.GC.numcpu}} +
+ Num. GoRoutines + + {{.GC.numgor}} +
+ Go Version
+ Arch/OS/Compiler +
+ {{.GC.version}}
+ {{.GC.arch}} / {{.GC.os}} / {{.GC.compiler}} +
+ Current Allocs + + {{.GC.memory.alloc}} +
+ Cumulative Allocs + + {{.GC.memory.calloc}} +
+ Used Sys Memory + + {{.GC.memory.sysmem}} +
+ Pointer Lookups + + {{.GC.memory.lookups}} +
+ MAllocs + + {{.GC.memory.mallocs}} +
+ MFrees + + {{.GC.memory.frees}} +
+ Live Objects + + {{.GC.memory.liveobj}} +
+ Heap Allocated + + {{.GC.memory.heapalloc}} +
+ Heap Released + + {{.GC.memory.heaprelease}} +
+ GC Metadata + + {{.GC.memory.gcmeta}} +
+ GC Pause + + {{.GC.memory.pause}} +
+ GC Invokations + + {{.GC.memory.gctimes}} +
+ GC Forced + + {{.GC.memory.fgctimes}} +
+ GC CPU Usage + + {{.GC.memory.cpufrac}} +
+
+
+

+ + \ No newline at end of file diff --git a/http/admin/status.go b/http/admin/status.go new file mode 100644 index 0000000..4bcf654 --- /dev/null +++ b/http/admin/status.go @@ -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())) +} diff --git a/http/board/board.go b/http/board/board.go index 649b772..791e579 100644 --- a/http/board/board.go +++ b/http/board/board.go @@ -18,18 +18,21 @@ func serveBoard(w http.ResponseWriter, r *http.Request) { ctx := middle.GetBaseCtx(r) err := db.View(func(tx *buntdb.Tx) error { bName := chi.URLParam(r, "board") + log.Println("Getting board") b, err := resources.GetBoard(tx, r.Host, bName) if err != nil { return err } ctx["Board"] = b + log.Println("Listing Threads...") threads, err := resources.ListThreads(tx, r.Host, bName) if err != nil { return err } log.Println("Number of Thread on board: ", len(threads)) + log.Println("Filling threads") for k := range threads { err := resources.FillReplies(tx, r.Host, threads[k]) if err != nil { diff --git a/http/board/handler.go b/http/board/handler.go index a8799ce..71b5c2c 100644 --- a/http/board/handler.go +++ b/http/board/handler.go @@ -27,9 +27,6 @@ var box = riceConf.MustFindBox("http/board/res/") var ( tmpls = template.New("base") - //dirTmpl = template.New("board/dir") - //boardTmpl = template.New("board/board") - //threadTmpl = template.New("board/thread") hdlFMap = template.FuncMap{ "renderText": resources.OperateReplyText, @@ -53,6 +50,10 @@ var ( "formatDate": func(date time.Time) string { return date.Format("02 Jan 06 15:04:05") }, + "isAdminSession": middle.IsAdminSession, + "isModSession": middle.IsModSession, + "captchaProb": resources.CaptchaProb, + "percentFloat": func(in float64) float64 { return in * 100 }, } ) @@ -93,6 +94,7 @@ func Router(r chi.Router) { func serveThumb(w http.ResponseWriter, r *http.Request) { dat := bytes.NewBuffer([]byte{}) + var date time.Time db := middle.GetDB(r) err := db.View(func(tx *buntdb.Tx) error { bName := chi.URLParam(r, "board") @@ -113,17 +115,19 @@ func serveThumb(w http.ResponseWriter, r *http.Request) { if err != nil { return err } + date = resources.DateFromId(reply.ID) return nil }) if err != nil { errw.ErrorWriter(err, w, r) return } - http.ServeContent(w, r, "thumb.png", time.Now(), bytes.NewReader(dat.Bytes())) + http.ServeContent(w, r, "thumb.png", date, bytes.NewReader(dat.Bytes())) } func serveFullImage(w http.ResponseWriter, r *http.Request) { dat := bytes.NewBuffer([]byte{}) + var date time.Time db := middle.GetDB(r) err := db.View(func(tx *buntdb.Tx) error { bName := chi.URLParam(r, "board") @@ -144,13 +148,14 @@ func serveFullImage(w http.ResponseWriter, r *http.Request) { if err != nil { return err } + date = resources.DateFromId(reply.ID) return nil }) if err != nil { errw.ErrorWriter(err, w, r) return } - http.ServeContent(w, r, "image.png", time.Now(), bytes.NewReader(dat.Bytes())) + http.ServeContent(w, r, "image.png", date, bytes.NewReader(dat.Bytes())) } func serveDir(w http.ResponseWriter, r *http.Request) { diff --git a/http/board/imageparser.go b/http/board/imageparser.go new file mode 100644 index 0000000..886a161 --- /dev/null +++ b/http/board/imageparser.go @@ -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 +} diff --git a/http/board/newreply.go b/http/board/newreply.go index 696fecd..774f963 100644 --- a/http/board/newreply.go +++ b/http/board/newreply.go @@ -1,20 +1,15 @@ package board import ( - "bytes" "fmt" - "github.com/nfnt/resize" "github.com/pressly/chi" "github.com/tidwall/buntdb" "go.rls.moe/nyx/http/errw" "go.rls.moe/nyx/http/middle" "go.rls.moe/nyx/resources" - "image" _ "image/gif" _ "image/jpeg" - "image/png" "net/http" - "strconv" ) func handleNewReply(w http.ResponseWriter, r *http.Request) { @@ -23,7 +18,7 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) { errw.ErrorWriter(err, w, r) return } - err = r.ParseMultipartForm(10 * 1024 * 1024) + err = r.ParseMultipartForm(4 * 1024 * 1024) if err != nil { errw.ErrorWriter(err, w, r) return @@ -39,66 +34,16 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) { var reply = &resources.Reply{} - reply.Board = chi.URLParam(r, "board") - tid, err := strconv.Atoi(chi.URLParam(r, "thread")) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - reply.Thread = tid - reply.Text = r.FormValue("text") - if len(reply.Text) > 10000 { - errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r) - return - } - if len(reply.Text) < 5 { - errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r) - return - } - - if score, err := resources.SpamScore(reply.Text); err != nil || !resources.CaptchaPass(score) { + err = parseReply(r, reply) + if err == trollThrottle { http.Redirect(w, r, fmt.Sprintf("/%s/%s/thread.html?err=trollthrottle", chi.URLParam(r, "board"), chi.URLParam(r, "thread")), http.StatusSeeOther) return - } - - { - file, _, err := r.FormFile("image") - if err != nil && err != http.ErrMissingFile { - errw.ErrorWriter(err, w, r) - return - } else if err != http.ErrMissingFile { - img, _, err := image.Decode(file) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - thumb := resize.Thumbnail(128, 128, img, resize.Lanczos3) - imgBuf := bytes.NewBuffer([]byte{}) - err = png.Encode(imgBuf, img) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - fmt.Println("Image has size ", len(imgBuf.Bytes())) - reply.Image = imgBuf.Bytes() - imgBuf = bytes.NewBuffer([]byte{}) - err = png.Encode(imgBuf, thumb) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - reply.Thumbnail = imgBuf.Bytes() - } - } - - reply.Metadata = map[string]string{} - if r.FormValue("tripcode") != "" { - reply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode")) - } else { - reply.Metadata["trip"] = "Anonymous" + } else if err != nil { + errw.ErrorWriter(err, w, r) + return } db := middle.GetDB(r) diff --git a/http/board/newthread.go b/http/board/newthread.go index 817292d..686b789 100644 --- a/http/board/newthread.go +++ b/http/board/newthread.go @@ -1,16 +1,12 @@ package board import ( - "bytes" "fmt" - "github.com/nfnt/resize" "github.com/pressly/chi" "github.com/tidwall/buntdb" "go.rls.moe/nyx/http/errw" "go.rls.moe/nyx/http/middle" "go.rls.moe/nyx/resources" - "image" - "image/png" "net/http" ) @@ -37,59 +33,18 @@ func handleNewThread(w http.ResponseWriter, r *http.Request) { var thread = &resources.Thread{} var mainReply = &resources.Reply{} - mainReply.Board = chi.URLParam(r, "board") thread.Board = chi.URLParam(r, "board") - mainReply.Text = r.FormValue("text") - if len(mainReply.Text) > 10000 { - errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r) - return - } - if len(mainReply.Text) < 5 { - errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r) - return - } - if score, err := resources.SpamScore(mainReply.Text); err != nil || !resources.CaptchaPass(score) { + err = parseReply(r, mainReply) + if err == trollThrottle { http.Redirect(w, r, fmt.Sprintf("/%s/board.html?err=trollthrottle", chi.URLParam(r, "board")), http.StatusSeeOther) return - } - - { - file, _, err := r.FormFile("image") - if err != nil && err != http.ErrMissingFile { - errw.ErrorWriter(err, w, r) - return - } else if err != http.ErrMissingFile { - img, _, err := image.Decode(file) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - thumb := resize.Thumbnail(128, 128, img, resize.Lanczos3) - imgBuf := bytes.NewBuffer([]byte{}) - err = png.Encode(imgBuf, img) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - fmt.Println("Image has size ", len(imgBuf.Bytes())) - mainReply.Image = imgBuf.Bytes() - imgBuf = bytes.NewBuffer([]byte{}) - err = png.Encode(imgBuf, thumb) - if err != nil { - errw.ErrorWriter(err, w, r) - return - } - mainReply.Thumbnail = imgBuf.Bytes() - } - } - - mainReply.Metadata = map[string]string{} - if r.FormValue("tripcode") != "" { - mainReply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode")) + } else if err != nil { + errw.ErrorWriter(err, w, r) + return } db := middle.GetDB(r) diff --git a/http/board/replyparser.go b/http/board/replyparser.go new file mode 100644 index 0000000..f440063 --- /dev/null +++ b/http/board/replyparser.go @@ -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 +} diff --git a/http/board/res/thread.tmpl.html b/http/board/res/thread.tmpl.html index 00176bb..0599cf3 100644 --- a/http/board/res/thread.tmpl.html +++ b/http/board/res/thread.tmpl.html @@ -78,6 +78,23 @@ {{ end }} + {{ if (isModSession .Session) }} + + + Mod Post + + + + {{ if (isAdminSession .Session) }} + + {{ end }} + + + {{ end }} @@ -100,6 +117,12 @@ {{ else }} Anonymous {{ end }} + {{ if .Reply.Metadata.modpost }} + (Mod) + {{ end }} + {{ if .Reply.Metadata.adminpost }} + [Admin] + {{ end }} {{dateFromID .Reply.ID | formatDate}} {{ if .Session }} @@ -126,7 +149,15 @@ {{ end }} {{ end }} - {{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 }} No.{{.Reply.ID}} diff --git a/http/middle/limitsize.go b/http/middle/limitsize.go new file mode 100644 index 0000000..ef59d37 --- /dev/null +++ b/http/middle/limitsize.go @@ -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) + }) + } +} diff --git a/http/middle/session.go b/http/middle/session.go index b62384c..580d778 100644 --- a/http/middle/session.go +++ b/http/middle/session.go @@ -13,3 +13,26 @@ func init() { func GetSession(r *http.Request) session.Session { return session.Get(r) } + +func IsAdminSession(sess session.Session) bool { + if sess == nil { + return false + } + if sess.CAttr("mode") == "admin" { + return true + } + return false +} + +func IsModSession(sess session.Session) bool { + if sess == nil { + return false + } + if IsAdminSession(sess) { + return true + } + if sess.CAttr("mode") == "mod" { + return true + } + return false +} diff --git a/http/server.go b/http/server.go index ca1ee37..37f1888 100644 --- a/http/server.go +++ b/http/server.go @@ -10,6 +10,7 @@ import ( "go.rls.moe/nyx/http/board" "go.rls.moe/nyx/http/middle" "net/http" + "time" ) var riceConf = rice.Config{ @@ -27,6 +28,7 @@ func Start(config *config.Config) { r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.CloseNotify) + r.Use(middle.LimitSize(config)) r.Use(middleware.DefaultCompress) r.Use(middle.ConfigCtx(config)) @@ -50,5 +52,13 @@ func Start(config *config.Config) { r.Group(board.Router) fmt.Println("Setup Complete, Starting Web Server...") - http.ListenAndServe(config.ListenOn, r) + srv := &http.Server{ + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + Handler: r, + Addr: config.ListenOn, + MaxHeaderBytes: 1 * 1024 * 1024, + } + srv.ListenAndServe() } diff --git a/resources/adminpass.go b/resources/adminpass.go index c2e9167..5cca7dd 100644 --- a/resources/adminpass.go +++ b/resources/adminpass.go @@ -19,8 +19,19 @@ func (a *AdminPass) HashLogin(pass string) error { return err } -func (a *AdminPass) VerifyLogin(pass string) error { - var err error +func (a *AdminPass) VerifyLogin(pass string) (err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("pkg: %v", r) + } + } + }() + if a == nil { + return errors.New("no login") + } err = passlib.VerifyNoUpgrade(pass, a.Password) return err } diff --git a/resources/db.go b/resources/db.go index f58a803..1471ed3 100644 --- a/resources/db.go +++ b/resources/db.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/tidwall/buntdb" + "regexp" "strings" ) @@ -19,6 +20,16 @@ const ( adminPassPath = "/jack/./pass/admin/%s/admin-data" ) +func GetHostnameFromKey(key string) (string, error) { + regex := regexp.MustCompile(`^/jack/(.+)/(board|pass)`) + res := regex.FindStringSubmatch(key) + if len(res) != 3 { + fmt.Printf("Found %d keys: %s", len(res), res) + return "", errors.New("Could not find host in key") + } + return unescapeString(res[1]), nil +} + func InitialSetup(db *buntdb.DB) error { return db.Update(func(tx *buntdb.Tx) error { if _, err := tx.Get(setup); err != nil { @@ -112,3 +123,17 @@ func escapeString(in string) string { in = strings.Replace(in, "<", ".arrow-right.", -1) return in } + +func unescapeString(in string) string { + in = strings.Replace(in, ".arrow-right.", "<", -1) + in = strings.Replace(in, ".arrow-left.", ">", -1) + in = strings.Replace(in, ".quote.", ">>", -1) + in = strings.Replace(in, ".at.", "@", -1) + in = strings.Replace(in, ".slash.", "/", -1) + in = strings.Replace(in, ".ask.", "?", -1) + in = strings.Replace(in, ".star.", "*", -1) + in = strings.Replace(in, ".backslash.", "\\", -1) + in = strings.Replace(in, ".minus.", "-", -1) + in = strings.Replace(in, ".dot.", ".", -1) + return in +} diff --git a/resources/text.go b/resources/text.go index 150b64f..b54bf7b 100644 --- a/resources/text.go +++ b/resources/text.go @@ -2,9 +2,9 @@ package resources import ( "compress/flate" - "fmt" "html/template" "io" + "log" "math" "math/rand" "strings" @@ -34,6 +34,7 @@ var ( "nazi", "beemovie", "bee movie", + "honey", } ) @@ -56,7 +57,13 @@ func SpamScore(spam string) (float64, error) { blScore += float64(strings.Count(spam, v)) } - score := float64(len(spam)) / float64(counter.p) + lines := strings.Count(spam, "\n") + + if lines == 0 { + lines = 1 + } + + score := float64(len(spam)*lines) / float64(counter.p) return (score * blScore) / 100, nil } @@ -70,15 +77,19 @@ func (b *byteCounter) Write(p []byte) (n int, err error) { return len(p), nil } -func CaptchaPass(spamScore float64) bool { - chance := math.Max( +func CaptchaProb(spamScore float64) float64 { + return math.Max( passScoreLimitMin, math.Min( passScoreReactive*math.Atan( passScoreAggressive*spamScore, ), passScoreLimitMax)) +} + +func CaptchaPass(spamScore float64) bool { + chance := CaptchaProb(spamScore) take := rand.Float64() - fmt.Printf("Chance: %f, Take %f", chance, take) + log.Printf("Chance: %f, Take %f\n", chance, take) return take > chance } diff --git a/resources/thread.go b/resources/thread.go index 5c45000..ee74eba 100644 --- a/resources/thread.go +++ b/resources/thread.go @@ -91,6 +91,21 @@ func GetThread(tx *buntdb.Tx, host, board string, id int) (*Thread, error) { } ret.intReply, err = GetReply(tx, host, board, id, ret.StartReply) + if err != nil && err == buntdb.ErrNotFound { + ret.intReply = &Reply{ + Board: ret.Board, + Thread: ret.ID, + ID: -1, + Image: nil, + Thumbnail: nil, + Metadata: map[string]string{ + "deleted": "not found", + }, + Text: "[not found]", + } + } else if err != nil { + return nil, err + } return ret, nil } @@ -132,7 +147,22 @@ func ListThreads(tx *buntdb.Tx, host, board string) ([]*Thread, error) { } thread.intReply, err = GetReply(tx, host, board, thread.ID, thread.StartReply) if err != nil { - return false + if err == buntdb.ErrNotFound { + err = nil + thread.intReply = &Reply{ + Board: thread.Board, + Thread: thread.ID, + ID: -1, + Image: nil, + Thumbnail: nil, + Metadata: map[string]string{ + "deleted": "not found", + }, + Text: "[not found]", + } + } else { + return false + } } threadList = append(threadList, thread) diff --git a/vendor/github.com/dustin/go-humanize/LICENSE b/vendor/github.com/dustin/go-humanize/LICENSE new file mode 100644 index 0000000..8d9a94a --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2005-2008 Dustin Sallings + +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. + + diff --git a/vendor/github.com/dustin/go-humanize/README.markdown b/vendor/github.com/dustin/go-humanize/README.markdown new file mode 100644 index 0000000..f69d3c8 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/README.markdown @@ -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 diff --git a/vendor/github.com/dustin/go-humanize/big.go b/vendor/github.com/dustin/go-humanize/big.go new file mode 100644 index 0000000..f49dc33 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/big.go @@ -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 +} diff --git a/vendor/github.com/dustin/go-humanize/bigbytes.go b/vendor/github.com/dustin/go-humanize/bigbytes.go new file mode 100644 index 0000000..1a2bf61 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/bigbytes.go @@ -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) +} diff --git a/vendor/github.com/dustin/go-humanize/bytes.go b/vendor/github.com/dustin/go-humanize/bytes.go new file mode 100644 index 0000000..0b498f4 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/bytes.go @@ -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) +} diff --git a/vendor/github.com/dustin/go-humanize/comma.go b/vendor/github.com/dustin/go-humanize/comma.go new file mode 100644 index 0000000..eb285cb --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/comma.go @@ -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:], ",") +} diff --git a/vendor/github.com/dustin/go-humanize/commaf.go b/vendor/github.com/dustin/go-humanize/commaf.go new file mode 100644 index 0000000..620690d --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/commaf.go @@ -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() +} diff --git a/vendor/github.com/dustin/go-humanize/ftoa.go b/vendor/github.com/dustin/go-humanize/ftoa.go new file mode 100644 index 0000000..c76190b --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/ftoa.go @@ -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)) +} diff --git a/vendor/github.com/dustin/go-humanize/humanize.go b/vendor/github.com/dustin/go-humanize/humanize.go new file mode 100644 index 0000000..a2c2da3 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/humanize.go @@ -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 diff --git a/vendor/github.com/dustin/go-humanize/number.go b/vendor/github.com/dustin/go-humanize/number.go new file mode 100644 index 0000000..dec6186 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/number.go @@ -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)) +} diff --git a/vendor/github.com/dustin/go-humanize/ordinals.go b/vendor/github.com/dustin/go-humanize/ordinals.go new file mode 100644 index 0000000..43d88a8 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/ordinals.go @@ -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 +} diff --git a/vendor/github.com/dustin/go-humanize/si.go b/vendor/github.com/dustin/go-humanize/si.go new file mode 100644 index 0000000..b24e481 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/si.go @@ -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 +} diff --git a/vendor/github.com/dustin/go-humanize/times.go b/vendor/github.com/dustin/go-humanize/times.go new file mode 100644 index 0000000..b311f11 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/times.go @@ -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...) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index c58d37f..cba20d6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -38,6 +38,12 @@ "revision": "9e952142169c3cd6268c6482a3a61c121536aca2", "revisionTime": "2015-07-28T12:50:59Z" }, + { + "checksumSHA1": "rhLUtXvcmouYuBwOq9X/nYKzvNg=", + "path": "github.com/dustin/go-humanize", + "revision": "259d2a102b871d17f30e3cd9881a642961a1e486", + "revisionTime": "2017-02-28T07:34:54Z" + }, { "checksumSHA1": "6defOlYtxIqheaUEG/cWWouQnIU=", "path": "github.com/hlandau/passlib",