ruby-on-railsfilebase64ziprubyzip

Generate files and download as zip using RubyZip


For my Ruby on Rails project (Rails version 5.1.2), I'm generating image files (png) and downloading them as a zipfile using RubyZip gem.

The image files are not stored in any directory. I have a model called Attachment. Each attachment has an attribute image_string that is a base64 string for an image. You can show the images using a tag like image_tag(src = "data:image/jpeg;base64, #{attachment.image_string}", style: "border-radius: 0;")

For multiple images, I want to create temporary file for each of them without storing them anywhere and download those images as a zip file.

The code I have now:

def bulk_download
  require('zip')
  ::Zip::File.open("/tmp/mms.zip", Zip::File::CREATE) do |zipfile|
    Attachment.all.each do |attachment|
      image_file = Tempfile.new("#{attachment.created_at.in_time_zone}.png")
      image_file.write(attachment.image_string)
      zipfile.add("#{attachment.created_at.in_time_zone}.png", image_file.path)
    end
  end
  send_file "/tmp/mms.zip", type: 'application/zip', disposition: 'attachment', filename: "my_archive.zip"
  respond_to do |format |
    format.all { head :ok, content_type: "text/html" }
  end
end

But the downloaded zipfile has no files in it and the size of it is 0 bytes. Thanks in advance.


Solution

  • You should need to close and unlink the zip file like so:

    require('zip')
    
    class SomeController < ApplicationController
      # ...
    
      def bulk_download
        filename = 'my_archive.zip'
        temp_file = Tempfile.new(filename)
    
        begin
          Zip::OutputStream.open(temp_file) { |zos| }
    
          Zip::File.open(temp_file.path, Zip::File::CREATE) do |zip|
            Attachment.all.each do |attachment|
              image_file = Tempfile.new("#{attachment.created_at.in_time_zone}.png")
              image_file.write(attachment.image_string)
              zipfile.add("#{attachment.created_at.in_time_zone}.png", image_file.path)
            end
          end
    
          zip_data = File.read(temp_file.path)
          send_data(zip_data, type: 'application/zip', disposition: 'attachment', filename: filename)
        ensure # important steps below
          temp_file.close
          temp_file.unlink
        end
      end
    end
    

    Here is a good blog post that I used as the source for this code: https://thinkingeek.com/2013/11/15/create-temporary-zip-file-send-response-rails/

    Also, it's good practice to keep all your library requirements at the top of the file (i.e. require('zip')).