Class: Uniword::Infrastructure::ZipPackager

Inherits:
Object
  • Object
show all
Defined in:
lib/uniword/infrastructure/zip_packager.rb

Overview

Packages content into ZIP archives (e.g., DOCX files).

Responsibility: Handle ZIP file creation and packaging operations. Does NOT handle: Document serialization or format-specific logic.

DOCX files are ZIP archives containing XML files and media. This class provides low-level ZIP packaging functionality.

Examples:

Package content into a DOCX file

packager = Uniword::Infrastructure::ZipPackager.new
content = {
  "word/document.xml" => xml_content,
  "[Content_Types].xml" => types_content
}
packager.package(content, "output.docx")

Instance Method Summary collapse

Instance Method Details

#add_file(zip_path, entry_path, entry_content) ⇒ void

Note:

On Windows, we must close the original ZIP handle before

This method returns an undefined value.

Add a file to an existing ZIP archive.

attempting to overwrite it. We extract content first, then close the handle, then package to a temp file and move.

Parameters:

  • zip_path (String)

    The path to the ZIP file

  • entry_path (String)

    The path for the file within the ZIP

  • entry_content (String)

    The content to add

Raises:

  • (ArgumentError)

    if arguments are invalid



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/uniword/infrastructure/zip_packager.rb', line 90

def add_file(zip_path, entry_path, entry_content)
  validate_zip_path(zip_path)
  raise ArgumentError, "Entry path cannot be nil" if entry_path.nil?
  raise ArgumentError, "Entry path cannot be empty" if entry_path.empty?

  # Extract existing content into a local variable
  content = {}
  Zip::File.open(zip_path) do |zip_file|
    zip_file.each do |entry|
      next if entry.directory?

      content[entry.name] = entry.get_input_stream.read
    end
  end
  # Handle is now fully closed before we modify the file

  # Add new entry and write to a temp file first, then move
  content[entry_path] = entry_content
  write_to_zip_file(content, zip_path)
end

#package(content, output_path) ⇒ void

Note:

On Windows, Zip::File::CREATE mode fails when target exists

This method returns an undefined value.

Package content into a ZIP file.

because Rubyzip tries to atomically rename a temp file over it. We use a temp file approach to avoid this issue.

Parameters:

  • content (Hash<String, String>)

    Hash mapping file paths to contents

  • output_path (String)

    The path for the output ZIP file

Raises:

  • (ArgumentError)

    if arguments are invalid



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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/uniword/infrastructure/zip_packager.rb', line 34

def package(content, output_path)
  validate_content(content)
  validate_output_path(output_path)

  # Ensure output directory exists
  FileUtils.mkdir_p(File.dirname(output_path))

  # Use Zip::OutputStream directly (not Zip::File) so that Entry objects
  # with internal_file_attributes=0 are preserved through serialization.
  # Zip::File wraps entries in StreamableStream which is NOT kind_of?(Entry),
  # causing put_next_entry to discard our Entry and create a fresh one.
  temp_path = "#{output_path}.#{Process.pid}.tmp"

  was_zip64 = Zip.write_zip64_support
  Zip.write_zip64_support = false
  begin
    Zip::OutputStream.open(temp_path) do |zos|
      content.each do |entry_path, entry_content|
        entry = Zip::Entry.new(temp_path, entry_path.to_s)
        entry.internal_file_attributes = 0
        entry.external_file_attributes = 0
        entry.fstype = Zip::FSTYPE_FAT

        zos.put_next_entry(entry)

        final_content =
          if entry_content.encoding == Encoding::ASCII_8BIT
            entry_content
          else
            entry_content.encode(
              "UTF-8", invalid: :replace, undef: :replace
            )
          end
        zos.write(final_content)
      end
    end
  ensure
    Zip.write_zip64_support = was_zip64
  end

  move_temp_to_output(temp_path, output_path)
ensure
  FileUtils.rm_f(temp_path) if defined?(temp_path) && temp_path && File.exist?(temp_path)
end

#remove_file(zip_path, entry_path) ⇒ Boolean

Note:

On Windows, we must close the original ZIP handle before

Remove a file from a ZIP archive.

attempting to overwrite it. We extract content first, then close the handle, then write to a temp file and move.

Parameters:

  • zip_path (String)

    The path to the ZIP file

  • entry_path (String)

    The path of the file to remove

Returns:

  • (Boolean)

    true if file was removed, false if not found

Raises:

  • (ArgumentError)

    if arguments are invalid



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/uniword/infrastructure/zip_packager.rb', line 121

def remove_file(zip_path, entry_path)
  validate_zip_path(zip_path)

  # Extract existing content into a local variable
  content = {}
  found = false
  Zip::File.open(zip_path) do |zip_file|
    zip_file.each do |entry|
      next if entry.directory?

      if entry.name == entry_path
        found = true
      else
        content[entry.name] = entry.get_input_stream.read
      end
    end
  end
  # Handle is now fully closed before we modify the file

  return false unless found

  write_to_zip_file(content, zip_path)
  true
end