Search code examples
rubydocx

Ruby Zip: Cannot open entry for reading while its open for writing


I'm trying to write some mail merge code where I open a docx file (as a zip) replace tags with data and then create a new docx file (as a zip) and iterate over the old zip file either adding my new replaced data or pulling the existing file from the old docx file and adding that instead.

The problem I'm getting is anytime I try to access the out.get_output_stream method, I'm getting the following error:

cannot open entry for reading while its open for writing - [Content_Types].xml (StandardError)

[Content_Types].xml happens to be first file in the docx so that's why its bombing on that particular file. What am I doing wrong?

require 'rubygems'
require 'zip' # rubyzip gem

class WordMailMerge
  def self.open(path, &block)
    self.new(path, &block)
  end

  def initialize(path, &block)
    @replace = {}
    if block_given?
      @zip = Zip::File.open(path)
      yield(self)
      @zip.close
    else
      @zip = Zip::File.open(path)
    end
  end

  def force_settings
    @replace["word/settings.xml"] = %{<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:settings xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main"><w:zoom w:percent="100"/></w:settings>}
  end

  def merge(rec)
    xml = @zip.read("word/document.xml")

    # replace tags with correct content

    @replace["word/document.xml"] = xml
  end

  def save(path)
    Zip::File.open(path, Zip::File::CREATE) do |out|
      @zip.each do |entry|

        if @replace[entry.name]
          # this line creates the error
          out.get_output_stream(entry.name).write(@replace[entry.name])
        else
          # this line also will do it.
          out.get_output_stream(entry.name).write(@zip.read(entry.name))
        end
      end
    end
  end

  def close
    @zip.close
  end
end

w = WordMailMerge.open("Option_2.docx")
w.force_settings
w.merge({})
w.save("Option_2_new.docx")

The following is the stack trace:

/home/aaron/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/delegate.rb:85:in `call': cannot open entry for reading while its open for writing - [Content_Types].xml (StandardError)
    from /home/aaron/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/delegate.rb:85:in `method_missing'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/streamable_stream.rb:28:in `get_input_stream'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/streamable_stream.rb:45:in `write_to_zip_output_stream'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:313:in `block (3 levels) in commit'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/entry_set.rb:38:in `block in each'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/entry_set.rb:37:in `each'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/entry_set.rb:37:in `each'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:312:in `block (2 levels) in commit'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/output_stream.rb:53:in `open'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:311:in `block in commit'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:409:in `block in on_success_replace'
    from /home/aaron/.rvm/rubies/ruby-2.4.1/lib/ruby/2.4.0/tmpdir.rb:130:in `create'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:407:in `on_success_replace'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:310:in `commit'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:334:in `close'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:103:in `ensure in open'
    from /home/aaron/.rvm/gems/ruby-2.4.1@appt/gems/rubyzip-1.2.1/lib/zip/file.rb:103:in `open'
    from zip.rb:34:in `save'
    from zip.rb:56:in `<main>'

Solution

  • You need to change your update code to below

      def save(path)
        Zip::File.open(path, Zip::File::CREATE) do |out|
          @zip.each do |entry|
    
            if @replace[entry.name]
              # this line creates the error
              out.get_output_stream(entry.name){ |f| f.puts @replace[entry.name] }
            else
              # this line also will do it.
              # out.get_output_stream(entry.name).write(@zip.read(entry.name))
              out.get_output_stream(entry.name){ |f|  f.puts @zip.read(entry.name) }
            end
          end
        end
      end
    

    And then the file will get created

    Edit-1

    Below is the final code that I had used for testing

    require 'rubygems'
    require 'zip' # rubyzip gem
    
    class WordMailMerge
      def self.open(path, &block)
        self.new(path, &block)
      end
    
      def initialize(path, &block)
        @replace = {}
        if block_given?
          @zip = Zip::File.open(path)
          yield(self)
          @zip.close
        else
          @zip = Zip::File.open(path)
        end
      end
    
      def force_settings
        @replace["word/settings.xml"] = %{<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <w:settings xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main"><w:zoom w:percent="100"/></w:settings>}
      end
    
      def merge(rec)
        xml = @zip.read("word/document.xml")
    
        # replace tags with correct content
    
        @replace["word/document.xml"] = xml.gsub("{name}", "Tarun lalwani")
      end
    
      def save(path)
        Zip::File.open(path, Zip::File::CREATE) do |out|
          @zip.each do |entry|
    
            if @replace[entry.name]
              # this line creates the error
              out.get_output_stream(entry.name){ |f| f.puts @replace[entry.name] }
            else
              # this line also will do it.
              # out.get_output_stream(entry.name).write(@zip.read(entry.name))
              out.get_output_stream(entry.name){ |f|  f.puts @zip.read(entry.name) }
            end
          end
        end
      end
    
      def close
        @zip.close
      end
    end
    
    w = WordMailMerge.open("Option_2.docx")
    w.force_settings
    w.merge({})
    w.save("Option_2_new.docx")
    

    Option_2.docx

    Old doc

    Option_2_new.doc

    Updated document