Railsでデータベースの中身をzipファイルにしてダウンロードする方法

Railsでデータベースの中身をzipファイルにしてダウンロードする方法です。
取ってきたいのは、

- コース
- コースに属するレッスン
- レッスンに属するソースコード

です。
それぞれ`has_many`と`belogs_to`でモデル間のアソシエーションを設定しています。

<img width="363" alt="スクリーンショット 2016-04-23 15.37.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/93394/04a57999-3037-2143-bacc-a7b67f4494b5.png">

実際のzipファイルの構造はzipの中に上記ディレクトリ構造でソースコードの中身が見れるようになっています。コースとレッスンはディレクトリのみで取ってきます。

使うgemは`rubyzip`です。まずはgemを`bundle install`します。

```rb:gemfile
gem 'rubyzip'
```

```bash
$ bundle install
```

次にルーティングを設定します。今回は`download_source_codeという名前のpathを生成することにしました。名前は任意のもので構いません。`これで`download_source_code_course_path`というpathが生成されます。

```rb:routes.rb
    resources :courses do
      post :download_source_code, on: :collection
    end
```

上記ルーティングで生成したpathを使ってviewにダウンロードリンクを貼ります。methodオプションで`post`を指定してください。

```views/courses/index.html.erb
<%= link_to 'ソースコードをダウンロード', download_source_code_courses_path, method: :post %>
```

controllerにメソッドを定義します。

1. `@courses = Course.all`でCourseモデルからインスタンスをすべてとってきます。モデル間のアソシエーションを行なっていれば、関係するレッスンモデル、ソースコードモデルも持ってこれることになります。

1. `Tempfile.new`でtempfileインスタンスを生成して、tempでzipファイルを生成します。

1. rubyzipのクラスメソッドZip::OutputStreamを使ってzipファイルを生成します。 

1. rubyzipのメソッドであるput_next_entryでディレクトリ構造を指定します。ここで指定した名前がディレクトリ名になります。今回は`/`を３回記述して３段階構造にしています。とってきたカラムに（今回はtitle）に`/`が含まれていると、`/`が含まれているとカラム中の`/`もディレクトリ構造として認識されます。※カラム名はご自身が設定したカラムを指定してください。

1. rubyzipのメソッドであるprintでソースコードの中身を出力します。

1. Railsのsend_fileメソッドでリンクをクリックされたときに処理される記述をします。filenameオプションで指定した名前が、zipファイルの名前になります。

1. closeでファイルを閉じます。

1. each文を３重にすることで
・コース
・コースに属するレッスン
・レッスンに属するソースコード
を取得することができます。※モデル間のアソシエーションを記述していることが前提です。

```rb:courses_controller.rb
  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](https://itunes.apple.com/jp/app/the-unarchiver/id425424353?mt=12)というアプリを使用することになります。
もしくは、上記コードの場合、あらかじめユニークバリデーションを設定しておけばよろしいかと思います。
この現象は世界的に起きているようです。[同一名だとmacのアーカイブユーティリティで開けません](https://github.com/rubyzip/rubyzip/issues/240)
下記のコードは同名のファイルがあった場合は、後が優先で上書きされる仕様になっています。（意図せずそうなった）

<img width="451" alt="スクリーンショット 2016-04-27 19.07.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/93394/91aad9b9-d4b2-8792-ef98-814073bbbd66.png">

```rb:courses_controller.rb
  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の中に、「/」があると、それも階層として認識するためディレクトリが１個深くなります。意図しない場合は、「/」を入れないように事前にバリデーションを実装することをおすすめします。