backup & restore client-side code
This commit is contained in:
parent
310a439f3d
commit
babcc3fc50
|
@ -0,0 +1 @@
|
|||
Discourse.AdminBackupsController = Ember.ObjectController.extend({});
|
|
@ -0,0 +1,77 @@
|
|||
Discourse.AdminBackupsIndexController = Ember.ArrayController.extend({
|
||||
needs: ["adminBackups"],
|
||||
status: Em.computed.alias("controllers.adminBackups"),
|
||||
|
||||
rollbackDisabled: Em.computed.not("rollbackEnabled"),
|
||||
|
||||
rollbackEnabled: function() {
|
||||
return this.get("status.canRollback") && this.get("restoreEnabled");
|
||||
}.property("status.canRollback", "restoreEnabled"),
|
||||
|
||||
restoreDisabled: Em.computed.not("restoreEnabled"),
|
||||
|
||||
restoreEnabled: function() {
|
||||
return Discourse.SiteSettings.allow_import && !this.get("status.isOperationRunning");
|
||||
}.property("status.isOperationRunning"),
|
||||
|
||||
restoreTitle: function() {
|
||||
if (!Discourse.SiteSettings.allow_import) {
|
||||
return I18n.t("admin.backups.operations.restore.is_disabled");
|
||||
} else if (this.get("status.isOperationRunning")) {
|
||||
return I18n.t("admin.backups.operation_already_running");
|
||||
} else {
|
||||
return I18n.t("admin.backups.operations.restore.title");
|
||||
}
|
||||
}.property("status.isOperationRunning"),
|
||||
|
||||
destroyTitle: function() {
|
||||
if (this.get("status.isOperationRunning")) {
|
||||
return I18n.t("admin.backups.operation_already_running");
|
||||
} else {
|
||||
return I18n.t("admin.backups.operations.destroy.title");
|
||||
}
|
||||
}.property("status.isOperationRunning"),
|
||||
|
||||
readOnlyModeTitle: function() { return this._readOnlyModeI18n("title"); }.property("Discourse.isReadOnly"),
|
||||
readOnlyModeText: function() { return this._readOnlyModeI18n("text"); }.property("Discourse.isReadOnly"),
|
||||
|
||||
_readOnlyModeI18n: function(value) {
|
||||
var action = Discourse.get("isReadOnly") ? "disable" : "enable";
|
||||
return I18n.t("admin.backups.read_only." + action + "." + value);
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
/**
|
||||
Toggle read-only mode
|
||||
|
||||
@method toggleReadOnlyMode
|
||||
**/
|
||||
toggleReadOnlyMode: function() {
|
||||
var self = this;
|
||||
if (!Discourse.get("isReadOnly")) {
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.backups.read_only.enable.confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) { self._toggleReadOnlyMode(true); }
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this._toggleReadOnlyMode(false);
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
_toggleReadOnlyMode: function(enable) {
|
||||
Discourse.ajax("/admin/backups/readonly", {
|
||||
type: "PUT",
|
||||
data: { enable: enable }
|
||||
}).then(function() {
|
||||
Discourse.set("isReadOnly", enable);
|
||||
});
|
||||
},
|
||||
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
Discourse.AdminBackupsLogsController = Ember.ArrayController.extend({
|
||||
needs: ["adminBackups"],
|
||||
status: Em.computed.alias("controllers.adminBackups"),
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
Data model for representing a backup
|
||||
|
||||
@class Backup
|
||||
@extends Discourse.Model
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.Backup = Discourse.Model.extend({
|
||||
|
||||
/**
|
||||
Destroys the current backup
|
||||
|
||||
@method destroy
|
||||
@returns {Promise} a promise that resolves when the backup has been destroyed
|
||||
**/
|
||||
destroy: function() {
|
||||
return Discourse.ajax("/admin/backups/" + this.get("filename"), { type: "DELETE" });
|
||||
},
|
||||
|
||||
/**
|
||||
Starts the restoration of the current backup
|
||||
|
||||
@method restore
|
||||
@returns {Promise} a promise that resolves when the backup has started being restored
|
||||
**/
|
||||
restore: function() {
|
||||
return Discourse.ajax("/admin/backups/" + this.get("filename") + "/restore", { type: "POST" });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Discourse.Backup.reopenClass({
|
||||
|
||||
/**
|
||||
Finds a list of backups
|
||||
|
||||
@method find
|
||||
@returns {Promise} a promise that resolves to the array of {Discourse.Backup} backup
|
||||
**/
|
||||
find: function() {
|
||||
return PreloadStore.getAndRemove("backups", function() {
|
||||
return Discourse.ajax("/admin/backups.json");
|
||||
}).then(function(backups) {
|
||||
return backups.map(function (backup) { return Discourse.Backup.create(backup); });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Starts a backup
|
||||
|
||||
@method start
|
||||
@returns {Promise} a promise that resolves when the backup has started
|
||||
**/
|
||||
start: function() {
|
||||
return Discourse.ajax("/admin/backups", { type: "POST" }).then(function(result) {
|
||||
if (!result.success) { bootbox.alert(result.message); }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Cancels a backup
|
||||
|
||||
@method cancel
|
||||
@returns {Promise} a promise that resolves when the backup has been cancelled
|
||||
**/
|
||||
cancel: function() {
|
||||
return Discourse.ajax("/admin/backups/cancel.json").then(function(result) {
|
||||
if (!result.success) { bootbox.alert(result.message); }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Rollbacks the database to the previous working state
|
||||
|
||||
@method rollback
|
||||
@returns {Promise} a promise that resolves when the rollback is done
|
||||
**/
|
||||
rollback: function() {
|
||||
return Discourse.ajax("/admin/backups/rollback.json").then(function(result) {
|
||||
if (!result.success) {
|
||||
bootbox.alert(result.message);
|
||||
} else {
|
||||
// redirect to homepage (session might be lost)
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
Discourse.AdminBackupsIndexRoute = Discourse.Route.extend({
|
||||
|
||||
model: function() {
|
||||
return Discourse.Backup.find();
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,150 @@
|
|||
Discourse.AdminBackupsRoute = Discourse.Route.extend({
|
||||
|
||||
LOG_CHANNEL: "/admin/backups/logs",
|
||||
|
||||
activate: function() {
|
||||
Discourse.MessageBus.subscribe(this.LOG_CHANNEL, this._processLogMessage.bind(this));
|
||||
},
|
||||
|
||||
_processLogMessage: function(log) {
|
||||
if (log.message === "[STARTED]") {
|
||||
this.controllerFor("adminBackups").set("isOperationRunning", true);
|
||||
this.controllerFor("adminBackupsLogs").clear();
|
||||
} else if (log.message === "[FAILED]") {
|
||||
this.controllerFor("adminBackups").set("isOperationRunning", false);
|
||||
bootbox.alert(I18n.t("admin.backups.operations.failed", { operation: log.operation }));
|
||||
} else if (log.message === "[SUCCESS]") {
|
||||
this.controllerFor("adminBackups").set("isOperationRunning", false);
|
||||
if (log.operation === "restore") {
|
||||
// redirect to homepage when the restore is done (session might be lost)
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
}
|
||||
} else {
|
||||
this.controllerFor("adminBackupsLogs").pushObject(Em.Object.create(log));
|
||||
}
|
||||
},
|
||||
|
||||
model: function() {
|
||||
return PreloadStore.getAndRemove("operations_status", function() {
|
||||
return Discourse.ajax("/admin/backups/status.json");
|
||||
}).then(function (status) {
|
||||
return Em.Object.create({
|
||||
isOperationRunning: status.is_operation_running,
|
||||
canRollback: status.can_rollback,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deactivate: function() {
|
||||
Discourse.MessageBus.unsubscribe(this.LOG_CHANNEL);
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
Starts a backup and redirect the user to the logs tab
|
||||
|
||||
@method startBackup
|
||||
**/
|
||||
startBackup: function() {
|
||||
var self = this;
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.backups.operations.backup.confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
Discourse.Backup.start().then(function() {
|
||||
self.controllerFor("adminBackupsLogs").clear();
|
||||
self.controllerFor("adminBackups").set("isOperationRunning", true);
|
||||
self.transitionTo("admin.backups.logs");
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
Destroys a backup
|
||||
|
||||
@method destroyBackup
|
||||
@param {Discourse.Backup} the backup to destroy
|
||||
**/
|
||||
destroyBackup: function(backup) {
|
||||
var self = this;
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.backups.operations.destroy.confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
backup.destroy().then(function() {
|
||||
self.controllerFor("adminBackupsIndex").removeObject(backup);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
Start a restore and redirect the user to the logs tab
|
||||
|
||||
@method startRestore
|
||||
@param {Discourse.Backup} the backup to restore
|
||||
**/
|
||||
startRestore: function(backup) {
|
||||
var self = this;
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.backups.operations.restore.confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
backup.restore().then(function() {
|
||||
self.controllerFor("adminBackupsLogs").clear();
|
||||
self.controllerFor("adminBackups").set("isOperationRunning", true);
|
||||
self.transitionTo("admin.backups.logs");
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
Cancels the current operation
|
||||
|
||||
@method cancelOperation
|
||||
**/
|
||||
cancelOperation: function() {
|
||||
var self = this;
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.backups.operations.cancel.confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
Discourse.Backup.cancel().then(function() {
|
||||
self.controllerFor("adminBackups").set("isOperationRunning", false);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
Rollback to previous working state
|
||||
|
||||
@method rollback
|
||||
**/
|
||||
rollback: function() {
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.backups.operations.rollback.confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) { Discourse.Backup.rollback(); }
|
||||
}
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
});
|
|
@ -18,6 +18,7 @@
|
|||
{{#if currentUser.admin}}
|
||||
<li>{{#link-to 'admin.customize'}}{{i18n admin.customize.title}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'admin.api'}}{{i18n admin.api.title}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'admin.backups'}}{{i18n admin.backups.title}}{{/link-to}}</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<div class="admin-controls">
|
||||
<div class="span15">
|
||||
<ul class="nav nav-pills">
|
||||
<li>{{#link-to "admin.backups.index"}}{{i18n admin.backups.menu.backups}}{{/link-to}}</li>
|
||||
<li>{{#link-to "admin.backups.logs"}}{{i18n admin.backups.menu.logs}}{{/link-to}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{{#if isOperationRunning}}
|
||||
<button {{action cancelOperation}} class="btn btn-danger" title="{{i18n admin.backups.operations.cancel.title}}"><i class="fa fa-times"></i>{{i18n admin.backups.operations.cancel.text}}</button>
|
||||
{{else}}
|
||||
<button {{action startBackup}} class="btn btn-primary" title="{{i18n admin.backups.operations.backup.title}}"><i class="fa fa-rocket"></i>{{i18n admin.backups.operations.backup.text}}</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-container">
|
||||
{{outlet}}
|
||||
</div>
|
|
@ -0,0 +1,29 @@
|
|||
<table>
|
||||
<tr>
|
||||
<th width="40%">{{i18n admin.backups.columns.filename}}</th>
|
||||
<th width="30%">{{i18n admin.backups.columns.size}}</th>
|
||||
<th>
|
||||
{{#if status.canRollback}}
|
||||
<button {{action rollback}} class="btn btn-rollback" title="{{i18n admin.backups.operations.rollback.title}}" {{bind-attr disabled=rollbackDisabled}}><i class="fa fa-ambulance fa-flip-horizontal"></i>{{i18n admin.backups.operations.rollback.text}}</button>
|
||||
{{/if}}
|
||||
<button {{action toggleReadOnlyMode}} class="btn" {{bind-attr title=readOnlyModeTitle}}><i class="fa fa-eye"></i>{{readOnlyModeText}}</button>
|
||||
</th>
|
||||
</tr>
|
||||
{{#each backup in model}}
|
||||
<tr>
|
||||
<td>{{backup.filename}}</td>
|
||||
<td>{{humanSize backup.size}}</td>
|
||||
<td>
|
||||
<a {{bind-attr href="backup.link"}} class="btn download" title="{{i18n admin.backups.operations.download.title}}"><i class="fa fa-download"></i>{{i18n admin.backups.operations.download.text}}</a>
|
||||
<button {{action destroyBackup backup}} class="btn btn-danger" {{bind-attr disabled="status.isOperationRunning" title="destroyTitle"}}><i class="fa fa-trash-o"></i>{{i18n admin.backups.operations.destroy.text}}</button>
|
||||
<button {{action startRestore backup}} class="btn" {{bind-attr disabled="restoreDisabled" title="restoreTitle"}}><i class="fa fa-undo"></i>{{i18n admin.backups.operations.restore.text}}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>{{i18n admin.backups.none}}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
|
@ -0,0 +1,50 @@
|
|||
Discourse.AdminBackupsLogsView = Discourse.View.extend({
|
||||
|
||||
classNames: ["admin-backups-logs"],
|
||||
|
||||
_initialize: function() { this._reset(); }.on("init"),
|
||||
|
||||
_reset: function() {
|
||||
this.setProperties({ formattedLogs: "", index: 0 });
|
||||
},
|
||||
|
||||
_updateFormattedLogs: function() {
|
||||
var logs = this.get("controller.model");
|
||||
if (logs.length === 0) {
|
||||
this._reset(); // reset the cached logs whenever the model is reset
|
||||
} else {
|
||||
// do the log formatting only once for HELLish performance
|
||||
var formattedLogs = this.get("formattedLogs");
|
||||
for (var i = this.get("index"), length = logs.length; i < length; i++) {
|
||||
var date = moment(logs[i].get("timestamp")).format("YYYY-MM-DD HH:mm:ss"),
|
||||
message = Handlebars.Utils.escapeExpression(logs[i].get("message"));
|
||||
formattedLogs += "[" + date + "] " + message + "\n";
|
||||
}
|
||||
// update the formatted logs & cache index
|
||||
this.setProperties({ formattedLogs: formattedLogs, index: logs.length });
|
||||
// force rerender
|
||||
this.rerender();
|
||||
}
|
||||
}.observes("controller.model.@each"),
|
||||
|
||||
render: function(buffer) {
|
||||
var formattedLogs = this.get("formattedLogs");
|
||||
if (formattedLogs && formattedLogs.length > 0) {
|
||||
buffer.push("<pre>");
|
||||
buffer.push(formattedLogs);
|
||||
buffer.push("</pre>");
|
||||
} else {
|
||||
buffer.push("<p>" + I18n.t("admin.backups.logs.none") + "</p>");
|
||||
}
|
||||
// add a loading indicator
|
||||
if (this.get("controller.status.isOperationRunning")) {
|
||||
buffer.push("<i class='fa fa-spinner fa-spin'></i>");
|
||||
}
|
||||
},
|
||||
|
||||
_forceScrollToBottom: function() {
|
||||
var $div = this.$()[0];
|
||||
$div.scrollTop = $div.scrollHeight;
|
||||
}.on("didInsertElement")
|
||||
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
Discourse.AdminBackupsView = Discourse.View.extend({
|
||||
classNames: ["admin-backups"],
|
||||
|
||||
_hijackDownloads: function() {
|
||||
this.$().on("mouseup.admin-backups", "a.download", function (e) {
|
||||
var $link = $(e.currentTarget);
|
||||
|
||||
if (!$link.data("href")) {
|
||||
$link.addClass("no-href");
|
||||
$link.data("href", $link.attr("href"));
|
||||
$link.attr("href", null);
|
||||
$link.data("auto-route", true);
|
||||
}
|
||||
|
||||
Discourse.URL.redirectTo($link.data("href"));
|
||||
});
|
||||
}.on("didInsertElement"),
|
||||
|
||||
_removeBindings: function() {
|
||||
this.$().off("mouseup.admin-backups");
|
||||
}.on("willDestroyElement")
|
||||
|
||||
});
|
Loading…
Reference in New Issue