diff options
author | Steve <nilslice@gmail.com> | 2017-01-24 10:35:18 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-01-24 10:35:18 -0800 |
commit | 3a897e4db97cc6f5e47f395662499402eb4c2bda (patch) | |
tree | 61350d459fb5cbf044e878042c0239acb060c7da | |
parent | 0cf0d36f7613bbb2e13c0c8406689de3be9ee8d5 (diff) |
[core] System backups (uploads, system.db, analytics.db) (#42)
-rw-r--r-- | system/admin/config/config.go | 46 | ||||
-rw-r--r-- | system/admin/handlers.go | 34 | ||||
-rw-r--r-- | system/admin/server.go | 4 | ||||
-rw-r--r-- | system/admin/upload/backup.go | 113 | ||||
-rw-r--r-- | system/api/analytics/backup.go | 26 | ||||
-rw-r--r-- | system/api/analytics/init.go | 4 | ||||
-rw-r--r-- | system/auth.go | 34 | ||||
-rw-r--r-- | system/db/backup.go | 26 |
8 files changed, 275 insertions, 12 deletions
diff --git a/system/admin/config/config.go b/system/admin/config/config.go index 0d55700..3a21e16 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -9,18 +9,27 @@ import ( type Config struct { item.Item - Name string `json:"name"` - Domain string `json:"domain"` - HTTPPort string `json:"http_port"` - HTTPSPort string `json:"https_port"` - AdminEmail string `json:"admin_email"` - ClientSecret string `json:"client_secret"` - Etag string `json:"etag"` - DisableCORS bool `json:"cors_disabled"` - DisableGZIP bool `json:"gzip_disabled"` - CacheInvalidate []string `json:"cache"` + Name string `json:"name"` + Domain string `json:"domain"` + HTTPPort string `json:"http_port"` + HTTPSPort string `json:"https_port"` + AdminEmail string `json:"admin_email"` + ClientSecret string `json:"client_secret"` + Etag string `json:"etag"` + DisableCORS bool `json:"cors_disabled"` + DisableGZIP bool `json:"gzip_disabled"` + CacheInvalidate []string `json:"cache"` + BackupBasicAuthUser string `json:"backup_basic_auth_user"` + BackupBasicAuthPassword string `json:"backup_basic_auth_password"` } +const ( + dbBackupInfo = ` + <p class="flow-text">Database Backup Credentials:</p> + <p>Add a user name and password to download a backup of your data via HTTP.</p> + ` +) + // String partially implements item.Identifiable and overrides Item's String() func (c *Config) String() string { return c.Name } @@ -97,6 +106,23 @@ func (c *Config) MarshalEditor() ([]byte, error) { "invalidate": "Invalidate Cache", }), }, + editor.Field{ + View: []byte(dbBackupInfo), + }, + editor.Field{ + View: editor.Input("BackupBasicAuthUser", c, map[string]string{ + "label": "HTTP Basic Auth User", + "placeholder": "Enter a user name for Basic Auth access", + "type": "text", + }), + }, + editor.Field{ + View: editor.Input("BackupBasicAuthPassword", c, map[string]string{ + "label": "HTTP Basic Auth Password", + "placeholder": "Enter a password for Basic Auth access", + "type": "password", + }), + }, ) if err != nil { return nil, err diff --git a/system/admin/handlers.go b/system/admin/handlers.go index 3195239..a99a62c 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -19,13 +19,14 @@ import ( "github.com/ponzu-cms/ponzu/system/admin/upload" "github.com/ponzu-cms/ponzu/system/admin/user" "github.com/ponzu-cms/ponzu/system/api" + "github.com/ponzu-cms/ponzu/system/api/analytics" "github.com/ponzu-cms/ponzu/system/db" "github.com/ponzu-cms/ponzu/system/item" - "github.com/tidwall/gjson" "github.com/gorilla/schema" emailer "github.com/nilslice/email" "github.com/nilslice/jwt" + "github.com/tidwall/gjson" ) func adminHandler(res http.ResponseWriter, req *http.Request) { @@ -188,6 +189,37 @@ func configHandler(res http.ResponseWriter, req *http.Request) { } +func backupHandler(res http.ResponseWriter, req *http.Request) { + switch req.URL.Query().Get("source") { + case "system": + err := db.Backup(res) + if err != nil { + log.Println("Failed to run backup on system:", err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + case "analytics": + err := analytics.Backup(res) + if err != nil { + log.Println("Failed to run backup on analytics:", err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + case "uploads": + err := upload.Backup(res) + if err != nil { + log.Println("Failed to run backup on uploads:", err) + res.WriteHeader(http.StatusInternalServerError) + return + } + + default: + res.WriteHeader(http.StatusBadRequest) + } +} + func configUsersHandler(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 11bfe6f..df00c21 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + "github.com/ponzu-cms/ponzu/system" "github.com/ponzu-cms/ponzu/system/admin/user" "github.com/ponzu-cms/ponzu/system/api" "github.com/ponzu-cms/ponzu/system/db" @@ -52,4 +53,7 @@ func Run() { // through the editor will not load within the admin system. uploadsDir := filepath.Join(pwd, "uploads") http.Handle("/api/uploads/", api.Record(api.CORS(db.CacheControl(http.StripPrefix("/api/uploads/", http.FileServer(restrict(http.Dir(uploadsDir)))))))) + + // Database & uploads backup via HTTP route registered with Basic Auth middleware. + http.HandleFunc("/admin/backup", system.BasicAuth(backupHandler)) } diff --git a/system/admin/upload/backup.go b/system/admin/upload/backup.go new file mode 100644 index 0000000..28b1b8e --- /dev/null +++ b/system/admin/upload/backup.go @@ -0,0 +1,113 @@ +package upload + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" +) + +// Backup creates an archive of a project's uploads and writes it +// to the response as a download +func Backup(res http.ResponseWriter) error { + ts := time.Now().Unix() + filename := fmt.Sprintf("uploads-%d.bak.tar.gz", ts) + tmp := os.TempDir() + backup := filepath.Join(tmp, filename) + + // create uploads-{stamp}.bak.tar.gz + f, err := os.Create(backup) + if err != nil { + return err + } + + // loop through directory and gzip files + // add all to uploads.bak.tar.gz tarball + gz := gzip.NewWriter(f) + tarball := tar.NewWriter(gz) + + err = filepath.Walk("uploads", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + hdr, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + + hdr.Name = path + + err = tarball.WriteHeader(hdr) + if err != nil { + return err + } + + if !info.IsDir() { + src, err := os.Open(path) + if err != nil { + return err + } + defer src.Close() + _, err = io.Copy(tarball, src) + if err != nil { + return err + } + + err = tarball.Flush() + if err != nil { + return err + } + + err = gz.Flush() + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + fmt.Println(err) + return err + } + + err = gz.Close() + if err != nil { + return err + } + err = tarball.Close() + if err != nil { + return err + } + err = f.Close() + if err != nil { + return err + } + + // write data to response + data, err := os.Open(backup) + if err != nil { + return err + } + defer data.Close() + defer os.Remove(backup) + + disposition := `attachment; filename=%s` + info, err := data.Stat() + if err != nil { + return err + } + + res.Header().Set("Content-Type", "application/octet-stream") + res.Header().Set("Content-Disposition", fmt.Sprintf(disposition, ts)) + res.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size())) + + _, err = io.Copy(res, data) + + return err +} diff --git a/system/api/analytics/backup.go b/system/api/analytics/backup.go new file mode 100644 index 0000000..07b1a46 --- /dev/null +++ b/system/api/analytics/backup.go @@ -0,0 +1,26 @@ +package analytics + +import ( + "fmt" + "net/http" + "time" + + "github.com/boltdb/bolt" +) + +// Backup writes a snapshot of the analytics.db database to an HTTP response +func Backup(res http.ResponseWriter) error { + err := store.View(func(tx *bolt.Tx) error { + ts := time.Now().Unix() + disposition := `attachment; filename="analytics-%d.db.bak"` + + res.Header().Set("Content-Type", "application/octet-stream") + res.Header().Set("Content-Disposition", fmt.Sprintf(disposition, ts)) + res.Header().Set("Content-Length", fmt.Sprintf("%d", int(tx.Size()))) + + _, err := tx.WriteTo(res) + return err + }) + + return err +} diff --git a/system/api/analytics/init.go b/system/api/analytics/init.go index 3eccc13..f24425b 100644 --- a/system/api/analytics/init.go +++ b/system/api/analytics/init.go @@ -43,13 +43,15 @@ const RANGE = 14 func Record(req *http.Request) { external := strings.Contains(req.URL.Path, "/external/") + ts := int64(time.Nanosecond) * time.Now().UnixNano() / int64(time.Millisecond) + r := apiRequest{ URL: req.URL.String(), Method: req.Method, Origin: req.Header.Get("Origin"), Proto: req.Proto, RemoteAddr: req.RemoteAddr, - Timestamp: time.Now().Unix() * 1000, + Timestamp: ts, External: external, } diff --git a/system/auth.go b/system/auth.go new file mode 100644 index 0000000..cf1adf2 --- /dev/null +++ b/system/auth.go @@ -0,0 +1,34 @@ +package system + +import ( + "net/http" + + "github.com/ponzu-cms/ponzu/system/db" +) + +// BasicAuth adds HTTP Basic Auth check for requests that should implement it +func BasicAuth(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + u := db.ConfigCache("backup_basic_auth_user").(string) + p := db.ConfigCache("backup_basic_auth_password").(string) + + if u == "" || p == "" { + res.WriteHeader(http.StatusForbidden) + return + } + + user, password, ok := req.BasicAuth() + + if !ok { + res.WriteHeader(http.StatusForbidden) + return + } + + if u != user || p != password { + res.WriteHeader(http.StatusUnauthorized) + return + } + + next.ServeHTTP(res, req) + }) +} diff --git a/system/db/backup.go b/system/db/backup.go new file mode 100644 index 0000000..735abe4 --- /dev/null +++ b/system/db/backup.go @@ -0,0 +1,26 @@ +package db + +import ( + "fmt" + "net/http" + "time" + + "github.com/boltdb/bolt" +) + +// Backup writes a snapshot of the system.db database to an HTTP response +func Backup(res http.ResponseWriter) error { + err := store.View(func(tx *bolt.Tx) error { + ts := time.Now().Unix() + disposition := `attachment; filename="system-%d.db.bak"` + + res.Header().Set("Content-Type", "application/octet-stream") + res.Header().Set("Content-Disposition", fmt.Sprintf(disposition, ts)) + res.Header().Set("Content-Length", fmt.Sprintf("%d", int(tx.Size()))) + + _, err := tx.WriteTo(res) + return err + }) + + return err +} |