From 516140dd7e32af8853f23c4c74e092058f9c6412 Mon Sep 17 00:00:00 2001 From: sauls8t Date: Mon, 15 Oct 2018 18:59:21 +0100 Subject: [PATCH] [WIP] Provide system restore facility Co-Authored-By: Harvey Kandola --- domain/audit/store.go | 1 - domain/backup/endpoint.go | 23 +- domain/backup/restore.go | 498 +++++++++++++++++- domain/organization/store.go | 6 +- domain/space/endpoint.go | 4 + domain/space/space_test.go | 6 + domain/space/store.go | 4 - .../components/customize/backup-restore.js | 39 +- gui/app/pods/customize/backup/controller.js | 8 +- gui/app/services/global.js | 12 +- .../components/customize/backup-restore.hbs | 40 +- model/backup/backup.go | 7 + 12 files changed, 597 insertions(+), 51 deletions(-) diff --git a/domain/audit/store.go b/domain/audit/store.go index 7fed1038..d48f1558 100644 --- a/domain/audit/store.go +++ b/domain/audit/store.go @@ -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 (?, ?, ?, ?, ?)"), e.OrgID, e.UserID, e.Type, e.IP, e.Created) - if err != nil { tx.Rollback() s.Runtime.Log.Error("prepare audit insert", err) diff --git a/domain/backup/endpoint.go b/domain/backup/endpoint.go index 4c56ce48..f11f204f 100644 --- a/domain/backup/endpoint.go +++ b/domain/backup/endpoint.go @@ -180,7 +180,28 @@ func (h *Handler) Restore(w http.ResponseWriter, r *http.Request) { 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) } diff --git a/domain/backup/restore.go b/domain/backup/restore.go index d8c7d568..84d2a333 100644 --- a/domain/backup/restore.go +++ b/domain/backup/restore.go @@ -12,9 +12,497 @@ // Package backup handle data backup/restore to/from ZIP format. package backup -// DESIGN -// ------ -// -// The restore operation allows an admin to upload a backup file +// The restore operation allows an admin to upload a backup file. +// ID, created and revised attributes values are maintained as per backup. -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 +} diff --git a/domain/organization/store.go b/domain/organization/store.go index 11368bde..ab571119 100644 --- a/domain/organization/store.go +++ b/domain/organization/store.go @@ -32,11 +32,7 @@ type Store struct { // AddOrganization inserts the passed organization record into the organization table. func (s Store) AddOrganization(ctx domain.RequestContext, org org.Organization) (err error) { - org.Created = time.Now().UTC() - 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), + _, 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), strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.MaxTags, org.Created, org.Revised) diff --git a/domain/space/endpoint.go b/domain/space/endpoint.go index e0b75b9f..f23b33a1 100644 --- a/domain/space/endpoint.go +++ b/domain/space/endpoint.go @@ -20,6 +20,7 @@ import ( "io/ioutil" "net/http" "strings" + "time" "github.com/documize/community/core/env" "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.Type = space.ScopePrivate 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) if err != nil { diff --git a/domain/space/space_test.go b/domain/space/space_test.go index fe3093f8..041694c3 100644 --- a/domain/space/space_test.go +++ b/domain/space/space_test.go @@ -31,6 +31,9 @@ func TestSpace(t *testing.T) { sp.UserID = ctx.UserID sp.Type = space.ScopePublic sp.Name = "PublicTestSpace" + sp.UserID = ctx.UserID + sp.Created = time.Now().UTC() + sp.Revised = time.Now().UTC() err = s.Space.Add(ctx, sp) if err != nil { @@ -98,6 +101,9 @@ func TestSpace(t *testing.T) { sp2.OrgID = ctx.OrgID sp2.Type = space.ScopePrivate sp2.Name = "PrivateTestSpace" + sp.UserID = ctx.UserID + sp.Created = time.Now().UTC() + sp.Revised = time.Now().UTC() err = s.Space.Add(ctx, sp2) if err != nil { diff --git a/domain/space/store.go b/domain/space/store.go index 91c56bcd..c91a9e92 100644 --- a/domain/space/store.go +++ b/domain/space/store.go @@ -30,10 +30,6 @@ type Store struct { // Add adds new folder into the store. 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)"), sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Lifecycle, sp.Likes, sp.Created, sp.Revised) diff --git a/gui/app/components/customize/backup-restore.js b/gui/app/components/customize/backup-restore.js index 11506d68..a9a07ec3 100644 --- a/gui/app/components/customize/backup-restore.js +++ b/gui/app/components/customize/backup-restore.js @@ -12,9 +12,10 @@ import $ from 'jquery'; import { inject as service } from '@ember/service'; import Notifier from '../../mixins/notifier'; +import Modal from '../../mixins/modal'; import Component from '@ember/component'; -export default Component.extend(Notifier, { +export default Component.extend(Notifier, Modal, { appMeta: service(), browserSvc: service('browser'), buttonLabel: 'Start Backup', @@ -23,9 +24,9 @@ export default Component.extend(Notifier, { backupError: false, backupSuccess: false, restoreSpec: null, - restoreButtonLabel: 'Perform Restore', - restoreUploading: false, + restoreButtonLabel: 'Restore', restoreUploadReady: false, + confirmRestore: '', didReceiveAttrs() { this._super(...arguments); @@ -41,6 +42,7 @@ export default Component.extend(Notifier, { }); this.set('restoreFile', null); + this.set('confirmRestore', ''); }, 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? // let files = document.getElementById("restore-file").files; // if (is.undefined(files) || is.null(files)) { @@ -111,17 +132,16 @@ export default Component.extend(Notifier, { this.get('onRestore')(spec, filedata).then(() => { this.showDone(); - this.set('buttonLabel', 'Perform Restore'); + this.set('buttonLabel', 'Restore'); this.set('restoreSuccess', true); }, ()=> { this.showDone(); - this.set('restorButtonLabel', 'Perform Restore'); + this.set('restorButtonLabel', 'Restore'); this.set('restoreFailed', true); }); }, upload(event) { - this.set('restoreUploading', true); this.set('restoreUploadReady', false); this.set('restoreFile', null); @@ -130,7 +150,6 @@ export default Component.extend(Notifier, { this.set('restoreFile', file); this.set('restoreUploadReady', true); - this.set('restoreUploading', false); // let imageData; // 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}} diff --git a/gui/app/pods/customize/backup/controller.js b/gui/app/pods/customize/backup/controller.js index 9e1fe559..1f2d4145 100644 --- a/gui/app/pods/customize/backup/controller.js +++ b/gui/app/pods/customize/backup/controller.js @@ -17,15 +17,11 @@ export default Controller.extend({ actions: { onBackup(spec) { - if(this.get('session.isAdmin')) { - return this.get('global').backup(spec); - } + return this.get('global').backup(spec); }, onRestore(spec, filedata) { - if(this.get('session.isAdmin')) { - return this.get('global').restore(spec, filedata); - } + return this.get('global').restore(spec, filedata); } } }); diff --git a/gui/app/services/global.js b/gui/app/services/global.js index c8416af7..8acabb6d 100644 --- a/gui/app/services/global.js +++ b/gui/app/services/global.js @@ -144,11 +144,11 @@ export default Service.extend({ // Run backup. backup(spec) { - if (!this.get('sessionService.isGlobalAdmin') || this.get('sessionService.isAdmin')) { - return; - } - return new EmberPromise((resolve, reject) => { + if (!this.get('sessionService.isGlobalAdmin') || !this.get('sessionService.isAdmin')) { + reject(); + } + let url = this.get('appMeta.endpoint'); let token = this.get('sessionService.session.content.authenticated.token'); let uploadUrl = `${url}/global/backup?token=${token}`; @@ -196,6 +196,10 @@ export default Service.extend({ data.set('restore-file', file); return new EmberPromise((resolve, reject) => { + if (!this.get('sessionService.isGlobalAdmin') || !this.get('sessionService.isAdmin')) { + reject(); + } + let url = this.get('appMeta.endpoint'); let token = this.get('sessionService.session.content.authenticated.token'); let uploadUrl = `${url}/global/restore?token=${token}&org=${spec.overwriteOrg}&users=${spec.recreateUsers}`; diff --git a/gui/app/templates/components/customize/backup-restore.hbs b/gui/app/templates/components/customize/backup-restore.hbs index ecb12277..f9d97c99 100644 --- a/gui/app/templates/components/customize/backup-restore.hbs +++ b/gui/app/templates/components/customize/backup-restore.hbs @@ -42,10 +42,8 @@
{{#if session.isGlobalAdmin}}
-

WARNING:

-

- You should only perform a restore on a new Documize instance and NOT on the original instance. - Duplicate data might exist if you restore onto the same instance without first removing previous data. +

+ You should only perform a restore to an empty Documize instance.

{{/if}} @@ -56,22 +54,10 @@
-
- {{#if restoreUploadReady}} -
Ready to start restore
- {{/if}} - {{#if restoreUploading}} - -
Uploading file
- {{/if}} -
{{#ui/ui-checkbox selected=restoreSpec.overwriteOrg}} Overwrite settings — SMTP, authentication, integrations and other settings {{/ui/ui-checkbox}} - {{#ui/ui-checkbox selected=restoreSpec.recreateUsers}} - Recreate user accounts — users, groups, permissions - {{/ui/ui-checkbox}} {{#if restoreFailed}} @@ -80,10 +66,30 @@
Restore completed — restart your browser and log in
{{else}} {{#if restoreUploadReady}} - + {{/if}} {{/if}} + + \ No newline at end of file diff --git a/model/backup/backup.go b/model/backup/backup.go index 110016ca..3899ca6a 100644 --- a/model/backup/backup.go +++ b/model/backup/backup.go @@ -13,6 +13,7 @@ package backup import ( + "github.com/documize/community/model/org" "time" "github.com/documize/community/core/env" @@ -67,4 +68,10 @@ type ImportSpec struct { // Recreate users. CreateUsers bool `json:"createUsers"` + + // As found in backup file. + Manifest Manifest + + // Handle to the current organization being used for restore process. + Org org.Organization }