Im using jquery-fileupload-rails
and paperclip
Im trying to get video files to upload asynchronously. I keep receiving internal server error
Migration
class CreateCourses < ActiveRecord::Migration[5.1]
def change
create_table :courses do |t|
t.string :name, null: false
t.string :status, null: false, index: true
t.attachment :upload
t.boolean :visible, default: false, null: false
t.timestamps null: false
end
end
end
Model
class Course < ActiveRecord::Base
# Variables
COURSE_STATUSES = %w(new uploading uploaded)
# Validations
validates :name, presence: true
validates :status, inclusion: { in: COURSE_STATUSES }
has_attached_file :upload, styles: {
:medium => {
:geometry => "640x480",
:format => 'mp4'
},
:thumb => { :geometry => "160x120", :format => 'jpeg', :time => 10}
}, :processors => [:transcoder]
def to_jq_upload(error=nil)
{
files: [
{
name: read_attribute(:upload_file_name),
size: read_attribute(:upload_file_size),
url: upload.url(:original),
delete_url: Rails.application.routes.url_helpers.course_path(self),
delete_type: "DELETE"
}
]
}
end
def all_formats_encoded?
self.webm_file.path && self.mp4_file.path && self.ogg_file.path ? true : false
end
def upload_done?
status.in? %w(uploaded)
end
def run_encoders
if video_file?
Mp4VideoEncoder.perform_async(self.id)
OgvVideoEncoder.perform_async(self.id)
WebmVideoEncoder.perform_async(self.id)
end
end
end
Controller
class CoursesController < ApplicationController
before_action :set_course, only: [:show, :edit, :update, :destroy, :upload, :do_upload, :resume_upload, :update_status, :reset_upload]
# GET /courses
# GET /courses.json
def index
@courses = Course.all.order(name: :asc)
end
# GET /courses/1
# GET /courses/1.json
def show
# If upload is not commenced or finished, redirect to upload page
return redirect_to upload_course_path(@course) if @course.status.in?(%w(new uploading))
end
# GET /courses/new
def new
@course = Course.new
end
# GET /courses/1/edit
def edit
end
# POST /courses
# POST /courses.json
def create
@course = Course.new(course_params)
@course.status = 'new'
respond_to do |format|
if @course.save
format.html { redirect_to course_path(@course), notice: 'Course was successfully created.' }
format.json { render :show, status: :created, location: @course }
else
format.html { render :new }
format.json { render json: @course.errors, status: :unprocessable_entity }
end
end
end
def update
@course.assign_attributes(status: 'new', upload: nil) if params[:delete_upload] == 'yes'
respond_to do |format|
if @course.update(course_params)
format.html { redirect_to @course, notice: 'Course was successfully updated.' }
format.json { render :show, status: :ok, location: @course }
else
format.html { render :edit }
format.json { render json: @course.errors, status: :unprocessable_entity }
end
end
end
# DELETE /courses/1
# DELETE /courses/1.json
def destroy
@course.destroy
respond_to do |format|
format.html { redirect_to courses_url, notice: 'Course was successfully destroyed.' }
format.json { head :no_content }
end
end
# GET /courses/:id/upload
def upload
end
# PATCH /courses/:id/upload.json
def do_upload
unpersisted_course = Course.new(upload_params)
# If no file has been uploaded or the uploaded file has a different filename,
# do a new upload from scratch
@course.assign_attributes(upload_params)
@course.status = 'uploading'
@course.save!
render json: @course.to_jq_upload and return
# If the already uploaded file has the same filename, try to resume
current_size = @course.upload_file_size
content_range = request.headers['CONTENT-RANGE']
begin_of_chunk = content_range[/\ (.*?)-/,1].to_i # "bytes 100-999999/1973660678" will return '100'
# If the there is a mismatch between the size of the incomplete upload and the content-range in the
# headers, then it's the wrong chunk!
# In this case, start the upload from scratch
unless begin_of_chunk == current_size
@course.update!(upload_params)
render json: @course.to_jq_upload and return
end
# Add the following chunk to the incomplete upload
File.open(@course.upload.path, "ab") { |f| f.write(upload_params[:upload, :image].read) }
# Update the upload_file_size attribute
@course.upload_file_size = @course.upload_file_size.nil? ? unpersisted_course.upload_file_size : @course.upload_file_size + unpersisted_course.upload_file_size
@course.save!
render json: @course.to_jq_upload and return
end
# GET /courses/:id/reset_upload
def reset_upload
# Allow users to delete uploads only if they are incomplete
raise StandardError, "Action not allowed" unless @course.status == 'uploading'
@course.update!(status: 'new', upload: nil)
redirect_to @course, notice: "Upload reset successfully. You can now start over"
end
# GET /courses/:id/resume_upload.json
def resume_upload
render json: { file: { name: @course.upload.url(:default, timestamp: false), size: @course.upload_file_size } } and return
end
# PATCH /courses/:id/update_upload_status
def update_status
raise ArgumentError, "Wrong status provided " + params[:status] unless @course.status == 'uploading' && params[:status] == 'uploaded'
@course.update!(status: params[:status])
head :ok
end
private
# Use callbacks to share common setup or constraints between actions.
def set_course
@course = Course.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def course_params
params.require(:course).permit(:name, :image, :upload)
end
def upload_params
params.require(:course).permit(:upload, :image)
end
end
Views upload.html.haml
%h1
Upload a file
%hr
= form_for @course, url: upload_course_path, html: { multipart: true, id: "fileupload" } do |f|
.row
%h3
Course name
.padding-left
= @course.name
.row.empty-space
.row
- unless @course.status == 'uploading'
.alert.alert-success.smaller-text
To begin an upload, please first select a file from your computer.
- else
.alert.alert-warning.smaller-text
The system detected an unfinished upload. To resume it, please select the same file and press the green button. <br />
Resume not working or receiving error messages? Then please click #{link_to 'here', reset_upload_course_path(@course)} to delete the temoporary file and start over.
.row.fileupload-buttonbar
#js-data{ data: { course: @course.id } }
.col-lg-7
.btn.btn-primary.fileinput-button
%i.glyphicon.glyphicon-plus
Select a file
= f.file_field :upload
%table.table.table-striped#js-file-container{ role: "presentation" }
%tbody.files
:javascript
$(document).ready(function () {
var course_id = $('#js-data').data('course');
var controller_name = $('body').data('controller');
var model_instance_path = '/' + controller_name + '/' + course_id;
var resume_path = model_instance_path + '/resume_upload';
var update_status_path = model_instance_path + '/update_status';
$('#fileupload').fileupload({
maxNumberOfFiles: 3,
maxChunkSize: 10000000, // 10 MB
type: 'PATCH',
add: function (e, data) {
var that = this;
$.getJSON(resume_path, { file: data.files[0].name }, function (result) {
var file = result.file;
data.uploadedBytes = file && file.size;
$.blueimp.fileupload.prototype
.options.add.call(that, e, data);
});
}
});
$('#fileupload')
.bind('fileuploadchunkdone', function (e, data) {
var perc = parseInt(data.loaded / data.total * 100, 10);
$('#js-completed').html(perc + "%");
})
.bind('fileuploaddone', function (e, data) {
$.ajax({
url: update_status_path,
type: "PATCH",
data: { status: 'uploaded'},
success: function(data) {
window.location.replace(model_instance_path);
}
});
});
});
:plain
<script id="template-upload" type="text/x-tmpl">
{% for (var i=0, file; file=o.files[i]; i++) { %}
<tr class="template-upload fade">
<td width="45%">
<p class="name"><h3>{%=file.name%}</h3></p>
<p class="size">Processing...</p>
<strong class="error text-danger"></strong>
{% if (!i && !o.options.autoUpload) { %}
<button class="btn btn-success start" disabled>
<i class="glyphicon glyphicon-upload"></i>
Start / resume
</button>
{% } %}
{% if (!i) { %}
<button class="btn btn-danger cancel">
<i class="glyphicon glyphicon-ban-circle"></i>
Cancel upload
</button>
{% } %}
</td>
<td width="45%" style="vertical-align: middle;">
<div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="progress-bar progress-bar-success" style="width:0%;"></div></div>
<p><h1 id="js-completed" align="center"></h1></p>
</td>
</tr>
{% } %}
</script>
<script id="template-download" type="text/x-tmpl">
{% for (var i=0, file; file=o.files[i]; i++) { %}
<tr class="template-download fade">
<td>
<span class="preview">
{% if (file.thumbnailUrl) { %}
<a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" data-gallery><img src="{%=file.thumbnailUrl%}"></a>
{% } %}
</span>
</td>
<td>
<p class="name">
{% if (file.url) { %}
<a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" {%=file.thumbnailUrl?'data-gallery':''%}>{%=file.name%}</a>
{% } else { %}
<span>{%=file.name%}</span>
{% } %}
</p>
{% if (file.error) { %}
<div><span class="label label-danger">Error</span> {%=file.error%}</div>
{% } %}
</td>
<td>
<span class="size">{%=o.formatFileSize(file.size)%}</span>
</td>
<td>
{% if (file.deleteUrl) { %}
<button class="btn btn-danger delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}>
<i class="glyphicon glyphicon-trash"></i>
<span>Delete</span>
</button>
<input type="checkbox" name="delete" value="1" class="toggle">
{% } else { %}
<button class="btn btn-warning cancel">
<i class="glyphicon glyphicon-ban-circle"></i>
<span>Cancel</span>
</button>
{% } %}
</td>
</tr>
{% } %}
</script>
I added a worker as well, and have sidekiq run in the background
mp4_video_encoder.rb
class Mp4VideoEncoder
include Sidekiq::Worker
def perform(video_id)
video = Video.find(video_id)
path = video.video_file.path
output = "/tmp/#{Time.now.getutc.to_f.to_s.delete('.')}.mp4"
_command = `ffmpeg -i #{path} -f mp4 -vcodec h264 -acodec aac -strict -2 #{output}`
if $?.to_i == 0
video.mp4_file = File.open(output, 'r')
video.save
FileUtils.rm(output)
else
raise $?
end
end
end
Here is my error logs
Started GET "/" for ::1 at 2017-03-14 17:21:28 -0400
Processing by CoursesController#index as HTML
Rendering courses/index.html.haml within layouts/application
Course Load (2.6ms) SELECT "courses".* FROM "courses" ORDER BY "courses"."name" ASC
Rendered courses/index.html.haml within layouts/application (61.3ms)
Rendered shared/_topbar.html.haml (3.9ms)
Completed 200 OK in 694ms (Views: 613.6ms | ActiveRecord: 2.6ms | Solr: 0.0ms)
Started GET "/courses/16/resume_upload?file=Flipping_Physics_Trailer.mp4" for ::1 at 2017-03-14 17:22:48 -0400
Processing by CoursesController#resume_upload as JSON
Parameters: {"file"=>"Flipping_Physics_Trailer.mp4", "id"=>"16"}
Course Load (1.3ms) SELECT "courses".* FROM "courses" WHERE "courses"."id" = ? LIMIT ? [["id", 16], ["LIMIT", 1]]
Completed 200 OK in 27ms (Views: 2.4ms | ActiveRecord: 1.3ms | Solr: 0.0ms)
Started PATCH "/courses/16/upload" for ::1 at 2017-03-14 17:22:50 -0400
Processing by CoursesController#do_upload as JSON
Parameters: {"utf8"=>"✓", "authenticity_token"=>"EqpP5O6LeE5/gIzmbA2nTNwyl6I0GryZlcenPbIWJGlIZbJ6+cZ/e1qhijxoP38vVIEFszo5RY7JVOucxbt3lg==", "course"=>{"upload"=>#<ActionDispatch::Http::UploadedFile:0x007fbd960130a0 @tempfile=#<Tempfile:/var/folders/j3/q9ym0gbj15l1w2d7_1x16nqc0000gn/T/RackMultipart20170314-69035-176nt7b.mp4>, @original_filename="Flipping_Physics_Trailer.mp4", @content_type="video/mp4", @headers="Content-Disposition: form-data; name=\"course[upload]\"; filename=\"Flipping_Physics_Trailer.mp4\"\r\nContent-Type: video/mp4\r\n">}, "id"=>"16"}
Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 16ms (ActiveRecord: 0.0ms)
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
actionpack (5.0.2) lib/action_controller/metal/request_forgery_protection.rb:195:in `handle_unverified_request'
actionpack (5.0.2) lib/action_controller/metal/request_forgery_protection.rb:223:in `handle_unverified_request'
devise (4.2.0) lib/devise/controllers/helpers.rb:253:in `handle_unverified_request'
actionpack (5.0.2) lib/action_controller/metal/request_forgery_protection.rb:218:in `verify_authenticity_token'
activesupport (5.0.2) lib/active_support/callbacks.rb:382:in `block in make_lambda'
activesupport (5.0.2) lib/active_support/callbacks.rb:169:in `block (2 levels) in halting'
actionpack (5.0.2) lib/abstract_controller/callbacks.rb:12:in `block (2 levels) in <module:Callbacks>'
activesupport (5.0.2) lib/active_support/callbacks.rb:170:in `block in halting'
activesupport (5.0.2) lib/active_support/callbacks.rb:454:in `block in call'
activesupport (5.0.2) lib/active_support/callbacks.rb:454:in `each'
activesupport (5.0.2) lib/active_support/callbacks.rb:454:in `call'
activesupport (5.0.2) lib/active_support/callbacks.rb:101:in `__run_callbacks__'
activesupport (5.0.2) lib/active_support/callbacks.rb:750:in `_run_process_action_callbacks'
activesupport (5.0.2) lib/active_support/callbacks.rb:90:in `run_callbacks'
actionpack (5.0.2) lib/abstract_controller/callbacks.rb:19:in `process_action'
actionpack (5.0.2) lib/action_controller/metal/rescue.rb:20:in `process_action'
actionpack (5.0.2) lib/action_controller/metal/instrumentation.rb:32:in `block in process_action'
activesupport (5.0.2) lib/active_support/notifications.rb:164:in `block in instrument'
activesupport (5.0.2) lib/active_support/notifications/instrumenter.rb:21:in `instrument'
activesupport (5.0.2) lib/active_support/notifications.rb:164:in `instrument'
actionpack (5.0.2) lib/action_controller/metal/instrumentation.rb:30:in `process_action'
actionpack (5.0.2) lib/action_controller/metal/params_wrapper.rb:248:in `process_action'
searchkick (2.1.1) lib/searchkick/logging.rb:209:in `process_action'
activerecord (5.0.2) lib/active_record/railties/controller_runtime.rb:18:in `process_action'
actionpack (5.0.2) lib/abstract_controller/base.rb:126:in `process'
actionview (5.0.2) lib/action_view/rendering.rb:30:in `process'
actionpack (5.0.2) lib/action_controller/metal.rb:190:in `dispatch'
actionpack (5.0.2) lib/action_controller/metal.rb:262:in `dispatch'
actionpack (5.0.2) lib/action_dispatch/routing/route_set.rb:50:in `dispatch'
actionpack (5.0.2) lib/action_dispatch/routing/route_set.rb:32:in `serve'
actionpack (5.0.2) lib/action_dispatch/journey/router.rb:39:in `block in serve'
actionpack (5.0.2) lib/action_dispatch/journey/router.rb:26:in `each'
actionpack (5.0.2) lib/action_dispatch/journey/router.rb:26:in `serve'
actionpack (5.0.2) lib/action_dispatch/routing/route_set.rb:725:in `call'
warden (1.2.7) lib/warden/manager.rb:36:in `block in call'
warden (1.2.7) lib/warden/manager.rb:35:in `catch'
warden (1.2.7) lib/warden/manager.rb:35:in `call'
rack (2.0.1) lib/rack/etag.rb:25:in `call'
rack (2.0.1) lib/rack/conditional_get.rb:38:in `call'
rack (2.0.1) lib/rack/head.rb:12:in `call'
rack (2.0.1) lib/rack/session/abstract/id.rb:222:in `context'
rack (2.0.1) lib/rack/session/abstract/id.rb:216:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/cookies.rb:613:in `call'
activerecord (5.0.2) lib/active_record/migration.rb:553:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/callbacks.rb:38:in `block in call'
activesupport (5.0.2) lib/active_support/callbacks.rb:97:in `__run_callbacks__'
activesupport (5.0.2) lib/active_support/callbacks.rb:750:in `_run_call_callbacks'
activesupport (5.0.2) lib/active_support/callbacks.rb:90:in `run_callbacks'
actionpack (5.0.2) lib/action_dispatch/middleware/callbacks.rb:36:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/executor.rb:12:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/remote_ip.rb:79:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/debug_exceptions.rb:49:in `call'
web-console (3.4.0) lib/web_console/middleware.rb:135:in `call_app'
web-console (3.4.0) lib/web_console/middleware.rb:28:in `block in call'
web-console (3.4.0) lib/web_console/middleware.rb:18:in `catch'
web-console (3.4.0) lib/web_console/middleware.rb:18:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/show_exceptions.rb:31:in `call'
railties (5.0.2) lib/rails/rack/logger.rb:36:in `call_app'
railties (5.0.2) lib/rails/rack/logger.rb:24:in `block in call'
activesupport (5.0.2) lib/active_support/tagged_logging.rb:69:in `block in tagged'
activesupport (5.0.2) lib/active_support/tagged_logging.rb:26:in `tagged'
activesupport (5.0.2) lib/active_support/tagged_logging.rb:69:in `tagged'
railties (5.0.2) lib/rails/rack/logger.rb:24:in `call'
sprockets-rails (3.2.0) lib/sprockets/rails/quiet_assets.rb:13:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/request_id.rb:24:in `call'
rack (2.0.1) lib/rack/method_override.rb:22:in `call'
rack (2.0.1) lib/rack/runtime.rb:22:in `call'
activesupport (5.0.2) lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/executor.rb:12:in `call'
actionpack (5.0.2) lib/action_dispatch/middleware/static.rb:136:in `call'
rack (2.0.1) lib/rack/sendfile.rb:111:in `call'
railties (5.0.2) lib/rails/engine.rb:522:in `call'
puma (3.8.2) lib/puma/configuration.rb:224:in `call'
puma (3.8.2) lib/puma/server.rb:600:in `handle_request'
puma (3.8.2) lib/puma/server.rb:435:in `process_client'
puma (3.8.2) lib/puma/server.rb:299:in `block in run'
puma (3.8.2) lib/puma/thread_pool.rb:120:in `block in spawn_thread'
Rendering /Users/jalenjackson/.rvm/gems/ruby-2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
Rendering /Users/jalenjackson/.rvm/gems/ruby-2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/middleware/templates/rescues/_source.text.erb
Rendered /Users/jalenjackson/.rvm/gems/ruby-2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/middleware/templates/rescues/_source.text.erb (2.6ms)
Rendering /Users/jalenjackson/.rvm/gems/ruby-2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
Rendered /Users/jalenjackson/.rvm/gems/ruby-2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb (3.1ms)
Rendering /Users/jalenjackson/.rvm/gems/ruby-2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
Rendered /Users/jalenjackson/.rvm/gems/ruby-2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb (10.1ms)
Rendered /Users/jalenjackson/.rvm/gems/ruby-2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb (96.8ms)
The app has one model Course, which has_attached_file :upload. Once a Course has been created, the user is redirected to the upload action of CoursesController. The user can choose a local file to upload.
When the user initiates the upload, jQuery File Upload does an AJAX request to the resume_upload action in order to establish whether there is an unfinished/interrupted upload.
I changed the model to match an image with paperclip
, and it uploaded the image fine, im having trouble getting the video to upload. My main goal is to get the jquery file upload to upload videos, images, and pdf's. Ive tried to change that in the back end , but it only seems to work for one type of file upload.