I spent 10 years now in web-based development and I never came across anything worse than trying to post big files to a web server.. Not only the problem of not being able to finish the upload because you kill the server but also the horrible wait for the operation to finish without any clue about the real status.. but now I have the final and robust solution for this task (with a cute and precise progress bar)!!
This example is obviously realized with RoR, but probably it’s not too hard to implement with PHP.
Let’s assume you have a User model in your application and let’s create a UserAttachment model to store and manage the uploaded files:
class CreateUserAttachments < ActiveRecord::Migration
def self.up
create_table :user_attachments do |t|
t.column :title, :string, :null => false
t.column :caption, :string
t.column :content_type, :string
t.column :original_filename, :string
t.column :crypted_filename,:string, :limit => 40
t.column :size,:integer
t.column :file_ext, :string, :limit => 4, :default => 0
t.column :is_public, :string, :limit => 1, :default => 0
t.column :needs_parsing, :string, :limit => 1, :default => 0
t.column :created_at, :datetime
t.column :updated_at, :datetime
t.column :user_id,:integer
end
end
def self.down
drop_table :user_attachments
end
end
The model will have some useful method for file management:
class UserAttachment < ActiveRecord::Base
belongs_to :user
before_destroy :delete_file
def write_path
user_dir = "%05d" % self.user_id
write_path = "#{RAILS_ROOT}/public/attachments/#{user_dir}"
Dir.mkdir(write_path) if !FileTest.exist?(write_path)
write_path
end
def full_path
"#{self.write_path}/#{self.crypted_filename}.#{self.file_ext}"
end
def full_url
user_dir = "%05d" % self.user_id
#write_path = "/attachments/#{user_dir}/#{self.crypted_filename}.#{self.file_ext}"
"/attachments/#{user_dir}/#{self.crypted_filename}.#{self.file_ext}"
end
def delete_file
if FileTest.exist?(self.full_path)
File.delete(self.full_path)
end
end
end
Now let’s face another little problem: when I realized I did not have any active session available to authenticate my user while using the upload interface (and I was too lazy to google any solution) I first thought I could pass the user_id to the flex interface and open the biggest security hole ever seen on earth but then I realized that using a ‘token’ could help:
class CreateRandomizer < ActiveRecord::Migration
def self.up
create_table :randomizers do |t|
t.column :random_user, :integer
t.column :random_token, :string, :limit => 40
t.column :created_at, :datetime
end
end
def self.down
drop_table :randomizers
end
end
I’m sure there are many better solutions.. but I like this one! (and I’m lazy) =)
in media_controller.rb
def upload
@randomized = current_user.get_random
end
in user.rb
def get_random
random_token = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
Randomizer.create(:random_user => self.id, :random_token => random_token)
return random_token
end
In the view upload.rhtml the SHA is passed to the FLEX, grabbed and passed along with the file:
<param name="FlashVars" value="member=<%= @randomized %>"
And now the magic of FLEX posting the file.. A good idea would be to cron/trigger delete all tokens older than one our in the table if you are paranoid..
in media_controller.rb
def receive
render(:xml => "true") if saveFileAttachment(params[:Filedata], params[:member])
end
#######
private
#######
def saveFileAttachment(pFile, random_token)
randomizer = Randomizer.find_by_random_token(random_token)
return false unless randomizer
current_user = User.find_by_id(randomizer.random_user)
return false unless current_user
randomizer.destroy
attachment = UserAttachment.new(to_attachment(pFile))
attachment.user_id = current_user.id
attachment.save if File.open(attachment.full_path, ”wb”) { |vBuffer| vBuffer.write(pFile.read)}
true
end
def to_attachment(pFile)
content_type = MIME::Types.type_for(pFile.original_filename)
{ :content_type => content_type.to_s,
:original_filename => pFile.original_filename,
:file_ext => pFile.original_filename.split(”.”).last || ”ext”,
:crypted_filename => Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join ),
:size => pFile.size }
end
I use MIME::Types.type_for to determine the content type because, as you noticed from the UserAttachment definition I plan to take different actions according to the file type such ad ffmpeg encoding for video files.. MIME can be installed as ’sudo gem install mime-types’ and included in your enviroment.rb with require ‘mime/types’.
It simply works so smooth I could not believe it! 5, 10, 20 megabytes on DreamHost and up to 97MB! Just FUNTASTIC..
Grab the source for file_uploader.mxml and go uploading!
I would not mind (team) getting a little deeper in this project, add a bunch of configurable options and plug-in-ize it..
Huge file uploads aren’t hard at all. We have them working well on our site (http://www.woobius.com). All you need is to have a proxy like nginx in front of Mongrel to cache the upload rather than lock up Rails while it’s happening. We just used Attachment-fu for our model.
You’re right, Daniel.. The “anything worse” is probably a reminiscence of old developing times! It’s easy now, but anyway Flex+Rails is not a widely used solution for small/personal applications yet. Good job at woobius.com!
Trackback page translation
I tried to follow your example in RoR, I made a User model and so on, but you are talking about a view (upload.rhtml) that you haven’t mentioned elsewhere also the media_controller.rb where does that fit in? It would be super if you could post the source for the RoR project as well. Also I get an error when I try to run the file_uploader.mxml in Flex: on params.member = paramObj["member"] it says that paramObj is a undefined property. Thanks in advance!
I will publish the whole source asap, Tobbe!
Thanks for your note: I accidentally deleted this line from file_uploader.mxml source:
var paramObj:Object = LoaderInfo(this.root.loaderInfo).parameters;