Quite often you might want to integrate upload capabilities in your Rails application. For example, you want to provide users the ability to configure their avatar or your e-commerce needs to display an image in the product page.
Creating an upload system in Rails is a piece of cake. There are tons of plugins out of there that you can install and configure in a couple of click. Here's a couple of examples:
Problems might arise at deployment time if you are managing your Rails application with Capistrano. By default Capistrano doesn't support an additional upload/images folder unless it is under version control and each time you'll deploy your Rails application the path stored for the uploaded files will be invalidated.
The following article shows you how to properly configure Capistrano in order to preserve your uploads folder across multiple releases.
The Scenario
Let me show you an example. You've just deployed your Rails project. The remote folder tree should look like the following one:
/deploy/folder
|
+ current (s => /deploy/folder/releases/200802251356)
|
+ relases
|
+ 200802251356
The uploaded files are probably stored somewhere within the public folder in /deploy/folder/releases/200802251356/public
. Let's assume the folder is /deploy/folder/releases/200802251356/public/uploads
.
You upload two files from your administration panel. The first one called first.png
and the second one called second.gif
. Now your folder tree will look as follows.
/deploy/folder
|
+ current (s => /deploy/folder/releases/200802251356)
|
+ relases/
|
+ 200802251356/
|
+ public/
|
+ uploads/
|
+ first.png
+ second.gif
You must bear in mind that we are now talking about server path. From the public side, your user doesn't know the full path where the files are stored. The application would probably reference the files with an absolute or relative path starting from your website domain.
In our example, the images are available at /uploads/first.png
and /uploads/second.gif
or http://example.com/uploads/first.png
and http://example.com/uploads/second.gif
if the application relies on the full url.
Where are my files?
Time passes and a new release is ready for deployment. Now you've got a problem: as soon as the new release is deployed, Capistrano updates the target of the current symlink to point to the new version.
/deploy/folder
|
+ current (s => /deploy/folder/releases/200802261100)
|
+ relases/
|
+ 200802251356/
| |
| + public/
| |
| + uploads/
| |
| + first.png
| + second.png
|
+ 200802261100/
| |
| + public/
| |
| + uploads/
Hey, wait a minute… where are my files? Because the files uploaded by the users are not under version control, as soon as the new version is checked out from the repository and the current symlink updated, all your files will become unavailable.
The URL http://example.com/uploads/second.gif
now points to /deploy/folder/releases/200802261100/public/uploads/second.gif
that doesn't exist.
You must tell Capistrano to move or copy the reference to the uploads folder each time a new version is deployed.
The solution
First of all, let me show you the solution. I'll explain that in more detail later.
# ==============================
# Uploads
# ==============================
namespace :uploads do
desc <<-EOD
Creates the upload folders unless they exist
and sets the proper upload permissions.
EOD
task :setup, :except => { :no_release => true } do
dirs = uploads_dirs.map { |d| File.join(shared_path, d) }
run "#{try_sudo} mkdir -p #{dirs.join(' ')} && #{try_sudo} chmod g+w #{dirs.join(' ')}"
end
desc <<-EOD
[internal] Creates the symlink to uploads shared folder
for the most recently deployed version.
EOD
task :symlink, :except => { :no_release => true } do
run "rm -rf #{release_path}/public/uploads"
run "ln -nfs #{shared_path}/uploads #{release_path}/public/uploads"
end
desc <<-EOD
[internal] Computes uploads directory paths
and registers them in Capistrano environment.
EOD
task :register_dirs do
set :uploads_dirs, %w(uploads uploads/partners)
set :shared_children, fetch(:shared_children) + fetch(:uploads_dirs)
end
after "deploy:finalize_update", "uploads:symlink"
on :start, "uploads:register_dirs"
end
In the course of time I realized the best way to accomplish this task is to use symlinks and callbacks.
On setup Capistrano creates a special folder called shared
, shared across all deployed revisions. This folder contains, for example, the Rails logs and temporary files. All shared folders are accessible in the Capistrano configuration with the :shared_children
variable.
The recipe tells Capistrano to create an additional folder on setup called uploads
. This folder is appended to the default :shared_children
array to take advantage of the original Capistrano deploy:setup
task.
Then, a new task called uploads:symlink
is created and configured as an after callback for the deploy:finalize_update
Capistrano task. Now, each time a new version is deployed, Capistrano creates a symlink called /public/uploads
in the fresh revision pointing to the shared/uploads
folder. Your files will no longer be lost across multiple deployments!
Now you just need to configure your Rails application to store all your uploaded files within the /public/uploads
folder, perhaps nicely organized in subfolders depending on content type.
Multistage recipe
The package capistrano-ext contains an excellent recipe to add support for multiple staging when deploying a Rails environment. I often use it to differentiate between the staging and production environment.
If you are using the multistage recipe and multiple stages share the same server, you must store files in different folders to prevent stage conflicts. The following recipe is a modified version of the original one with support for multiple environments on the same server.
# ==============================
# Uploads
# ==============================
namespace :uploads do
desc <<-EOD
Creates the upload folders unless they exist
and sets the proper upload permissions.
EOD
task :setup, :except => { :no_release => true } do
dirs = uploads_dirs.map { |d| File.join(shared_path, d) }
run "#{try_sudo} mkdir -p #{dirs.join(' ')} && #{try_sudo} chmod g+w #{dirs.join(' ')}"
end
desc <<-EOD
[internal] Creates the symlink to uploads shared folder
for the most recently deployed version.
EOD
task :symlink, :except => { :no_release => true } do
run "rm -rf #{release_path}/public/uploads"
run "ln -nfs #{shared_path}/#{stage}/uploads #{release_path}/public/uploads"
end
desc <<-EOD
[internal] Computes uploads directory paths
and registers them in Capistrano environment.
Note. I can't set value for directories directly in the code because
I don't know in advance selected stage.
EOD
task :register_dirs do
set :uploads_dirs, %w(uploads uploads/partners).map { |d| "#{stage}/#{d}" }
set :shared_children, fetch(:shared_children) + fetch(:uploads_dirs)
end
after "deploy:finalize_update", "uploads:symlink"
on :start, "uploads:register_dirs", :except => stages + ['multistage:prepare']
end