Skip to content

Instantly share code, notes, and snippets.

@thedanbob
Created December 6, 2021 19:16
Show Gist options
  • Save thedanbob/84ae41d3a521dd2c6cb6d5ada11c75b9 to your computer and use it in GitHub Desktop.
Save thedanbob/84ae41d3a521dd2c6cb6d5ada11c75b9 to your computer and use it in GitHub Desktop.
class FoldersController < ApplicationController
include Streamable
def download
@folder = Folder.find(params[:id]) # has_many_attached :files
response.headers['Last-Modified'] = @folder.updated_at.httpdate
zip = ZipStream.open
send_stream(filename: "folder_#{@folder.id}.zip", type: :zip) do |stream|
@folder.files.includes(:blob).each do |file|
data = zip.add_stored(file.filename.to_s, file.download)
stream.write(data)
end
stream.write(zip.close)
end
end
end
module Streamable
# Adds `send_stream` method from rails 7 alpha
# https://github.com/rails/rails/pull/41488
extend ActiveSupport::Concern
include ActionController::Live
def send_stream(filename:, disposition: 'attachment', type: nil)
response.headers['Content-Type'] =
(type.is_a?(Symbol) ? Mime[type].to_s : type) ||
Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete('.')) ||
'application/octet-stream'
response.headers['Content-Disposition'] =
ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
yield response.stream
ensure
response.stream.close
end
end
require 'zip'
class ZipStream < Zip::OutputStream
# Rolling buffer to trick RubyZip into thinking it's working with the entire zip file
# while only holding a single entry in memory at a time
class ZipBuffer < StringIO
# buffer_pos keeps track of the buffer's absolute position in the zip file
attr_accessor :buffer_pos
def initialize(*)
@buffer_pos = 0
super
end
# RubyZip uses `tell` to calculate header offsets
def tell
@buffer_pos + super
end
# Advance buffer_pos to end of file, return contents, and clear buffer
def dump
@buffer_pos += size
tmp = string.dup
self.string = ''
tmp
end
end
class << self
def open
io = ZipBuffer.new('')
io.binmode
new(io, true)
end
end
# Equivalent to Zip::File#add_stored
def add_stored(filename, data)
put_next_entry(filename, nil, nil, Zip::Entry::STORED)
write(data)
# Save current entry before finalize clears it
entry = @current_entry
finalize_current_entry
# Rewrite entry updated by finalize_current_entry
@output_stream.pos = entry.local_header_offset - @output_stream.buffer_pos
entry.write_local_entry(@output_stream, true)
# Return entry data and advance buffer
@output_stream.dump
end
# Close zip and return final data
def close
return if @closed
write_central_directory
data = @output_stream.dump
@output_stream.close
@closed = true
data
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment