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
@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
# 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
@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
@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
@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