jl118
jl118

Reputation: 331

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.

Upvotes: 3

Views: 8988

Answers (3)

Masroor
Masroor

Reputation: 1604

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

Upvotes: 2

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

Upvotes: 0

Will
Will

Reputation: 386

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

Upvotes: 9

Related Questions