diff options
author | Steve Manuel <nilslice@gmail.com> | 2017-04-25 13:23:37 -0700 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2017-04-25 13:23:37 -0700 |
commit | 099d000119447708d7d0d0482758d352438fa7e5 (patch) | |
tree | f4386ae2ff25a5b6b15c2e6442d4c56705e8271e | |
parent | 7092fb8979869f3c09b364d454d8d8081bb7c0bc (diff) |
adding support for file upload type and API handler to fetch file info
-rw-r--r-- | system/admin/upload/upload.go | 25 | ||||
-rw-r--r-- | system/api/handlers.go | 41 | ||||
-rw-r--r-- | system/api/server.go | 4 | ||||
-rw-r--r-- | system/db/init.go | 5 | ||||
-rw-r--r-- | system/db/upload.go | 142 | ||||
-rw-r--r-- | system/item/file.go | 96 | ||||
-rw-r--r-- | system/system.go (renamed from system/auth.go) | 0 |
7 files changed, 309 insertions, 4 deletions
diff --git a/system/admin/upload/upload.go b/system/admin/upload/upload.go index dbcdc17..35fb8f9 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(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..b8b90df 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.StatusInternalServerError) + 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..3157f13 --- /dev/null +++ b/system/db/upload.go @@ -0,0 +1,142 @@ +package db + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + "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(data url.Values) error { + // set new UUID for upload + data.Set("uuid", uuid.NewV4().String()) + + // create slug based on filename and timestamp/updated fields + slug := data.Get("name") + slug, err := checkSlugForDuplicate(slug) + if err != nil { + return err + } + data.Set("slug", slug) + + ts := fmt.Sprintf("%d", time.Now().Unix()*1000) + data.Set("timestamp", ts) + data.Set("updated", ts) + + // store in database + err = store.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte("__uploads")) + if err != nil { + return err + } + + // get sequential ID for item + id, err := b.NextSequence() + if err != nil { + return err + } + 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 + } + + err = b.Put([]byte(data.Get("id")), j) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + // add slug to __contentIndex for lookup + return store.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucketIfNotExists([]byte("__contentIndex")) + if err != nil { + return err + } + + k := []byte(data.Get("slug")) + v := []byte(fmt.Sprintf("%s:%s", "__uploads", data.Get("id"))) + err = b.Put(k, v) + if err != nil { + return err + } + + return 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 := []byte(parts[1]) + + 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 + } + + target := b.Get([]byte(slug)) + if target == nil { + return fmt.Errorf("no value for target in %s", "__contentIndex") + } + j, err := Upload(string(target)) + if err != nil { + return err + } + + _, err = val.Write(j) + + return err + }) + + return val.Bytes(), err +} diff --git a/system/item/file.go b/system/item/file.go new file mode 100644 index 0000000..3b33b04 --- /dev/null +++ b/system/item/file.go @@ -0,0 +1,96 @@ +package item + +import ( + "fmt" + + "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: []byte(` + <div class="input-field col s12"> + <label class="active">{{ .Name }}</label> + <!-- Add your custom editor field view here. --> + <h4>` + f.Name + `</h4> + + <img class="preview" src="` + f.Path + `"/> + <p>File information:</p> + <ul> + <li>Content-Length: ` + fmt.Sprintf("%d", f.ContentLength) + `</li> + <li>Content-Type: ` + f.ContentType + `</li> + </ul> + </div> + `), + }, + ) + if err != nil { + return nil, err + } + + open := []byte(` + <div class="card"> + <div class="card-content"> + <div class="card-title">File Uploads</div> + </div> + <form action="/admin/uploads" method="post"> + `) + close := []byte(`</form></div>`) + script := []byte(` + <script> + $(function() { + // 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 + fields.find('.save-post').hide(); + fields.find('.delete-post').show(); + }); + </script> + `) + + view = append(open, view...) + view = append(view, close...) + view = append(view, script...) + + return view, nil +} + +func (f *FileUpload) Push() []string { + return []string{ + "path", + } +} diff --git a/system/auth.go b/system/system.go index 8b12ab5..8b12ab5 100644 --- a/system/auth.go +++ b/system/system.go |