Railsでデータベースの中身をzipファイルにしてダウンロードする方法です。
取ってきたいのは、
- コース
- コースに属するレッスン
- レッスンに属するソースコード
です。
それぞれhas_many
とbelogs_to
でモデル間のアソシエーションを設定しています。

実際のzipファイルの構造はzipの中に上記ディレクトリ構造でソースコードの中身が見れるようになっています。コースとレッスンはディレクトリのみで取ってきます。
使うgemはrubyzip
です。まずはgemをbundle install
します。
gem 'rubyzip'
$ bundle install
次にルーティングを設定します。今回はdownload_source_codeという名前のpathを生成することにしました。名前は任意のもので構いません。
これでdownload_source_code_course_path
というpathが生成されます。
resources :courses do
post :download_source_code, on: :collection
end
上記ルーティングで生成したpathを使ってviewにダウンロードリンクを貼ります。methodオプションでpost
を指定してください。
<%= link_to 'ソースコードをダウンロード', download_source_code_courses_path, method: :post %>
controllerにメソッドを定義します。
-
@courses = Course.all
でCourseモデルからインスタンスをすべてとってきます。モデル間のアソシエーションを行なっていれば、関係するレッスンモデル、ソースコードモデルも持ってこれることになります。 -
Tempfile.new
でtempfileインスタンスを生成して、tempでzipファイルを生成します。 -
rubyzipのクラスメソッドZip::OutputStreamを使ってzipファイルを生成します。
-
rubyzipのメソッドであるput_next_entryでディレクトリ構造を指定します。ここで指定した名前がディレクトリ名になります。今回は
/
を3回記述して3段階構造にしています。とってきたカラムに(今回はtitle)に/
が含まれていると、/
が含まれているとカラム中の/
もディレクトリ構造として認識されます。※カラム名はご自身が設定したカラムを指定してください。 -
rubyzipのメソッドであるprintでソースコードの中身を出力します。
-
Railsのsend_fileメソッドでリンクをクリックされたときに処理される記述をします。filenameオプションで指定した名前が、zipファイルの名前になります。
-
closeでファイルを閉じます。
-
each文を3重にすることで
・コース
・コースに属するレッスン
・レッスンに属するソースコード
を取得することができます。※モデル間のアソシエーションを記述していることが前提です。
def download_source_code
@courses = Course.all
t = Tempfile.new("source_codes_file")
Zip::OutputStream.open(t.path) do |z|
@courses.each do |course|
course.lessons.each do |lesson|
lesson.source_codes.each do |source_code|
z.put_next_entry("#{course.title}/#{lesson.title}/#{source_code.title}")
z.print("#{source_code.body}")
end
end
end
end
send_file(t.path, :type => 'application/zip', :dispositon => 'attachment', :filename => "SourceCode#{Time.now}.zip")
t.close
end
これでviewのリンクをクリックすると、コース、レッスン、ソースコードすべてデータベースから取得して、ディレクトリ構造で取得することができます。
上記コードはリファクタリングの余地は十分にありますし、rubyzipの他のメソッドを使ったほうが綺麗に記述できるかもしれませんが、とりあえずは上記コードでも動作します。
データベースの中身をzipでダウンロードしたいという方の参考になれば幸いです。
2016/4/27修正
上記コードだと、アソシエーションの子、孫にデータがない場合、その親のZIPファイルにディレクトリが作成されない現象が起きました。(そのような実装がよければ上記コードで可)
また、同名のソースコードファイルがあった場合、macのアーカイブユーティリティで、zipが解凍できない事態が発生しました。chromeを使うとmacのアーカイブユーティリティで解凍することになるため、chromeを使用せずsafariを利用するか、The Unarchiverというアプリを使用することになります。
もしくは、上記コードの場合、あらかじめユニークバリデーションを設定しておけばよろしいかと思います。
この現象は世界的に起きているようです。同一名だとmacのアーカイブユーティリティで開けません
下記のコードは同名のファイルがあった場合は、後が優先で上書きされる仕様になっています。(意図せずそうなった)

def download_source_code
@courses = Course.all
t = Tempfile.new("source_codes_zip") #=>tempファイル作成
Zip::File.open(t.path, Zip::File::CREATE) do |zip|
@courses.each do |course|
if course.lessons.blank?
zip.mkdir "#{course.title}"#=>コースに紐付いているレッスンが無かった場合、コースのみのディレクトリを先に作成する
next
end
course.lessons.each do |lesson|
if lesson.source_codes.blank?
zip.mkdir "#{course.title}/#{lesson.title}"#=>レッスンに紐付いているソースコードが無かった場合、レッスンのみのディレクトリを先に作成する
next
end
lesson.source_codes.each do |source_code|
zip.get_output_stream( "#{course.title}/#{lesson.title}" + "/#{source_code.title}"){ |s| s.print("#{source_code.body}")}#=>コースディレクトリ/レッスンディレクトリがあれば、それに紐づくソースコードファイルを作成。そのソースコードファイルの中身をprintメソッドで作成。
end
end
end
end
send_file(t.path, type: 'application/zip', dispositon: 'attachment', filename: "SourceCode_#{Time.now.strftime("%Y%m%d")}.zip")#=>ダウンロードできるように、send_fileヘルパーを使う。
t.close#=>tempファイルを削除する。
end
ziprubyはすべての「/」を階層として認識する
ziprubyはすべての「/」をディレクトリとして認識します。自分でcontroller.rbに記述した以外にも、例えばcourse.titleの中に、「/」があると、それも階層として認識するためディレクトリが1個深くなります。意図しない場合は、「/」を入れないように事前にバリデーションを実装することをおすすめします。