mirror of
https://github.com/documize/community.git
synced 2025-07-20 05:39:42 +02:00
[WIP] Provide system restore facility
Co-Authored-By: Harvey Kandola <harvey@documize.com>
This commit is contained in:
parent
71a2860716
commit
516140dd7e
12 changed files with 597 additions and 51 deletions
|
@ -43,7 +43,6 @@ func (s Store) Record(ctx domain.RequestContext, t audit.EventType) {
|
||||||
|
|
||||||
_, err = tx.Exec(s.Bind("INSERT INTO dmz_audit_log (c_orgid, c_userid, c_eventtype, c_ip, c_created) VALUES (?, ?, ?, ?, ?)"),
|
_, err = tx.Exec(s.Bind("INSERT INTO dmz_audit_log (c_orgid, c_userid, c_eventtype, c_ip, c_created) VALUES (?, ?, ?, ?, ?)"),
|
||||||
e.OrgID, e.UserID, e.Type, e.IP, e.Created)
|
e.OrgID, e.UserID, e.Type, e.IP, e.Created)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
s.Runtime.Log.Error("prepare audit insert", err)
|
s.Runtime.Log.Error("prepare audit insert", err)
|
||||||
|
|
|
@ -180,7 +180,28 @@ func (h *Handler) Restore(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Runtime.Log.Info(fmt.Sprintf("%s %d %v %v", fileheader.Filename, len(b.Bytes()), overwriteOrg, createUsers))
|
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.
|
||||||
|
spec := m.ImportSpec{OverwriteOrg: overwriteOrg, CreateUsers: createUsers, Org: org}
|
||||||
|
rh := restoreHandler{Runtime: h.Runtime, Store: h.Store, Context: ctx, Spec: spec}
|
||||||
|
|
||||||
|
err = rh.PerformRestore(b.Bytes(), r.ContentLength)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
h.Runtime.Log.Error(method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Runtime.Log.Info("Restore completed")
|
||||||
|
|
||||||
response.WriteEmpty(w)
|
response.WriteEmpty(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,497 @@
|
||||||
// Package backup handle data backup/restore to/from ZIP format.
|
// Package backup handle data backup/restore to/from ZIP format.
|
||||||
package backup
|
package backup
|
||||||
|
|
||||||
// DESIGN
|
// The restore operation allows an admin to upload a backup file.
|
||||||
// ------
|
// ID, created and revised attributes values are maintained as per backup.
|
||||||
//
|
|
||||||
// The restore operation allows an admin to upload a backup file
|
|
||||||
|
|
||||||
import ()
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/documize/community/core/env"
|
||||||
|
"github.com/documize/community/domain"
|
||||||
|
"github.com/documize/community/domain/store"
|
||||||
|
"github.com/documize/community/model/action"
|
||||||
|
"github.com/documize/community/model/audit"
|
||||||
|
m "github.com/documize/community/model/backup"
|
||||||
|
"github.com/documize/community/model/category"
|
||||||
|
"github.com/documize/community/model/org"
|
||||||
|
"github.com/documize/community/model/space"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler contains the runtime information such as logging and database.
|
||||||
|
type restoreHandler struct {
|
||||||
|
Runtime *env.Runtime
|
||||||
|
Store *store.Store
|
||||||
|
Spec m.ImportSpec
|
||||||
|
Context domain.RequestContext
|
||||||
|
Zip *zip.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerformRestore will unzip backup file and verify contents
|
||||||
|
// are suitable for restore operation.
|
||||||
|
func (r *restoreHandler) PerformRestore(b []byte, l int64) (err error) {
|
||||||
|
// Read zip file into handler for subsequent processing.
|
||||||
|
z, err := zip.NewReader(bytes.NewReader(b), l)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "cannot read zip file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Zip = z
|
||||||
|
|
||||||
|
// Unpack manifest for backup host details.
|
||||||
|
err = r.manifest()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization.
|
||||||
|
err = r.dmzOrg()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config.
|
||||||
|
err = r.dmzConfig()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audit Log.
|
||||||
|
err = r.dmzAudit()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action.
|
||||||
|
err = r.dmzAction()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space.
|
||||||
|
err = r.dmzSpace()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category.
|
||||||
|
err = r.dmzCategory()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryMember.
|
||||||
|
err = r.dmzCategoryMember()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *restoreHandler) manifest() (err error) {
|
||||||
|
found, zi, err := r.readZip("manifest.json")
|
||||||
|
if !found {
|
||||||
|
err = errors.Wrap(err, "missing manifest.json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "failed to process manifest.json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(zi, &r.Spec.Manifest)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "failed to read manifest as JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info("Extracted manifest.json")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads file and unmarshals content as JSON.
|
||||||
|
func (r *restoreHandler) fileJSON(filename string, v interface{}) (err error) {
|
||||||
|
found, zi, err := r.readZip(filename)
|
||||||
|
if !found {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("missing %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to process %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(zi, &v)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to read %s as JSON", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches file from zip reader.
|
||||||
|
func (r *restoreHandler) readZip(filename string) (found bool, b []byte, err error) {
|
||||||
|
found = false
|
||||||
|
for _, zf := range r.Zip.File {
|
||||||
|
if zf.Name == filename {
|
||||||
|
src, e := zf.Open()
|
||||||
|
if e != nil {
|
||||||
|
e = errors.Wrap(e, fmt.Sprintf("cannot open %s", filename))
|
||||||
|
return true, b, e
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
b, e = ioutil.ReadAll(src)
|
||||||
|
if e != nil {
|
||||||
|
e = errors.Wrap(e, fmt.Sprintf("cannot read %s", filename))
|
||||||
|
return true, b, e
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true
|
||||||
|
err = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization.
|
||||||
|
func (r *restoreHandler) dmzOrg() (err error) {
|
||||||
|
filename := "dmz_org.json"
|
||||||
|
|
||||||
|
org := []org.Organization{}
|
||||||
|
err = r.fileJSON(filename, &org)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to load %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
|
||||||
|
|
||||||
|
r.Context.Transaction, err = r.Runtime.Db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to start TX for %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range org {
|
||||||
|
// If same tenant (domain) then patch OrgID.
|
||||||
|
if org[i].Domain == r.Spec.Org.Domain {
|
||||||
|
org[i].RefID = r.Spec.Org.RefID
|
||||||
|
|
||||||
|
// Update org settings if allowed to do so.
|
||||||
|
if !r.Spec.OverwriteOrg {
|
||||||
|
org[i].AllowAnonymousAccess = r.Spec.Org.AllowAnonymousAccess
|
||||||
|
org[i].AuthProvider = r.Spec.Org.AuthProvider
|
||||||
|
org[i].AuthConfig = r.Spec.Org.AuthConfig
|
||||||
|
org[i].Company = r.Spec.Org.Company
|
||||||
|
org[i].ConversionEndpoint = r.Spec.Org.ConversionEndpoint
|
||||||
|
org[i].Email = r.Spec.Org.Email
|
||||||
|
org[i].MaxTags = r.Spec.Org.MaxTags
|
||||||
|
org[i].Message = r.Spec.Org.Message
|
||||||
|
org[i].Serial = r.Spec.Org.Serial
|
||||||
|
org[i].Title = r.Spec.Org.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.Context.Transaction.NamedExec(`UPDATE dmz_org SET
|
||||||
|
c_anonaccess=:allowanonymousaccess,
|
||||||
|
c_authprovider=:authprovider,
|
||||||
|
c_authconfig=:authconfig,
|
||||||
|
c_company=:company,
|
||||||
|
c_service=:conversionendpoint,
|
||||||
|
c_email=:email,
|
||||||
|
c_maxtags=:maxtags,
|
||||||
|
c_message=:message,
|
||||||
|
c_title=:title,
|
||||||
|
c_serial=:serial
|
||||||
|
WHERE c_refid=:refid`, &org[i])
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, "unable to overwrite current organization settings")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add new organization.
|
||||||
|
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind(`
|
||||||
|
INSERT INTO dmz_org (c_refid, c_company, c_title, c_message,
|
||||||
|
c_domain, c_service, c_email, c_anonaccess, c_authprovider, c_authconfig,
|
||||||
|
c_maxtags, c_verified, c_serial, c_active, c_created, c_revised)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
||||||
|
org[i].RefID, org[i].Company, org[i].Title, org[i].Message,
|
||||||
|
strings.ToLower(org[i].Domain), org[i].ConversionEndpoint, strings.ToLower(org[i].Email),
|
||||||
|
org[i].AllowAnonymousAccess, org[i].AuthProvider, org[i].AuthConfig,
|
||||||
|
org[i].MaxTags, true, org[i].Serial, org[i].Active, org[i].Created, org[i].Revised)
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %s", filename, org[i].RefID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Context.Transaction.Commit()
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to commit %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Processed %s %d records", filename, len(org)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config.
|
||||||
|
func (r *restoreHandler) dmzConfig() (err error) {
|
||||||
|
filename := "dmz_config.json"
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
ConfigKey string `json:"key"`
|
||||||
|
ConfigValue string `json:"config"`
|
||||||
|
}
|
||||||
|
c := []config{}
|
||||||
|
err = r.fileJSON(filename, &c)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to load %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
|
||||||
|
|
||||||
|
for i := range c {
|
||||||
|
err = r.Store.Setting.Set(c[i].ConfigKey, c[i].ConfigValue)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %s", filename, c[i].ConfigKey))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Processed %s %d records", filename, len(c)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audit Log.
|
||||||
|
func (r *restoreHandler) dmzAudit() (err error) {
|
||||||
|
filename := "dmz_audit_log.json"
|
||||||
|
|
||||||
|
log := []audit.AppEvent{}
|
||||||
|
err = r.fileJSON(filename, &log)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to load %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
|
||||||
|
|
||||||
|
r.Context.Transaction, err = r.Runtime.Db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to start TX for %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range log {
|
||||||
|
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind("INSERT INTO dmz_audit_log (c_orgid, c_userid, c_eventtype, c_ip, c_created) VALUES (?, ?, ?, ?, ?)"),
|
||||||
|
log[i].OrgID, log[i].UserID, log[i].Type, log[i].IP, log[i].Created)
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %d", filename, log[i].ID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind("INSERT INTO dmz_audit_log (c_orgid, c_userid, c_eventtype, c_ip, c_created) VALUES (?, ?, ?, ?, ?)"),
|
||||||
|
r.Context.OrgID, r.Context.UserID, "restored-database", r.Context.ClientIP, time.Now().UTC())
|
||||||
|
|
||||||
|
err = r.Context.Transaction.Commit()
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to commit %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Processed %s %d records", filename, len(log)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action.
|
||||||
|
func (r *restoreHandler) dmzAction() (err error) {
|
||||||
|
filename := "dmz_action.json"
|
||||||
|
|
||||||
|
ac := []action.UserAction{}
|
||||||
|
err = r.fileJSON(filename, &ac)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to load %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
|
||||||
|
|
||||||
|
r.Context.Transaction, err = r.Runtime.Db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to start TX for %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range ac {
|
||||||
|
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind("INSERT INTO dmz_action (c_refid, c_orgid, c_userid, c_docid, c_actiontype, c_note, c_requestorid, c_requested, c_due, c_reftype, c_reftypeid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
||||||
|
ac[i].RefID, ac[i].OrgID, ac[i].UserID, ac[i].DocumentID, ac[i].ActionType, ac[i].Note, ac[i].RequestorID, ac[i].Requested, ac[i].Due, ac[i].RefType, ac[i].RefTypeID)
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %s", filename, ac[i].RefID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Context.Transaction.Commit()
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to commit %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Processed %s %d records", filename, len(ac)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space.
|
||||||
|
func (r *restoreHandler) dmzSpace() (err error) {
|
||||||
|
filename := "dmz_space.json"
|
||||||
|
|
||||||
|
sp := []space.Space{}
|
||||||
|
err = r.fileJSON(filename, &sp)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to load %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
|
||||||
|
|
||||||
|
r.Context.Transaction, err = r.Runtime.Db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to start TX for %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range sp {
|
||||||
|
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind("INSERT INTO dmz_space (c_refid, c_name, c_orgid, c_userid, c_type, c_lifecycle, c_likes, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
||||||
|
sp[i].RefID, sp[i].Name, sp[i].OrgID, sp[i].UserID, sp[i].Type, sp[i].Lifecycle, sp[i].Likes, sp[i].Created, sp[i].Revised)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %s", filename, sp[i].RefID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Context.Transaction.Commit()
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to commit %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Processed %s %d records", filename, len(sp)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category.
|
||||||
|
func (r *restoreHandler) dmzCategory() (err error) {
|
||||||
|
filename := "dmz_category.json"
|
||||||
|
|
||||||
|
ct := []category.Category{}
|
||||||
|
err = r.fileJSON(filename, &ct)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to load %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
|
||||||
|
|
||||||
|
r.Context.Transaction, err = r.Runtime.Db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to start TX for %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range ct {
|
||||||
|
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind(`
|
||||||
|
INSERT INTO dmz_category (c_refid, c_orgid, c_spaceid, c_name, c_created, c_revised)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`),
|
||||||
|
ct[i].RefID, ct[i].OrgID, ct[i].SpaceID, ct[i].Name, ct[i].Created, ct[i].Revised)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %s", filename, ct[i].RefID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Context.Transaction.Commit()
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to commit %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Processed %s %d records", filename, len(ct)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryMember.
|
||||||
|
func (r *restoreHandler) dmzCategoryMember() (err error) {
|
||||||
|
filename := "dmz_category_member.json"
|
||||||
|
|
||||||
|
cm := []category.Member{}
|
||||||
|
err = r.fileJSON(filename, &cm)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("failed to load %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Extracted %s", filename))
|
||||||
|
|
||||||
|
r.Context.Transaction, err = r.Runtime.Db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to start TX for %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cm {
|
||||||
|
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind(`
|
||||||
|
INSERT INTO dmz_category_member
|
||||||
|
(c_refid, c_orgid, c_categoryid, c_spaceid, c_docid, c_created, c_revised)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`),
|
||||||
|
cm[i].RefID, cm[i].OrgID, cm[i].CategoryID, cm[i].SpaceID, cm[i].DocumentID, cm[i].Created, cm[i].Revised)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to insert %s %s", filename, cm[i].RefID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Context.Transaction.Commit()
|
||||||
|
if err != nil {
|
||||||
|
r.Context.Transaction.Rollback()
|
||||||
|
err = errors.Wrap(err, fmt.Sprintf("unable to commit %s", filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Runtime.Log.Info(fmt.Sprintf("Processed %s %d records", filename, len(cm)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -32,11 +32,7 @@ type Store struct {
|
||||||
|
|
||||||
// AddOrganization inserts the passed organization record into the organization table.
|
// AddOrganization inserts the passed organization record into the organization table.
|
||||||
func (s Store) AddOrganization(ctx domain.RequestContext, org org.Organization) (err error) {
|
func (s Store) AddOrganization(ctx domain.RequestContext, org org.Organization) (err error) {
|
||||||
org.Created = time.Now().UTC()
|
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_org (c_refid, c_company, c_title, c_message, c_domain, c_email, c_anonaccess, c_serial, c_maxtags, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
||||||
org.Revised = time.Now().UTC()
|
|
||||||
|
|
||||||
_, err = ctx.Transaction.Exec(
|
|
||||||
s.Bind("INSERT INTO dmz_org (c_refid, c_company, c_title, c_message, c_domain, c_email, c_anonaccess, c_serial, c_maxtags, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
||||||
org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.Domain),
|
org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.Domain),
|
||||||
strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.MaxTags, org.Created, org.Revised)
|
strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.MaxTags, org.Created, org.Revised)
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/documize/community/core/env"
|
"github.com/documize/community/core/env"
|
||||||
"github.com/documize/community/core/event"
|
"github.com/documize/community/core/event"
|
||||||
|
@ -102,6 +103,9 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
|
||||||
sp.UserID = ctx.UserID
|
sp.UserID = ctx.UserID
|
||||||
sp.Type = space.ScopePrivate
|
sp.Type = space.ScopePrivate
|
||||||
sp.Lifecycle = wf.LifecycleLive
|
sp.Lifecycle = wf.LifecycleLive
|
||||||
|
sp.UserID = ctx.UserID
|
||||||
|
sp.Created = time.Now().UTC()
|
||||||
|
sp.Revised = time.Now().UTC()
|
||||||
|
|
||||||
err = h.Store.Space.Add(ctx, sp)
|
err = h.Store.Space.Add(ctx, sp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -31,6 +31,9 @@ func TestSpace(t *testing.T) {
|
||||||
sp.UserID = ctx.UserID
|
sp.UserID = ctx.UserID
|
||||||
sp.Type = space.ScopePublic
|
sp.Type = space.ScopePublic
|
||||||
sp.Name = "PublicTestSpace"
|
sp.Name = "PublicTestSpace"
|
||||||
|
sp.UserID = ctx.UserID
|
||||||
|
sp.Created = time.Now().UTC()
|
||||||
|
sp.Revised = time.Now().UTC()
|
||||||
|
|
||||||
err = s.Space.Add(ctx, sp)
|
err = s.Space.Add(ctx, sp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -98,6 +101,9 @@ func TestSpace(t *testing.T) {
|
||||||
sp2.OrgID = ctx.OrgID
|
sp2.OrgID = ctx.OrgID
|
||||||
sp2.Type = space.ScopePrivate
|
sp2.Type = space.ScopePrivate
|
||||||
sp2.Name = "PrivateTestSpace"
|
sp2.Name = "PrivateTestSpace"
|
||||||
|
sp.UserID = ctx.UserID
|
||||||
|
sp.Created = time.Now().UTC()
|
||||||
|
sp.Revised = time.Now().UTC()
|
||||||
|
|
||||||
err = s.Space.Add(ctx, sp2)
|
err = s.Space.Add(ctx, sp2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -30,10 +30,6 @@ type Store struct {
|
||||||
|
|
||||||
// Add adds new folder into the store.
|
// Add adds new folder into the store.
|
||||||
func (s Store) Add(ctx domain.RequestContext, sp space.Space) (err error) {
|
func (s Store) Add(ctx domain.RequestContext, sp space.Space) (err error) {
|
||||||
sp.UserID = ctx.UserID
|
|
||||||
sp.Created = time.Now().UTC()
|
|
||||||
sp.Revised = time.Now().UTC()
|
|
||||||
|
|
||||||
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_space (c_refid, c_name, c_orgid, c_userid, c_type, c_lifecycle, c_likes, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_space (c_refid, c_name, c_orgid, c_userid, c_type, c_lifecycle, c_likes, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
||||||
sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Lifecycle, sp.Likes, sp.Created, sp.Revised)
|
sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Lifecycle, sp.Likes, sp.Created, sp.Revised)
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,10 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import Notifier from '../../mixins/notifier';
|
import Notifier from '../../mixins/notifier';
|
||||||
|
import Modal from '../../mixins/modal';
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
|
|
||||||
export default Component.extend(Notifier, {
|
export default Component.extend(Notifier, Modal, {
|
||||||
appMeta: service(),
|
appMeta: service(),
|
||||||
browserSvc: service('browser'),
|
browserSvc: service('browser'),
|
||||||
buttonLabel: 'Start Backup',
|
buttonLabel: 'Start Backup',
|
||||||
|
@ -23,9 +24,9 @@ export default Component.extend(Notifier, {
|
||||||
backupError: false,
|
backupError: false,
|
||||||
backupSuccess: false,
|
backupSuccess: false,
|
||||||
restoreSpec: null,
|
restoreSpec: null,
|
||||||
restoreButtonLabel: 'Perform Restore',
|
restoreButtonLabel: 'Restore',
|
||||||
restoreUploading: false,
|
|
||||||
restoreUploadReady: false,
|
restoreUploadReady: false,
|
||||||
|
confirmRestore: '',
|
||||||
|
|
||||||
didReceiveAttrs() {
|
didReceiveAttrs() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -41,6 +42,7 @@ export default Component.extend(Notifier, {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set('restoreFile', null);
|
this.set('restoreFile', null);
|
||||||
|
this.set('confirmRestore', '');
|
||||||
},
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
|
@ -79,7 +81,26 @@ export default Component.extend(Notifier, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onRestore() {
|
onShowRestoreModal() {
|
||||||
|
this.modalOpen("#confirm-restore-modal", {"show": true}, '#confirm-restore');
|
||||||
|
},
|
||||||
|
|
||||||
|
onRestore(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let typed = this.get('confirmRestore');
|
||||||
|
typed = typed.toLowerCase();
|
||||||
|
|
||||||
|
if (typed !== 'restore' || typed === '') {
|
||||||
|
$("#confirm-restore").addClass("is-invalid").focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('confirmRestore', '');
|
||||||
|
$("#confirm-restore").removeClass("is-invalid");
|
||||||
|
|
||||||
|
this.modalClose('#confirm-restore-modal');
|
||||||
|
|
||||||
// do we have upload file?
|
// do we have upload file?
|
||||||
// let files = document.getElementById("restore-file").files;
|
// let files = document.getElementById("restore-file").files;
|
||||||
// if (is.undefined(files) || is.null(files)) {
|
// if (is.undefined(files) || is.null(files)) {
|
||||||
|
@ -111,17 +132,16 @@ export default Component.extend(Notifier, {
|
||||||
|
|
||||||
this.get('onRestore')(spec, filedata).then(() => {
|
this.get('onRestore')(spec, filedata).then(() => {
|
||||||
this.showDone();
|
this.showDone();
|
||||||
this.set('buttonLabel', 'Perform Restore');
|
this.set('buttonLabel', 'Restore');
|
||||||
this.set('restoreSuccess', true);
|
this.set('restoreSuccess', true);
|
||||||
}, ()=> {
|
}, ()=> {
|
||||||
this.showDone();
|
this.showDone();
|
||||||
this.set('restorButtonLabel', 'Perform Restore');
|
this.set('restorButtonLabel', 'Restore');
|
||||||
this.set('restoreFailed', true);
|
this.set('restoreFailed', true);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
upload(event) {
|
upload(event) {
|
||||||
this.set('restoreUploading', true);
|
|
||||||
this.set('restoreUploadReady', false);
|
this.set('restoreUploadReady', false);
|
||||||
this.set('restoreFile', null);
|
this.set('restoreFile', null);
|
||||||
|
|
||||||
|
@ -130,7 +150,6 @@ export default Component.extend(Notifier, {
|
||||||
|
|
||||||
this.set('restoreFile', file);
|
this.set('restoreFile', file);
|
||||||
this.set('restoreUploadReady', true);
|
this.set('restoreUploadReady', true);
|
||||||
this.set('restoreUploading', false);
|
|
||||||
|
|
||||||
// let imageData;
|
// let imageData;
|
||||||
// reader.onload = () => {
|
// reader.onload = () => {
|
||||||
|
@ -146,3 +165,7 @@ export default Component.extend(Notifier, {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// {{#ui/ui-checkbox selected=restoreSpec.recreateUsers}}
|
||||||
|
// Recreate user accounts — users, groups, permissions
|
||||||
|
// {{/ui/ui-checkbox}}
|
||||||
|
|
|
@ -17,15 +17,11 @@ export default Controller.extend({
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
onBackup(spec) {
|
onBackup(spec) {
|
||||||
if(this.get('session.isAdmin')) {
|
|
||||||
return this.get('global').backup(spec);
|
return this.get('global').backup(spec);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRestore(spec, filedata) {
|
onRestore(spec, filedata) {
|
||||||
if(this.get('session.isAdmin')) {
|
|
||||||
return this.get('global').restore(spec, filedata);
|
return this.get('global').restore(spec, filedata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -144,11 +144,11 @@ export default Service.extend({
|
||||||
|
|
||||||
// Run backup.
|
// Run backup.
|
||||||
backup(spec) {
|
backup(spec) {
|
||||||
if (!this.get('sessionService.isGlobalAdmin') || this.get('sessionService.isAdmin')) {
|
return new EmberPromise((resolve, reject) => {
|
||||||
return;
|
if (!this.get('sessionService.isGlobalAdmin') || !this.get('sessionService.isAdmin')) {
|
||||||
|
reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EmberPromise((resolve, reject) => {
|
|
||||||
let url = this.get('appMeta.endpoint');
|
let url = this.get('appMeta.endpoint');
|
||||||
let token = this.get('sessionService.session.content.authenticated.token');
|
let token = this.get('sessionService.session.content.authenticated.token');
|
||||||
let uploadUrl = `${url}/global/backup?token=${token}`;
|
let uploadUrl = `${url}/global/backup?token=${token}`;
|
||||||
|
@ -196,6 +196,10 @@ export default Service.extend({
|
||||||
data.set('restore-file', file);
|
data.set('restore-file', file);
|
||||||
|
|
||||||
return new EmberPromise((resolve, reject) => {
|
return new EmberPromise((resolve, reject) => {
|
||||||
|
if (!this.get('sessionService.isGlobalAdmin') || !this.get('sessionService.isAdmin')) {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
let url = this.get('appMeta.endpoint');
|
let url = this.get('appMeta.endpoint');
|
||||||
let token = this.get('sessionService.session.content.authenticated.token');
|
let token = this.get('sessionService.session.content.authenticated.token');
|
||||||
let uploadUrl = `${url}/global/restore?token=${token}&org=${spec.overwriteOrg}&users=${spec.recreateUsers}`;
|
let uploadUrl = `${url}/global/restore?token=${token}&org=${spec.overwriteOrg}&users=${spec.recreateUsers}`;
|
||||||
|
|
|
@ -42,10 +42,8 @@
|
||||||
<div class="restore-zone">
|
<div class="restore-zone">
|
||||||
{{#if session.isGlobalAdmin}}
|
{{#if session.isGlobalAdmin}}
|
||||||
<div class="explain">
|
<div class="explain">
|
||||||
<p class="font-weight-bold">WARNING:</p>
|
<p class="font-weight-bold">
|
||||||
<p>
|
You should only perform a restore to an empty Documize instance.
|
||||||
You should only perform a restore on a <b>new Documize instance</b> and NOT on the original instance.
|
|
||||||
Duplicate data might exist if you restore onto the same instance without first removing previous data.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -56,22 +54,10 @@
|
||||||
<input type="file" class="custom-file-input" id="restore-file" accept="application/zip" multiple=false onchange={{action "upload"}}>
|
<input type="file" class="custom-file-input" id="restore-file" accept="application/zip" multiple=false onchange={{action "upload"}}>
|
||||||
<label class="custom-file-label" for="restore-file">Choose backup file</label>
|
<label class="custom-file-label" for="restore-file">Choose backup file</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="restore-upload-busy">
|
|
||||||
{{#if restoreUploadReady}}
|
|
||||||
<div class="ready">Ready to start restore</div>
|
|
||||||
{{/if}}
|
|
||||||
{{#if restoreUploading}}
|
|
||||||
<img src="/assets/img/busy-gray.gif" />
|
|
||||||
<div class="wait">Uploading file</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
<div class="margin-top-20"></div>
|
<div class="margin-top-20"></div>
|
||||||
{{#ui/ui-checkbox selected=restoreSpec.overwriteOrg}}
|
{{#ui/ui-checkbox selected=restoreSpec.overwriteOrg}}
|
||||||
Overwrite settings — SMTP, authentication, integrations and other settings
|
Overwrite settings — SMTP, authentication, integrations and other settings
|
||||||
{{/ui/ui-checkbox}}
|
{{/ui/ui-checkbox}}
|
||||||
{{#ui/ui-checkbox selected=restoreSpec.recreateUsers}}
|
|
||||||
Recreate user accounts — users, groups, permissions
|
|
||||||
{{/ui/ui-checkbox}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if restoreFailed}}
|
{{#if restoreFailed}}
|
||||||
|
@ -80,10 +66,30 @@
|
||||||
<div class="restore-success">Restore completed — restart your browser and log in</div>
|
<div class="restore-success">Restore completed — restart your browser and log in</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if restoreUploadReady}}
|
{{#if restoreUploadReady}}
|
||||||
<button class="btn btn-danger mb-3" {{action 'onRestore'}}>{{restoreButtonLabel}}</button>
|
<button class="btn btn-danger mb-3" {{action 'onShowRestoreModal'}}>{{restoreButtonLabel}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="confirm-restore-modal" class="modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">Confirm Restore</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form onsubmit={{action 'onRestore'}}>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="delete-space-name">Please type RESTORE to commence the process</label>
|
||||||
|
{{input type='text' id="confirm-restore" class="form-control mousetrap" placeholder="Please type RESTORE" value=confirmRestore}}
|
||||||
|
<small class="form-text text-muted">You should only restore to an empty Documize instance</small>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger" onclick={{action 'onRestore'}}>Start Restore</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -13,6 +13,7 @@
|
||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/documize/community/model/org"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/documize/community/core/env"
|
"github.com/documize/community/core/env"
|
||||||
|
@ -67,4 +68,10 @@ type ImportSpec struct {
|
||||||
|
|
||||||
// Recreate users.
|
// Recreate users.
|
||||||
CreateUsers bool `json:"createUsers"`
|
CreateUsers bool `json:"createUsers"`
|
||||||
|
|
||||||
|
// As found in backup file.
|
||||||
|
Manifest Manifest
|
||||||
|
|
||||||
|
// Handle to the current organization being used for restore process.
|
||||||
|
Org org.Organization
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue