class Rack::Archive::Zip::Extract

{Rack::Archive::Zip::Extract Rack::Archive::Zip::Extract} is a Rack application which serves files in zip archives. @example

run Rack::Archive::Zip::Extract.new('path/to/docroot')

@example

run Rack::Archive::Zip::Extract.new('path/to/docroot', extensions: %w[.epub .zip .jar .odt .docx])

@note

{Rack::Archive::Zip::Extract Rack::Archive::Zip::Extract} does not serve a zip file itself. Use Rack::File or so to do so.

Constants

CONTENT_LENGTH
CONTENT_TYPE
DOT
DOUBLE_DOT
EMPTY_BODY
EMPTY_HEADERS
ETAG
IF_MODIFIED_SINCE
IF_NONE_MATCH
LAST_MODIFIED
METHOD_NOT_ALLOWED
NOT_FOUND
NOT_MODIFIED
OCTET_STREAM
PATH_INFO
REQUEST_METHOD

Public Class Methods

new(root, extensions: %w[.zip], mime_types: {}, buffer_size: ExtractedFile::BUFFER_SIZE) click to toggle source

@param root [Pathname, to_path, String] path to document root @param extensions [Array<String>] extensions which is recognized as a zip file @param mime_types [Hash{String => String}] pairs of extesion and content type @param buffer_size [Integer] buffer size to read content, in bytes @raise [ArgumentError] if root is not a directory

# File lib/rack/archive/zip/extract.rb, line 43
def initialize(root, extensions: %w[.zip], mime_types: {}, buffer_size: ExtractedFile::BUFFER_SIZE)
  @root = root.kind_of?(Pathname) ? root : Pathname(root)
  @root = @root.expand_path
  @extensions = extensions.map {|extention| extention.dup.freeze}.lazy
  @mime_types = Rack::Mime::MIME_TYPES.merge(mime_types)
  @buffer_size = buffer_size
  raise ArgumentError, "Not a directory: #{@root}" unless @root.directory?
end

Public Instance Methods

call(env) click to toggle source
# File lib/rack/archive/zip/extract.rb, line 52
def call(env)
  return METHOD_NOT_ALLOWED unless Rack::File::ALLOWED_VERBS.include? env[REQUEST_METHOD]

  path_info = unescape(env[PATH_INFO])
  file = @extensions.map {|ext|
    zip_file, inner_path = find_zip_file_and_inner_path(path_info, ext)
    extract_file(zip_file, inner_path)
  }.select {|file| file}.first
  return NOT_FOUND if file.nil?

  if_modified_since = Time.parse(env[IF_MODIFIED_SINCE]) rescue Time.new(0)
  if_none_match = env[IF_NONE_MATCH]

  if file.mtime <= if_modified_since or env[IF_NONE_MATCH] == file.etag
    file.close
    NOT_MODIFIED
  else
    [
      status_code(:ok),
      {
        CONTENT_TYPE => @mime_types.fetch(::File.extname(path_info), OCTET_STREAM),
        CONTENT_LENGTH => file.size.to_s,
        LAST_MODIFIED => file.mtime.httpdate,
        ETAG => file.etag
      },
      file
    ]
  end
end
extract_file(zip_file_path, inner_path) click to toggle source

@param zip_file_path [Pathname] path to zip file @param inner_path [String] path to file in zip archive @return [ExtractedFile] @return [nil] if zip_file_path is nil or inner_path is empty @return [nil] if inner_path doesn’t exist in zip archive

# File lib/rack/archive/zip/extract.rb, line 101
def extract_file(zip_file_path, inner_path)
  return if zip_file_path.nil? or inner_path.empty?
  archive = ::Zip::Archive.open(zip_file_path.to_path)
  if archive.locate_name(inner_path) < 0
    archive.close
    nil
  else
    ExtractedFile.new(archive, inner_path, @buffer_size)
  end
end
find_zip_file_and_inner_path(path_info, extension) click to toggle source

@param path_info [String] @param extension [String] @return [Array] a pair of Pathname(zip file) and String(file path in zip archive)

# File lib/rack/archive/zip/extract.rb, line 85
def find_zip_file_and_inner_path(path_info, extension)
  segments = path_info_to_clean_segments(path_info)
  current = @root
  zip_file = nil
  while segment = segments.shift
    zip_file = current + "#{segment}#{extension}"
    return zip_file, ::File.join(segments) if zip_file.file?
    current += segment
  end
end
path_info_to_clean_segments(path_info) click to toggle source

@param path_info [String] @return [Array<String>] segments of clean path @see rubydoc.info/gems/rack/Rack/File#_call-instance_method Algorithm stolen from Rack::File#_call

# File lib/rack/archive/zip/extract.rb, line 115
def path_info_to_clean_segments(path_info)
  segments = path_info.split PATH_SEPS
  clean = []
  segments.each do |segment|
    next if segment.empty? || segment == DOT
    segment == DOUBLE_DOT ? clean.pop : clean << segment
  end
  clean
end