diff --git a/app/components/file_upload_component.rb b/app/components/file_upload_component.rb index 67b7205ed..894cd6d4a 100644 --- a/app/components/file_upload_component.rb +++ b/app/components/file_upload_component.rb @@ -2,7 +2,7 @@ # A component for uploading files to Danbooru. Used on the /uploads/new page. class FileUploadComponent < ApplicationComponent - attr_reader :url, :referer_url, :drop_target, :max_file_size + attr_reader :url, :referer_url, :drop_target, :max_file_size, :max_files_per_upload # @param url [String] Optional. The URL to upload. If present, the URL field # will be prefilled in the widget and the upload will be immediately triggered. @@ -11,11 +11,13 @@ class FileUploadComponent < ApplicationComponent # events. If "body", then files can be dropped anywhere on the page, not # just on the upload widget itself. # @param max_file_size [Integer] The max size in bytes of an upload. - def initialize(url: nil, referer_url: nil, drop_target: nil, max_file_size: Danbooru.config.max_file_size) + # @param max_files_per_upload [Integer] The maximum number of files per upload. + def initialize(url: nil, referer_url: nil, drop_target: nil, max_file_size: Danbooru.config.max_file_size, max_files_per_upload: Upload::MAX_FILES_PER_UPLOAD) @url = url @referer_url = referer_url @drop_target = drop_target @max_file_size = max_file_size + @max_files_per_upload = max_files_per_upload super end end diff --git a/app/components/file_upload_component/file_upload_component.html.erb b/app/components/file_upload_component/file_upload_component.html.erb index 4570360fc..98f024ae2 100644 --- a/app/components/file_upload_component/file_upload_component.html.erb +++ b/app/components/file_upload_component/file_upload_component.html.erb @@ -1,8 +1,8 @@ -
+
<%= simple_form_for(Upload.new, url: uploads_path(format: :json), html: { class: "flex flex-col", autocomplete: "off" }, remote: true) do |f| %> - <%= f.input :file, as: :file, wrapper_html: { class: "hidden" } %> + <%= f.input :files, as: :file, wrapper_html: { class: "hidden" }, input_html: { "multiple": max_files_per_upload > 1 } %> -
+
Choose file or drag image here
Max size: <%= number_to_human_size(Danbooru.config.max_file_size) %>.
@@ -19,21 +19,17 @@ <%= spinner_icon(class: "hidden animate-spin absolute inset-0 m-auto h-12 link-color") %>
diff --git a/app/components/file_upload_component/file_upload_component.scss b/app/components/file_upload_component/file_upload_component.scss index cede94190..9ad96b006 100644 --- a/app/components/file_upload_component/file_upload_component.scss +++ b/app/components/file_upload_component/file_upload_component.scss @@ -31,4 +31,8 @@ background-color: var(--uploads-dropzone-progress-bar-foreground-color); } } + + &.dz-error .dz-error-message { + display: block; + } } diff --git a/app/javascript/src/javascripts/file_upload_component.js b/app/javascript/src/javascripts/file_upload_component.js index 66981c1c0..f4f356d0d 100644 --- a/app/javascript/src/javascripts/file_upload_component.js +++ b/app/javascript/src/javascripts/file_upload_component.js @@ -33,16 +33,19 @@ export default class FileUploadComponent { let dropzone = new Dropzone(this.$dropTarget.get(0), { url: "/uploads.json", - paramName: "upload[file]", + paramName: "upload[files]", clickable: this.$dropzone.get(0), previewsContainer: this.$dropzone.get(0), thumbnailHeight: null, thumbnailWidth: null, addRemoveLinks: false, - maxFiles: 1, + parallelUploads: this.maxFiles, + maxFiles: this.maxFiles, maxFilesize: this.maxFileSize, maxThumbnailFilesize: this.maxFileSize, timeout: 0, + uploadMultiple: true, + createImageThumbnails: false, acceptedFiles: "image/jpeg,image/png,image/gif,video/mp4,video/webm", previewTemplate: this.$component.find(".dropzone-preview-template").html(), }); @@ -54,13 +57,6 @@ export default class FileUploadComponent { dropzone.on("addedfile", file => { this.$dropzone.removeClass("error"); this.$dropzone.find(".dropzone-hint").hide(); - - // Remove all files except the file just added. - dropzone.files.forEach(f => { - if (f !== file) { - dropzone.removeFile(f); - } - }); }); dropzone.on("success", file => { @@ -70,7 +66,9 @@ export default class FileUploadComponent { }); dropzone.on("error", (file, msg) => { - this.$dropzone.addClass("error"); + this.$dropzone.find(".dropzone-hint").show(); + dropzone.removeFile(file); + Utility.error(msg); }); return dropzone; @@ -164,6 +162,10 @@ export default class FileUploadComponent { return Number(this.$component.attr("data-max-file-size")) / (1024 * 1024); } + get maxFiles() { + return Number(this.$component.attr("data-max-files-per-upload")); + } + // The element to listen for drag and drop events and paste events. By default, // it's the `.file-upload-component` element. If `data-drop-target` is the `body` // element, then you can drop images or paste URLs anywhere on the page. diff --git a/app/models/upload.rb b/app/models/upload.rb index 539e1e7aa..21581cb5c 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -4,7 +4,9 @@ class Upload < ApplicationRecord extend Memoist class Error < StandardError; end - attr_accessor :file + MAX_FILES_PER_UPLOAD = 100 + + attr_accessor :files belongs_to :uploader, class_name: "User" has_many :upload_media_assets, dependent: :destroy @@ -13,6 +15,7 @@ class Upload < ApplicationRecord normalize :source, :normalize_source + validates :files, length: { maximum: MAX_FILES_PER_UPLOAD, message: "can't have more than #{MAX_FILES_PER_UPLOAD} files per upload" } validates :source, format: { with: %r{\Ahttps?://}i, message: "is not a valid URL" }, if: -> { source.present? } validates :referer_url, format: { with: %r{\Ahttps?://}i, message: "is not a valid URL" }, if: -> { referer_url.present? } validate :validate_file_and_source, on: :create @@ -55,9 +58,9 @@ class Upload < ApplicationRecord concerning :ValidationMethods do def validate_file_and_source - if file.present? && source.present? + if files.present? && source.present? errors.add(:base, "Can't give both a file and a source") - elsif file.blank? && source.blank? + elsif files.blank? && source.blank? errors.add(:base, "No file or source given") end end @@ -86,8 +89,8 @@ class Upload < ApplicationRecord end def async_process_upload! - if file.present? - ProcessUploadJob.perform_now(self) + if files.present? + process_upload! elsif source.present? ProcessUploadJob.perform_later(self) else @@ -98,12 +101,10 @@ class Upload < ApplicationRecord def process_upload! update!(status: "processing") - if file.present? - media_file = MediaFile.open(file.tempfile) - media_asset = MediaAsset.upload!(media_file) - upload_media_asset = UploadMediaAsset.new(media_asset: media_asset, source_url: "file://#{file.original_filename}", status: "active") - - update!(upload_media_assets: [upload_media_asset], status: "completed", media_asset_count: 1) + if files.present? + upload_media_assets = files.map do |_index, file| + UploadMediaAsset.new(file: file.tempfile, source_url: "file://#{file.original_filename}") + end elsif source.present? page_url = source_strategy.page_url image_urls = source_strategy.image_urls @@ -115,11 +116,11 @@ class Upload < ApplicationRecord upload_media_assets = image_urls.map do |image_url| UploadMediaAsset.new(source_url: image_url, page_url: page_url, media_asset: nil) end - - update!(upload_media_assets: upload_media_assets, media_asset_count: upload_media_assets.size) else raise Error, "No file or source given" # Should never happen end + + update!(upload_media_assets: upload_media_assets, media_asset_count: upload_media_assets.size) rescue Exception => e update!(status: "error", error: e.message) end diff --git a/app/models/upload_media_asset.rb b/app/models/upload_media_asset.rb index 3521dd2b7..3cdbc591f 100644 --- a/app/models/upload_media_asset.rb +++ b/app/models/upload_media_asset.rb @@ -3,6 +3,8 @@ class UploadMediaAsset < ApplicationRecord extend Memoist + attr_accessor :file + belongs_to :upload belongs_to :media_asset, optional: true has_one :post, through: :media_asset @@ -62,16 +64,22 @@ class UploadMediaAsset < ApplicationRecord end def async_process_upload! - return if file_upload? - ProcessUploadMediaAssetJob.perform_later(self) + if file.present? + process_upload! + else + ProcessUploadMediaAssetJob.perform_later(self) + end end def process_upload! - return if file_upload? update!(status: :processing) - strategy = Sources::Strategies.find(source_url) - media_file = strategy.download_file!(source_url) + if file.present? + media_file = MediaFile.open(file) + else + media_file = source_strategy.download_file!(source_url) + end + MediaAsset.upload!(media_file) do |media_asset| update!(media_asset: media_asset) end diff --git a/app/policies/upload_policy.rb b/app/policies/upload_policy.rb index 70f787092..8c70c1606 100644 --- a/app/policies/upload_policy.rb +++ b/app/policies/upload_policy.rb @@ -18,6 +18,6 @@ class UploadPolicy < ApplicationPolicy end def permitted_attributes - %i[file source referer_url] + [:source, :referer_url, files: {}] end end diff --git a/test/factories/upload.rb b/test/factories/upload.rb index 31940aa64..e2c040c14 100644 --- a/test/factories/upload.rb +++ b/test/factories/upload.rb @@ -17,7 +17,7 @@ FactoryBot.define do status { "completed" } source { nil } media_asset_count { 1 } - file { Rack::Test::UploadedFile.new("#{Rails.root}/test/files/test.jpg") } + files { { "0" => Rack::Test::UploadedFile.new("#{Rails.root}/test/files/test.jpg") } } upload_media_assets do [build(:upload_media_asset, media_asset: build(:media_asset, file: "test/files/test.jpg"), source_url: "file://test.jpg", status: "active")] diff --git a/test/functional/uploads_controller_test.rb b/test/functional/uploads_controller_test.rb index 66dd62999..00b251620 100644 --- a/test/functional/uploads_controller_test.rb +++ b/test/functional/uploads_controller_test.rb @@ -162,7 +162,7 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest assert_no_difference("Upload.count") do file = File.open("test/files/test.jpg") source = "https://files.catbox.moe/om3tcw.webm" - post_auth uploads_path(format: :json), @user, params: { upload: { file: file, source: source }} + post_auth uploads_path(format: :json), @user, params: { upload: { files: { "0" => file }, source: source }} end assert_response 422 @@ -171,7 +171,7 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest should "fail if given an unsupported filetype" do file = Rack::Test::UploadedFile.new("test/files/ugoira.json") - post_auth uploads_path(format: :json), @user, params: { upload: { file: file }} + post_auth uploads_path(format: :json), @user, params: { upload: { files: { "0" => file } }} assert_response 201 assert_match("File is not an image or video", Upload.last.error) @@ -247,12 +247,9 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest context "when re-uploading a media asset stuck in the 'processing' state" do should "mark the asset as failed" do asset = create(:media_asset, file: File.open("test/files/test.jpg"), status: "processing") - file = Rack::Test::UploadedFile.new("test/files/test.jpg") + create_upload!("test/files/test.jpg", user: @user) - post_auth uploads_path, @user, params: { upload: { file: file }} upload = Upload.last - - assert_redirected_to upload assert_match("Upload failed, try again", upload.reload.error) assert_equal("failed", asset.reload.status) end @@ -287,6 +284,24 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest # should_upload_successfully("test/files/compressed.swf") end + context "uploading multiple files from your computer" do + should "work" do + files = { + "0" => Rack::Test::UploadedFile.new("test/files/test.jpg"), + "1" => Rack::Test::UploadedFile.new("test/files/test.png"), + "2" => Rack::Test::UploadedFile.new("test/files/test.gif"), + } + + post_auth uploads_path(format: :json), @user, params: { upload: { files: files }} + + upload = Upload.last + assert_response 201 + assert_equal("", upload.error.to_s) + assert_equal("completed", upload.status) + assert_equal(3, upload.media_asset_count) + end + end + context "uploading a file from a source" do should_upload_successfully("https://www.artstation.com/artwork/04XA4") should_upload_successfully("https://dantewontdie.artstation.com/projects/YZK5q") diff --git a/test/test_helpers/upload_test_helper.rb b/test/test_helpers/upload_test_helper.rb index ddab1ce11..775a38513 100644 --- a/test/test_helpers/upload_test_helper.rb +++ b/test/test_helpers/upload_test_helper.rb @@ -7,7 +7,7 @@ module UploadTestHelper source = { source: source_or_file_path } else file = Rack::Test::UploadedFile.new(Rails.root.join(source_or_file_path)) - source = { file: file } + source = { files: { "0" => file } } end perform_enqueued_jobs do