FEATURE: uploads are processed a faster

Also cleans up API to always return 422 on upload error. (previously returned 200)

Uploads are processed using new hijack pattern
This commit is contained in:
Sam 2017-11-27 12:43:18 +11:00
parent 71942e4f62
commit eb428ef54d
5 changed files with 82 additions and 87 deletions

View File

@ -416,20 +416,9 @@ export default Ember.Component.extend({
} }
}); });
$element.on("fileuploadfail", (e, data) => { $element.on("fileuploaddone", (e, data) => {
this._resetUpload(true); let upload = data.result;
const userCancelled = this._xhr && this._xhr._userCancelled;
this._xhr = null;
if (!userCancelled) {
displayErrorForUpload(data);
}
});
this.messageBus.subscribe("/uploads/composer", upload => {
// replace upload placeholder
if (upload && upload.url) {
if (!this._xhr || !this._xhr._userCancelled) { if (!this._xhr || !this._xhr._userCancelled) {
const markdown = getUploadMarkdown(upload); const markdown = getUploadMarkdown(upload);
cacheShortUploadUrl(upload.short_url, upload.url); cacheShortUploadUrl(upload.short_url, upload.url);
@ -438,9 +427,16 @@ export default Ember.Component.extend({
} else { } else {
this._resetUpload(true); this._resetUpload(true);
} }
} else { });
$element.on("fileuploadfail", (e, data) => {
this._resetUpload(true); this._resetUpload(true);
displayErrorForUpload(upload);
const userCancelled = this._xhr && this._xhr._userCancelled;
this._xhr = null;
if (!userCancelled) {
displayErrorForUpload(data.jqXHR.responseJSON);
} }
}); });

View File

@ -18,12 +18,9 @@ export default Em.Mixin.create({
uploadUrl = Discourse.getURL(this.getWithDefault("uploadUrl", "/uploads")), uploadUrl = Discourse.getURL(this.getWithDefault("uploadUrl", "/uploads")),
reset = () => this.setProperties({ uploading: false, uploadProgress: 0}); reset = () => this.setProperties({ uploading: false, uploadProgress: 0});
this.messageBus.subscribe("/uploads/" + this.get("type"), upload => { $upload.on("fileuploaddone", (e, data) => {
if (upload && upload.url) { let upload = data.result;
this.uploadDone(upload); this.uploadDone(upload);
} else {
displayErrorForUpload(upload);
}
reset(); reset();
}); });
@ -59,7 +56,7 @@ export default Em.Mixin.create({
}); });
$upload.on("fileuploadfail", (e, data) => { $upload.on("fileuploadfail", (e, data) => {
displayErrorForUpload(data); displayErrorForUpload(data.jqXHR.responseJSON);
reset(); reset();
}); });
}.on("didInsertElement"), }.on("didInsertElement"),

View File

@ -20,19 +20,23 @@ class UploadsController < ApplicationController
file = params[:file] || params[:files]&.first file = params[:file] || params[:files]&.first
pasted = params[:pasted] == "true" pasted = params[:pasted] == "true"
for_private_message = params[:for_private_message] == "true" for_private_message = params[:for_private_message] == "true"
is_api = is_api?
retain_hours = params[:retain_hours].to_i
if params[:synchronous] && (current_user.staff? || is_api?) # note, atm hijack is processed in its own context and has not access to controller
data = create_upload(current_user, file, url, type, for_private_message, pasted) # longer term we may change this
render json: serialize_upload(data) hijack do
else info = UploadsController.create_upload(
Scheduler::Defer.later("Create Upload") do current_user: me,
begin file: file,
data = create_upload(me, file, url, type, for_private_message, pasted) url: url,
ensure type: type,
MessageBus.publish("/uploads/#{type}", serialize_upload(data), client_ids: [params[:client_id]]) for_private_message: for_private_message,
end pasted: pasted,
end is_api: is_api,
render json: success_json retain_hours: retain_hours
)
render json: UploadsController.serialize_upload(info), status: Upload === info ? 200 : 422
end end
end end
@ -72,20 +76,20 @@ class UploadsController < ApplicationController
protected protected
def serialize_upload(data) def render_404
raise Discourse::NotFound
end
def self.serialize_upload(data)
# as_json.as_json is not a typo... as_json in AM serializer returns keys as symbols, we need them # as_json.as_json is not a typo... as_json in AM serializer returns keys as symbols, we need them
# as strings here # as strings here
serialized = UploadSerializer.new(data, root: nil).as_json.as_json if Upload === data serialized = UploadSerializer.new(data, root: nil).as_json.as_json if Upload === data
serialized ||= (data || {}).as_json serialized ||= (data || {}).as_json
end end
def render_404 def self.create_upload(current_user:, file:, url:, type:, for_private_message:, pasted:, is_api:, retain_hours:)
raise Discourse::NotFound
end
def create_upload(current_user, file, url, type, for_private_message, pasted)
if file.nil? if file.nil?
if url.present? && is_api? if url.present? && is_api
maximum_upload_size = [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes maximum_upload_size = [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes
tempfile = FileHelper.download( tempfile = FileHelper.download(
url, url,
@ -112,7 +116,6 @@ class UploadsController < ApplicationController
upload = UploadCreator.new(tempfile, filename, opts).create_for(current_user.id) upload = UploadCreator.new(tempfile, filename, opts).create_for(current_user.id)
if upload.errors.empty? && current_user.admin? if upload.errors.empty? && current_user.admin?
retain_hours = params[:retain_hours].to_i
upload.update_columns(retain_hours: retain_hours) if retain_hours > 0 upload.update_columns(retain_hours: retain_hours) if retain_hours > 0
end end

View File

@ -25,9 +25,17 @@ module Hijack
end end
if opts.key?(:plain) if opts.key?(:plain)
@content_type = 'text/plain' @content_type = 'text/plain; charset=utf-8'
@body = opts[:plain].to_s @body = opts[:plain].to_s
end end
if opts.key?(:json)
@content_type = 'application/json; charset=utf-8'
@body = opts[:json]
unless String === @body
@body = @body.to_json
end
end
end end
end end

View File

@ -40,12 +40,10 @@ describe UploadsController do
it 'is successful with an image' do it 'is successful with an image' do
Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything) Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything)
message = MessageBus.track_publish('/uploads/avatar') do
post :create, params: { file: logo, type: "avatar", format: :json } post :create, params: { file: logo, type: "avatar", format: :json }
end.first
expect(response.status).to eq 200 expect(response.status).to eq 200
expect(message.data["id"]).to be expect(JSON.parse(response.body)["id"]).to be
end end
it 'is successful with an attachment' do it 'is successful with an attachment' do
@ -53,12 +51,11 @@ describe UploadsController do
Jobs.expects(:enqueue).never Jobs.expects(:enqueue).never
message = MessageBus.track_publish('/uploads/composer') do
post :create, params: { file: text_file, type: "composer", format: :json } post :create, params: { file: text_file, type: "composer", format: :json }
end.first
expect(response.status).to eq 200 expect(response.status).to eq 200
expect(message.data["id"]).to be id = JSON.parse(response.body)["id"]
expect(id).to be
end end
it 'is successful with synchronous api' do it 'is successful with synchronous api' do
@ -88,28 +85,25 @@ describe UploadsController do
log_in :admin log_in :admin
Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never
message = MessageBus.track_publish('/uploads/profile_background') do
post :create, params: { post :create, params: {
file: logo, file: logo,
retain_hours: 100, retain_hours: 100,
type: "profile_background", type: "profile_background",
format: :json format: :json
} }
end.first
id = message.data["id"] id = JSON.parse(response.body)["id"]
expect(Upload.find(id).retain_hours).to eq(100) expect(Upload.find(id).retain_hours).to eq(100)
end end
it 'requires a file' do it 'requires a file' do
Jobs.expects(:enqueue).never Jobs.expects(:enqueue).never
message = MessageBus.track_publish('/uploads/composer') do
post :create, params: { type: "composer", format: :json } post :create, params: { type: "composer", format: :json }
end.first
expect(response.status).to eq 200 message = JSON.parse(response.body)
expect(message.data["errors"]).to contain_exactly(I18n.t("upload.file_missing")) expect(response.status).to eq 422
expect(message["errors"]).to contain_exactly(I18n.t("upload.file_missing"))
end end
it 'properly returns errors' do it 'properly returns errors' do
@ -117,12 +111,11 @@ describe UploadsController do
Jobs.expects(:enqueue).never Jobs.expects(:enqueue).never
message = MessageBus.track_publish("/uploads/avatar") do
post :create, params: { file: text_file, type: "avatar", format: :json } post :create, params: { file: text_file, type: "avatar", format: :json }
end.first
expect(response.status).to eq 200 expect(response.status).to eq 422
expect(message.data["errors"]).to be errors = JSON.parse(response.body)["errors"]
expect(errors).to be
end end
it 'ensures allow_uploaded_avatars is enabled when uploading an avatar' do it 'ensures allow_uploaded_avatars is enabled when uploading an avatar' do
@ -142,28 +135,26 @@ describe UploadsController do
SiteSetting.allow_staff_to_upload_any_file_in_pm = true SiteSetting.allow_staff_to_upload_any_file_in_pm = true
@user.update_columns(moderator: true) @user.update_columns(moderator: true)
message = MessageBus.track_publish('/uploads/composer') do
post :create, params: { post :create, params: {
file: text_file, file: text_file,
type: "composer", type: "composer",
for_private_message: "true", for_private_message: "true",
format: :json format: :json
} }
end.first
expect(response).to be_success expect(response).to be_success
expect(message.data["id"]).to be id = JSON.parse(response.body)["id"]
expect(id).to be
end end
it 'returns an error when it could not determine the dimensions of an image' do it 'returns an error when it could not determine the dimensions of an image' do
Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never
message = MessageBus.track_publish('/uploads/composer') do
post :create, params: { file: fake_jpg, type: "composer", format: :json } post :create, params: { file: fake_jpg, type: "composer", format: :json }
end.first
expect(response.status).to eq 200 expect(response.status).to eq 422
expect(message.data["errors"]).to contain_exactly(I18n.t("upload.images.size_not_found")) message = JSON.parse(response.body)["errors"]
expect(message).to contain_exactly(I18n.t("upload.images.size_not_found"))
end end
end end