diff options
-rw-r--r-- | management/editor/editor.go | 14 | ||||
-rw-r--r-- | system/admin/handlers.go | 128 | ||||
-rw-r--r-- | system/admin/upload/upload.go | 4 | ||||
-rw-r--r-- | system/api/analytics/batch.go | 47 | ||||
-rw-r--r-- | system/api/analytics/init.go | 12 | ||||
-rw-r--r-- | system/api/handlers.go | 48 | ||||
-rw-r--r-- | system/api/server.go | 12 | ||||
-rw-r--r-- | system/db/content.go | 87 |
8 files changed, 248 insertions, 104 deletions
diff --git a/management/editor/editor.go b/management/editor/editor.go index 68b787b..2cfe1ea 100644 --- a/management/editor/editor.go +++ b/management/editor/editor.go @@ -120,6 +120,7 @@ func Form(post Editable, fields ...Field) ([]byte, error) { <script> $(function() { var form = $('form'), + save = form.find('button.save-post'), del = form.find('button.delete-post'), approve = form.find('.post-controls.external'), id = form.find('input[name=id]'); @@ -131,10 +132,21 @@ func Form(post Editable, fields ...Field) ([]byte, error) { } // hide approval if not on a pending content item - if (getParam("status") !== "pending") { + if (getParam('status') !== 'pending') { approve.hide(); } + save.on('click', function(e) { + e.preventDefault(); + + if (getParam('status') === 'pending') { + var action = form.attr('action'); + form.attr('action', action + '?status=pending') + } + + form.submit(); + }); + del.on('click', function(e) { e.preventDefault(); var action = form.attr('action'); diff --git a/system/admin/handlers.go b/system/admin/handlers.go index 742b898..12750e2 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -7,6 +7,7 @@ import ( "fmt" "log" "net/http" + "strconv" "strings" "time" @@ -535,10 +536,11 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { } order := strings.ToLower(q.Get("order")) - status := q.Get("status") + if order != "asc" { + order = "desc" + } - posts := db.ContentAll(t + "_sorted") - b := &bytes.Buffer{} + status := q.Get("status") if _, ok := content.Types[t]; !ok { res.WriteHeader(http.StatusBadRequest) @@ -571,6 +573,47 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { hasExt = true } + count, err := strconv.Atoi(q.Get("count")) // int: determines number of posts to return (10 default, -1 is all) + if err != nil { + if q.Get("count") == "" { + count = 10 + } else { + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + } + + offset, err := strconv.Atoi(q.Get("offset")) // int: multiplier of count for pagination (0 default) + if err != nil { + if q.Get("offset") == "" { + offset = 0 + } else { + res.WriteHeader(http.StatusInternalServerError) + errView, err := Error500() + if err != nil { + return + } + + res.Write(errView) + return + } + } + + opts := db.QueryOptions{ + Count: count, + Offset: offset, + Order: order, + } + + posts := db.Query(t+"_sorted", opts) + b := &bytes.Buffer{} + html := `<div class="col s9 card"> <div class="card-content"> <div class="row"> @@ -634,40 +677,6 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { <a href="` + pendingURL + `">Pending</a> </div>` - case "pending": - // get _pending posts of type t from the db - posts = db.ContentAll(t + "_pending") - - html += `<div class="row externalable"> - <span class="description">Status:</span> - <a href="` + publicURL + `">Public</a> - | - <span class="active">Pending</span> - </div>` - } - - } - html += `<ul class="posts row">` - - switch order { - case "desc", "": - if hasExt { - // reverse the order of posts slice - for i := len(posts) - 1; i >= 0; i-- { - err := json.Unmarshal(posts[i], &p) - if err != nil { - log.Println("Error unmarshal json into", t, err, posts[i]) - - post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` - b.Write([]byte(post)) - continue - } - - post := adminPostListItem(p, t, status) - b.Write(post) - } - } else { - // keep natural order of posts slice, as returned from sorted bucket for i := range posts { err := json.Unmarshal(posts[i], &p) if err != nil { @@ -681,26 +690,18 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { post := adminPostListItem(p, t, status) b.Write(post) } - } - case "asc": - if hasExt { - // keep natural order of posts slice, as returned from sorted bucket - for i := range posts { - err := json.Unmarshal(posts[i], &p) - if err != nil { - log.Println("Error unmarshal json into", t, err, posts[i]) + case "pending": + // get _pending posts of type t from the db + posts = db.Query(t+"_pending", opts) - post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` - b.Write([]byte(post)) - continue - } + html += `<div class="row externalable"> + <span class="description">Status:</span> + <a href="` + publicURL + `">Public</a> + | + <span class="active">Pending</span> + </div>` - post := adminPostListItem(p, t, status) - b.Write(post) - } - } else { - // reverse the order of posts slice for i := len(posts) - 1; i >= 0; i-- { err := json.Unmarshal(posts[i], &p) if err != nil { @@ -715,7 +716,9 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { b.Write(post) } } + } + html += `<ul class="posts row">` b.Write([]byte(`</ul></div></div>`)) @@ -765,9 +768,10 @@ func adminPostListItem(p editor.Editable, t, status string) []byte { cid := fmt.Sprintf("%d", p.ContentID()) - if status == "public" { + switch status { + case "public", "": status = "" - } else { + default: status = "_" + status } @@ -1046,8 +1050,16 @@ func editHandler(res http.ResponseWriter, req *http.Request) { host := req.URL.Host path := req.URL.Path sid := fmt.Sprintf("%d", id) - desURL := scheme + host + path + "?type=" + t + "&id=" + sid - http.Redirect(res, req, desURL, http.StatusFound) + if strings.Contains(t, "_") { + t = strings.Split(t, "_")[0] + } + redir := scheme + host + path + "?type=" + t + "&id=" + sid + + if req.URL.Query().Get("status") == "pending" { + redir += "&status=pending" + } + + http.Redirect(res, req, redir, http.StatusFound) default: res.WriteHeader(http.StatusMethodNotAllowed) diff --git a/system/admin/upload/upload.go b/system/admin/upload/upload.go index 169bffe..323f371 100644 --- a/system/admin/upload/upload.go +++ b/system/admin/upload/upload.go @@ -45,7 +45,7 @@ func StoreFiles(req *http.Request) (map[string]string, error) { urlPathPrefix := "api" uploadDirName := "uploads" - uploadDir := filepath.Join(pwd, uploadDirName, fmt.Sprintf("%d", tm.Year()), fmt.Sprintf("%d", tm.Month())) + uploadDir := filepath.Join(pwd, uploadDirName, fmt.Sprintf("%d", tm.Year()), fmt.Sprintf("%02d", tm.Month())) err = os.MkdirAll(uploadDir, os.ModeDir|os.ModePerm) // loop over all files and save them to disk @@ -81,7 +81,7 @@ func StoreFiles(req *http.Request) (map[string]string, error) { } // add name:urlPath to req.PostForm to be inserted into db - urlPath := fmt.Sprintf("/%s/%s/%d/%d/%s", urlPathPrefix, uploadDirName, tm.Year(), tm.Month(), filename) + urlPath := fmt.Sprintf("/%s/%s/%d/%02d/%s", urlPathPrefix, uploadDirName, tm.Year(), tm.Month(), filename) urlPaths[name] = urlPath } diff --git a/system/api/analytics/batch.go b/system/api/analytics/batch.go new file mode 100644 index 0000000..1fee247 --- /dev/null +++ b/system/api/analytics/batch.go @@ -0,0 +1,47 @@ +package analytics + +import ( + "encoding/json" + "strconv" + + "github.com/boltdb/bolt" +) + +// batchInsert is effectively a specialized version of SetContentMulti from the +// db package, iterating over a []apiRequest instead of []url.Values +func batchInsert(batch []apiRequest) error { + err := store.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte("requests")) + if err != nil { + return err + } + + for _, apiReq := range batch { + // get the next available ID and convert to string + // also set effectedID to int of ID + id, err := b.NextSequence() + if err != nil { + return err + } + cid := strconv.FormatUint(id, 10) + + j, err := json.Marshal(apiReq) + if err != nil { + return err + } + + err = b.Put([]byte(cid), j) + if err != nil { + return err + } + } + + return nil + + }) + if err != nil { + return err + } + + return nil +} diff --git a/system/api/analytics/init.go b/system/api/analytics/init.go index c351bed..3af1407 100644 --- a/system/api/analytics/init.go +++ b/system/api/analytics/init.go @@ -15,6 +15,7 @@ import ( type apiRequest struct { URL string `json:"url"` Method string `json:"http_method"` + Origin string `json:"origin"` RemoteAddr string `json:"ip_address"` Timestamp int64 `json:"timestamp"` External bool `json:"external"` @@ -32,6 +33,7 @@ func Record(req *http.Request) { r := apiRequest{ URL: req.URL.String(), Method: req.Method, + Origin: req.Header.Get("Origin"), RemoteAddr: req.RemoteAddr, Timestamp: time.Now().Unix() * 1000, External: external, @@ -39,7 +41,6 @@ func Record(req *http.Request) { // put r on buffered recordChan to take advantage of batch insertion in DB recordChan <- r - } // Close exports the abillity to close our db file. Should be called with defer @@ -64,10 +65,6 @@ func Init() { go serve() - err = store.Update(func(tx *bolt.Tx) error { - - return nil - }) if err != nil { log.Fatalln(err) } @@ -93,6 +90,11 @@ func serve() { reqs = append(reqs, <-recordChan) } + err := batchInsert(reqs) + if err != nil { + log.Println(err) + } + case <-pruneDBTimer.C: default: diff --git a/system/api/handlers.go b/system/api/handlers.go index 8356683..d1758f0 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -36,6 +36,11 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { return } + if _, ok := content.Types[t]; !ok { + res.WriteHeader(http.StatusNotFound) + return + } + count, err := strconv.Atoi(q.Get("count")) // int: determines number of posts to return (10 default, -1 is all) if err != nil { if q.Get("count") == "" { @@ -57,46 +62,23 @@ func postsHandler(res http.ResponseWriter, req *http.Request) { } order := strings.ToLower(q.Get("order")) // string: sort order of posts by timestamp ASC / DESC (DESC default) - if order != "asc" || order == "" { + if order != "asc" { order = "desc" } - // TODO: time-based ?after=time.Time, ?before=time.Time between=time.Time|time.Time - - posts := db.ContentAll(t + "_sorted") - var all = []json.RawMessage{} - for _, post := range posts { - all = append(all, post) + opts := db.QueryOptions{ + Count: count, + Offset: offset, + Order: order, } - var start, end int - switch count { - case -1: - start = 0 - end = len(posts) - - default: - start = count * offset - end = start + count - } - - // bounds check on posts given the start & end count - if start > len(posts) { - start = len(posts) - count - } - if end > len(posts) { - end = len(posts) - } - - // reverse the sorted order if ASC - if order == "asc" { - all = []json.RawMessage{} - for i := len(posts) - 1; i >= 0; i-- { - all = append(all, posts[i]) - } + bb := db.Query(t+"_sorted", opts) + var result = []json.RawMessage{} + for i := range bb { + result = append(result, bb[i]) } - j, err := fmtJSON(all[start:end]...) + j, err := fmtJSON(result...) if err != nil { res.WriteHeader(http.StatusInternalServerError) return diff --git a/system/api/server.go b/system/api/server.go index 816bc21..4468394 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -1,14 +1,16 @@ package api -import "net/http" +import ( + "net/http" +) // Run adds Handlers to default http listener for API func Run() { - http.HandleFunc("/api/types", CORS(typesHandler)) + http.HandleFunc("/api/types", CORS(Record(typesHandler))) - http.HandleFunc("/api/posts", CORS(postsHandler)) + http.HandleFunc("/api/posts", CORS(Record(postsHandler))) - http.HandleFunc("/api/post", CORS(postHandler)) + http.HandleFunc("/api/post", CORS(Record(postHandler))) - http.HandleFunc("/api/external/posts", CORS(externalPostsHandler)) + http.HandleFunc("/api/external/posts", CORS(Record(externalPostsHandler))) } diff --git a/system/db/content.go b/system/db/content.go index bd1ee4b..714e0c6 100644 --- a/system/db/content.go +++ b/system/db/content.go @@ -202,6 +202,93 @@ func ContentAll(namespace string) [][]byte { return posts } +// QueryOptions holds options for a query +type QueryOptions struct { + Count int + Offset int + Order string +} + +// Query retrieves a set of content from the db based on options +func Query(namespace string, opts QueryOptions) [][]byte { + var posts [][]byte + store.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(namespace)) + if b == nil { + return nil + } + + c := b.Cursor() + n := b.Stats().KeyN + + var start, end int + switch opts.Count { + case -1: + start = 0 + end = n + + default: + start = opts.Count * opts.Offset + end = start + opts.Count + } + + // bounds check on posts given the start & end count + if start > n { + start = n - opts.Count + } + if end > n { + end = n + } + + i := 0 // count of num posts added + cur := 0 // count of num cursor moves + switch opts.Order { + case "asc": + for k, v := c.Last(); k != nil; k, v = c.Prev() { + if cur < start { + cur++ + continue + } + + if cur >= end { + break + } + + posts = append(posts, v) + i++ + cur++ + } + + case "desc": + for k, v := c.First(); k != nil; k, v = c.Next() { + if cur < start { + cur++ + continue + } + + if cur >= end { + break + } + + posts = append(posts, v) + i++ + cur++ + } + } + + return nil + }) + + // if opts.order == "asc" { + // posts = []json.RawMessage{} + // for i := len(posts) - 1; i >= 0; i-- { + // posts = append(all, posts[i]) + // } + // } + + return posts +} + // SortContent sorts all content of the type supplied as the namespace by time, // in descending order, from most recent to least recent // Should be called from a goroutine after SetContent is successful |