diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/axlsx.rb | 1 | ||||
| -rw-r--r-- | lib/axlsx/package.rb | 21 | ||||
| -rw-r--r-- | lib/axlsx/util/zip_command.rb | 73 |
3 files changed, 91 insertions, 4 deletions
diff --git a/lib/axlsx.rb b/lib/axlsx.rb index e60660b6..be4a2cd5 100644 --- a/lib/axlsx.rb +++ b/lib/axlsx.rb @@ -10,6 +10,7 @@ require 'axlsx/util/accessors.rb' require 'axlsx/util/serialized_attributes' require 'axlsx/util/options_parser' require 'axlsx/util/mime_type_utils' +require 'axlsx/util/zip_command' require 'axlsx/stylesheet/styles.rb' diff --git a/lib/axlsx/package.rb b/lib/axlsx/package.rb index 124375d4..3afb6f8d 100644 --- a/lib/axlsx/package.rb +++ b/lib/axlsx/package.rb @@ -75,6 +75,9 @@ module Axlsx # # @param [String] output The name of the file you want to serialize your package to # @param [Boolean] confirm_valid Validate the package prior to serialization. + # @param [String, nil] zip_command When `nil`, `#serialize` with RubyZip to + # zip the XLSX file contents. When a String, the provided zip command (e.g., + # "zip") is used to zip the file contents (may be faster for large files) # @return [Boolean] False if confirm_valid and validation errors exist. True if the package was serialized # @note A tremendous amount of effort has gone into ensuring that you cannot create invalid xlsx documents. # confirm_valid should be used in the rare case that you cannot open the serialized file. @@ -88,13 +91,23 @@ module Axlsx # # ......add cool stuff to your workbook...... # p.serialize("example.xlsx") # + # # Serialize to a file, using a system zip binary + # p.serialize("example.xlsx", false, zip_command: "zip") + # p.serialize("example.xlsx", false, zip_command: "/path/to/zip") + # p.serialize("example.xlsx", false, zip_command: "zip -1") + # # # Serialize to a stream # s = p.to_stream() # File.open('example_streamed.xlsx', 'w') { |f| f.write(s.read) } - def serialize(output, confirm_valid=false) + def serialize(output, confirm_valid=false, zip_command: nil) return false unless !confirm_valid || self.validate.empty? + zip_provider = if zip_command + ZipCommand.new(zip_command) + else + Zip::OutputStream + end Relationship.initialize_ids_cache - Zip::OutputStream.open(output) do |zip| + zip_provider.open(output) do |zip| write_parts(zip) end true @@ -153,8 +166,8 @@ module Axlsx private # Writes the package parts to a zip archive. - # @param [Zip::OutputStream] zip - # @return [Zip::OutputStream] + # @param [Zip::OutputStream, ZipCommand] zip + # @return [Zip::OutputStream, ZipCommand] def write_parts(zip) p = parts p.each do |part| diff --git a/lib/axlsx/util/zip_command.rb b/lib/axlsx/util/zip_command.rb new file mode 100644 index 00000000..fb336209 --- /dev/null +++ b/lib/axlsx/util/zip_command.rb @@ -0,0 +1,73 @@ +# encoding: UTF-8 +require 'open3' +require 'shellwords' + +module Axlsx + + # The ZipCommand class supports zipping the Excel file contents using + # a binary zip program instead of RubyZip's `Zip::OutputStream`. + # + # The methods provided here mimic `Zip::OutputStream` so that `ZipCommand` can + # be used as a drop-in replacement. Note that method signatures are not + # identical to `Zip::OutputStream`, they are only sufficiently close so that + # `ZipCommand` and `Zip::OutputStream` can be interchangeably used within + # `caxlsx`. + class ZipCommand + # Raised when the zip command exits with a non-zero status. + class ZipError < StandardError; end + + def initialize(zip_command) + @current_file = nil + @files = [] + @zip_command = zip_command + end + + # Create a temporary directory for writing files to. + # + # The directory and its contents are removed at the end of the block. + def open(output, &block) + Dir.mktmpdir do |dir| + @dir = dir + block.call(self) + write_file + zip_parts(output) + end + end + + # Closes the current entry and opens a new for writing. + def put_next_entry(entry) + write_file + @current_file = "#{@dir}/#{entry.name}" + @files << entry.name + FileUtils.mkdir_p(File.dirname(@current_file)) + end + + # Write to a buffer that will be written to the current entry + def write(content) + @buffer << content + end + alias << write + + private + + def write_file + if @current_file + @buffer.rewind + File.open(@current_file, "wb") { |f| f.write @buffer.read } + end + @current_file = nil + @buffer = StringIO.new + end + + def zip_parts(output) + output = Shellwords.shellescape(File.absolute_path(output)) + inputs = Shellwords.shelljoin(@files) + escaped_dir = Shellwords.shellescape(@dir) + command = "cd #{escaped_dir} && #{@zip_command} #{output} #{inputs}" + stdout_and_stderr, status = Open3.capture2e(command) + if !status.success? + raise(ZipError.new(stdout_and_stderr)) + end + end + end +end |
