hensei-api/lib/granblue/downloaders/base_downloader.rb
Justin Edmund 7d164b540c
Adds documentation to some lib functions (#168)
* Add sigs and docs to transformers

* Add sigs and docs to downloaders

* Adds sigs and docs to importers
2025-01-18 03:09:29 -08:00

246 lines
7.7 KiB
Ruby

# frozen_string_literal: true
module Granblue
module Downloaders
# Abstract base class for downloading game asset images in various sizes.
# Handles local and S3 storage, with support for test mode and verbose logging.
#
# @abstract Subclass must implement {#object_type}, {#base_url}, and {#directory_for_size}
#
# @example Downloading assets for a specific ID
# class MyDownloader < BaseDownloader
# def object_type; "weapon"; end
# def base_url; "http://example.com/assets"; end
# def directory_for_size(size)
# case size
# when "main" then "large"
# when "grid" then "medium"
# when "square" then "small"
# end
# end
# end
#
# downloader = MyDownloader.new("1234", storage: :both)
# downloader.download
#
# @note Supports three image sizes: main, grid, and square
# @note Can store images locally, in S3, or both
class BaseDownloader
# @return [Array<String>] Available image size variants
SIZES = %w[main grid square].freeze
# Initialize a new downloader instance
# @param id [String] ID of the object to download images for
# @param test_mode [Boolean] When true, only logs actions without downloading
# @param verbose [Boolean] When true, enables detailed logging
# @param storage [Symbol] Storage mode (:local, :s3, or :both)
# @return [void]
def initialize(id, test_mode: false, verbose: false, storage: :both)
@id = id
@base_url = base_url
@test_mode = test_mode
@verbose = verbose
@storage = storage
@aws_service = AwsService.new
ensure_directories_exist unless @test_mode
end
# Download images for all sizes
# @return [void]
def download
log_info "-> #{@id}"
return if @test_mode
SIZES.each_with_index do |size, index|
path = download_path(size)
url = build_url(size)
process_download(url, size, path, last: index == SIZES.size - 1)
end
end
private
# Process download for a specific size variant
# @param url [String] URL to download from
# @param size [String] Size variant being processed
# @param path [String] Local path for download
# @param last [Boolean] Whether this is the last size being processed
# @return [void]
def process_download(url, size, path, last: false)
filename = File.basename(url)
s3_key = build_s3_key(size, filename)
download_uri = "#{path}/#{filename}"
should_process = should_download?(download_uri, s3_key)
return unless should_process
if last
log_info "\t#{size}: #{url}..."
else
log_info "\t#{size}: #{url}..."
end
case @storage
when :local
download_to_local(url, download_uri)
when :s3
stream_to_s3(url, s3_key)
when :both
download_to_both(url, download_uri, s3_key)
end
rescue OpenURI::HTTPError
log_info "\t404 returned\t#{url}"
end
# Download file to local storage
# @param url [String] Source URL
# @param download_uri [String] Local destination path
# @return [void]
def download_to_local(url, download_uri)
download = URI.parse(url).open
IO.copy_stream(download, download_uri)
end
# Stream file directly to S3
# @param url [String] Source URL
# @param s3_key [String] S3 object key
# @return [void]
def stream_to_s3(url, s3_key)
return if @aws_service.file_exists?(s3_key)
URI.parse(url).open do |file|
@aws_service.upload_stream(file, s3_key)
end
end
# Download file to both local storage and S3
# @param url [String] Source URL
# @param download_uri [String] Local destination path
# @param s3_key [String] S3 object key
# @return [void]
def download_to_both(url, download_uri, s3_key)
download = URI.parse(url).open
# Write to local file
IO.copy_stream(download, download_uri)
# Reset file pointer for S3 upload
download.rewind
# Upload to S3 if it doesn't exist
unless @aws_service.file_exists?(s3_key)
@aws_service.upload_stream(download, s3_key)
end
end
# Check if file should be downloaded based on storage mode
# @param local_path [String] Local file path
# @param s3_key [String] S3 object key
# @return [Boolean] true if file should be downloaded
def should_download?(local_path, s3_key)
if @storage == :local
!File.exist?(local_path)
elsif @storage == :s3
!@aws_service.file_exists?(s3_key)
else
# :both
!File.exist?(local_path) || !@aws_service.file_exists?(s3_key)
end
end
# Ensure local directories exist for each size
# @return [void]
def ensure_directories_exist
return unless store_locally?
SIZES.each do |size|
FileUtils.mkdir_p(download_path(size))
end
end
# Check if local storage is being used
# @return [Boolean] true if storing locally
def store_locally?
%i[local both].include?(@storage)
end
# Get local download path for a size
# @param size [String] Image size variant
# @return [String] Local directory path
def download_path(size)
"#{Rails.root}/download/#{object_type}-#{size}"
end
# Build S3 key for an image
# @param size [String] Image size variant
# @param filename [String] Image filename
# @return [String] Complete S3 key
def build_s3_key(size, filename)
"#{object_type}-#{size}/#{filename}"
end
# Log informational message if verbose
# @param message [String] Message
def log_info(message)
puts message if @verbose
end
# Download elemental variant image
# @param url [String] Source URL
# @param size [String] Image size variant
# @param path [String] Destination path
# @param filename [String] Image filename
# @return [void]
def download_elemental_image(url, size, path, filename)
return if @test_mode
filepath = "#{path}/#{filename}"
URI.open(url) do |file|
content = file.read
if content
File.open(filepath, 'wb') do |output|
output.write(content)
end
else
raise "Failed to read content from #{url}"
end
end
log_info "-> #{size}:\t#{url}..."
rescue OpenURI::HTTPError
log_info "\t404 returned\t#{url}"
rescue StandardError => e
log_info "\tError downloading #{url}: #{e.message}"
end
# Get asset type (e.g., "weapon", "character")
# @abstract
# @return [String] Asset type name
def object_type
raise NotImplementedError, 'Subclasses must define object_type'
end
# Get base URL for assets
# @abstract
# @return [String] Base URL
def base_url
raise NotImplementedError, 'Subclasses must define base_url'
end
# Get directory name for a size variant
# @abstract
# @param size [String] Image size variant
# @return [String] Directory name
def directory_for_size(size)
raise NotImplementedError, 'Subclasses must define directory_for_size'
end
# Build complete URL for a size variant
# @param size [String] Image size variant
# @return [String] Complete download URL
def build_url(size)
directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{@id}.jpg"
end
end
end
end