summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve Manuel <nilslice@gmail.com>2016-10-07 00:24:53 -0700
committerSteve Manuel <nilslice@gmail.com>2016-10-07 00:24:53 -0700
commitb2e4fd9372f27d3202f07a329a5077b93a4b7390 (patch)
tree7a00e7753fdf3b4b8f8976e35fb4dd4c564c45a9
parentb5f028f0a720f1d23d2ce79d3e885fcb524bb79a (diff)
adding cache control and etags to responses for static assets + moved handlers/helper upload func
-rw-r--r--system/admin/cache.go28
-rw-r--r--system/admin/config/config.go31
-rw-r--r--system/admin/handlers.go39
-rw-r--r--system/admin/server.go141
-rw-r--r--system/admin/upload.go113
-rw-r--r--system/db/config.go32
-rw-r--r--system/db/init.go10
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