Class: Rack::Archive::Zip::Extract

Inherits:
Object
  • Object
show all
Extended by:
Utils
Includes:
Utils
Defined in:
lib/rack/archive/zip/extract.rb

Overview

Note:

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

Rack::Archive::Zip::Extract is a Rack application which serves files in zip archives.

Examples:

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

Defined Under Namespace

Classes: ExtractedFile

Constant Summary collapse

DOT =
'.'.freeze
DOUBLE_DOT =
'..'.freeze
CONTENT_TYPE =
'Content-Type'.freeze
CONTENT_LENGTH =
'Content-Length'.freeze
IF_MODIFIED_SINCE =
'HTTP_IF_MODIFIED_SINCE'.freeze
LAST_MODIFIED =
'Last-Modified'.freeze
IF_NONE_MATCH =
'HTTP_IF_NONE_MATCH'.freeze
ETAG =
'ETag'.freeze
REQUEST_METHOD =
'REQUEST_METHOD'.freeze
PATH_INFO =
'PATH_INFO'.freeze
EMPTY_BODY =
[].freeze
EMPTY_HEADERS =
{}.freeze
METHOD_NOT_ALLOWED =
[status_code(:method_not_allowd), {'Allow'.freeze => Rack::File::ALLOWED_VERBS.join(', ').freeze}.freeze, EMPTY_BODY]
NOT_FOUND =
[status_code(:not_found), EMPTY_HEADERS, EMPTY_BODY].freeze
NOT_MODIFIED =
[status_code(:not_modified), EMPTY_HEADERS, EMPTY_BODY].freeze
OCTET_STREAM =
'application/octet-stream'.freeze

Instance Method Summary collapse

Constructor Details

#initialize(root, extensions: %w[.zip], mime_types: {}, buffer_size: ExtractedFile::BUFFER_SIZE) ⇒ Extract

Returns a new instance of Extract.

Parameters:

  • root (Pathname, #to_path, String)

    path to document root

  • extensions (Array<String>) (defaults to: %w[.zip])

    extensions which is recognized as a zip file

  • mime_types (Hash{String => String}) (defaults to: {})

    pairs of extesion and content type

  • buffer_size (Integer) (defaults to: ExtractedFile::BUFFER_SIZE)

    buffer size to read content, in bytes

Raises:

  • (ArgumentError)

    if root is not a directory



43
44
45
46
47
48
49
50
# 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

Instance Method Details

#call(env) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 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) ⇒ ExtractedFile?

Parameters:

  • zip_file_path (Pathname)

    path to zip file

  • inner_path (String)

    path to file in zip archive

Returns:

  • (ExtractedFile)
  • (nil)

    if zip_file_path is nil or inner_path is empty

  • (nil)

    if inner_path doesn’t exist in zip archive



101
102
103
104
105
106
107
108
109
110
# 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) ⇒ Array

Returns a pair of Pathname(zip file) and String(file path in zip archive).

Parameters:

  • path_info (String)
  • extension (String)

Returns:

  • (Array)

    a pair of Pathname(zip file) and String(file path in zip archive)



85
86
87
88
89
90
91
92
93
94
# 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) ⇒ Array<String>

Returns segments of clean path.

Parameters:

  • path_info (String)

Returns:

  • (Array<String>)

    segments of clean path

See Also:



115
116
117
118
119
120
121
122
123
# 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