diff options
author | Steve Manuel <nilslice@gmail.com> | 2016-10-07 00:24:53 -0700 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2016-10-07 00:24:53 -0700 |
commit | b2e4fd9372f27d3202f07a329a5077b93a4b7390 (patch) | |
tree | 7a00e7753fdf3b4b8f8976e35fb4dd4c564c45a9 | |
parent | b5f028f0a720f1d23d2ce79d3e885fcb524bb79a (diff) |
adding cache control and etags to responses for static assets + moved handlers/helper upload func
-rw-r--r-- | system/admin/cache.go | 28 | ||||
-rw-r--r-- | system/admin/config/config.go | 31 | ||||
-rw-r--r-- | system/admin/handlers.go | 39 | ||||
-rw-r--r-- | system/admin/server.go | 141 | ||||
-rw-r--r-- | system/admin/upload.go | 113 | ||||
-rw-r--r-- | system/db/config.go | 32 | ||||
-rw-r--r-- | system/db/init.go | 10 |
7 files changed, 244 insertions, 150 deletions
diff --git a/system/admin/cache.go b/system/admin/cache.go new file mode 100644 index 0000000..9962249 --- /dev/null +++ b/system/admin/cache.go @@ -0,0 +1,28 @@ +package admin + +import ( + "fmt" + "net/http" + "strings" + + "github.com/nilslice/cms/system/db" +) + +// CacheControl sets the default cache policy on static asset responses +func CacheControl(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + etag := db.ConfigCache("etag") + policy := fmt.Sprintf("max-age=%d, public, must-revalidate, proxy-revalidate", 60*60*24*30) + res.Header().Add("Etag", etag) + res.Header().Add("Cache-Control", policy) + + if match := res.Header().Get("If-None-Match"); match != "" { + if strings.Contains(match, etag) { + res.WriteHeader(http.StatusNotModified) + return + } + } + + next.ServeHTTP(res, req) + }) +} diff --git a/system/admin/config/config.go b/system/admin/config/config.go index 791510e..cd27054 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -10,9 +10,11 @@ type Config struct { content.Item editor editor.Editor - Name string `json:"name"` - Domain string `json:"domain"` - ClientSecret string `json:"client_secret"` + Name string `json:"name"` + Domain string `json:"domain"` + ClientSecret string `json:"client_secret"` + Etag string `json:"etag"` + CacheInvalidate []string `json:"-"` } // SetContentID partially implements editor.Editable @@ -51,6 +53,29 @@ func (c *Config) MarshalEditor() ([]byte, error) { "disabled": "true", }), }, + editor.Field{ + View: editor.Input("ClientSecret", c, map[string]string{ + "type": "hidden", + }), + }, + editor.Field{ + View: editor.Input("Etag", c, map[string]string{ + "label": "Etag Header (used for static asset cache)", + "disabled": "true", + }), + }, + editor.Field{ + View: editor.Input("Etag", c, map[string]string{ + "type": "hidden", + }), + }, + editor.Field{ + View: editor.Checkbox("CacheInvalidate", c, map[string]string{ + "label": "Invalidate cache on save", + }, map[string]string{ + "cache": "invalidate", + }), + }, ) if err != nil { return nil, err diff --git a/system/admin/handlers.go b/system/admin/handlers.go index 3ef0e7d..1e8ba4f 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -5,7 +5,10 @@ import ( "encoding/base64" "encoding/json" "fmt" + "log" "net/http" + "os" + "path/filepath" "strings" "time" @@ -71,7 +74,7 @@ func initHandler(res http.ResponseWriter, req *http.Request) { return } - email := req.FormValue("email") + email := strings.ToLower(req.FormValue("email")) password := req.FormValue("password") usr := user.NewUser(email, password) @@ -216,7 +219,7 @@ func loginHandler(res http.ResponseWriter, req *http.Request) { } // check email & password - j, err := db.User(req.FormValue("email")) + j, err := db.User(strings.ToLower(req.FormValue("email"))) if err != nil { fmt.Println(err) http.Redirect(res, req, req.URL.String(), http.StatusFound) @@ -587,3 +590,35 @@ func searchHandler(res http.ResponseWriter, req *http.Request) { res.Header().Set("Content-Type", "text/html") res.Write(adminView) } + +func staticAssetHandler(res http.ResponseWriter, req *http.Request) { + path := req.URL.Path + pathParts := strings.Split(path, "/")[1:] + pwd, err := os.Getwd() + if err != nil { + log.Fatal("Coudln't get current directory to set static asset source.") + } + + filePathParts := make([]string, len(pathParts)+2, len(pathParts)+2) + filePathParts = append(filePathParts, pwd) + filePathParts = append(filePathParts, "system") + filePathParts = append(filePathParts, pathParts...) + + http.ServeFile(res, req, filepath.Join(filePathParts...)) +} + +func staticUploadHandler(res http.ResponseWriter, req *http.Request) { + path := req.URL.Path + pathParts := strings.Split(path, "/")[2:] + + pwd, err := os.Getwd() + if err != nil { + log.Fatal("Coudln't get current directory to set static asset source.") + } + + filePathParts := make([]string, len(pathParts)+1, len(pathParts)+1) + filePathParts = append(filePathParts, pwd) + filePathParts = append(filePathParts, pathParts...) + + http.ServeFile(res, req, filepath.Join(filePathParts...)) +} diff --git a/system/admin/server.go b/system/admin/server.go index 9461759..2c84479 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -1,15 +1,7 @@ package admin import ( - "fmt" - "io" - "log" "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" "github.com/nilslice/cms/system/admin/user" ) @@ -32,139 +24,10 @@ func Run() { http.HandleFunc("/admin/edit", user.Auth(editHandler)) http.HandleFunc("/admin/edit/upload", user.Auth(editUploadHandler)) - http.HandleFunc("/admin/static/", func(res http.ResponseWriter, req *http.Request) { - path := req.URL.Path - pathParts := strings.Split(path, "/")[1:] - pwd, err := os.Getwd() - if err != nil { - log.Fatal("Coudln't get current directory to set static asset source.") - } - - filePathParts := make([]string, len(pathParts)+2, len(pathParts)+2) - filePathParts = append(filePathParts, pwd) - filePathParts = append(filePathParts, "system") - filePathParts = append(filePathParts, pathParts...) - - http.ServeFile(res, req, filepath.Join(filePathParts...)) - }) + http.HandleFunc("/admin/static/", CacheControl(staticAssetHandler)) // API path needs to be registered within server package so that it is handled // even if the API server is not running. Otherwise, images/files uploaded // through the editor will not load within the admin system. - http.HandleFunc("/api/uploads/", func(res http.ResponseWriter, req *http.Request) { - path := req.URL.Path - pathParts := strings.Split(path, "/")[2:] - - pwd, err := os.Getwd() - if err != nil { - log.Fatal("Coudln't get current directory to set static asset source.") - } - - filePathParts := make([]string, len(pathParts)+1, len(pathParts)+1) - filePathParts = append(filePathParts, pwd) - filePathParts = append(filePathParts, pathParts...) - - http.ServeFile(res, req, filepath.Join(filePathParts...)) - }) -} - -func storeFileUploads(req *http.Request) (map[string]string, error) { - err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB - if err != nil { - return nil, fmt.Errorf("%s", err) - } - - ts := req.FormValue("timestamp") - - // To use for FormValue name:urlPath - urlPaths := make(map[string]string) - - // get ts values individually to use as directory names when storing - // uploaded images - date := make(map[string]int) - if ts == "" { - now := time.Now() - date["year"] = now.Year() - date["month"] = int(now.Month()) - date["day"] = now.Day() - - // create timestamp format 'yyyy-mm-dd' and set in PostForm for - // db insertion - ts = fmt.Sprintf("%d-%02d-%02d", date["year"], date["month"], date["day"]) - req.PostForm.Set("timestamp", ts) - } else { - tsParts := strings.Split(ts, "-") - year, err := strconv.Atoi(tsParts[0]) - if err != nil { - return nil, fmt.Errorf("%s", err) - } - - month, err := strconv.Atoi(tsParts[1]) - if err != nil { - return nil, fmt.Errorf("%s", err) - } - - day, err := strconv.Atoi(tsParts[2]) - if err != nil { - return nil, fmt.Errorf("%s", err) - } - - date["year"] = year - date["month"] = month - date["day"] = day - } - - // get or create upload directory to save files from request - pwd, err := os.Getwd() - if err != nil { - err := fmt.Errorf("Failed to locate current directory: %s", err) - return nil, err - } - - tsParts := strings.Split(ts, "-") - urlPathPrefix := "api" - uploadDirName := "uploads" - - uploadDir := filepath.Join(pwd, uploadDirName, tsParts[0], tsParts[1]) - err = os.MkdirAll(uploadDir, os.ModeDir|os.ModePerm) - - // loop over all files and save them to disk - for name, fds := range req.MultipartForm.File { - filename := fds[0].Filename - src, err := fds[0].Open() - if err != nil { - err := fmt.Errorf("Couldn't open uploaded file: %s", err) - return nil, err - - } - defer src.Close() - - // check if file at path exists, if so, add timestamp to file - absPath := filepath.Join(uploadDir, filename) - - if _, err := os.Stat(absPath); !os.IsNotExist(err) { - filename = fmt.Sprintf("%d-%s", time.Now().Unix(), filename) - absPath = filepath.Join(uploadDir, filename) - } - - // save to disk (TODO: or check if S3 credentials exist, & save to cloud) - dst, err := os.Create(absPath) - if err != nil { - err := fmt.Errorf("Failed to create destination file for upload: %s", err) - return nil, err - } - - // copy file from src to dst on disk - if _, 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/%s/%s/%s", urlPathPrefix, uploadDirName, tsParts[0], tsParts[1], filename) - - urlPaths[name] = urlPath - } - - return urlPaths, nil + http.HandleFunc("/api/uploads/", CacheControl(staticUploadHandler)) } diff --git a/system/admin/upload.go b/system/admin/upload.go new file mode 100644 index 0000000..a2cde4c --- /dev/null +++ b/system/admin/upload.go @@ -0,0 +1,113 @@ +package admin + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +func storeFileUploads(req *http.Request) (map[string]string, error) { + err := req.ParseMultipartForm(1024 * 1024 * 4) // maxMemory 4MB + if err != nil { + return nil, fmt.Errorf("%s", err) + } + + ts := req.FormValue("timestamp") + + // To use for FormValue name:urlPath + urlPaths := make(map[string]string) + + // get ts values individually to use as directory names when storing + // uploaded images + date := make(map[string]int) + if ts == "" { + now := time.Now() + date["year"] = now.Year() + date["month"] = int(now.Month()) + date["day"] = now.Day() + + // create timestamp format 'yyyy-mm-dd' and set in PostForm for + // db insertion + ts = fmt.Sprintf("%d-%02d-%02d", date["year"], date["month"], date["day"]) + req.PostForm.Set("timestamp", ts) + } else { + tsParts := strings.Split(ts, "-") + year, err := strconv.Atoi(tsParts[0]) + if err != nil { + return nil, fmt.Errorf("%s", err) + } + + month, err := strconv.Atoi(tsParts[1]) + if err != nil { + return nil, fmt.Errorf("%s", err) + } + + day, err := strconv.Atoi(tsParts[2]) + if err != nil { + return nil, fmt.Errorf("%s", err) + } + + date["year"] = year + date["month"] = month + date["day"] = day + } + + // get or create upload directory to save files from request + pwd, err := os.Getwd() + if err != nil { + err := fmt.Errorf("Failed to locate current directory: %s", err) + return nil, err + } + + tsParts := strings.Split(ts, "-") + urlPathPrefix := "api" + uploadDirName := "uploads" + + uploadDir := filepath.Join(pwd, uploadDirName, tsParts[0], tsParts[1]) + err = os.MkdirAll(uploadDir, os.ModeDir|os.ModePerm) + + // loop over all files and save them to disk + for name, fds := range req.MultipartForm.File { + filename := fds[0].Filename + src, err := fds[0].Open() + if err != nil { + err := fmt.Errorf("Couldn't open uploaded file: %s", err) + return nil, err + + } + defer src.Close() + + // check if file at path exists, if so, add timestamp to file + absPath := filepath.Join(uploadDir, filename) + + if _, err := os.Stat(absPath); !os.IsNotExist(err) { + filename = fmt.Sprintf("%d-%s", time.Now().Unix(), filename) + absPath = filepath.Join(uploadDir, filename) + } + + // save to disk (TODO: or check if S3 credentials exist, & save to cloud) + dst, err := os.Create(absPath) + if err != nil { + err := fmt.Errorf("Failed to create destination file for upload: %s", err) + return nil, err + } + + // copy file from src to dst on disk + if _, 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/%s/%s/%s", urlPathPrefix, uploadDirName, tsParts[0], tsParts[1], filename) + + urlPaths[name] = urlPath + } + + return urlPaths, nil +} diff --git a/system/db/config.go b/system/db/config.go index e421ff5..5bee0b3 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -2,19 +2,32 @@ package db import ( "bytes" + "encoding/base64" "encoding/json" + "fmt" "net/url" + "time" "github.com/boltdb/bolt" "github.com/gorilla/schema" "github.com/nilslice/cms/system/admin/config" ) +var configCache url.Values + +func init() { + configCache = make(url.Values) +} + // SetConfig sets key:value pairs in the db for configuration settings func SetConfig(data url.Values) error { err := store.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("_config")) + if data.Get("cache") == "invalidate" { + data.Set("etag", NewEtag()) + } + cfg := &config.Config{} dec := schema.NewDecoder() dec.SetAliasTag("json") // allows simpler struct tagging when creating a content type @@ -40,6 +53,8 @@ func SetConfig(data url.Values) error { return err } + configCache = data + return nil } @@ -52,6 +67,10 @@ func Config(key string) ([]byte, error) { return nil, err } + if len(cfg) < 1 { + return nil, nil + } + err = json.Unmarshal(cfg, &kv) if err != nil { return nil, err @@ -75,3 +94,16 @@ func ConfigAll() ([]byte, error) { return val.Bytes(), nil } + +// ConfigCache is a in-memory cache of the Configs for quicker lookups +func ConfigCache(key string) string { + return configCache.Get(key) +} + +// NewEtag generates a new Etag for response caching +func NewEtag() string { + now := fmt.Sprintf("%d", time.Now().Unix()) + etag := base64.StdEncoding.EncodeToString([]byte(now)) + + return etag +} diff --git a/system/db/init.go b/system/db/init.go index 7d6005e..008449f 100644 --- a/system/db/init.go +++ b/system/db/init.go @@ -52,13 +52,10 @@ func Init() { } } - clientSecret, err := Config("client_secret") - if err != nil { - return err - } + clientSecret := ConfigCache("client_secret") - if clientSecret != nil { - jwt.Secret(clientSecret) + if clientSecret != "" { + jwt.Secret([]byte(clientSecret)) } return nil @@ -76,6 +73,7 @@ func SystemInitComplete() bool { err := store.View(func(tx *bolt.Tx) error { users := tx.Bucket([]byte("_users")) + err := users.ForEach(func(k, v []byte) error { complete = true return nil |