# Copyright (c) 2013, 2014 The University of Manchester, UK.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the names of The University of Manchester nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Author: Robert Haines
require 'forwardable'
module ZipContainer
# This class represents a ZipContainer file in PK Zip format. See the
# {OCF}[http://www.idpf.org/epub/30/spec/epub30-ocf.html] and
# {UCF}[https://learn.adobe.com/wiki/display/PDFNAV/Universal+Container+Format]
# specifications for more details.
#
# This class provides most of the facilities of the Zip::File
# class in the rubyzip gem. Please also consult the
# {rubyzip documentation}[http://rubydoc.info/gems/rubyzip/1.1.0/frames]
# alongside these pages.
#
# There are code examples available with the source code of this library.
class File < Container
extend Forwardable
def_delegators :@container, :comment, :comment=, :commit_required?, :each,
:entries, :extract, :get_input_stream, :name, :read, :size
private_class_method :new
# :stopdoc:
def initialize(location)
super(location)
@on_disk = true
# Here we fake up the connection to the rubyzip filesystem classes so
# that they also respect the reserved names that we define.
mapped_zip = ::Zip::FileSystem::ZipFileNameMapper.new(self)
@fs_dir = ::Zip::FileSystem::ZipFsDir.new(mapped_zip)
@fs_file = ::Zip::FileSystem::ZipFsFile.new(mapped_zip)
@fs_dir.file = @fs_file
@fs_file.dir = @fs_dir
end
# :startdoc:
# :call-seq:
# File.create(filename, mimetype) -> container
# File.create(filename, mimetype) {|container| ...}
#
# Create a new ZipContainer file on disk with the specified mimetype.
def self.create(filename, mimetype)
::Zip::OutputStream.open(filename) do |stream|
stream.put_next_entry(MIMETYPE_FILE, nil, nil, ::Zip::Entry::STORED)
stream.write mimetype
end
# Now open the newly created container.
c = new(filename)
if block_given?
begin
yield c
ensure
c.close
end
end
c
end
# :call-seq:
# File.each_entry -> Enumerator
# File.each_entry {|entry| ...}
#
# Iterate over the entries in the ZipContainer file. The entry objects
# returned by this method are Zip::Entry objects. Please see the
# rubyzip documentation for details.
def self.each_entry(filename, &block)
c = new(filename)
if block_given?
begin
c.each(&block)
ensure
c.close
end
end
c.each
end
# :call-seq:
# add(entry, src_path, &continue_on_exists_proc)
#
# Convenience method for adding the contents of a file to the ZipContainer
# file. If asked to add a file with a reserved name, such as the special
# mimetype header file, this method will raise a
# ReservedNameClashError.
#
# See the rubyzip documentation for details of the
# +continue_on_exists_proc+ parameter.
def add(entry, src_path, &continue_on_exists_proc)
if reserved_entry?(entry) || managed_directory?(entry)
raise ReservedNameClashError, entry.to_s
end
@container.add(entry, src_path, &continue_on_exists_proc)
end
# :call-seq:
# commit -> boolean
# close -> boolean
#
# Commits changes that have been made since the previous commit to the
# ZipContainer file. Returns +true+ if anything was actually done, +false+
# otherwise.
def commit
return false unless commit_required?
@container.commit if on_disk?
end
alias close commit
# :call-seq:
# dir -> Zip::ZipFsDir
#
# Returns an object which can be used like ruby's built in +Dir+ (class)
# object, except that it works on the ZipContainer file on which this
# method is invoked.
#
# See the rubyzip documentation for details.
def dir
@fs_dir
end
# :call-seq:
# file -> Zip::ZipFsFile
#
# Returns an object which can be used like ruby's built in +File+ (class)
# object, except that it works on the ZipContainer file on which this
# method is invoked.
#
# See the rubyzip documentation for details.
def file
@fs_file
end
# :call-seq:
# find_entry(entry_name, options = {}) -> Zip::Entry or nil
#
# Searches for the entry with the specified name. Returns +nil+ if no
# entry is found or if the specified entry is hidden for normal use. You
# can specify :include_hidden => true to include hidden entries
# in the search.
def find_entry(entry_name, options = {})
options = { include_hidden: false }.merge(options)
unless options[:include_hidden]
return if hidden_entry?(entry_name)
end
@container.find_entry(entry_name)
end
# :call-seq:
# get_entry(entry, options = {}) -> Zip::Entry or nil
#
# Searches for an entry like find_entry, but throws Errno::ENOENT if no
# entry is found or if the specified entry is hidden for normal use. You
# can specify :include_hidden => true to include hidden entries
# in the search.
def get_entry(entry, options = {})
options = { include_hidden: false }.merge(options)
unless options[:include_hidden]
raise Errno::ENOENT, entry if hidden_entry?(entry)
end
@container.get_entry(entry)
end
# :call-seq:
# get_output_stream(entry, permission = nil) -> stream
# get_output_stream(entry, permission = nil) {|stream| ...}
#
# Returns an output stream to the specified entry. If a block is passed
# the stream object is passed to the block and the stream is automatically
# closed afterwards just as with ruby's built-in +File.open+ method.
#
# See the rubyzip documentation for details of the +permission_int+
# parameter.
def get_output_stream(entry, permission = nil, &block)
if reserved_entry?(entry) || managed_directory?(entry)
raise ReservedNameClashError, entry.to_s
end
@container.get_output_stream(entry, permission, &block)
end
# :call-seq:
# glob(pattern) -> Array
# glob(pattern) { |entry| ... }
# glob(pattern, *parameters) -> Array
# glob(pattern, *parameters) { |entry| ... }
#
# Searches for entries given a glob. Hidden files are ignored by default.
#
# The parameters that can be supplied are:
# * +flags+ - A bitwise OR of the FNM_xxx parameters defined in
# File::Constants. The default value is
# ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH
# * +options+ - :include_hidden => true will include hidden
# entries in the search.
def glob(pattern, *params)
flags = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH
options = { include_hidden: false }
params.each do |param|
case param
when Hash
options = options.merge(param)
else
flags = param
end
end
entries.map do |entry|
next if !options[:include_hidden] && hidden_entry?(entry)
next unless ::File.fnmatch(pattern, entry.name.chomp('/'), flags)
yield(entry) if block_given?
entry
end.compact
end
# :call-seq:
# in_memory? -> boolean
#
# Is this ZipContainer file memory resident as opposed to stored on disk?
def in_memory?
!@on_disk
end
# :call-seq:
# mkdir(name, permission = 0755)
#
# Creates a directory in the ZipContainer file. If asked to create a
# directory with a name reserved for use by a file this method will raise
# a ReservedNameClashError.
#
# The new directory will be created with the supplied unix-style
# permissions. The default (+0755+) is owner read, write and list; group
# read and list; and world read and list.
def mkdir(name, permission = 0o0755)
if reserved_entry?(name) || managed_file?(name)
raise ReservedNameClashError, name
end
@container.mkdir(name, permission)
end
# :call-seq:
# on_disk? -> boolean
#
# Is this ZipContainer file stored on disk as opposed to memory resident?
def on_disk?
@on_disk
end
# :call-seq:
# remove(entry)
#
# Removes the specified entry from the ZipContainer file. If asked to
# remove any reserved files such as the special mimetype header file this
# method will do nothing.
def remove(entry)
return if reserved_entry?(entry)
@container.remove(entry)
end
# :call-seq:
# rename(entry, new_name, &continue_on_exists_proc)
#
# Renames the specified entry in the ZipContainer file. If asked to rename
# any reserved files such as the special mimetype header file this method
# will do nothing. If asked to rename a file _to_ one of the reserved
# names a ReservedNameClashError is raised.
#
# See the rubyzip documentation for details of the
# +continue_on_exists_proc+ parameter.
def rename(entry, new_name, &continue_on_exists_proc)
return if reserved_entry?(entry)
raise ReservedNameClashError, new_name if reserved_entry?(new_name)
@container.rename(entry, new_name, &continue_on_exists_proc)
end
# :call-seq:
# replace(entry, src_path)
#
# Replaces the specified entry of the ZipContainer file with the contents
# of +src_path+ (from the file system). If asked to replace any reserved
# files such as the special mimetype header file this method will do
# nothing.
def replace(entry, src_path)
return if reserved_entry?(entry)
@container.replace(entry, src_path)
end
# :call-seq:
# to_s -> String
#
# Return a textual summary of this ZipContainer file.
def to_s
@container.to_s + " - #{@mimetype}"
end
private
def open_container(document)
::Zip::File.new(document)
end
def verify_mimetype
# Check mimetype file is present and correct.
entry = @container.find_entry(MIMETYPE_FILE)
return "'mimetype' file is missing." if entry.nil?
if entry.local_header_offset != 0
return "'mimetype' file is not at offset 0 in the archive."
end
if entry.compression_method != ::Zip::Entry::STORED
return "'mimetype' file is compressed."
end
end
def read_mimetype
@container.read(MIMETYPE_FILE)
end
public
# Lots of extra docs out of the way at the end here...
##
# :method: comment
# :call-seq:
# comment -> String
#
# Returns the ZipContainer file comment, if it has one.
##
# :method: comment=
# :call-seq:
# comment = comment
#
# Set the ZipContainer file comment to the new value.
##
# :method: commit_required?
# :call-seq:
# commit_required? -> boolean
#
# Returns +true+ if any changes have been made to this ZipContainer file
# since the last commit, +false+ otherwise.
##
# :method: each
# :call-seq:
# each -> Enumerator
# each {|entry| ...}
#
# Iterate over the entries in the ZipContainer file. The entry objects
# returned by this method are Zip::Entry objects. Please see the
# rubyzip documentation for details.
##
# :method: entries
# :call-seq:
# entries -> Enumerable
#
# Returns an Enumerable containing all the entries in the ZipContainer
# file The entry objects returned by this method are Zip::Entry
# objects. Please see the rubyzip documentation for details.
##
# :method: extract
# :call-seq:
# extract(entry, dest_path, &on_exists_proc)
#
# Extracts the specified entry of the ZipContainer file to +dest_path+.
#
# See the rubyzip documentation for details of the +on_exists_proc+
# parameter.
##
# :method: find_entry
# :call-seq:
# find_entry(entry) -> Zip::Entry
#
# Searches for entries within the ZipContainer file with the specified
# name. Returns +nil+ if no entry is found. See also +get_entry+.
##
# :method: get_entry
# :call-seq:
# get_entry(entry) -> Zip::Entry
#
# Searches for an entry within the ZipContainer file in a similar manner
# to +find_entry+, but throws +Errno::ENOENT+ if no entry is found.
##
# :method: get_input_stream
# :call-seq:
# get_input_stream(entry) -> stream
# get_input_stream(entry) {|stream| ...}
#
# Returns an input stream to the specified entry. If a block is passed the
# stream object is passed to the block and the stream is automatically
# closed afterwards just as with ruby's built in +File.open+ method.
##
# :method: glob
# :call-seq:
# glob(*args) -> Array of Zip::Entry
# glob(*args) {|entry| ...}
#
# Searches for entries within the ZipContainer file that match the given
# glob.
#
# See the rubyzip documentation for details of the parameters that can be
# passed in.
##
# :method: name
# :call-seq:
# name -> String
#
# Returns the filename of this ZipContainer file.
##
# :method: read
# :call-seq:
# read(entry) -> String
#
# Returns a string containing the contents of the specified entry.
##
# :method: size
# :call-seq:
# size -> int
#
# Returns the number of entries in the ZipContainer file.
end
end