summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/ponzu/generate.go4
-rw-r--r--management/editor/elements.go2
-rw-r--r--system/admin/admin.go15
-rw-r--r--system/admin/handlers.go656
-rw-r--r--system/admin/server.go4
-rw-r--r--system/admin/upload/upload.go25
-rw-r--r--system/api/handlers.go41
-rw-r--r--system/api/server.go4
-rw-r--r--system/db/init.go5
-rw-r--r--system/db/upload.go235
-rw-r--r--system/item/upload.go140
-rw-r--r--system/system.go (renamed from system/auth.go)0
12 files changed, 1116 insertions, 15 deletions
diff --git a/cmd/ponzu/generate.go b/cmd/ponzu/generate.go
index 83579df..d28f0a2 100644
--- a/cmd/ponzu/generate.go
+++ b/cmd/ponzu/generate.go
@@ -24,7 +24,7 @@ type generateField struct {
View string
}
-var reservedFieledNames = map[string]string{
+var reservedFieldNames = map[string]string{
"uuid": "UUID",
"item": "Item",
"id": "ID",
@@ -36,7 +36,7 @@ var reservedFieledNames = map[string]string{
func legalFieldNames(fields ...generateField) (bool, map[string]string) {
conflicts := make(map[string]string)
for _, field := range fields {
- for jsonName, fieldName := range reservedFieledNames {
+ for jsonName, fieldName := range reservedFieldNames {
if field.JSONName == jsonName || field.Name == fieldName {
conflicts[jsonName] = fieldName
}
diff --git a/management/editor/elements.go b/management/editor/elements.go
index b179960..2dfab40 100644
--- a/management/editor/elements.go
+++ b/management/editor/elements.go
@@ -276,7 +276,7 @@ func Richtext(fieldName string, p interface{}, attrs map[string]string) []byte {
data.append("file", files[0]);
$.ajax({
data: data,
- type: 'POST',
+ type: 'PUT',
url: '/admin/edit/upload',
cache: false,
contentType: false,
diff --git a/system/admin/admin.go b/system/admin/admin.go
index 7761e71..ab3fda6 100644
--- a/system/admin/admin.go
+++ b/system/admin/admin.go
@@ -66,6 +66,7 @@ var mainAdminHTML = `
<div class="row collection-item">
<li><a class="col s12" href="/admin/configure"><i class="tiny left material-icons">settings</i>Configuration</a></li>
<li><a class="col s12" href="/admin/configure/users"><i class="tiny left material-icons">supervisor_account</i>Admin Users</a></li>
+ <li><a class="col s12" href="/admin/uploads"><i class="tiny left material-icons">swap_vert</i>Uploads</a></li>
<li><a class="col s12" href="/admin/addons"><i class="tiny left material-icons">settings_input_svideo</i>Addons</a></li>
</div>
</ul>
@@ -368,41 +369,41 @@ func UsersList(req *http.Request) ([]byte, error) {
<div class="card user-management">
<div class="card-title">Edit your account:</div>
<form class="row" enctype="multipart/form-data" action="/admin/configure/users/edit" method="post">
- <div class="input-feild col s9">
+ <div class="col s9">
<label class="active">Email Address</label>
<input type="email" name="email" value="{{ .User.Email }}"/>
</div>
- <div class="input-feild col s9">
+ <div class="col s9">
<div>To approve changes, enter your password:</div>
<label class="active">Current Password</label>
<input type="password" name="password"/>
</div>
- <div class="input-feild col s9">
+ <div class="col s9">
<label class="active">New Password: (leave blank if no password change needed)</label>
<input name="new_password" type="password"/>
</div>
- <div class="input-feild col s9">
+ <div class="col s9">
<button class="btn waves-effect waves-light green right" type="submit">Save</button>
</div>
</form>
<div class="card-title">Add a new user:</div>
<form class="row" enctype="multipart/form-data" action="/admin/configure/users" method="post">
- <div class="input-feild col s9">
+ <div class="col s9">
<label class="active">Email Address</label>
<input type="email" name="email" value=""/>
</div>
- <div class="input-feild col s9">
+ <div class="col s9">
<label class="active">Password</label>
<input type="password" name="password"/>
</div>
- <div class="input-feild col s9">
+ <div class="col s9">
<button class="btn waves-effect waves-light green right" type="submit">Add User</button>
</div>
</form>
diff --git a/system/admin/handlers.go b/system/admin/handlers.go
index 4734ba0..bb36e39 100644
--- a/system/admin/handlers.go
+++ b/system/admin/handlers.go
@@ -827,6 +827,266 @@ func recoveryKeyHandler(res http.ResponseWriter, req *http.Request) {
}
}
+func uploadContentsHandler(res http.ResponseWriter, req *http.Request) {
+ q := req.URL.Query()
+
+ order := strings.ToLower(q.Get("order"))
+ if order != "asc" {
+ order = "desc"
+ }
+
+ pt := interface{}(&item.FileUpload{})
+
+ p, ok := pt.(editor.Editable)
+ if !ok {
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ 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") == "" {
+ 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,
+ }
+
+ b := &bytes.Buffer{}
+ var total int
+ var posts [][]byte
+
+ html := `<div class="col s9 card">
+ <div class="card-content">
+ <div class="row">
+ <div class="col s8">
+ <div class="row">
+ <div class="card-title col s7">Uploaded Items</div>
+ <div class="col s5 input-field inline">
+ <select class="browser-default __ponzu sort-order">
+ <option value="DESC">New to Old</option>
+ <option value="ASC">Old to New</option>
+ </select>
+ <label class="active">Sort:</label>
+ </div>
+ <script>
+ $(function() {
+ var sort = $('select.__ponzu.sort-order');
+
+ sort.on('change', function() {
+ var path = window.location.pathname;
+ var s = sort.val();
+
+ window.location.replace(path + '?order=' + s);
+ });
+
+ var order = getParam('order');
+ if (order !== '') {
+ sort.val(order);
+ }
+
+ });
+ </script>
+ </div>
+ </div>
+ <form class="col s4" action="/admin/uploads/search" method="get">
+ <div class="input-field post-search inline">
+ <label class="active">Search:</label>
+ <i class="right material-icons search-icon">search</i>
+ <input class="search" name="q" type="text" placeholder="Within all Upload fields" class="search"/>
+ <input type="hidden" name="type" value="__uploads" />
+ </div>
+ </form>
+ </div>`
+
+ t := "__uploads"
+ status := ""
+ total, posts = db.Query(t, opts)
+
+ for i := range posts {
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal json into", t, err, string(posts[i]))
+
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ _, err := b.Write([]byte(post))
+ if err != nil {
+ log.Println(err)
+
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ log.Println(err)
+ }
+
+ res.Write(errView)
+ return
+ }
+ continue
+ }
+
+ post := adminPostListItem(p, t, status)
+ _, err = b.Write(post)
+ if err != nil {
+ log.Println(err)
+
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ log.Println(err)
+ }
+
+ res.Write(errView)
+ return
+ }
+ }
+
+ html += `<ul class="posts row">`
+
+ _, err = b.Write([]byte(`</ul>`))
+ if err != nil {
+ log.Println(err)
+
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ log.Println(err)
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ statusDisabled := "disabled"
+ prevStatus := ""
+ nextStatus := ""
+ // total may be less than 10 (default count), so reset count to match total
+ if total < count {
+ count = total
+ }
+ // nothing previous to current list
+ if offset == 0 {
+ prevStatus = statusDisabled
+ }
+ // nothing after current list
+ if (offset+1)*count >= total {
+ nextStatus = statusDisabled
+ }
+
+ // set up pagination values
+ urlFmt := req.URL.Path + "?count=%d&offset=%d&&order=%s"
+ prevURL := fmt.Sprintf(urlFmt, count, offset-1, order)
+ nextURL := fmt.Sprintf(urlFmt, count, offset+1, order)
+ start := 1 + count*offset
+ end := start + count - 1
+
+ if total < end {
+ end = total
+ }
+
+ pagination := fmt.Sprintf(`
+ <ul class="pagination row">
+ <li class="col s2 waves-effect %s"><a href="%s"><i class="material-icons">chevron_left</i></a></li>
+ <li class="col s8">%d to %d of %d</li>
+ <li class="col s2 waves-effect %s"><a href="%s"><i class="material-icons">chevron_right</i></a></li>
+ </ul>
+ `, prevStatus, prevURL, start, end, total, nextStatus, nextURL)
+
+ // show indicator that a collection of items will be listed implicitly, but
+ // that none are created yet
+ if total < 1 {
+ pagination = `
+ <ul class="pagination row">
+ <li class="col s2 waves-effect disabled"><a href="#"><i class="material-icons">chevron_left</i></a></li>
+ <li class="col s8">0 to 0 of 0</li>
+ <li class="col s2 waves-effect disabled"><a href="#"><i class="material-icons">chevron_right</i></a></li>
+ </ul>
+ `
+ }
+
+ _, err = b.Write([]byte(pagination + `</div></div>`))
+ if err != nil {
+ log.Println(err)
+
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ log.Println(err)
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ script := `
+ <script>
+ $(function() {
+ var del = $('.quick-delete-post.__ponzu span');
+ del.on('click', function(e) {
+ if (confirm("[Ponzu] Please confirm:\n\nAre you sure you want to delete this post?\nThis cannot be undone.")) {
+ $(e.target).parent().submit();
+ }
+ });
+ });
+
+ // disable link from being clicked if parent is 'disabled'
+ $(function() {
+ $('ul.pagination li.disabled a').on('click', function(e) {
+ e.preventDefault();
+ });
+ });
+ </script>
+ `
+
+ btn := `<div class="col s3"><a href="/admin/edit/upload" class="btn new-post waves-effect waves-light">New Upload</a></div></div>`
+ html = html + b.String() + script + btn
+
+ adminView, err := Admin([]byte(html))
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "text/html")
+ res.Write(adminView)
+}
+
func contentsHandler(res http.ResponseWriter, req *http.Request) {
q := req.URL.Query()
t := q.Get("type")
@@ -1279,9 +1539,14 @@ func adminPostListItem(e editor.Editable, typeName, status string) []byte {
status = "__" + status
}
+ link := `<a href="/admin/edit?type=` + typeName + `&status=` + strings.TrimPrefix(status, "__") + `&id=` + cid + `">` + i.String() + `</a>`
+ if strings.HasPrefix(typeName, "__") {
+ link = `<a href="/admin/edit/upload?id=` + cid + `">` + i.String() + `</a>`
+ }
+
post := `
<li class="col s12">
- <a href="/admin/edit?type=` + typeName + `&status=` + strings.TrimPrefix(status, "__") + `&id=` + cid + `">` + i.String() + `</a>
+ ` + link + `
<span class="post-detail">Updated: ` + updatedTime + `</span>
<span class="publish-date right">` + publishTime + `</span>
@@ -1796,6 +2061,291 @@ func deleteHandler(res http.ResponseWriter, req *http.Request) {
http.Redirect(res, req, redir, http.StatusFound)
}
+func deleteUploadHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodPost {
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ id := req.FormValue("id")
+ t := "__uploads"
+
+ if id == "" || t == "" {
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ post := interface{}(&item.FileUpload{})
+ hook, ok := post.(item.Hookable)
+ if !ok {
+ log.Println("Type", t, "does not implement item.Hookable or embed item.Item.")
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error400()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ err = hook.BeforeDelete(res, req)
+ if err != nil {
+ log.Println("Error running BeforeDelete method in deleteHandler for:", t, err)
+ return
+ }
+
+ err = db.DeleteUpload(t + ":" + id)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ err = hook.AfterDelete(res, req)
+ if err != nil {
+ log.Println("Error running AfterDelete method in deleteHandler for:", t, err)
+ return
+ }
+
+ redir := "/admin/uploads"
+ http.Redirect(res, req, redir, http.StatusFound)
+}
+
+func editUploadHandler(res http.ResponseWriter, req *http.Request) {
+ switch req.Method {
+ case http.MethodGet:
+ q := req.URL.Query()
+ i := q.Get("id")
+ t := "__uploads"
+
+ post := &item.FileUpload{}
+
+ if i != "" {
+ data, err := db.Upload(t + ":" + i)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ if len(data) < 1 || data == nil {
+ res.WriteHeader(http.StatusNotFound)
+ errView, err := Error404()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ err = json.Unmarshal(data, post)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+ } else {
+ it, ok := interface{}(post).(item.Identifiable)
+ if !ok {
+ log.Println("Content type", t, "doesn't implement item.Identifiable")
+ return
+ }
+
+ it.SetItemID(-1)
+ }
+
+ m, err := manager.Manage(interface{}(post).(editor.Editable), t)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ adminView, err := Admin(m)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "text/html")
+ res.Write(adminView)
+
+ case http.MethodPost:
+ err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ t := req.FormValue("type")
+ pt := "__uploads"
+ ts := req.FormValue("timestamp")
+ up := req.FormValue("updated")
+
+ // create a timestamp if one was not set
+ if ts == "" {
+ ts = fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UTC().UnixNano()/int64(time.Millisecond))
+ req.PostForm.Set("timestamp", ts)
+ }
+
+ if up == "" {
+ req.PostForm.Set("updated", ts)
+ }
+
+ post := interface{}(&item.FileUpload{})
+ hook, ok := post.(item.Hookable)
+ if !ok {
+ log.Println("Type", pt, "does not implement item.Hookable or embed item.Item.")
+ res.WriteHeader(http.StatusBadRequest)
+ errView, err := Error400()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ err = hook.BeforeSave(res, req)
+ if err != nil {
+ log.Println("Error running BeforeSave method in editHandler for:", t, err)
+ return
+ }
+
+ // StoreFiles has the SetUpload call (which is equivalent of SetContent in other handlers)
+ urlPaths, err := upload.StoreFiles(req)
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ return
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ for name, urlPath := range urlPaths {
+ req.PostForm.Set(name, urlPath)
+ }
+
+ // check for any multi-value fields (ex. checkbox fields)
+ // and correctly format for db storage. Essentially, we need
+ // fieldX.0: value1, fieldX.1: value2 => fieldX: []string{value1, value2}
+ fieldOrderValue := make(map[string]map[string][]string)
+ ordVal := make(map[string][]string)
+ for k, v := range req.PostForm {
+ if strings.Contains(k, ".") {
+ fo := strings.Split(k, ".")
+
+ // put the order and the field value into map
+ field := string(fo[0])
+ order := string(fo[1])
+ fieldOrderValue[field] = ordVal
+
+ // orderValue is 0:[?type=Thing&id=1]
+ orderValue := fieldOrderValue[field]
+ orderValue[order] = v
+ fieldOrderValue[field] = orderValue
+
+ // discard the post form value with name.N
+ req.PostForm.Del(k)
+ }
+
+ }
+
+ // add/set the key & value to the post form in order
+ for f, ov := range fieldOrderValue {
+ for i := 0; i < len(ov); i++ {
+ position := fmt.Sprintf("%d", i)
+ fieldValue := ov[position]
+
+ if req.PostForm.Get(f) == "" {
+ for i, fv := range fieldValue {
+ if i == 0 {
+ req.PostForm.Set(f, fv)
+ } else {
+ req.PostForm.Add(f, fv)
+ }
+ }
+ } else {
+ for _, fv := range fieldValue {
+ req.PostForm.Add(f, fv)
+ }
+ }
+ }
+ }
+
+ err = hook.AfterSave(res, req)
+ if err != nil {
+ log.Println("Error running AfterSave method in editHandler for:", t, err)
+ return
+ }
+
+ scheme := req.URL.Scheme
+ host := req.URL.Host
+ redir := scheme + host + "/admin/uploads"
+ http.Redirect(res, req, redir, http.StatusFound)
+
+ case http.MethodPut:
+ urlPaths, err := upload.StoreFiles(req)
+ if err != nil {
+ log.Println("Couldn't store file uploads.", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "application/json")
+ res.Write([]byte(`{"data": [{"url": "` + urlPaths["file"] + `"}]}`))
+ default:
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+}
+
+/*
func editUploadHandler(res http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
res.WriteHeader(http.StatusMethodNotAllowed)
@@ -1812,6 +2362,7 @@ func editUploadHandler(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "application/json")
res.Write([]byte(`{"data": [{"url": "` + urlPaths["file"] + `"}]}`))
}
+*/
func searchHandler(res http.ResponseWriter, req *http.Request) {
q := req.URL.Query()
@@ -1922,6 +2473,109 @@ func searchHandler(res http.ResponseWriter, req *http.Request) {
res.Write(adminView)
}
+func uploadSearchHandler(res http.ResponseWriter, req *http.Request) {
+ q := req.URL.Query()
+ t := "__uploads"
+ search := q.Get("q")
+ status := q.Get("status")
+
+ if t == "" || search == "" {
+ http.Redirect(res, req, req.URL.Scheme+req.URL.Host+"/admin", http.StatusFound)
+ return
+ }
+
+ posts := db.UploadAll()
+ b := &bytes.Buffer{}
+ p := interface{}(&item.FileUpload{}).(editor.Editable)
+
+ html := `<div class="col s9 card">
+ <div class="card-content">
+ <div class="row">
+ <div class="card-title col s7">Uploads Results</div>
+ <form class="col s4" action="/admin/uploads/search" method="get">
+ <div class="input-field post-search inline">
+ <label class="active">Search:</label>
+ <i class="right material-icons search-icon">search</i>
+ <input class="search" name="q" type="text" placeholder="Within all Upload fields" class="search"/>
+ <input type="hidden" name="type" value="` + t + `" />
+ </div>
+ </form>
+ </div>
+ <ul class="posts row">`
+
+ for i := range posts {
+ // skip posts that don't have any matching search criteria
+ match := strings.ToLower(search)
+ all := strings.ToLower(string(posts[i]))
+ if !strings.Contains(all, match) {
+ continue
+ }
+
+ err := json.Unmarshal(posts[i], &p)
+ if err != nil {
+ log.Println("Error unmarshal search result json into", t, err, posts[i])
+
+ post := `<li class="col s12">Error decoding data. Possible file corruption.</li>`
+ _, err = b.Write([]byte(post))
+ if err != nil {
+ log.Println(err)
+
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ log.Println(err)
+ }
+
+ res.Write(errView)
+ return
+ }
+ continue
+ }
+
+ post := adminPostListItem(p, t, status)
+ _, err = b.Write([]byte(post))
+ if err != nil {
+ log.Println(err)
+
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ log.Println(err)
+ }
+
+ res.Write(errView)
+ return
+ }
+ }
+
+ _, err := b.WriteString(`</ul></div></div>`)
+ if err != nil {
+ log.Println(err)
+
+ res.WriteHeader(http.StatusInternalServerError)
+ errView, err := Error500()
+ if err != nil {
+ log.Println(err)
+ }
+
+ res.Write(errView)
+ return
+ }
+
+ btn := `<div class="col s3"><a href="/admin/edit/upload" class="btn new-post waves-effect waves-light">New Upload</a></div></div>`
+ html = html + b.String() + btn
+
+ adminView, err := Admin([]byte(html))
+ if err != nil {
+ log.Println(err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ res.Header().Set("Content-Type", "text/html")
+ res.Write(adminView)
+}
+
func addonsHandler(res http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
diff --git a/system/admin/server.go b/system/admin/server.go
index df00c21..2180441 100644
--- a/system/admin/server.go
+++ b/system/admin/server.go
@@ -32,6 +32,9 @@ func Run() {
http.HandleFunc("/admin/configure/users/edit", user.Auth(configUsersEditHandler))
http.HandleFunc("/admin/configure/users/delete", user.Auth(configUsersDeleteHandler))
+ http.HandleFunc("/admin/uploads", user.Auth(uploadContentsHandler))
+ http.HandleFunc("/admin/uploads/search", user.Auth(uploadSearchHandler))
+
http.HandleFunc("/admin/contents", user.Auth(contentsHandler))
http.HandleFunc("/admin/contents/search", user.Auth(searchHandler))
@@ -39,6 +42,7 @@ func Run() {
http.HandleFunc("/admin/edit/delete", user.Auth(deleteHandler))
http.HandleFunc("/admin/edit/approve", user.Auth(approveContentHandler))
http.HandleFunc("/admin/edit/upload", user.Auth(editUploadHandler))
+ http.HandleFunc("/admin/edit/upload/delete", user.Auth(deleteUploadHandler))
pwd, err := os.Getwd()
if err != nil {
diff --git a/system/admin/upload/upload.go b/system/admin/upload/upload.go
index dbcdc17..a0568e4 100644
--- a/system/admin/upload/upload.go
+++ b/system/admin/upload/upload.go
@@ -5,12 +5,16 @@ package upload
import (
"fmt"
"io"
+ "log"
+ "mime/multipart"
"net/http"
+ "net/url"
"os"
"path/filepath"
"strconv"
"time"
+ "github.com/ponzu-cms/ponzu/system/db"
"github.com/ponzu-cms/ponzu/system/item"
)
@@ -86,16 +90,33 @@ func StoreFiles(req *http.Request) (map[string]string, error) {
}
// copy file from src to dst on disk
- if _, err = io.Copy(dst, src); err != nil {
+ var size int64
+ if size, err = io.Copy(dst, src); err != nil {
err := fmt.Errorf("Failed to copy uploaded file to destination: %s", err)
return nil, err
}
// add name:urlPath to req.PostForm to be inserted into db
urlPath := fmt.Sprintf("/%s/%s/%d/%02d/%s", urlPathPrefix, uploadDirName, tm.Year(), tm.Month(), filename)
-
urlPaths[name] = urlPath
+
+ // add upload information to db
+ go storeFileInfo(size, filename, urlPath, fds)
}
return urlPaths, nil
}
+
+func storeFileInfo(size int64, filename, urlPath string, fds []*multipart.FileHeader) {
+ data := url.Values{
+ "name": []string{filename},
+ "path": []string{urlPath},
+ "content_type": []string{fds[0].Header.Get("Content-Type")},
+ "content_length": []string{fmt.Sprintf("%d", size)},
+ }
+
+ _, err := db.SetUpload("__uploads:-1", data)
+ if err != nil {
+ log.Println("Error saving file upload record to database:", err)
+ }
+}
diff --git a/system/api/handlers.go b/system/api/handlers.go
index 83bbe43..0a9c177 100644
--- a/system/api/handlers.go
+++ b/system/api/handlers.go
@@ -194,3 +194,44 @@ func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) {
sendData(res, req, j)
}
+
+func uploadsHandler(res http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodGet {
+ res.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ slug := req.URL.Query().Get("slug")
+ if slug == "" {
+ res.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ upload, err := db.UploadBySlug(slug)
+ if err != nil {
+ log.Println("Error finding upload by slug:", slug, err)
+ res.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ it := func() interface{} {
+ return new(item.FileUpload)
+ }
+
+ push(res, req, it, upload)
+
+ j, err := fmtJSON(json.RawMessage(upload))
+ if err != nil {
+ log.Println("Error fmtJSON on upload:", err)
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ j, err = omit(it(), j)
+ if err != nil {
+ res.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ sendData(res, req, j)
+}
diff --git a/system/api/server.go b/system/api/server.go
index a7bd056..b51936c 100644
--- a/system/api/server.go
+++ b/system/api/server.go
@@ -17,5 +17,7 @@ func Run() {
http.HandleFunc("/api/content/delete", Record(CORS(deleteContentHandler)))
- http.HandleFunc("/api/search", Record(CORS(searchContentHandler)))
+ http.HandleFunc("/api/search", Record(CORS(Gzip(searchContentHandler))))
+
+ http.HandleFunc("/api/uploads", Record(CORS(Gzip(uploadsHandler))))
}
diff --git a/system/db/init.go b/system/db/init.go
index eb5f7ee..a401a2a 100644
--- a/system/db/init.go
+++ b/system/db/init.go
@@ -51,7 +51,10 @@ func Init() {
}
// init db with other buckets as needed
- buckets := []string{"__config", "__users", "__contentIndex", "__addons"}
+ buckets := []string{
+ "__config", "__users", "__contentIndex",
+ "__addons", "__uploads",
+ }
for _, name := range buckets {
_, err := tx.CreateBucketIfNotExists([]byte(name))
if err != nil {
diff --git a/system/db/upload.go b/system/db/upload.go
new file mode 100644
index 0000000..beeee2d
--- /dev/null
+++ b/system/db/upload.go
@@ -0,0 +1,235 @@
+package db
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/ponzu-cms/ponzu/system/item"
+
+ "github.com/boltdb/bolt"
+ "github.com/gorilla/schema"
+ uuid "github.com/satori/go.uuid"
+)
+
+// SetUpload stores information about files uploaded to the system
+func SetUpload(target string, data url.Values) (int, error) {
+ parts := strings.Split(target, ":")
+ if parts[0] != "__uploads" {
+ return 0, fmt.Errorf("cannot call SetUpload with target type: %s", parts[0])
+ }
+ pid := parts[1]
+
+ if data.Get("uuid") == "" ||
+ data.Get("uuid") == (uuid.UUID{}).String() {
+ // set new UUID for upload
+ data.Set("uuid", uuid.NewV4().String())
+ }
+
+ if data.Get("slug") == "" {
+ // create slug based on filename and timestamp/updated fields
+ slug := data.Get("name")
+ slug, err := checkSlugForDuplicate(slug)
+ if err != nil {
+ return 0, err
+ }
+ data.Set("slug", slug)
+ }
+
+ ts := fmt.Sprintf("%d", time.Now().Unix()*1000)
+ if data.Get("timestamp") == "" {
+ data.Set("timestamp", ts)
+ }
+
+ data.Set("updated", ts)
+
+ // store in database
+ var id uint64
+ var err error
+ err = store.Update(func(tx *bolt.Tx) error {
+ b, err := tx.CreateBucketIfNotExists([]byte("__uploads"))
+ if err != nil {
+ return err
+ }
+
+ if pid == "-1" {
+ // get sequential ID for item
+ id, err = b.NextSequence()
+ if err != nil {
+ return err
+ }
+ data.Set("id", fmt.Sprintf("%d", id))
+ } else {
+ uid, err := strconv.ParseInt(pid, 10, 64)
+ if err != nil {
+ return err
+ }
+ id = uint64(uid)
+ data.Set("id", fmt.Sprintf("%d", id))
+ }
+
+ file := &item.FileUpload{}
+ dec := schema.NewDecoder()
+ dec.SetAliasTag("json") // allows simpler struct tagging when creating a content type
+ dec.IgnoreUnknownKeys(true) // will skip over form values submitted, but not in struct
+ err = dec.Decode(file, data)
+ if err != nil {
+ return err
+ }
+
+ // marshal data to json for storage
+ j, err := json.Marshal(file)
+ if err != nil {
+ return err
+ }
+
+ uploadKey, err := key(data.Get("id"))
+ if err != nil {
+ return err
+ }
+ err = b.Put(uploadKey, j)
+ if err != nil {
+ return err
+ }
+
+ // add slug to __contentIndex for lookup
+ b, err = tx.CreateBucketIfNotExists([]byte("__contentIndex"))
+ if err != nil {
+ return err
+ }
+
+ k := []byte(data.Get("slug"))
+ v := []byte(fmt.Sprintf("__uploads:%d", id))
+
+ err = b.Put(k, v)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+ if err != nil {
+ return 0, err
+ }
+
+ return int(id), nil
+}
+
+// Upload returns the value for an upload by its target (__uploads:{id})
+func Upload(target string) ([]byte, error) {
+ val := &bytes.Buffer{}
+ parts := strings.Split(target, ":")
+ if len(parts) < 2 {
+ return nil, fmt.Errorf("invalid target for upload: %s", target)
+ }
+
+ id, err := key(parts[1])
+ if err != nil {
+ return nil, err
+ }
+
+ err = store.View(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte("__uploads"))
+ if b == nil {
+ return bolt.ErrBucketNotFound
+ }
+
+ j := b.Get(id)
+ _, err := val.Write(j)
+ return err
+ })
+
+ return val.Bytes(), err
+}
+
+// UploadBySlug returns the value for an upload by its slug
+func UploadBySlug(slug string) ([]byte, error) {
+ val := &bytes.Buffer{}
+ // get target from __contentIndex or return nil if not exists
+ err := store.View(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte("__contentIndex"))
+ if b == nil {
+ return bolt.ErrBucketNotFound
+ }
+
+ v := b.Get([]byte(slug))
+ if v == nil {
+ return fmt.Errorf("no value for key '%s' in __contentIndex", slug)
+ }
+
+ j, err := Upload(string(v))
+ if err != nil {
+ return err
+ }
+
+ _, err = val.Write(j)
+
+ return err
+ })
+
+ return val.Bytes(), err
+}
+
+// UploadAll returns a [][]byte containing all upload data from the system
+func UploadAll() [][]byte {
+ var uploads [][]byte
+ err := store.View(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte("__uploads"))
+ if b == nil {
+ return bolt.ErrBucketNotFound
+ }
+
+ numKeys := b.Stats().KeyN
+ uploads = make([][]byte, 0, numKeys)
+
+ return b.ForEach(func(k, v []byte) error {
+ uploads = append(uploads, v)
+ return nil
+ })
+ })
+ if err != nil {
+ log.Println("Error in UploadAll:", err)
+ return nil
+ }
+
+ return uploads
+}
+
+// DeleteUpload removes the value for an upload at its key id, based on the
+// target provided i.e. __uploads:{id}
+func DeleteUpload(target string) error {
+ parts := strings.Split(target, ":")
+ if len(parts) < 2 {
+ return fmt.Errorf("Error deleting upload, invalid target %s", target)
+ }
+ id, err := key(parts[1])
+ if err != nil {
+ return err
+ }
+
+ return store.Update(func(tx *bolt.Tx) error {
+ b := tx.Bucket([]byte(parts[0]))
+ if b == nil {
+ return bolt.ErrBucketNotFound
+ }
+
+ return b.Delete(id)
+ })
+}
+
+func key(sid string) ([]byte, error) {
+ id, err := strconv.Atoi(sid)
+ if err != nil {
+ return nil, err
+ }
+
+ b := make([]byte, 8)
+ binary.BigEndian.PutUint64(b, uint64(id))
+ return b, err
+}
diff --git a/system/item/upload.go b/system/item/upload.go
new file mode 100644
index 0000000..800f663
--- /dev/null
+++ b/system/item/upload.go
@@ -0,0 +1,140 @@
+package item
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/ponzu-cms/ponzu/management/editor"
+)
+
+// FileUpload represents the file uploaded to the system
+type FileUpload struct {
+ Item
+
+ Name string `json:"name"`
+ Path string `json:"path"`
+ ContentLength int64 `json:"content_length"`
+ ContentType string `json:"content_type"`
+}
+
+// String partially implements item.Identifiable and overrides Item's String()
+func (f *FileUpload) String() string { return f.Name }
+
+// MarshalEditor writes a buffer of html to edit a Post and partially implements editor.Editable
+func (f *FileUpload) MarshalEditor() ([]byte, error) {
+ view, err := editor.Form(f,
+ editor.Field{
+ View: func() []byte {
+ if f.Path == "" {
+ return nil
+ }
+
+ return []byte(`
+ <div class="input-field col s12">
+ <!-- Add your custom editor field view here. -->
+ <h5>` + f.Name + `</h5>
+ <ul>
+ <li><span class="grey-text text-lighten-1">Content-Length:</span> ` + fmt.Sprintf("%s", FmtBytes(float64(f.ContentLength))) + `</li>
+ <li><span class="grey-text text-lighten-1">Content-Type:</span> ` + f.ContentType + `</li>
+ <li><span class="grey-text text-lighten-1">Uploaded:</span> ` + FmtTime(f.Timestamp) + `</li>
+ </ul>
+ </div>
+ `)
+ }(),
+ },
+ editor.Field{
+ View: editor.File("Path", f, map[string]string{
+ "label": "File Upload",
+ "placeholder": "Upload the file here",
+ }),
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ script := []byte(`
+ <script>
+ $(function() {
+ // change form action to upload-specific endpoint
+ var form = $('form');
+ form.attr('action', '/admin/edit/upload');
+
+ // hide default fields & labels unnecessary for the config
+ var fields = $('.default-fields');
+ fields.css('position', 'relative');
+ fields.find('input:not([type=submit])').remove();
+ fields.find('label').remove();
+ fields.find('button').css({
+ position: 'absolute',
+ top: '-10px',
+ right: '0px'
+ });
+
+ var contentOnly = $('.content-only.__ponzu');
+ contentOnly.hide();
+ contentOnly.find('input, textarea, select').attr('name', '');
+
+ // adjust layout of td so save button is in same location as usual
+ fields.find('td').css('float', 'right');
+
+ // stop some fixed config settings from being modified
+ fields.find('input[name=client_secret]').attr('name', '');
+
+ // hide save, show delete
+ if ($('h5').length > 0) {
+ fields.find('.save-post').hide();
+ fields.find('.delete-post').show();
+ } else {
+ fields.find('.save-post').show();
+ fields.find('.delete-post').hide();
+ }
+ });
+ </script>
+ `)
+
+ view = append(view, script...)
+
+ return view, nil
+}
+
+func (f *FileUpload) Push() []string {
+ return []string{
+ "path",
+ }
+}
+
+// FmtBytes converts the numeric byte size value to the appropriate magnitude
+// size in KB, MB, GB, TB, PB, or EB.
+func FmtBytes(size float64) string {
+ unit := float64(1024)
+ BYTE := unit
+ KBYTE := BYTE * unit
+ MBYTE := KBYTE * unit
+ GBYTE := MBYTE * unit
+ TBYTE := GBYTE * unit
+ PBYTE := TBYTE * unit
+
+ switch {
+ case size < BYTE:
+ return fmt.Sprintf("%0.f B", size)
+ case size < KBYTE:
+ return fmt.Sprintf("%.1f KB", size/BYTE)
+ case size < MBYTE:
+ return fmt.Sprintf("%.1f MB", size/KBYTE)
+ case size < GBYTE:
+ return fmt.Sprintf("%.1f GB", size/MBYTE)
+ case size < TBYTE:
+ return fmt.Sprintf("%.1f TB", size/GBYTE)
+ case size < PBYTE:
+ return fmt.Sprintf("%.1f PB", size/TBYTE)
+ default:
+ return fmt.Sprintf("%0.f B", size)
+ }
+
+}
+
+// FmtTime shows a human readable time based on the timestamp
+func FmtTime(t int64) string {
+ return time.Unix(t/1000, 0).Format("03:04 PM Jan 2, 2006") + " (UTC)"
+}
diff --git a/system/auth.go b/system/system.go
index 8b12ab5..8b12ab5 100644
--- a/system/auth.go
+++ b/system/system.go