Files
w3dhub_linux_launcher/lib/cache.rb
2025-10-08 14:16:33 -05:00

185 lines
6.5 KiB
Ruby

class W3DHub
class Cache
LOG_TAG = "W3DHub::Cache".freeze
def self.path(uri)
ext = File.basename(uri).split(".").last
"#{CACHE_PATH}/#{Digest::SHA2.hexdigest(uri)}.#{ext}"
end
# Fetch a generic uri
def self.fetch(uri:, force_fetch: false, async: true, backend: :w3dhub)
path = path(uri)
if !force_fetch && File.exist?(path)
path
elsif async
BackgroundWorker.job(
-> { Api.fetch(uri, W3DHub::Api::DEFAULT_HEADERS, nil, backend) },
->(response) { File.open(path, "wb") { |f| f.write response.body } if response.status == 200 }
)
else
response = Api.fetch(uri, W3DHub::Api::DEFAULT_HEADERS, nil, backend)
File.open(path, "wb") { |f| f.write response.body } if response.status == 200
end
end
def self.create_directories(path, is_directory = false)
target_directory = is_directory ? path : File.dirname(path)
FileUtils.mkdir_p(target_directory) unless Dir.exist?(target_directory)
end
def self.package_path(category, subcategory, name, version)
package_cache_dir = Store.settings[:package_cache_dir]
if version.to_s.length.zero?
"#{package_cache_dir}/#{category}/#{subcategory}/#{name}.package"
else
"#{package_cache_dir}/#{category}/#{subcategory}/#{version}/#{name}.package"
end
end
def self.install_path(application, channel)
game_data = Store.settings[:games]&.dig(:"#{application.id}_#{channel.id}")
return game_data[:install_directory] if game_data && game_data[:install_directory]
"#{Store.settings[:app_install_dir]}/#{application.category}/#{application.id}/#{channel.id}"
end
# Download a W3D Hub package
# TODO: More work needed to make this work reliably
def self._async_fetch_package(package, block)
path = package_path(package.category, package.subcategory, package.name, package.version)
headers = Api::FORM_ENCODED_HEADERS
start_from_bytes = package.custom_partially_valid_at_bytes
logger.info(LOG_TAG) { " Start from bytes: #{start_from_bytes} of #{package.size}" }
create_directories(path)
file = File.open(path, start_from_bytes.positive? ? "r+b" : "wb")
if start_from_bytes.positive?
headers = Api::FORM_ENCODED_HEADERS + [["Range", "bytes=#{start_from_bytes}-"]]
file.pos = start_from_bytes
end
body = "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}"
response = Api.post("/apis/launcher/1/get-package", headers, body)
total_bytes = package.size
remaining_bytes = total_bytes - start_from_bytes
response.each do |chunk|
file.write(chunk)
remaining_bytes -= chunk.size
block.call(chunk, remaining_bytes, total_bytes)
end
response.status == 200
ensure
file&.close
end
# Download a W3D Hub package
def self.fetch_package(package, block)
endpoint_download_url = package.download_url || "#{Api::W3DHUB_API_ENDPOINT}/apis/launcher/1/get-package"
if package.download_url
uri_path = package.download_url.split("/").last
endpoint_download_url = package.download_url.sub(uri_path, URI.encode_uri_component(uri_path))
end
path = package_path(package.category, package.subcategory, package.name, package.version)
headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": Api::USER_AGENT }
headers["Authorization"] = "Bearer #{Store.account.access_token}" if Store.account && !package.download_url
body = "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}"
start_from_bytes = package.custom_partially_valid_at_bytes
logger.info(LOG_TAG) { " Start from bytes: #{start_from_bytes} of #{package.size}" }
create_directories(path)
file = File.open(path, start_from_bytes.positive? ? "r+b" : "wb")
if start_from_bytes.positive?
headers["Range"] = "bytes=#{start_from_bytes}-"
file.pos = start_from_bytes
end
streamer = lambda do |chunk, remaining_bytes, total_bytes|
file.write(chunk)
block.call(chunk, remaining_bytes, total_bytes)
end
# Create a new connection due to some weirdness somewhere in Excon
response = Excon.send(
package.download_url ? :get : :post,
endpoint_download_url,
tcp_nodelay: true,
headers: headers,
body: package.download_url ? "" : body,
chunk_size: 50_000,
response_block: streamer,
middlewares: Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower]
)
if response.status == 200 || response.status == 206
return true
else
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false
end
rescue Excon::Error::Timeout => e
logger.error(LOG_TAG) { " Connection to \"#{endpoint_download_url}\" timed out after: #{W3DHub::Api::API_TIMEOUT} seconds" }
logger.error(LOG_TAG) { e }
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false
rescue Excon::Error => e
logger.error(LOG_TAG) { " Connection to \"#{endpoint_download_url}\" errored:" }
logger.error(LOG_TAG) { e }
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false
ensure
file&.close
end
def self.acquire_net_lock(key)
Store["net_locks"] ||= {}
if Store["net_locks"][key]
false
else
Store["net_locks"][key] = true
end
end
def self.release_net_lock(key)
Store["net_locks"] ||= {}
if Store["net_locks"][key]
Store["net_locks"].delete(key)
else
warn "!!! net_lock not found for #{key.inspect}"
end
end
def self.net_lock?(key)
Store["net_locks"] ||= {}
Store["net_locks"][key]
end
end
end