Class: Uniword::Infrastructure::ZipPackager
- Inherits:
-
Object
- Object
- Uniword::Infrastructure::ZipPackager
- 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.
Instance Method Summary collapse
-
#add_file(zip_path, entry_path, entry_content) ⇒ void
Add a file to an existing ZIP archive.
-
#package(content, output_path) ⇒ void
Package content into a ZIP file.
-
#remove_file(zip_path, entry_path) ⇒ Boolean
Remove a file from a ZIP archive.
Instance Method Details
#add_file(zip_path, entry_path, entry_content) ⇒ void
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.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/uniword/infrastructure/zip_packager.rb', line 97 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
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.
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 78 79 80 81 82 83 84 |
# 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 temp file to avoid Windows atomic rename issues # Rubyzip's CREATE mode fails when output_path exists temp_path = "#{output_path}.#{Process.pid}.tmp" Zip::File.open(temp_path, Zip::File::CREATE) do |zip_file| content.each do |entry_path, entry_content| zip_file.get_output_stream(entry_path) do |stream| # Binary data (ASCII-8BIT) is written as-is; # text content is ensured to be UTF-8 final_content = if entry_content.encoding == Encoding::ASCII_8BIT entry_content else entry_content.encode( "UTF-8", invalid: :replace, undef: :replace ) end stream.write(final_content) end end end # On Windows, the handle may still be held briefly after block ends # and FileUtils.mv (rename) can fail with EACCES on locked files. # Use File.binwrite + unlink instead, which is more Windows-friendly. retries = 10 begin FileUtils.rm_f(output_path) # Wait for Windows to release the lock sleep(0.5) # Copy content instead of renaming to avoid rename locks File.binwrite(output_path, File.binread(temp_path)) FileUtils.rm_f(temp_path) rescue Errno::EACCES retries -= 1 raise unless retries.positive? sleep(0.5) retry end ensure # Clean up temp file if it still exists FileUtils.rm_f(temp_path) if defined?(temp_path) && temp_path && File.exist?(temp_path) end |
#remove_file(zip_path, entry_path) ⇒ Boolean
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.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/uniword/infrastructure/zip_packager.rb', line 128 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 |