Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement chunky uploads #6931

Merged
merged 7 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading