Skip to content

Instantly share code, notes, and snippets.

@serek
Created April 8, 2010 08:26
Show Gist options
  • Save serek/359901 to your computer and use it in GitHub Desktop.
Save serek/359901 to your computer and use it in GitHub Desktop.
Polymorphic file upload with paperclip, delayed_job, has_many_polymorphs, swfupload, formtastic and haml. Make same for any other types like Image, Audio, Document and so on.
module ApplicationHelper
# Access Levels
ACCESS_LEVELS = { :private => 0, :friends => 1, :public => 2 }
# Access levels i18n translation
def access_levels
ACCESS_LEVELS.collect{|k,v| [I18n.t("access." + k.to_s), v]}
end
end
- content_for :head do
= javascript_include_tag 'jquery-1.3.2', 'swfupload/swfupload', 'swfupload/swfupload.swfobject', 'swfupload/handlers', 'swfupload/fileprogress', 'swfupload/swfupload.queue'
- session_key_name = ActionController::Base.session_options[:key]
#form_degrade_gracefully
%h1 New #{model_name}
- semantic_form_for [parent, model], :html => {:multipart => true} do |f|
- f.inputs do
= f.input :title
= f.input :description, :as => :text
= f.input :access, :as => :select, :collection => access_levels, :prompt => false, :selected => 2
= f.input :data, :as => :file
- f.buttons do
= f.commit_button
%div#swfupload_container{:style => 'display: none'}
%h1 New #{model_name_plural}
= select("flash", "access", access_levels, {:prompt => false, :selected => 2})
:javascript
jQuery(function() {
jQuery("#flash_access").change(function() {
swf_upload.addPostParam("access", jQuery('#flash_access').val());
});
});
#fsUploadProgress.fieldset.flash
%span.legend Queue
#divStatus 0 uploaded #{model_name} (status kolejki)
%div
%span#spanButtonPlaceHolder
%input#btnCancel{ :type => "button", :value => "Cancel all", :disabled => "disabled", :onclick => "swf_upload.cancelQueue();" }
:javascript
var swf_upload;
SWFUpload.onload = function() {
var swf_settings = {
// SWFObject settings
minimum_flash_version: "9.0.28",
// Authencity config (Rails)
post_params: {
"#{session_key_name}": "#{cookies[session_key_name]}",
"authenticity_token": "#{form_authenticity_token}",
"access": "2",
},
// Backend settings
upload_url: "#{polymorphic_path([parent, model])}",
// SWFUpload flash file
flash_url: '/flash/swfupload/swfupload.swf',
// Flash file settings
file_size_limit : "#{file_size_limit}",
file_types : "#{file_types_extensions}",
file_types_description : "#{file_types_description}",
file_upload_limit : "0",
file_queue_limit : "0",
// Event handler settings
file_queued_handler : fileQueued,
file_queue_error_handler : fileQueueError,
file_dialog_complete_handler : fileDialogComplete,
upload_start_handler : uploadStart,
upload_progress_handler : uploadProgress,
upload_error_handler : uploadError,
upload_success_handler : uploadSuccess,
upload_complete_handler : uploadComplete,
queue_complete_handler : queueComplete, // Queue plugin event
// Button Settings
button_image_url : "/images/swfupload/SmallSpyGlassWithTransperancy_17x18.png",
button_placeholder_id : "spanButtonPlaceHolder",
button_width: 17,
button_height: 18,
custom_settings : {
progressTarget : "fsUploadProgress",
cancelButtonId : "btnCancel"
},
// Degrade gracefully normal form
swfupload_pre_load_handler: function() {
$('#form_degrade_gracefully').hide();
$('#swfupload_container').show();
},
// Debug settings
debug: false
}
swf_upload = new SWFUpload(swf_settings);
};
class CreateVideos < ActiveRecord::Migration
def self.up
create_table :videos do |t|
# Security
t.integer :access, :default => 2
t.string :secure_token
# Default data
t.string :title
t.text :description
# For Delayed Job status
t.boolean :processing, :default => true
# Paperclip
t.string :data_file_name
t.string :data_content_type
t.integer :data_file_size
t.datetime :data_updated_at
t.timestamps
end
add_index :videos, :secure_token
add_index :videos, :title
add_index :videos, :description
add_index :videos, :data_file_name
add_index :videos, [:title, :description, :data_file_name]
end
def self.down
drop_table :videos
end
end
ActionController::Routing::Routes.draw do |map|
map.resources :users do |u|
u.resources :videos, :controller => 'videos'
u.resources :images, :controller => 'images'
end
end
class User < ActiveRecord::Base
# Used plugin has_many_polymorphs for easy polymorphic associations
# Example:
# User.first.assets # => [#Video, #Image, #Image]
# User.first.images # => [#Image, #Image]
has_many_polymorphs :assets, :from => [:videos, :images]
end
class Video < ActiveRecord::Base
# Allowable content types for file upload
VIDEO_TYPES = MIME::Types[/(video)/].map(&:content_type) + [ 'application/x-flv', 'application/x-wmv', 'application/x-avi', 'application/x-mpg' ]
# Video formats for ffmpeg (http://ffmpeg.org/ffmpeg-doc.html)
# qvga => 320x240
# vga => 640x480
# hd480 => 852x480
# Paperclip attachment
has_attached_file :data,
:url => "/uploads/videos/:id/:style/:basename.:extension",
:path => ":rails_root/public/uploads/videos/:id/:style/:basename.:extension",
:styles => {
:i_icon => { :size => "32x32", :format => "jpg" },
:i_small => { :size => "120x90", :format => "jpg" },
:i_normal => { :size => "320x240", :format => "jpg" },
:i_large => { :size => "640x480", :format => "jpg" },
:i_hd => { :size => "852x480", :format => "jpg" },
:v_normal => { :vid_size => "qvga", :format => "flv" },
:v_large => { :vid_size => "vga", :format => "flv" },
:v_hd => { :vid_size => "hd480", :format => "flv" }
},
:processors => "video_transcoder"
# Secure mass assignment
attr_protected :data_file_name, :data_content_type, :data_size, :secure_token
# Validations
validates_presence_of :title
validates_attachment_presence :data
validates_attachment_size :data, :less_than => 200.megabytes
validates_attachment_content_type :data, :content_type => VIDEO_TYPES
# Stop processing after upload
before_post_process do |video|
false if video.processing?
end
# Push processing to Delayed Job
after_create { |video| video.send_later(:perform) }
# Named scopes
named_scope :private, :conditions => ['access = ?', 0]
named_scope :friends, :conditions => ['access = ?', 1]
named_scope :public, :conditions => ['access = ?', 2]
# Processings pushed to Delayed Job
def perform
update_attribute(:processing, false)
set_token
data.reprocess!
save
end
# Default URL with processing checks
def url(style = :original)
if (self.data && processing? && style != :original) or !File.exist?(self.data.path(style))
return data.url(:original)
end
data.url(style)
end
# Default secure URL with processing checks
def secure_url(style = :original)
if (self.data && processing? && style != :original) or !File.exist?(self.data.path(style))
return "/not_generated_yet"
end
"/#{secure_token}##{style.to_s}"
end
# Set secure token
def set_token
update_attribute(:secure_token, Digest::MD5.hexdigest(data_file_name + created_at.to_s + id.to_s + self.class.name))
end
end
= render :partial => "shared/asset_form", :locals => { :model => @video, :parent => @parent, :model_name => "Video", :model_name_plural => "Videos", :file_size_limit => "200 MB", :file_types_extensions => "*.avi; *.flv; *.mov; *.mpg; *.mpeg; *.mp4; *.wmv; *.3g2; *.3gp; *.m4v; *.asf;", :file_types_description => "Any file video" }
module Paperclip
class VideoTranscoder < Processor
attr_accessor :options
def initialize file, options = {}, attachment = nil
super
@current_format = File.extname(@file.path)
@basename = File.basename(@file.path, @current_format)
@options = options
@format = options[:format]
end
def make
dst = Tempfile.new([@basename, @format].compact.join("."))
dst.binmode
RVideo.logger = RAILS_DEFAULT_LOGGER
# Screenshot grabbing
if @format == 'jpg'
# Initalize transcoder
transcoder = RVideo::Transcoder.new
# Calculate time offset (50%)
offset = "50"
inspector = RVideo::Inspector.new :file => file.path
time = (inspector.duration.to_i / 1000.0) * (offset.to_f / 100.0)
# Recipe for ffmpeg screenshot grabbing
recipe = "ffmpeg -y -i $input_file$ -ss $time_offset$ -vframes 1 -vcodec mjpeg -f image2 -s $vid_size$ $output_file$"
begin
transcoder.execute(recipe, {:input_file => file.path, :output_file => dst.path, :vid_size => @options[:size], :time_offset => time})
rescue TranscoderError => e
puts "Unable to generate screenshot: #{e.class} - #{e.message}"
end
end
# Video transcoder
if @format == 'flv'
# Initalize transcoder
transcoder = RVideo::Transcoder.new
# Recipe for ffmpeg video transcoding
recipe = "ffmpeg -y -i $input_file$ -vcodec libx264 -acodec libmp3lame -ac 2 -ar 44100 -ab 64000 -b 650kb -f flv -r 25 -s $vid_size$ -vpre default $output_file$"
begin
transcoder.execute(recipe, {:input_file => file.path, :output_file => dst.path, :vid_size => @options[:vid_size]})
rescue TranscoderError => e
puts "Unable to transcode file: #{e.class} - #{e.message}"
end
end
dst
end
end
end
class VideosController < ApplicationController
before_filter :find_parent
def index
@videos = @parent.videos.all
respond_to do |format|
format.html
end
end
def new
@video = @parent.videos.new
respond_to do |format|
format.html # new.html.erb
end
end
def create
if params[:Filedata]
# SWFUpload
@data = params[:Filedata]
@data.content_type = MIME::Types.type_for(@data.original_filename).to_s
@video = @parent.videos.create(:title => @data.original_filename, :data => @data, :access => params[:access])
if @video.save
render :nothing => true # Sends 200 OK
else
render :text => "Error"
end
else
# Standard form
@video = @parent.videos.create(params[:video])
if @video.save
redirect_to([@parent, :videos])
else
render :action => "new"
end
end
end
private
def find_parent
params.each do |name, value|
if name =~ /(.+)_id$/
@parent = $1.classify.constantize.find(value) and return
end
end
nil
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment