FEATURE: Support backup uploads/downloads directly to/from S3.

This commit is contained in:
Gerhard Schlager
2018-09-10 16:48:34 +02:00
committed by Guo Xiang Tan
parent 5039a6c3f1
commit c29a4dddc1
52 changed files with 1079 additions and 420 deletions

View File

@@ -1,9 +1,15 @@
import { ajax } from "discourse/lib/ajax";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend({
adminBackups: Ember.inject.controller(),
status: Ember.computed.alias("adminBackups.model"),
@computed
localBackupStorage() {
return this.siteSettings.backup_location === "local";
},
uploadLabel: function() {
return I18n.t("admin.backups.upload.label");
}.property(),

View File

@@ -1,5 +1,4 @@
import { ajax } from "discourse/lib/ajax";
import PreloadStore from "preload-store";
const Backup = Discourse.Model.extend({
destroy() {
@@ -16,9 +15,9 @@ const Backup = Discourse.Model.extend({
Backup.reopenClass({
find() {
return PreloadStore.getAndRemove("backups", () =>
ajax("/admin/backups.json")
).then(backups => backups.map(backup => Backup.create(backup)));
return ajax("/admin/backups.json").then(backups =>
backups.map(backup => Backup.create(backup))
);
},
start(withUploads) {

View File

@@ -151,6 +151,15 @@ export default Discourse.Route.extend({
message: message
})
);
},
remoteUploadSuccess() {
Backup.find().then(backups => {
this.controllerFor("adminBackupsIndex").set(
"model",
backups.map(backup => Backup.create(backup))
);
});
}
}
});

View File

@@ -1,5 +1,10 @@
<div class="backup-options">
{{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title"}}
{{#if localBackupStorage}}
{{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title"}}
{{else}}
{{backup-uploader done="remoteUploadSuccess"}}
{{/if}}
{{#if site.isReadOnly}}
{{d-button icon="eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}}
{{else}}
@@ -10,7 +15,7 @@
<thead>
<th width="55%">{{i18n 'admin.backups.columns.filename'}}</th>
<th width="10%">{{i18n 'admin.backups.columns.size'}}</th>
<th></th>
<th></th>
</thead>
<tbody>
{{#each model as |backup|}}

View File

@@ -1,7 +1,7 @@
<label class="btn {{if addDisabled 'disabled'}}">
{{d-icon "upload"}}
{{i18n 'admin.watched_words.form.upload'}}
<input disabled={{addDisabled}} type="file" accept="text/plain,text/csv" style="visibility: hidden; position: absolute;" />
<input class="hidden-upload-field" disabled={{addDisabled}} type="file" accept="text/plain,text/csv" />
</label>
<br/>
<span class="instructions">One word per line</span>

View File

@@ -0,0 +1,51 @@
import { ajax } from "discourse/lib/ajax";
import computed from "ember-addons/ember-computed-decorators";
import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, {
tagName: "span",
@computed("uploading", "uploadProgress")
uploadButtonText(uploading, progress) {
return uploading
? I18n.t("admin.backups.upload.uploading_progress", { progress })
: I18n.t("admin.backups.upload.label");
},
validateUploadedFilesOptions() {
return { skipValidation: true };
},
uploadDone() {
this.sendAction("done");
},
calculateUploadUrl() {
return "";
},
uploadOptions() {
return {
type: "PUT",
dataType: "xml",
autoUpload: false
};
},
_init: function() {
const $upload = this.$();
$upload.on("fileuploadadd", (e, data) => {
ajax("/admin/backups/upload_url", {
data: { filename: data.files[0].name }
}).then(result => {
if (!result.success) {
bootbox.alert(result.message);
} else {
data.url = result.url;
data.submit();
}
});
});
}.on("didInsertElement")
});

View File

@@ -225,6 +225,7 @@ export function validateUploadedFiles(files, opts) {
}
export function validateUploadedFile(file, opts) {
if (opts.skipValidation) return true;
if (!authorizesOneOrMoreExtensions()) return false;
opts = opts || {};

View File

@@ -2,6 +2,7 @@ import {
displayErrorForUpload,
validateUploadedFiles
} from "discourse/lib/utilities";
import getUrl from "discourse-common/lib/get-url";
export default Em.Mixin.create({
uploading: false,
@@ -15,13 +16,25 @@ export default Em.Mixin.create({
return {};
},
calculateUploadUrl() {
return (
getUrl(this.getWithDefault("uploadUrl", "/uploads")) +
".json?client_id=" +
this.messageBus.clientId +
"&authenticity_token=" +
encodeURIComponent(Discourse.Session.currentProp("csrfToken"))
);
},
uploadOptions() {
return {};
},
_initialize: function() {
const $upload = this.$(),
csrf = Discourse.Session.currentProp("csrfToken"),
uploadUrl = Discourse.getURL(
this.getWithDefault("uploadUrl", "/uploads")
),
reset = () => this.setProperties({ uploading: false, uploadProgress: 0 });
const $upload = this.$();
const reset = () =>
this.setProperties({ uploading: false, uploadProgress: 0 });
const maxFiles = this.getWithDefault("maxFiles", 10);
$upload.on("fileuploaddone", (e, data) => {
let upload = data.result;
@@ -29,20 +42,21 @@ export default Em.Mixin.create({
reset();
});
$upload.fileupload({
url:
uploadUrl +
".json?client_id=" +
this.messageBus.clientId +
"&authenticity_token=" +
encodeURIComponent(csrf),
dataType: "json",
dropZone: $upload,
pasteZone: $upload
});
$upload.fileupload(
_.merge(
{
url: this.calculateUploadUrl(),
dataType: "json",
replaceFileInput: false,
dropZone: $upload,
pasteZone: $upload
},
this.uploadOptions()
)
);
$upload.on("fileuploaddrop", (e, data) => {
if (data.files.length > 10) {
if (data.files.length > maxFiles) {
bootbox.alert(I18n.t("post.errors.too_many_dragged_and_dropped_files"));
return false;
} else {
@@ -56,7 +70,8 @@ export default Em.Mixin.create({
this.validateUploadedFilesOptions()
);
const isValid = validateUploadedFiles(data.files, opts);
let form = { type: this.get("type") };
const type = this.get("type");
let form = type ? { type } : {};
if (this.get("data")) {
form = $.extend(form, this.get("data"));
}

View File

@@ -1,6 +1,6 @@
<label class="btn" disabled={{uploading}} title="{{i18n 'user.change_avatar.upload_title'}}">
{{d-icon "picture-o"}}&nbsp;{{uploadButtonText}}
<input disabled={{uploading}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" />
<input class="hidden-upload-field" disabled={{uploading}} type="file" accept="image/*" />
</label>
{{#if uploading}}
<span>{{i18n 'upload_selector.uploading'}} {{uploadProgress}}%</span>

View File

@@ -0,0 +1,4 @@
<label class="btn" disabled={{uploading}} title="{{i18n 'admin.backups.upload.title'}}">
{{d-icon "upload"}}&nbsp;{{uploadButtonText}}
<input class="hidden-upload-field" disabled={{uploading}} type="file" accept=".gz" />
</label>

View File

@@ -1,6 +1,6 @@
<label class="btn" disabled={{uploadButtonDisabled}}>
{{d-icon "upload"}}&nbsp;{{uploadButtonText}}
<input disabled={{uploading}} type="file" accept=".csv" style="visibility: hidden; position: absolute;" />
<input class="hidden-upload-field" disabled={{uploading}} type="file" accept=".csv" />
</label>
{{#if uploading}}
<span>{{i18n 'upload_selector.uploading'}} {{uploadProgress}}%</span>

View File

@@ -2,5 +2,5 @@
<label class="btn btn-primary {{if addDisabled 'disabled'}}">
{{d-icon "plus"}}
{{i18n 'admin.emoji.add'}}
<input disabled={{addDisabled}} type="file" accept=".png,.gif" style="visibility: hidden; position: absolute;" />
<input class="hidden-upload-field" disabled={{addDisabled}} type="file" accept=".png,.gif" />
</label>

View File

@@ -2,7 +2,7 @@
<div class="image-upload-controls">
<label class="btn pad-left no-text {{if uploading 'disabled'}}">
{{d-icon "picture-o"}}
<input disabled={{uploading}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" />
<input class="hidden-upload-field" disabled={{uploading}} type="file" accept="image/*" />
</label>
{{#if backgroundStyle}}
<button {{action "trash"}} class="btn btn-danger pad-left no-text">{{d-icon "trash-o"}}</button>

View File

@@ -1,6 +1,6 @@
<label class="btn" disabled={{uploading}} title="{{i18n "admin.site_settings.uploaded_image_list.upload.title"}}">
{{d-icon "picture-o"}}&nbsp;{{uploadButtonText}}
<input disabled={{uploading}} type="file" accept="image/*" multiple style="visibility: hidden; position: absolute;" />
<input class="hidden-upload-field" disabled={{uploading}} type="file" accept="image/*" multiple />
</label>
{{#if uploading}}
<span>{{i18n 'upload_selector.uploading'}} {{uploadProgress}}%</span>

View File

@@ -10,5 +10,5 @@
{{d-icon "picture-o"}}
{{/if}}
<input disabled={{uploading}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" />
<input class="wizard-hidden-upload-field" disabled={{uploading}} type="file" accept="image/*" />
</label>

View File

@@ -14,3 +14,8 @@
background-size: contain;
}
}
.hidden-upload-field {
visibility: hidden;
position: absolute;
}

View File

@@ -328,6 +328,11 @@ body.wizard {
}
}
.wizard-hidden-upload-field {
visibility: hidden;
position: absolute;
}
.wizard-step-footer {
display: flex;
flex-direction: row;