Skip to content

Commit

Permalink
[core] System backups (uploads, system.db, analytics.db) (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
nilslice committed Jan 24, 2017
1 parent 0cf0d36 commit 3a897e4
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 12 deletions.
46 changes: 36 additions & 10 deletions system/admin/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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
Expand Down
34 changes: 33 additions & 1 deletion system/admin/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions system/admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
}
113 changes: 113 additions & 0 deletions system/admin/upload/backup.go
Original file line number Diff line number Diff line change
@@ -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
}
26 changes: 26 additions & 0 deletions system/api/analytics/backup.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 3 additions & 1 deletion system/api/analytics/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
34 changes: 34 additions & 0 deletions system/auth.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
26 changes: 26 additions & 0 deletions system/db/backup.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 3a897e4

Please sign in to comment.