Generate files and download as zip using RubyZip

8k Views Asked by At

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.

3

There are 3 best solutions below

0
Will On BEST ANSWER

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')).

1
Masroor On

The accepted solution is indeed correct. However, I'm going to extend the already provided solution to get it working with ActiveStorage attachments.
While using the accepted solution I found that the image_string method does not work for ActiveStorage attachment and throws an error like this

NoMethodError - undefined method `image_string' for #<ActiveStorage::Attached::One:0x00007f78cc686298>

Suppose we have a rails model called Product with an ActiveStorage attribute called attachment

class Product < ApplicationRecord
  has_one_attached :attachment
end

In order to get this working for ActiveStorage attachments, we need to update the code as follows

begin
  Zip::OutputStream.open(temp_file) { |zos| }

  Zip::File.open(temp_file.path, Zip::File::CREATE) do |zipfile|
    Product.all.each do |product|
      image_file = Tempfile.new("#{product.attachment.created_at.in_time_zone}.png")
      
    # image_file.write(product.attachment.image_string) #this does not work for ActiveStorage attachments
      
      # use this instead
      File.open(image_file.path, 'w', encoding: 'ASCII-8BIT') do |file|
        product.attachment.download do |chunk|
          file.write(chunk)
        end
      end

      zipfile.add("#{product.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
0
Juan Camilo Camacho Beltrán On

It works for me (I need to load MyModel document based on Carrierwave):

require 'zip'
require 'open-uri'

class TestsController < ApplicationController
  def index
    filename = 'test.zip'
    temp_file = ::Tempfile.new(filename)

    my_model_document = ::MyModel.last
    my_model_document_name = ::File.basename(my_model_document.document.path)

    begin
      ::Zip::OutputStream.open(temp_file) { |zos| }
      ::Zip::File.open(temp_file.path, ::Zip::File::CREATE) do |zipfile|
        dr_temp_file = Tempfile.new(my_model_document_name)
        dr_temp_file.write(open(my_model_document.document.url).read.force_encoding("UTF-8"))
        zipfile.add(my_model_document_name, dr_temp_file.path)
      end

      zip_data = File.read(temp_file.path)
      send_data(zip_data, type: 'application/zip', disposition: 'attachment', filename: filename)
    ensure
      temp_file.close
      temp_file.unlink
    end
  end
end