Class: Export::ZipStreamer

Inherits:
Object
  • Object
show all
Defined in:
lib/export/zip_streamer.rb

Instance Method Summary collapse

Instance Method Details

#build_entry_name(entry, used_names, file_name:, entry_id:) ⇒ Object (private)



63
64
65
66
67
68
69
70
71
72
# File 'lib/export/zip_streamer.rb', line 63

def build_entry_name(entry, used_names, file_name:, entry_id:)
  base = file_name.call(entry).presence || "file_#{entry_id.call(entry)}"
  candidate = Zaru.sanitize!(base)
  return reserve_name(candidate, used_names) if used_names[candidate].nil?

  ext = File.extname(candidate)
  stem = File.basename(candidate, ext)
  disambiguated = "#{stem}-#{entry_id.call(entry)}#{ext}"
  reserve_name(Zaru.sanitize!(disambiguated), used_names)
end

#reserve_name(name, used_names) ⇒ Object (private)



74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/export/zip_streamer.rb', line 74

def reserve_name(name, used_names)
  final = name
  suffix = 1
  ext = File.extname(name)
  stem = File.basename(name, ext)

  while used_names[final]
    suffix += 1
    final = "#{stem}-#{suffix}#{ext}"
  end

  used_names[final] = true
  final
end

#stream(entries:, zip_streamer:, file_path:, file_name:, entry_id:, logger_prefix: 'ZipStreamer', on_entry: nil, after_stream: nil) ⇒ Object

Streams entries into a zip archive.

Parameters:

  • entries (Array)

    objects to stream

  • zip_streamer (#call)

    ZipTricks-compatible callable that yields a zip writer (e.g. method(:zip_tricks_stream))

  • file_path (Proc)

    extracts file path from entry

  • file_name (Proc)

    extracts original filename from entry

  • entry_id (Proc)

    extracts unique ID from entry (for disambiguation)

  • logger_prefix (String) (defaults to: 'ZipStreamer')

    prefix for log messages

  • on_entry (Proc, nil) (defaults to: nil)

    optional callback (entry, filename, results) -> void

  • after_stream (Proc, nil) (defaults to: nil)

    optional callback (zip, results, written) -> void



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/export/zip_streamer.rb', line 30

def stream(entries:, zip_streamer:, file_path:, file_name:, entry_id:, logger_prefix: 'ZipStreamer', on_entry: nil, after_stream: nil)
  zip_streamer.call do |zip|
    written = false
    used_names = {}
    results = []

    entries.each do |entry|
      begin
        name = build_entry_name(
          entry,
          used_names,
          file_name: file_name,
          entry_id: entry_id
        )
        zip.write_deflated_file(name) do |sink|
          stream_file(entry, sink, file_path: file_path, entry_id: entry_id)
        end
        on_entry&.call(entry, name, results)
        written = true
      rescue StandardError => e
        Rails.logger.warn(
          "#{logger_prefix}: failed to stream entry #{entry_id.call(entry)}: #{e.class} #{e.message}"
        )
      end
    end

    after_stream&.call(zip, results, written)
    write_error_fallback(zip) unless written
  end
end

#stream_file(entry, sink, file_path:, entry_id:) ⇒ Object (private)



89
90
91
92
93
94
# File 'lib/export/zip_streamer.rb', line 89

def stream_file(entry, sink, file_path:, entry_id:)
  path = file_path.call(entry)
  raise "File missing for entry #{entry_id.call(entry)}" if path.blank? || !File.exist?(path)

  File.open(path, 'rb') { |io| IO.copy_stream(io, sink) }
end

#write_error_fallback(zip) ⇒ Object (private)



96
97
98
99
100
# File 'lib/export/zip_streamer.rb', line 96

def write_error_fallback(zip)
  zip.write_deflated_file('errors.txt') do |sink|
    sink.write("No files could be streamed for this package.\n")
  end
end