Class: ZipKit::RemoteIO

Inherits:
Object
  • Object
show all
Defined in:
lib/zip_kit/remote_io.rb

Overview

An object that fakes just-enough of an IO to be dangerous

  • or, more precisely, to be useful as a source for the FileReader central directory parser. Effectively we substitute an IO object for an object that fetches parts of the remote file over HTTP using Range: headers. The RemoteIO acts as an adapter between an object that performs the actual fetches over HTTP and an object that expects a handful of IO methods to be available.

Instance Method Summary collapse

Constructor Details

#initialize(url) ⇒ RemoteIO

Returns a new instance of RemoteIO.

Parameters:

  • url (String, URI)

    the HTTP/HTTPS URL of the object to be retrieved



14
15
16
17
18
# File 'lib/zip_kit/remote_io.rb', line 14

def initialize(url)
  @pos = 0
  @uri = URI(url)
  @remote_size = nil
end

Instance Method Details

#read(n_bytes = nil) ⇒ String

Emulates IO#read, but requires the number of bytes to read The read will be limited to the size of the remote resource relative to the current offset in the IO, so if you are at offset 0 in the IO of size 10, doing a read(20) will only return you 10 bytes of result, and not raise any exceptions.

Parameters:

  • n_bytes (Integer, nil) (defaults to: nil)

    how many bytes to read, or nil to read all the way to the end

Returns:

  • (String)

    the read bytes

Raises:

  • (ArgumentError)


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/zip_kit/remote_io.rb', line 45

def read(n_bytes = nil)
  # If the resource is empty there is nothing to read
  return if size.zero?

  maximum_avaialable = size - @pos
  n_bytes ||= maximum_avaialable # nil == read to the end of file
  return "" if n_bytes.zero?
  raise ArgumentError, "No negative reads(#{n_bytes})" if n_bytes < 0

  n_bytes = clamp(0, n_bytes, maximum_avaialable)

  http_range = (@pos..(@pos + n_bytes - 1))
  request_range(http_range).tap do |data|
    raise "Remote read returned #{data.bytesize} bytes instead of #{n_bytes} as requested" if data.bytesize != n_bytes
    @pos = clamp(0, @pos + data.bytesize, size)
  end
end

#request_object_sizeInteger (protected)

For working with S3 it is a better idea to perform a GET request for one byte, since doing a HEAD request needs a different permission - and standard GET presigned URLs are not allowed to perform it

Returns:

  • (Integer)

    the size of the remote resource, parsed either from Content-Length or Content-Range header



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/zip_kit/remote_io.rb', line 93

def request_object_size
  http = Net::HTTP.start(@uri.hostname, @uri.port)
  request = Net::HTTP::Get.new(@uri)
  request.range = 0..0
  response = http.request(request)
  case response.code
  when "206"
    content_range_header_value = response["Content-Range"]
    content_range_header_value.split("/").last.to_i
  when "200"
    response["Content-Length"].to_i
  else
    raise "Remote at #{@uri} replied with code #{response.code}"
  end
end

#request_range(range) ⇒ String (protected)

Only used internally when reading the remote ZIP.

Parameters:

  • range (Range)

    the HTTP range of data to fetch from remote

Returns:

  • (String)

    the response body of the ranged request



76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/zip_kit/remote_io.rb', line 76

def request_range(range)
  http = Net::HTTP.start(@uri.hostname, @uri.port)
  request = Net::HTTP::Get.new(@uri)
  request.range = range
  response = http.request(request)
  case response.code
  when "206", "200"
    response.body
  else
    raise "Remote at #{@uri} replied with code #{response.code}"
  end
end

#seek(offset, mode = IO::SEEK_SET) ⇒ Object

Emulates IO#seek

Parameters:

  • offset (Integer)

    absolute offset in the remote resource to seek to

  • mode (Integer) (defaults to: IO::SEEK_SET)

    The seek mode (only SEEK_SET is supported)



23
24
25
26
27
28
# File 'lib/zip_kit/remote_io.rb', line 23

def seek(offset, mode = IO::SEEK_SET)
  raise "Unsupported read mode #{mode}" unless mode == IO::SEEK_SET
  @remote_size ||= request_object_size
  @pos = clamp(0, offset, @remote_size)
  0 # always return 0!
end

#sizeInteger

Emulates IO#size.

Returns:

  • (Integer)

    the size of the remote resource



33
34
35
# File 'lib/zip_kit/remote_io.rb', line 33

def size
  @remote_size ||= request_object_size
end

#tellInteger

Returns the current pointer position within the IO

Returns:

  • (Integer)


66
67
68
# File 'lib/zip_kit/remote_io.rb', line 66

def tell
  @pos
end