summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--management/editor/editor.go14
-rw-r--r--system/admin/handlers.go128
-rw-r--r--system/admin/upload/upload.go4
-rw-r--r--system/api/analytics/batch.go47
-rw-r--r--system/api/analytics/init.go12
-rw-r--r--system/api/handlers.go48
-rw-r--r--system/api/server.go12
-rw-r--r--system/db/content.go87
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>
- &nbsp;&vert;&nbsp;
- <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>
+ &nbsp;&vert;&nbsp;
+ <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