Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 9 additions & 25 deletions lib/remote_input.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require_relative "remote_input/cache-entry"
require_relative "remote_input/cache-path"
require_relative "remote_input/downloader"
require_relative "remote_input/zip-extractor"
Expand All @@ -20,16 +21,20 @@ def open(...)

def initialize(url,
*fallback_urls,
cache_path: nil,
encoding: nil,
internal_encoding: nil,
external_encoding: nil,
**http_options)
@url = URI(url)
@encoding = encoding
@internal_encoding = internal_encoding
@external_encoding = external_encoding
@downloader = Downloader.new(url, *fallback_urls, **http_options)
@cache_path = nil
if cache_path
@cache_entry = CacheEntry.from_path(cache_path)
else
@cache_entry = CacheEntry.from_url(url)
end
@local_file = nil
@closed = false
end
Expand All @@ -44,34 +49,13 @@ def close
end

def clear_cache
cache_path.remove
@cache_entry.remove
end

private

def path
cache_path.base_dir + File.basename(normalize_path)
end

def cache_path
return @cache_path if @cache_path
dirname = File.dirname(normalize_path).delete_suffix("/")
cache_id = to_cache_id("#{@url.host}#{dirname}")
query = @url.query
cache_id += "+#{to_cache_id(query)}" if query and not query.empty?
@cache_path = CachePath.new(cache_id)
end

def to_cache_id(s)
allow_list = "0-9A-Za-z._~=-"
s.tr("^#{allow_list}", "-")
end

def normalize_path
url_path = @url.path
url_path = "/" if url_path.empty?
url_path += "data" if url_path.end_with?("/")
url_path
@cache_entry.path
end

def local_file
Expand Down
69 changes: 69 additions & 0 deletions lib/remote_input/cache-entry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require_relative "cache-path"

class RemoteInput
# @api private
class CacheEntry
class << self
def from_url(url)
url = URI(url)
path = url.path
path = "/" if path.empty?
path += "data" if path.end_with?("/")
dirname = File.dirname(path).delete_suffix("/")
id = to_id("#{url.host}#{dirname}")
query = url.query
id += "+#{to_id(query)}" if query and not query.empty?
new(id, File.basename(path))
end

def from_path(cache_path)
path = Pathname(cache_path)
if path.absolute?
raise ArgumentError,
"cache_path must be relative: #{cache_path.inspect}"
end
id, *rest_filenames = path.each_filename.to_a
if [id, *rest_filenames].intersect?([".", ".."])
raise ArgumentError,
"cache_path must not include '.' or '..': #{cache_path.inspect}"
end
if rest_filenames.empty?
raise ArgumentError,
"cache_path must be <cache ID>/<path in cache>: " +
cache_path.inspect
end
new(id, File.join(*rest_filenames))
end

private
def to_id(s)
allow_list = "0-9A-Za-z._~=-"
s.tr("^#{allow_list}", "-")
end
end

def initialize(id, path_in_cache)
@id = id
@path_in_cache = path_in_cache
@cache_path = CachePath.new(@id)
end
Comment on lines +6 to +49

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid using custom constructors (from_url/`from_path``)?

Can we use only new like the following?

def initialize(target)
  if target.is_a?(URL)
    # from_url like process
  elif target.is_a?(Pathname)
    # from_path like process
  else
    raise ArgumentError, "..."
  end
  ...
end


def path
@cache_path.base_dir + @path_in_cache
end

def remove
@cache_path.remove
end

def ==(other)
other.is_a?(self.class) and
@id == other.id and
@path_in_cache == other.path_in_cache
end

protected
attr_reader :id
attr_reader :path_in_cache
end
end
49 changes: 49 additions & 0 deletions test/test-cache-entry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require_relative "helper"

class CacheEntryTest < Test::Unit::TestCase
data("no path", ["example.com", "data", "https://example.com"])
data("root", ["example.com", "data", "https://example.com/"])
data("file", ["example.com", "file", "https://example.com/file"])
data("query", ["example.com+a=-", "file", "https://example.com/file?a=+"])
data("directory", ["example.com-a", "data", "https://example.com/a/"])
data("nested file", ["example.com-a", "file", "https://example.com/a/file"])
data("deeply nested", ["example.com-a-b", "file", "https://example.com/a/b/file"])
def test_from_url(data)
id, path_in_cache, url = data
cache_entry = RemoteInput::CacheEntry.from_url(URI(url))
assert_equal(RemoteInput::CacheEntry.new(id, path_in_cache),
cache_entry)
end

def test_from_valid_path
cache_path = "example/sub-directory/data.csv"
cache_entry = RemoteInput::CacheEntry.from_path(cache_path)
assert_equal(RemoteInput::CacheEntry.new("example",
"sub-directory/data.csv"),
cache_entry)
end

data("absolute", "/data.csv")
data("no cache ID", "data.csv")
data("no path in cache", "example/")
data("root cache", "./data.csv")
data("path traversal", "../example/data.csv")
def test_from_invalid_path(cache_path)
assert_raise(ArgumentError) do
RemoteInput::CacheEntry.from_path(cache_path)
end
end

def test_equal_same_path_different_id
cache_entry1 = RemoteInput::CacheEntry.new("example.com/a", "file")
cache_entry2 = RemoteInput::CacheEntry.new("example.com", "a/file")
assert_equal([
true,
false,
],
[
cache_entry1.path == cache_entry2.path,
cache_entry1 == cache_entry2,
])
end
end
15 changes: 4 additions & 11 deletions test/test-remote-input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,11 @@ def test_open_with_block_raised
end
end

data("no path", ["/example.com/data", "https://example.com"])
data("root", ["/example.com/data", "https://example.com/"])
data("file", ["/example.com/file", "https://example.com/file"])
data("query", ["/example.com+a=-/file", "https://example.com/file?a=+"])
data("directory", ["/example.com-a/data", "https://example.com/a/"])
data("nested file", ["/example.com-a/file", "https://example.com/a/file"])
data("deeply nested", ["/example.com-a-b/file", "https://example.com/a/b/file"])
def test_path(data)
expected, url = data
RemoteInput.open(url) do |input|
def test_cache_path
RemoteInput.open("https://example.com/file",
cache_path: "example/sub-directory/data.csv") do |input|
assert do
input.send(:path).to_s.end_with?(expected)
input.send(:path).to_s.end_with?("/example/sub-directory/data.csv")
end
end
end
Expand Down
Loading