2018-10-04 21:03:47 +01:00
|
|
|
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
|
|
|
//
|
|
|
|
// This software (Documize Community Edition) is licensed under
|
|
|
|
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
//
|
|
|
|
// You can operate outside the AGPL restrictions by purchasing
|
|
|
|
// Documize Enterprise Edition and obtaining a commercial license
|
|
|
|
// by contacting <sales@documize.com>.
|
|
|
|
//
|
|
|
|
// https://documize.com
|
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
// Package backup handle data backup/restore to/from ZIP format.
|
2018-10-04 21:03:47 +01:00
|
|
|
package backup
|
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
// Documize data is all held in the SQL database in relational format.
|
|
|
|
// The objective is to export the data into a compressed file that
|
|
|
|
// can be restored again as required.
|
|
|
|
//
|
|
|
|
// This allows for the following scenarios to be supported:
|
|
|
|
//
|
|
|
|
// 1. Copying data from one Documize instance to another.
|
|
|
|
// 2. Changing database provider (e.g. from MySQL to PostgreSQL).
|
|
|
|
// 3. Moving between Documize Cloud and self-hosted instances.
|
|
|
|
// 4. GDPR compliance (send copy of data and nuke whatever remains).
|
|
|
|
// 5. Setting up sample Documize instance with pre-defined content.
|
|
|
|
//
|
|
|
|
// The initial implementation is restricted to tenant or global
|
|
|
|
// backup/restore operations and can only be performed by a verified
|
|
|
|
// Global Administrator.
|
|
|
|
//
|
|
|
|
// In future the process should be able to support per space backup/restore
|
|
|
|
// operations. This is subject to further review.
|
|
|
|
|
2018-10-04 21:03:47 +01:00
|
|
|
import (
|
2018-10-12 17:54:30 +01:00
|
|
|
"bytes"
|
2018-10-04 21:03:47 +01:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2018-10-12 17:54:30 +01:00
|
|
|
"io"
|
2018-10-04 21:03:47 +01:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
2018-10-12 17:54:30 +01:00
|
|
|
"strconv"
|
2018-10-04 21:03:47 +01:00
|
|
|
|
2019-02-10 13:12:17 +00:00
|
|
|
"github.com/documize/community/core/request"
|
|
|
|
"github.com/documize/community/model/audit"
|
|
|
|
|
2018-10-04 21:03:47 +01:00
|
|
|
"github.com/documize/community/core/env"
|
|
|
|
"github.com/documize/community/core/response"
|
|
|
|
"github.com/documize/community/core/streamutil"
|
|
|
|
"github.com/documize/community/domain"
|
|
|
|
indexer "github.com/documize/community/domain/search"
|
|
|
|
"github.com/documize/community/domain/store"
|
2018-10-10 15:13:09 +01:00
|
|
|
m "github.com/documize/community/model/backup"
|
2018-10-04 21:03:47 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// Handler contains the runtime information such as logging and database.
|
|
|
|
type Handler struct {
|
|
|
|
Runtime *env.Runtime
|
|
|
|
Store *store.Store
|
|
|
|
Indexer indexer.Indexer
|
|
|
|
}
|
|
|
|
|
|
|
|
// Backup generates binary file of all instance settings and contents.
|
|
|
|
// The content is pulled directly from the database and marshalled to JSON.
|
|
|
|
// A zip file is then sent to the caller.
|
|
|
|
func (h *Handler) Backup(w http.ResponseWriter, r *http.Request) {
|
|
|
|
method := "system.backup"
|
|
|
|
ctx := domain.GetRequestContext(r)
|
|
|
|
|
|
|
|
if !ctx.Administrator {
|
|
|
|
response.WriteForbiddenError(w)
|
|
|
|
h.Runtime.Log.Info(fmt.Sprintf("Non-admin attempted system backup operation (user ID: %s)", ctx.UserID))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
defer streamutil.Close(r.Body)
|
|
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
response.WriteBadRequestError(w, method, err.Error())
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
spec := m.ExportSpec{}
|
2018-10-04 21:03:47 +01:00
|
|
|
err = json.Unmarshal(body, &spec)
|
|
|
|
if err != nil {
|
|
|
|
response.WriteBadRequestError(w, method, err.Error())
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-10-11 16:19:11 +01:00
|
|
|
h.Runtime.Log.Info("Backup started")
|
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
bh := backerHandler{Runtime: h.Runtime, Store: h.Store, Context: ctx, Spec: spec}
|
2018-10-04 21:03:47 +01:00
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
// Produce zip file on disk.
|
|
|
|
filename, err := bh.GenerateBackup()
|
|
|
|
if err != nil {
|
|
|
|
response.WriteServerError(w, method, err)
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
return
|
|
|
|
}
|
2018-10-04 21:03:47 +01:00
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
// Read backup file into memory.
|
|
|
|
// DEBT: write file directly to HTTP response stream?
|
2018-10-26 14:41:26 +01:00
|
|
|
// defer out.Close()
|
|
|
|
// io.Copy(out, resp.Body)
|
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
bk, err := ioutil.ReadFile(filename)
|
2018-10-04 21:03:47 +01:00
|
|
|
if err != nil {
|
|
|
|
response.WriteServerError(w, method, err)
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-10-12 17:54:30 +01:00
|
|
|
h.Runtime.Log.Info(fmt.Sprintf("Backup size pending download %d", len(bk)))
|
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
// Standard HTTP headers.
|
2018-10-04 21:03:47 +01:00
|
|
|
w.Header().Set("Content-Type", "application/zip")
|
2018-10-10 15:13:09 +01:00
|
|
|
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`" ; `+`filename*="`+filename+`"`)
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bk)))
|
2018-10-12 17:54:30 +01:00
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
// Custom HTTP header helps API consumer to extract backup filename cleanly
|
|
|
|
// instead of parsing 'Content-Disposition' header.
|
|
|
|
// This HTTP header is CORS white-listed.
|
|
|
|
w.Header().Set("x-documize-filename", filename)
|
2019-11-08 10:40:10 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2018-10-10 15:13:09 +01:00
|
|
|
|
|
|
|
// Write backup to response stream.
|
|
|
|
x, err := w.Write(bk)
|
2018-10-04 21:03:47 +01:00
|
|
|
if err != nil {
|
|
|
|
response.WriteServerError(w, method, err)
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
h.Runtime.Log.Info(fmt.Sprintf("Backup completed for %s by %s, size %d", ctx.OrgID, ctx.UserID, x))
|
2018-10-20 17:31:59 +01:00
|
|
|
h.Store.Audit.Record(ctx, audit.EventTypeDatabaseBackup)
|
2018-10-04 21:03:47 +01:00
|
|
|
|
2018-10-10 15:13:09 +01:00
|
|
|
// Delete backup file if not requested to keep it.
|
|
|
|
if !spec.Retain {
|
|
|
|
os.Remove(filename)
|
2018-10-04 21:03:47 +01:00
|
|
|
}
|
|
|
|
}
|
2018-10-12 17:54:30 +01:00
|
|
|
|
|
|
|
// Restore receives ZIP file for restore operation.
|
|
|
|
// Options are specified as HTTP query paramaters.
|
|
|
|
func (h *Handler) Restore(w http.ResponseWriter, r *http.Request) {
|
|
|
|
method := "system.restore"
|
|
|
|
ctx := domain.GetRequestContext(r)
|
|
|
|
|
|
|
|
if !ctx.Administrator {
|
|
|
|
response.WriteForbiddenError(w)
|
|
|
|
h.Runtime.Log.Info(fmt.Sprintf("Non-admin attempted system restore operation (user ID: %s)", ctx.UserID))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
h.Runtime.Log.Info(fmt.Sprintf("Restored attempted by user: %s", ctx.UserID))
|
|
|
|
|
|
|
|
overwriteOrg, err := strconv.ParseBool(request.Query(r, "org"))
|
|
|
|
if err != nil {
|
|
|
|
h.Runtime.Log.Info("Restore invoked without 'org' parameter")
|
|
|
|
response.WriteMissingDataError(w, method, "org=false/true missing")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
filedata, fileheader, err := r.FormFile("restore-file")
|
|
|
|
if err != nil {
|
|
|
|
response.WriteMissingDataError(w, method, "restore-file")
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
_, err = io.Copy(b, filedata)
|
|
|
|
if err != nil {
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
response.WriteServerError(w, method, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-10-15 18:59:21 +01:00
|
|
|
h.Runtime.Log.Info(fmt.Sprintf("Restore file: %s %d", fileheader.Filename, len(b.Bytes())))
|
|
|
|
|
|
|
|
//
|
|
|
|
org, err := h.Store.Organization.GetOrganization(ctx, ctx.OrgID)
|
|
|
|
if err != nil {
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
response.WriteServerError(w, method, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prepare context and start restore process.
|
2018-10-20 12:41:26 +01:00
|
|
|
spec := m.ImportSpec{OverwriteOrg: overwriteOrg, Org: org}
|
2018-10-15 18:59:21 +01:00
|
|
|
rh := restoreHandler{Runtime: h.Runtime, Store: h.Store, Context: ctx, Spec: spec}
|
|
|
|
|
2018-10-19 12:40:45 +01:00
|
|
|
// Run the restore process.
|
2018-10-15 18:59:21 +01:00
|
|
|
err = rh.PerformRestore(b.Bytes(), r.ContentLength)
|
|
|
|
if err != nil {
|
|
|
|
response.WriteServerError(w, method, err)
|
|
|
|
h.Runtime.Log.Error(method, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-10-19 12:40:45 +01:00
|
|
|
h.Runtime.Log.Infof("Restore remapped %d OrgID values", len(rh.MapOrgID))
|
|
|
|
h.Runtime.Log.Infof("Restore remapped %d UserID values", len(rh.MapUserID))
|
2018-10-15 18:59:21 +01:00
|
|
|
h.Runtime.Log.Info("Restore completed")
|
2018-10-12 17:54:30 +01:00
|
|
|
|
2019-06-25 15:26:53 +01:00
|
|
|
h.Runtime.Log.Info("Building search index")
|
|
|
|
go h.Indexer.Rebuild(ctx)
|
|
|
|
|
2018-10-12 17:54:30 +01:00
|
|
|
response.WriteEmpty(w)
|
|
|
|
}
|