diff --git a/lib/remote_input.rb b/lib/remote_input.rb index 0c77230..b0fe0e9 100644 --- a/lib/remote_input.rb +++ b/lib/remote_input.rb @@ -1,6 +1,80 @@ -require_relative "remote_input/downloader" require_relative "remote_input/cache-path" +require_relative "remote_input/downloader" require_relative "remote_input/zip-extractor" -module RemoteInput +class RemoteInput + class << self + def open(...) + input = new(...) + if block_given? + begin + yield(input) + ensure + input.close + end + else + input + end + end + end + + def initialize(url, + *fallback_urls, + 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 + @local_file = nil + @closed = false + end + + def read(maxlen=nil, out_string=nil) + local_file.read(maxlen, out_string) + end + + def close + @local_file.close if @local_file and not @local_file.closed? + @closed = true + end + + def clear_cache + cache_path.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 = "#{@url.host}#{dirname}".tr("/", "-") + @cache_path = CachePath.new(cache_id) + end + + def normalize_path + url_path = @url.path + url_path = "/" if url_path.empty? + url_path += "data" if url_path.end_with?("/") + url_path + end + + def local_file + raise IOError, "closed stream" if @closed + return @local_file if @local_file + @downloader.download(path) + options = {} + options[:encoding] = @encoding if @encoding + options[:internal_encoding] = @internal_encoding if @internal_encoding + options[:external_encoding] = @external_encoding if @external_encoding + @local_file = path.open(**options) + end end diff --git a/lib/remote_input/cache-path.rb b/lib/remote_input/cache-path.rb index c21a4c3..43a5bdd 100644 --- a/lib/remote_input/cache-path.rb +++ b/lib/remote_input/cache-path.rb @@ -1,4 +1,4 @@ -module RemoteInput +class RemoteInput class CachePath def initialize(id) @id = id diff --git a/lib/remote_input/downloader.rb b/lib/remote_input/downloader.rb index 3056779..59f164a 100644 --- a/lib/remote_input/downloader.rb +++ b/lib/remote_input/downloader.rb @@ -9,7 +9,7 @@ require_relative "error" require_relative "version" -module RemoteInput +class RemoteInput class Downloader class TooManyRedirects < Error; end diff --git a/lib/remote_input/error.rb b/lib/remote_input/error.rb index da653da..68b859c 100644 --- a/lib/remote_input/error.rb +++ b/lib/remote_input/error.rb @@ -1,4 +1,4 @@ -module RemoteInput +class RemoteInput class Error < StandardError end end diff --git a/lib/remote_input/version.rb b/lib/remote_input/version.rb index d25a8ed..23f7807 100644 --- a/lib/remote_input/version.rb +++ b/lib/remote_input/version.rb @@ -1,3 +1,3 @@ -module RemoteInput +class RemoteInput VERSION = "0.0.0" end diff --git a/lib/remote_input/zip-extractor.rb b/lib/remote_input/zip-extractor.rb index 3a98332..44b9f3f 100644 --- a/lib/remote_input/zip-extractor.rb +++ b/lib/remote_input/zip-extractor.rb @@ -1,6 +1,6 @@ require 'zip' -module RemoteInput +class RemoteInput class ZipExtractor def initialize(path) @path = path diff --git a/test/test-remote-input.rb b/test/test-remote-input.rb new file mode 100644 index 0000000..0b71b3a --- /dev/null +++ b/test/test-remote-input.rb @@ -0,0 +1,99 @@ +require_relative "helper" + +class RemoteInputTest < Test::Unit::TestCase + def test_read_once + open_input do |input| + input.send(:path).parent.mkpath + input.send(:path).write("12") + assert_equal("12", input.read) + end + end + + def test_read_twice + open_input do |input| + input.send(:path).parent.mkpath + input.send(:path).write("12") + assert_equal("1", input.read(1)) + assert_equal("2", input.read(1)) + end + end + + def test_read_with_encoding + open_input(encoding: "Windows-31J:UTF-8") do |input| + input.send(:path).parent.mkpath + input.send(:path).write("入力", encoding: "Windows-31J:UTF-8") + assert_equal("入力", input.read) + end + end + + def test_read_after_close + open_input do |input| + input.close + assert_raise(IOError.new("closed stream")) do + input.read + end + end + end + + def test_open_with_block + opened_input = nil + open_input do |input| + opened_input = input + end + assert_raise(IOError.new("closed stream")) do + opened_input.read + end + end + + def test_open_with_block_raised + opened_input = nil + assert_raise(RuntimeError.new("error in block")) do + open_input do |input| + opened_input = input + raise "error in block" + end + end + assert_raise(IOError.new("closed stream")) do + opened_input.read + 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/file", "https://example.com/file?a=b"]) + 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| + assert do + input.send(:path).to_s.end_with?(expected) + end + end + end + + def test_clear_cache + open_input do |input| + input.send(:path).parent.mkpath + input.send(:path).write("1") + input.clear_cache + assert do + not input.send(:path).parent.exist? + end + end + end + + private + + def open_input(**options) + input = RemoteInput.open("https://example.com/file", **options) + begin + yield(input) + ensure + input.close + input.clear_cache + end + end +end