diff --git a/app/assets/javascripts/hyrax/uploader.js b/app/assets/javascripts/hyrax/uploader.js index 9b7a93981f..8cbc5cd565 100644 --- a/app/assets/javascripts/hyrax/uploader.js +++ b/app/assets/javascripts/hyrax/uploader.js @@ -10,7 +10,6 @@ * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan - * https://blueimp.net * * Licensed under the MIT license: * http://www.opensource.org/licenses/MIT @@ -21,45 +20,48 @@ $.fn.extend({ hyraxUploader: function( options ) { - // Initialize our jQuery File Upload widget. this.fileupload($.extend({ - // xhrFields: {withCredentials: true}, // to send cross-domain cookies - // acceptFileTypes: /(\.|\/)(png|mov|jpe?g|pdf)$/i, // not a strong check, just a regex on the filename - // limitMultiFileUploadSize: 500000000, // bytes + maxChunkSize: 10000000, // 10 MB chunk size autoUpload: true, url: '/uploads/', type: 'POST', - dropZone: $(this).find('.dropzone') + dropZone: $(this).find('.dropzone'), + add: function (e, data) { + var that = this; + $.post('/uploads/', { files: [data.files[0].name] }, function (result) { + data.formData = {id: result.files[0].id}; + $.blueimp.fileupload.prototype.options.add.call(that, e, data); + }); + } }, Hyrax.config.uploader, options)) .on('fileuploadadded', function (e, data) { $(e.currentTarget).find('button.cancel').removeAttr("hidden"); }); - $(document).on('dragover', function(e) { var dropZone = $('.dropzone'), timeout = window.dropZoneTimeout; if (!timeout) { - dropZone.addClass('in'); + dropZone.addClass('in'); } else { - clearTimeout(timeout); + clearTimeout(timeout); } var found = false, node = e.target; do { - if (node === dropZone[0]) { - found = true; - break; - } - node = node.parentNode; + if (node === dropZone[0]) { + found = true; + break; + } + node = node.parentNode; } while (node !== null); if (found) { - dropZone.addClass('hover'); + dropZone.addClass('hover'); } else { - dropZone.removeClass('hover'); + dropZone.removeClass('hover'); } window.dropZoneTimeout = setTimeout(function () { - window.dropZoneTimeout = null; - dropZone.removeClass('in hover'); + window.dropZoneTimeout = null; + dropZone.removeClass('in hover'); }, 100); }); } diff --git a/app/controllers/hyrax/uploads_controller.rb b/app/controllers/hyrax/uploads_controller.rb index 8583e3d8d3..cb744dcfab 100644 --- a/app/controllers/hyrax/uploads_controller.rb +++ b/app/controllers/hyrax/uploads_controller.rb @@ -4,8 +4,11 @@ class UploadsController < ApplicationController load_and_authorize_resource class: Hyrax::UploadedFile def create - @upload.attributes = { file: params[:files].first, - user: current_user } + if params[:id].blank? + handle_new_upload + else + handle_chunked_upload + end @upload.save! end @@ -13,5 +16,42 @@ def destroy @upload.destroy head :no_content end + + private + + def handle_new_upload + @upload.attributes = { file: params[:files].first, user: current_user } + end + + def handle_chunked_upload + @upload = Hyrax::UploadedFile.find(params[:id]) + unpersisted_upload = Hyrax::UploadedFile.new(file: params[:files].first, user: current_user) + + if chunk_valid?(@upload) + append_chunk(@upload) + else + replace_file(@upload, unpersisted_upload) + end + end + + def chunk_valid?(upload) + current_size = upload.file.size + content_range = request.headers['CONTENT-RANGE'] + + return false unless content_range + + begin_of_chunk = content_range[/\ (.*?)-/, 1].to_i + upload.file.present? && begin_of_chunk == current_size + end + + def append_chunk(upload) + File.open(upload.file.path, "ab") { |f| f.write(params[:files].first.read) } + end + + def replace_file(upload, unpersisted_upload) + upload.file = unpersisted_upload.file + upload.save! + upload.reload + end end end diff --git a/app/views/hyrax/uploads/create.json.jbuilder b/app/views/hyrax/uploads/create.json.jbuilder index 1db266cabe..58a16b5b35 100644 --- a/app/views/hyrax/uploads/create.json.jbuilder +++ b/app/views/hyrax/uploads/create.json.jbuilder @@ -1,8 +1,8 @@ # frozen_string_literal: true json.files [@upload] do |uploaded_file| json.id uploaded_file.id - json.name uploaded_file.file.file.filename - json.size uploaded_file.file.file.size + json.name uploaded_file.file&.file&.filename + json.size uploaded_file.file&.file&.size # TODO: implement these # json.url "/uploads/#{uploaded_file.id}" # json.thumbnail_url uploaded_file.id diff --git a/spec/controllers/hyrax/uploads_controller_spec.rb b/spec/controllers/hyrax/uploads_controller_spec.rb index a27e9d324b..095531951e 100644 --- a/spec/controllers/hyrax/uploads_controller_spec.rb +++ b/spec/controllers/hyrax/uploads_controller_spec.rb @@ -1,14 +1,17 @@ # frozen_string_literal: true RSpec.describe Hyrax::UploadsController do let(:user) { create(:user) } + let(:chunk) { fixture_file_upload('/world.png', 'image/png') } describe "#create" do let(:file) { fixture_file_upload('/world.png', 'image/png') } + let!(:existing_file) { create(:uploaded_file, file: file, user: user) } context "when signed in" do before do sign_in user end + it "is successful" do post :create, params: { files: [file], format: 'json' } expect(response).to be_successful @@ -16,6 +19,53 @@ expect(assigns(:upload)).to be_persisted expect(assigns(:upload).user).to eq user end + + context "when uploading in chunks" do + it "appends chunks when they are valid" do + original_file = fixture_file_upload('/world.png', 'image/png') + post :create, params: { files: [original_file], format: 'json' } + original_upload = assigns(:upload) + + request.headers['CONTENT-RANGE'] = 'bytes 0-99/1000' + new_chunk = fixture_file_upload('/world.png', 'image/png') + post :create, params: { files: [new_chunk], id: original_upload.id, format: 'json' } + + original_upload.reload + expect(original_upload.file.size).to eq(File.size(original_upload.file.path)) + end + + it "replaces file if chunks are mismatched" do + original_file = fixture_file_upload('/world.png', 'image/png') + post :create, params: { files: [original_file], format: 'json' } + original_upload = assigns(:upload) + original_content = File.read(original_upload.file.path) + + request.headers['CONTENT-RANGE'] = 'bytes 101-200/1000' + different_chunk = fixture_file_upload('/different_file.png', 'image/png') + post :create, params: { files: [different_chunk], id: original_upload.id, format: 'json' } + + original_upload.reload + new_content = File.read(original_upload.file.path) + + expect(new_content).not_to eq(original_content) + end + + it "updates the file size after replacing mismatched chunks" do + original_file = fixture_file_upload('/world.png', 'image/png') + post :create, params: { files: [original_file], format: 'json' } + original_upload = assigns(:upload) + original_size = original_upload.file.size + + request.headers['CONTENT-RANGE'] = 'bytes 101-200/1000' + different_chunk = fixture_file_upload('/different_file.png', 'image/png') + post :create, params: { files: [different_chunk], id: original_upload.id, format: 'json' } + + original_upload.reload + new_size = original_upload.file.size + + expect(new_size).not_to eq(original_size) + end + end end context "when not signed in" do @@ -34,6 +84,7 @@ before do sign_in user end + it "destroys the uploaded file" do delete :destroy, params: { id: uploaded_file } expect(response.status).to eq 204 diff --git a/spec/fixtures/different_file.png b/spec/fixtures/different_file.png new file mode 100644 index 0000000000..aa1eb92f57 Binary files /dev/null and b/spec/fixtures/different_file.png differ