summaryrefslogtreecommitdiffhomepage
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/axlsx.rb1
-rw-r--r--lib/axlsx/package.rb21
-rw-r--r--lib/axlsx/util/zip_command.rb73
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