mirror of
https://github.com/discourse/discourse.git
synced 2025-02-21 23:49:39 +08:00
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"),
|
||||||
|
});
|
90
app/assets/javascripts/admin/models/backup.js
Normal file
90
app/assets/javascripts/admin/models/backup.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
150
app/assets/javascripts/admin/routes/admin_backups_route.js
Normal file
150
app/assets/javascripts/admin/routes/admin_backups_route.js
Normal file
@ -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}}
|
{{#if currentUser.admin}}
|
||||||
<li>{{#link-to 'admin.customize'}}{{i18n admin.customize.title}}{{/link-to}}</li>
|
<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.api'}}{{i18n admin.api.title}}{{/link-to}}</li>
|
||||||
|
<li>{{#link-to 'admin.backups'}}{{i18n admin.backups.title}}{{/link-to}}</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
19
app/assets/javascripts/admin/templates/backups.js.handlebars
Normal file
19
app/assets/javascripts/admin/templates/backups.js.handlebars
Normal file
@ -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")
|
||||||
|
|
||||||
|
});
|
23
app/assets/javascripts/admin/views/admin_backups_view.js
Normal file
23
app/assets/javascripts/admin/views/admin_backups_view.js
Normal file
@ -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…
x
Reference in New Issue
Block a user