Skip to content

Commit

Permalink
Merge pull request #6931 from samvera/i865-implement-chunky-uploads
Browse files Browse the repository at this point in the history
Implement chunky uploads
  • Loading branch information
ShanaLMoore authored Oct 25, 2024
2 parents e11d5d0 + 0f75098 commit 583ae3f
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 22 deletions.
38 changes: 20 additions & 18 deletions app/assets/javascripts/hyrax/uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
});
}
Expand Down
44 changes: 42 additions & 2 deletions app/controllers/hyrax/uploads_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,54 @@ 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

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
4 changes: 2 additions & 2 deletions app/views/hyrax/uploads/create.json.jbuilder
Original file line number Diff line number Diff line change
@@ -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
Expand Down
51 changes: 51 additions & 0 deletions spec/controllers/hyrax/uploads_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,71 @@
# 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
expect(assigns(:upload)).to be_kind_of Hyrax::UploadedFile
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
Expand All @@ -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
Expand Down
Binary file added spec/fixtures/different_file.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 583ae3f

Please sign in to comment.