diff --git a/cyberarm_engine.gemspec b/cyberarm_engine.gemspec index 26fa840..51016f4 100644 --- a/cyberarm_engine.gemspec +++ b/cyberarm_engine.gemspec @@ -28,6 +28,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib", "assets"] + spec.add_dependency "excon", "~> 0.76.0" spec.add_dependency "gosu", "~> 0.15.0" spec.add_dependency "gosu_more_drawables", "~> 0.3" spec.add_dependency "clipboard", "~> 1.3.4" diff --git a/lib/cyberarm_engine.rb b/lib/cyberarm_engine.rb index 39b0a63..0d1d195 100644 --- a/lib/cyberarm_engine.rb +++ b/lib/cyberarm_engine.rb @@ -7,6 +7,7 @@ rescue LoadError => e require "gosu" end require "json" +require "excon" require "gosu_more_drawables" require "clipboard" diff --git a/lib/cyberarm_engine/cache.rb b/lib/cyberarm_engine/cache.rb new file mode 100644 index 0000000..170c764 --- /dev/null +++ b/lib/cyberarm_engine/cache.rb @@ -0,0 +1,4 @@ +module CyberarmEngine + module Cache + end +end \ No newline at end of file diff --git a/lib/cyberarm_engine/cache/download_manager.rb b/lib/cyberarm_engine/cache/download_manager.rb new file mode 100644 index 0000000..e9278fc --- /dev/null +++ b/lib/cyberarm_engine/cache/download_manager.rb @@ -0,0 +1,119 @@ +module CyberarmEngine + module Cache + class DownloadManager + attr_reader :downloads + + def initialize(max_parallel_downloads: 4) + @max_parallel_downloads = max_parallel_downloads + @downloads = [] + end + + def download(url:, save_as: nil, &callback) + uri = URI(url) + save_as ||= "filename_path" # TODO: if no save_as path is provided, then get one from the Cache controller + + @downloads << Download.new(uri: uri, save_as: save_as, callback: callback) + end + + def status + if active_downloads > 0 + :busy + else + :idle + end + end + + def progress + remaining_bytes = @downloads.map { |d| d.remaining_bytes }.sum + total_bytes = @downloads.map { |d| d.total_bytes }.sum + + v = 1.0 - (remaining_bytes.to_f / total_bytes.to_f) + return 0.0 if v.nan? + return v + end + + def active_downloads + @downloads.select { |d| [:pending, :downloading].include?(d.status) } + end + + def update + @downloads.each do |download| + if download.status == :pending && active_downloads.size <= @max_parallel_downloads + download.status = :downloading + Thread.start { download.download } + end + end + end + + def prune + @downloads.delete_if { |d| d.status == :finished || d.status == :failed } + end + + class Download + attr_accessor :status + attr_reader :uri, :save_as, :callback, :remaining_bytes, :total_downloaded_bytes, :total_bytes, + :error_message, :started_at, :finished_at + def initialize(uri:, save_as:, callback: nil) + @uri = uri + @save_as = save_as + @callback = callback + + @status = :pending + + @remaining_bytes = 0.0 + @total_downloaded_bytes = 0.0 + @total_bytes = 0.0 + + @error_message = "" + end + + def progress + v = 1.0 - (@remaining_bytes.to_f / total_bytes.to_f) + return 0.0 if v.nan? + return v + end + + def download + @status = :downloading + @started_at = Time.now # TODO: monotonic time + + io = File.open(@save_as, "w") + streamer = lambda do |chunk, remaining_bytes, total_bytes| + io.write(chunk) + + @remaining_bytes = remaining_bytes.to_f + @total_downloaded_bytes += chunk.size + @total_bytes = total_bytes.to_f + end + + begin + response = Excon.get( + @uri.to_s, + middlewares: Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower], + response_block: streamer + ) + + if response.status == 200 + @status = :finished + @finished_at = Time.now # TODO: monotonic time + @callback.call(self) if @callback + else + @error_message = "Got a non 200 HTTP status of #{response.status}" + @status = :failed + @finished_at = Time.now # TODO: monotonic time + @callback.call(self) if @callback + end + rescue => e # TODO: cherrypick errors to cature + @status = :failed + @finished_at = Time.now # TODO: monotonic time + @error_message = e.message + @callback.call(self) if @callback + end + + ensure + io.close if io + end + end + end + end +end \ No newline at end of file