8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-04-23

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

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

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

スクリーンショット 2016-04-23 15.37.06.png

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

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

gemfile
gem 'rubyzip'
$ bundle install

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

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モデルからインスタンスをすべてとってきます。モデル間のアソシエーションを行なっていれば、関係するレッスンモデル、ソースコードモデルも持ってこれることになります。

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

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

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

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

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

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

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

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というアプリを使用することになります。
もしくは、上記コードの場合、あらかじめユニークバリデーションを設定しておけばよろしいかと思います。
この現象は世界的に起きているようです。同一名だとmacのアーカイブユーティリティで開けません
下記のコードは同名のファイルがあった場合は、後が優先で上書きされる仕様になっています。(意図せずそうなった)

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

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?