Fix image embeds 3 (#176)
* Add default preview images * Update application.rb * Adds app assets path in API mode * Cleans up file * Create assets.rb An initializer for font assets (for image generation) * Updates to Canvas and Coordinator * Update parties_controller.rb * Adds retry header if generation is still in progress * Streams S3 content instead of redirecting to prevent 302 * Update coordinator.rbs * Create previews.rake A rake task for generating images offline * Add commands to build phase
This commit is contained in:
parent
ad2e2cc028
commit
11d324efe9
14 changed files with 337 additions and 123 deletions
|
|
@ -117,10 +117,41 @@ module Api
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview
|
def preview
|
||||||
party = Party.find_by!(shortcode: params[:id])
|
coordinator = PreviewService::Coordinator.new(@party)
|
||||||
|
|
||||||
preview_service = PreviewService::Coordinator.new(party)
|
if coordinator.generation_in_progress?
|
||||||
redirect_to preview_service.preview_url
|
response.headers['Retry-After'] = '2'
|
||||||
|
default_path = Rails.root.join('public', 'default-previews', "#{@party.element || 'default'}.png")
|
||||||
|
send_file default_path,
|
||||||
|
type: 'image/png',
|
||||||
|
disposition: 'inline'
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Try to get the preview or send default
|
||||||
|
begin
|
||||||
|
if Rails.env.production?
|
||||||
|
# Stream S3 content instead of redirecting
|
||||||
|
s3_object = coordinator.get_s3_object
|
||||||
|
send_data s3_object.body.read,
|
||||||
|
filename: "#{@party.shortcode}.png",
|
||||||
|
type: 'image/png',
|
||||||
|
disposition: 'inline'
|
||||||
|
else
|
||||||
|
# In development, serve from local filesystem
|
||||||
|
send_file coordinator.local_preview_path,
|
||||||
|
type: 'image/png',
|
||||||
|
disposition: 'inline'
|
||||||
|
end
|
||||||
|
rescue Aws::S3::Errors::NoSuchKey
|
||||||
|
# Schedule generation if needed
|
||||||
|
coordinator.schedule_generation unless coordinator.generation_in_progress?
|
||||||
|
|
||||||
|
# Return default preview while generating
|
||||||
|
send_file Rails.root.join('public', 'default-previews', "#{@party.element || 'default'}.png"),
|
||||||
|
type: 'image/png',
|
||||||
|
disposition: 'inline'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def regenerate_preview
|
def regenerate_preview
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,25 @@ module PreviewService
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_text(image, party_name, job_icon: nil, user: nil, **options)
|
def add_text(image, party_name, job_icon: nil, user: nil, **options)
|
||||||
|
party_name = party_name.to_s.strip
|
||||||
|
party_name = 'Untitled' if party_name.empty?
|
||||||
|
|
||||||
font_size = options.fetch(:size, '32')
|
font_size = options.fetch(:size, '32')
|
||||||
font_color = options.fetch(:color, 'white')
|
font_color = options.fetch(:color, 'white')
|
||||||
|
|
||||||
# Load custom font for username, for later use
|
# Try multiple font locations
|
||||||
@font_path = Rails.root.join('app', 'assets', 'fonts', 'Gk-Bd.otf').to_s
|
font_locations = [
|
||||||
|
Rails.root.join('app', 'assets', 'fonts', 'Gk-Bd.otf').to_s,
|
||||||
|
Rails.root.join('public', 'assets', 'fonts', 'Gk-Bd.otf').to_s
|
||||||
|
]
|
||||||
|
|
||||||
|
@font_path = font_locations.find { |path| File.exist?(path) }
|
||||||
|
|
||||||
|
unless @font_path
|
||||||
|
Rails.logger.error("Font file not found in any location: #{font_locations.join(', ')}")
|
||||||
|
raise "Font file not found"
|
||||||
|
end
|
||||||
|
|
||||||
Rails.logger.info("Using font path: #{@font_path}")
|
Rails.logger.info("Using font path: #{@font_path}")
|
||||||
unless File.exist?(@font_path)
|
unless File.exist?(@font_path)
|
||||||
Rails.logger.error("Font file not found at: #{@font_path}")
|
Rails.logger.error("Font file not found at: #{@font_path}")
|
||||||
|
|
@ -94,16 +108,18 @@ module PreviewService
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw_party_name(image, party_name, text_metrics, job_icon, font_color, font_size)
|
def draw_party_name(image, party_name, text_metrics, job_icon, font_color, font_size)
|
||||||
# Determine x position based on presence of job_icon
|
|
||||||
text_x = job_icon ? PADDING + 64 + 16 : PADDING
|
text_x = job_icon ? PADDING + 64 + 16 : PADDING
|
||||||
text_y = PADDING + text_metrics[:height]
|
text_y = PADDING + text_metrics[:height]
|
||||||
|
|
||||||
image.combine_options do |c|
|
image.combine_options do |c|
|
||||||
c.font @font_path
|
c.gravity 'NorthWest'
|
||||||
c.fill font_color
|
c.fill font_color
|
||||||
|
c.font @font_path
|
||||||
c.pointsize font_size
|
c.pointsize font_size
|
||||||
c.draw "text #{text_x},#{text_y} '#{party_name}'"
|
# Escape quotes and use pango markup for better text handling
|
||||||
|
c.annotate "0x0+#{text_x}+#{text_y}", party_name.gsub('"', '\"')
|
||||||
end
|
end
|
||||||
|
|
||||||
image
|
image
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -154,7 +170,13 @@ module PreviewService
|
||||||
image
|
image
|
||||||
end
|
end
|
||||||
|
|
||||||
def measure_text(text, font_size, font: 'Arial')
|
def measure_text(text, font_size, font: @font_path)
|
||||||
|
# Ensure text is not empty and is properly escaped
|
||||||
|
text = text.to_s.strip
|
||||||
|
text = 'Untitled' if text.empty?
|
||||||
|
|
||||||
|
# Escape text for shell command
|
||||||
|
escaped_text = text.gsub(/'/, "'\\\\''")
|
||||||
|
|
||||||
# Create a temporary file for the text measurement
|
# Create a temporary file for the text measurement
|
||||||
temp_file = Tempfile.new(['text_measure', '.png'])
|
temp_file = Tempfile.new(['text_measure', '.png'])
|
||||||
|
|
@ -167,7 +189,7 @@ module PreviewService
|
||||||
'-fill', 'black',
|
'-fill', 'black',
|
||||||
'-font', font,
|
'-font', font,
|
||||||
'-pointsize', font_size.to_s,
|
'-pointsize', font_size.to_s,
|
||||||
"label:#{text}",
|
"label:'#{escaped_text}'", # Quote the text
|
||||||
temp_file.path
|
temp_file.path
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -181,15 +203,15 @@ module PreviewService
|
||||||
height: image.height,
|
height: image.height,
|
||||||
width: image.width
|
width: image.width
|
||||||
}
|
}
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Text measurement error: #{e.message}"
|
||||||
|
# Fallback dimensions
|
||||||
|
{ height: 50, width: 200 }
|
||||||
ensure
|
ensure
|
||||||
# Close and unlink the temporary file
|
# Close and unlink the temporary file
|
||||||
temp_file.close
|
temp_file.close
|
||||||
temp_file.unlink
|
temp_file.unlink
|
||||||
end
|
end
|
||||||
rescue => e
|
|
||||||
Rails.logger.error "Text measurement error: #{e.message}"
|
|
||||||
# Fallback dimensions
|
|
||||||
{ height: 50, width: 200 }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ module PreviewService
|
||||||
GENERATION_TIMEOUT = 5.minutes
|
GENERATION_TIMEOUT = 5.minutes
|
||||||
LOCAL_STORAGE_PATH = Rails.root.join('storage', 'party-previews')
|
LOCAL_STORAGE_PATH = Rails.root.join('storage', 'party-previews')
|
||||||
|
|
||||||
|
# Public Interface - Core Operations
|
||||||
|
|
||||||
# Initialize the party preview service
|
# Initialize the party preview service
|
||||||
#
|
#
|
||||||
# @param party [Party] The party to generate a preview for
|
# @param party [Party] The party to generate a preview for
|
||||||
|
|
@ -103,6 +105,8 @@ module PreviewService
|
||||||
end
|
end
|
||||||
|
|
||||||
# Deletes the existing preview image for the party
|
# Deletes the existing preview image for the party
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def delete_preview
|
def delete_preview
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
delete_s3_preview
|
delete_s3_preview
|
||||||
|
|
@ -118,6 +122,8 @@ module PreviewService
|
||||||
Rails.logger.error("Failed to delete preview for party #{@party.id}: #{e.message}")
|
Rails.logger.error("Failed to delete preview for party #{@party.id}: #{e.message}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# State Management - Public
|
||||||
|
|
||||||
# Determines if a new preview should be generated
|
# Determines if a new preview should be generated
|
||||||
#
|
#
|
||||||
# @return [Boolean] True if a new preview should be generated, false otherwise
|
# @return [Boolean] True if a new preview should be generated, false otherwise
|
||||||
|
|
@ -150,15 +156,43 @@ module PreviewService
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
# Checks if a preview generation is currently in progress
|
||||||
|
#
|
||||||
|
# @return [Boolean] True if a preview is being generated, false otherwise
|
||||||
|
def generation_in_progress?
|
||||||
|
in_progress = Rails.cache.exist?("party_preview_generating_#{@party.id}")
|
||||||
|
Rails.logger.info("Cache key check for generation_in_progress: #{in_progress}")
|
||||||
|
in_progress
|
||||||
|
end
|
||||||
|
|
||||||
# Sets up the appropriate storage system based on environment
|
# Retrieves the S3 object for the party's preview image
|
||||||
def setup_storage
|
#
|
||||||
# Always initialize AWS service for potential image fetching
|
# @return [Aws::S3::Types::GetObjectOutput] S3 object containing the preview image
|
||||||
@aws_service = AwsService.new
|
# @raise [Aws::S3::Errors::NoSuchKey] If the preview image doesn't exist in S3
|
||||||
|
# @raise [Aws::S3::Errors::NoSuchBucket] If the configured bucket doesn't exist
|
||||||
|
def get_s3_object
|
||||||
|
@aws_service.s3_client.get_object(
|
||||||
|
bucket: @aws_service.bucket,
|
||||||
|
key: preview_key
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
# Create local storage paths in development
|
# Schedules a background job to generate the preview
|
||||||
FileUtils.mkdir_p(LOCAL_STORAGE_PATH) unless Dir.exist?(LOCAL_STORAGE_PATH.to_s)
|
#
|
||||||
|
# @return [void]
|
||||||
|
def schedule_generation
|
||||||
|
GeneratePartyPreviewJob
|
||||||
|
.set(wait: 30.seconds)
|
||||||
|
.perform_later(@party.id)
|
||||||
|
|
||||||
|
@party.update!(preview_state: :queued)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the full path for storing preview images locally
|
||||||
|
#
|
||||||
|
# @return [Pathname] Full path where the preview image should be stored
|
||||||
|
def local_preview_path
|
||||||
|
LOCAL_STORAGE_PATH.join(preview_filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Creates the preview image for the party
|
# Creates the preview image for the party
|
||||||
|
|
@ -225,6 +259,21 @@ module PreviewService
|
||||||
image
|
image
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Sets up the appropriate storage system based on environment
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def setup_storage
|
||||||
|
# Always initialize AWS service for potential image fetching
|
||||||
|
@aws_service = AwsService.new
|
||||||
|
|
||||||
|
# Create local storage paths in development
|
||||||
|
FileUtils.mkdir_p(LOCAL_STORAGE_PATH) unless Dir.exist?(LOCAL_STORAGE_PATH.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Image Generation Pipeline
|
||||||
|
|
||||||
# Adds the job icon to the preview image
|
# Adds the job icon to the preview image
|
||||||
#
|
#
|
||||||
# @param image [MiniMagick::Image] The base image
|
# @param image [MiniMagick::Image] The base image
|
||||||
|
|
@ -241,6 +290,7 @@ module PreviewService
|
||||||
# Organizes and draws weapons on the preview image
|
# Organizes and draws weapons on the preview image
|
||||||
#
|
#
|
||||||
# @param image [MiniMagick::Image] The base image
|
# @param image [MiniMagick::Image] The base image
|
||||||
|
# @param grid_layout [Hash] The layout configuration for the grid
|
||||||
# @return [MiniMagick::Image] The updated image with weapons
|
# @return [MiniMagick::Image] The updated image with weapons
|
||||||
def organize_and_draw_weapons(image, grid_layout)
|
def organize_and_draw_weapons(image, grid_layout)
|
||||||
mainhand_weapon = @party.weapons.find(&:mainhand)
|
mainhand_weapon = @party.weapons.find(&:mainhand)
|
||||||
|
|
@ -277,9 +327,12 @@ module PreviewService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Storage Operations
|
||||||
|
|
||||||
# Saves the preview image to the appropriate storage system
|
# Saves the preview image to the appropriate storage system
|
||||||
#
|
#
|
||||||
# @param image [MiniMagick::Image] The image to save
|
# @param image [MiniMagick::Image] The image to save
|
||||||
|
# @return [void]
|
||||||
def save_preview(image)
|
def save_preview(image)
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
upload_to_s3(image)
|
upload_to_s3(image)
|
||||||
|
|
@ -291,14 +344,14 @@ module PreviewService
|
||||||
# Uploads the preview image to S3
|
# Uploads the preview image to S3
|
||||||
#
|
#
|
||||||
# @param image [MiniMagick::Image] The image to upload
|
# @param image [MiniMagick::Image] The image to upload
|
||||||
|
# @return [void]
|
||||||
def upload_to_s3(image)
|
def upload_to_s3(image)
|
||||||
temp_file = Tempfile.new(['preview', '.png'])
|
temp_file = Tempfile.new(%w[preview .png])
|
||||||
begin
|
begin
|
||||||
image.write(temp_file.path)
|
image.write(temp_file.path)
|
||||||
|
|
||||||
# Use timestamped filename similar to local storage
|
# Use fixed key without timestamp
|
||||||
timestamp = Time.current.strftime('%Y%m%d%H%M%S')
|
key = "#{PREVIEW_FOLDER}/#{@party.shortcode}.png"
|
||||||
key = "#{PREVIEW_FOLDER}/#{@party.shortcode}_#{timestamp}.png"
|
|
||||||
|
|
||||||
File.open(temp_file.path, 'rb') do |file|
|
File.open(temp_file.path, 'rb') do |file|
|
||||||
@aws_service.s3_client.put_object(
|
@aws_service.s3_client.put_object(
|
||||||
|
|
@ -310,7 +363,6 @@ module PreviewService
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Optionally, store this key on the party record if needed for retrieval
|
|
||||||
@party.update!(preview_s3_key: key)
|
@party.update!(preview_s3_key: key)
|
||||||
ensure
|
ensure
|
||||||
temp_file.close
|
temp_file.close
|
||||||
|
|
@ -321,29 +373,18 @@ module PreviewService
|
||||||
# Saves the preview image to local storage
|
# Saves the preview image to local storage
|
||||||
#
|
#
|
||||||
# @param image [MiniMagick::Image] The image to save
|
# @param image [MiniMagick::Image] The image to save
|
||||||
|
# @return [void]
|
||||||
def save_to_local_storage(image)
|
def save_to_local_storage(image)
|
||||||
# Remove any existing previews for this party
|
|
||||||
Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}_*.png").to_s).each do |file|
|
|
||||||
File.delete(file)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Save new version
|
|
||||||
image.write(local_preview_path)
|
image.write(local_preview_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generates a timestamped filename for the preview image
|
# Path & URL Generation
|
||||||
#
|
|
||||||
# @return [String] Filename in format "shortcode_YYYYMMDDHHMMSS.png"
|
|
||||||
def preview_filename
|
|
||||||
timestamp = Time.current.strftime('%Y%m%d%H%M%S')
|
|
||||||
"#{@party.shortcode}_#{timestamp}.png"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the full path for storing preview images locally
|
# Generates a filename for the preview image
|
||||||
#
|
#
|
||||||
# @return [Pathname] Full path where the preview image should be stored
|
# @return [String] Filename for the preview image
|
||||||
def local_preview_path
|
def preview_filename
|
||||||
LOCAL_STORAGE_PATH.join(preview_filename)
|
"#{@party.shortcode}.png"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the URL for accessing locally stored preview images
|
# Returns the URL for accessing locally stored preview images
|
||||||
|
|
@ -364,6 +405,8 @@ module PreviewService
|
||||||
"#{PREVIEW_FOLDER}/#{@party.shortcode}.png"
|
"#{PREVIEW_FOLDER}/#{@party.shortcode}.png"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Preview State Management
|
||||||
|
|
||||||
# Checks if a preview image exists for the party
|
# Checks if a preview image exists for the party
|
||||||
#
|
#
|
||||||
# @return [Boolean] True if a preview exists, false otherwise
|
# @return [Boolean] True if a preview exists, false otherwise
|
||||||
|
|
@ -371,10 +414,9 @@ module PreviewService
|
||||||
return false unless @party.preview_state == 'generated'
|
return false unless @party.preview_state == 'generated'
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
@aws_service.s3_client.head_object(bucket: S3_BUCKET, key: preview_key)
|
@aws_service.file_exists?(preview_key)
|
||||||
true
|
|
||||||
else
|
else
|
||||||
!Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}_*.png").to_s).empty?
|
!Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}.png").to_s).empty?
|
||||||
end
|
end
|
||||||
rescue Aws::S3::Errors::NotFound
|
rescue Aws::S3::Errors::NotFound
|
||||||
false
|
false
|
||||||
|
|
@ -387,22 +429,15 @@ module PreviewService
|
||||||
signer = Aws::S3::Presigner.new(client: @aws_service.s3_client)
|
signer = Aws::S3::Presigner.new(client: @aws_service.s3_client)
|
||||||
signer.presigned_url(
|
signer.presigned_url(
|
||||||
:get_object,
|
:get_object,
|
||||||
bucket: S3_BUCKET,
|
bucket: @aws_service.bucket,
|
||||||
key: preview_key,
|
key: preview_key,
|
||||||
expires_in: 1.hour
|
expires_in: 1.hour.to_i
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Checks if a preview generation is currently in progress
|
|
||||||
#
|
|
||||||
# @return [Boolean] True if a preview is being generated, false otherwise
|
|
||||||
def generation_in_progress?
|
|
||||||
in_progress = Rails.cache.exist?("party_preview_generating_#{@party.id}")
|
|
||||||
Rails.logger.info("Cache key check for generation_in_progress: #{in_progress}")
|
|
||||||
in_progress
|
|
||||||
end
|
|
||||||
|
|
||||||
# Marks the preview generation as in progress
|
# Marks the preview generation as in progress
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def set_generation_in_progress
|
def set_generation_in_progress
|
||||||
Rails.cache.write(
|
Rails.cache.write(
|
||||||
"party_preview_generating_#{@party.id}",
|
"party_preview_generating_#{@party.id}",
|
||||||
|
|
@ -412,18 +447,15 @@ module PreviewService
|
||||||
end
|
end
|
||||||
|
|
||||||
# Clears the in-progress flag for preview generation
|
# Clears the in-progress flag for preview generation
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def clear_generation_in_progress
|
def clear_generation_in_progress
|
||||||
Rails.cache.delete("party_preview_generating_#{@party.id}")
|
Rails.cache.delete("party_preview_generating_#{@party.id}")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Schedules a background job to generate the preview
|
# Job Scheduling
|
||||||
def schedule_generation
|
|
||||||
GeneratePartyPreviewJob
|
|
||||||
.set(wait: 30.seconds)
|
|
||||||
.perform_later(@party.id)
|
|
||||||
|
|
||||||
@party.update!(preview_state: :queued)
|
# URL Generation
|
||||||
end
|
|
||||||
|
|
||||||
# Provides a default preview URL based on party attributes
|
# Provides a default preview URL based on party attributes
|
||||||
#
|
#
|
||||||
|
|
@ -436,24 +468,33 @@ module PreviewService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Cleanup Operations
|
||||||
|
|
||||||
# Deletes the preview from S3
|
# Deletes the preview from S3
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def delete_s3_preview
|
def delete_s3_preview
|
||||||
@aws_service.s3_client.delete_object(
|
@aws_service.s3_client.delete_object(
|
||||||
bucket: S3_BUCKET,
|
bucket: @aws_service.bucket,
|
||||||
key: preview_key
|
key: preview_key
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Deletes local preview files
|
# Deletes local preview files
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def delete_local_previews
|
def delete_local_previews
|
||||||
Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}_*.png").to_s).each do |file|
|
Dir.glob(LOCAL_STORAGE_PATH.join("#{@party.shortcode}_*.png").to_s).each do |file|
|
||||||
File.delete(file)
|
File.delete(file)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Error Handling
|
||||||
|
|
||||||
# Handles errors during preview generation
|
# Handles errors during preview generation
|
||||||
#
|
#
|
||||||
# @param error [Exception] The error that occurred
|
# @param error [Exception] The error that occurred
|
||||||
|
# @return [void]
|
||||||
def handle_preview_generation_error(error)
|
def handle_preview_generation_error(error)
|
||||||
Rails.logger.error("Preview generation failed for party #{@party.id}")
|
Rails.logger.error("Preview generation failed for party #{@party.id}")
|
||||||
Rails.logger.error("Error: #{error.class} - #{error.message}")
|
Rails.logger.error("Error: #{error.class} - #{error.message}")
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,35 @@
|
||||||
require_relative "boot"
|
require_relative "boot"
|
||||||
|
|
||||||
require "rails"
|
require "rails"
|
||||||
# Pick the frameworks you want:
|
|
||||||
require "active_model/railtie"
|
|
||||||
require "active_job/railtie"
|
|
||||||
require "active_record/railtie"
|
|
||||||
require "active_storage/engine"
|
|
||||||
require "action_controller/railtie"
|
|
||||||
# require "action_mailer/railtie"
|
|
||||||
# require "action_mailbox/engine"
|
|
||||||
require "action_text/engine"
|
|
||||||
require "action_view/railtie"
|
|
||||||
require "action_cable/engine"
|
|
||||||
require "rails/test_unit/railtie"
|
|
||||||
|
|
||||||
# Require the gems listed in Gemfile, including any gems
|
# Include only the Rails frameworks we need
|
||||||
# you've limited to :test, :development, or :production.
|
require "active_model/railtie" # Basic model functionality
|
||||||
|
require "active_job/railtie" # Background job processing
|
||||||
|
require "active_record/railtie" # Database support
|
||||||
|
require "active_storage/engine" # File upload and storage
|
||||||
|
require "action_controller/railtie" # API controller support
|
||||||
|
require "action_text/engine" # Rich text handling
|
||||||
|
require "action_view/railtie" # View rendering (needed for some API responses)
|
||||||
|
require "rails/test_unit/railtie" # Testing framework
|
||||||
|
|
||||||
|
# Load gems from Gemfile
|
||||||
Bundler.require(*Rails.groups)
|
Bundler.require(*Rails.groups)
|
||||||
|
|
||||||
module HenseiApi
|
module HenseiApi
|
||||||
class Application < Rails::Application
|
class Application < Rails::Application
|
||||||
# Initialize configuration defaults for originally generated Rails version.
|
# Use Rails 7.0 defaults
|
||||||
config.load_defaults 7.0
|
config.load_defaults 7.0
|
||||||
|
|
||||||
# Configuration for the application, engines, and railties goes here.
|
# Configure autoloading
|
||||||
#
|
|
||||||
# These settings can be overridden in specific environments using the files
|
|
||||||
# in config/environments, which are processed later.
|
|
||||||
#
|
|
||||||
# config.time_zone = "Central Time (US & Canada)"
|
|
||||||
# config.eager_load_paths << Rails.root.join("extras")
|
|
||||||
|
|
||||||
config.autoload_paths << Rails.root.join("lib")
|
config.autoload_paths << Rails.root.join("lib")
|
||||||
config.eager_load_paths << Rails.root.join("lib")
|
config.eager_load_paths << Rails.root.join("lib")
|
||||||
|
|
||||||
# Only loads a smaller set of middleware suitable for API only apps.
|
# Configure asset handling for API mode
|
||||||
# Middleware like session, flash, cookies can be added back manually.
|
config.paths["app/assets"] ||= []
|
||||||
# Skip views, helpers and assets when generating a new resource.
|
config.paths["app/assets"].unshift(Rails.root.join("app", "assets").to_s)
|
||||||
|
config.assets.paths << Rails.root.join("app", "assets", "fonts")
|
||||||
|
|
||||||
|
# API-only application configuration
|
||||||
config.api_only = true
|
config.api_only = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
12
config/initializers/assets.rb
Normal file
12
config/initializers/assets.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
Rails.application.config.assets.precompile += %w( .otf )
|
||||||
|
|
||||||
|
# Ensure fonts directory exists in production
|
||||||
|
fonts_dir = Rails.root.join('public', 'assets', 'fonts')
|
||||||
|
FileUtils.mkdir_p(fonts_dir) unless File.directory?(fonts_dir)
|
||||||
|
|
||||||
|
# Copy fonts to public directory in production
|
||||||
|
if Rails.env.production?
|
||||||
|
Dir[Rails.root.join('app', 'assets', 'fonts', '*')].each do |font|
|
||||||
|
FileUtils.cp(font, fonts_dir) if File.file?(font)
|
||||||
|
end
|
||||||
|
end
|
||||||
106
lib/tasks/previews.rake
Normal file
106
lib/tasks/previews.rake
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
namespace :previews do
|
||||||
|
desc 'Generate and upload missing preview images'
|
||||||
|
task generate_all: :environment do
|
||||||
|
coordinator_class = PreviewService::Coordinator
|
||||||
|
aws_service = AwsService.new
|
||||||
|
|
||||||
|
# Find all parties without previews
|
||||||
|
parties = Party.where(preview_state: ['pending', 'failed', nil])
|
||||||
|
total = parties.count
|
||||||
|
|
||||||
|
puts "Found #{total} parties needing preview generation"
|
||||||
|
|
||||||
|
parties.find_each.with_index(1) do |party, index|
|
||||||
|
puts "[#{index}/#{total}] Processing party #{party.shortcode}..."
|
||||||
|
|
||||||
|
begin
|
||||||
|
coordinator = coordinator_class.new(party)
|
||||||
|
temp_file = Tempfile.new(['preview', '.png'])
|
||||||
|
|
||||||
|
# Create preview image
|
||||||
|
image = coordinator.create_preview_image
|
||||||
|
image.write(temp_file.path)
|
||||||
|
|
||||||
|
# Upload to S3
|
||||||
|
key = "previews/#{party.shortcode}.png"
|
||||||
|
File.open(temp_file.path, 'rb') do |file|
|
||||||
|
aws_service.s3_client.put_object(
|
||||||
|
bucket: aws_service.bucket,
|
||||||
|
key: key,
|
||||||
|
body: file,
|
||||||
|
content_type: 'image/png',
|
||||||
|
acl: 'private'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update party state
|
||||||
|
party.update!(
|
||||||
|
preview_state: :generated,
|
||||||
|
preview_s3_key: key,
|
||||||
|
preview_generated_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
puts " ✓ Preview generated and uploaded to S3"
|
||||||
|
rescue => e
|
||||||
|
puts " ✗ Error: #{e.message}"
|
||||||
|
ensure
|
||||||
|
temp_file&.close
|
||||||
|
temp_file&.unlink
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "\nPreview generation complete"
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Regenerate all preview images'
|
||||||
|
task regenerate_all: :environment do
|
||||||
|
coordinator_class = PreviewService::Coordinator
|
||||||
|
aws_service = AwsService.new
|
||||||
|
|
||||||
|
parties = Party.all
|
||||||
|
total = parties.count
|
||||||
|
|
||||||
|
puts "Found #{total} parties to regenerate"
|
||||||
|
|
||||||
|
parties.find_each.with_index(1) do |party, index|
|
||||||
|
puts "[#{index}/#{total}] Processing party #{party.shortcode}..."
|
||||||
|
|
||||||
|
begin
|
||||||
|
coordinator = coordinator_class.new(party)
|
||||||
|
temp_file = Tempfile.new(['preview', '.png'])
|
||||||
|
|
||||||
|
# Create preview image
|
||||||
|
image = coordinator.create_preview_image
|
||||||
|
image.write(temp_file.path)
|
||||||
|
|
||||||
|
# Upload to S3
|
||||||
|
key = "previews/#{party.shortcode}.png"
|
||||||
|
File.open(temp_file.path, 'rb') do |file|
|
||||||
|
aws_service.s3_client.put_object(
|
||||||
|
bucket: aws_service.bucket,
|
||||||
|
key: key,
|
||||||
|
body: file,
|
||||||
|
content_type: 'image/png',
|
||||||
|
acl: 'private'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update party state
|
||||||
|
party.update!(
|
||||||
|
preview_state: :generated,
|
||||||
|
preview_s3_key: key,
|
||||||
|
preview_generated_at: Time.current
|
||||||
|
)
|
||||||
|
|
||||||
|
puts " ✓ Preview regenerated and uploaded to S3"
|
||||||
|
rescue => e
|
||||||
|
puts " ✗ Error: #{e.message}"
|
||||||
|
ensure
|
||||||
|
temp_file&.close
|
||||||
|
temp_file&.unlink
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "\nPreview regeneration complete"
|
||||||
|
end
|
||||||
|
end
|
||||||
BIN
public/default-previews/1.png
Normal file
BIN
public/default-previews/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 455 KiB |
BIN
public/default-previews/2.png
Normal file
BIN
public/default-previews/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 436 KiB |
BIN
public/default-previews/3.png
Normal file
BIN
public/default-previews/3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 413 KiB |
BIN
public/default-previews/4.png
Normal file
BIN
public/default-previews/4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
BIN
public/default-previews/5.png
Normal file
BIN
public/default-previews/5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 440 KiB |
BIN
public/default-previews/6.png
Normal file
BIN
public/default-previews/6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
|
|
@ -22,6 +22,11 @@ dependsOn = ["setup"]
|
||||||
|
|
||||||
[phases.build]
|
[phases.build]
|
||||||
dependsOn = ["install"]
|
dependsOn = ["install"]
|
||||||
|
cmds = [
|
||||||
|
"mkdir -p public/assets/fonts",
|
||||||
|
"cp -r app/assets/fonts/* public/assets/fonts/",
|
||||||
|
"bundle exec rake assets:precompile"
|
||||||
|
]
|
||||||
|
|
||||||
[start]
|
[start]
|
||||||
cmd = "bin/rails server"
|
cmd = "bin/rails server"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# sig/services/preview_service/coordinator.rbs
|
||||||
|
|
||||||
module PreviewService
|
module PreviewService
|
||||||
class Coordinator
|
class Coordinator
|
||||||
PREVIEW_FOLDER: String
|
PREVIEW_FOLDER: String
|
||||||
|
|
@ -6,69 +8,71 @@ module PreviewService
|
||||||
PREVIEW_EXPIRY: ActiveSupport::Duration
|
PREVIEW_EXPIRY: ActiveSupport::Duration
|
||||||
GENERATION_TIMEOUT: ActiveSupport::Duration
|
GENERATION_TIMEOUT: ActiveSupport::Duration
|
||||||
LOCAL_STORAGE_PATH: Pathname
|
LOCAL_STORAGE_PATH: Pathname
|
||||||
|
|
||||||
@party: Party
|
@party: Party
|
||||||
@image_fetcher: ImageFetcherService
|
@image_fetcher: ImageFetcherService
|
||||||
@grid_service: Grid
|
@grid_service: Grid
|
||||||
@canvas_service: Canvas
|
@canvas_service: Canvas
|
||||||
@aws_service: AwsService
|
@aws_service: AwsService
|
||||||
|
|
||||||
def initialize: (Party party) -> void
|
def initialize: (party: Party) -> void
|
||||||
|
|
||||||
def preview_url: -> String
|
def get_s3_object: () -> Aws::S3::Types::GetObjectOutput
|
||||||
|
|
||||||
def generate_preview: -> bool
|
def preview_url: () -> String
|
||||||
|
|
||||||
def force_regenerate: -> bool
|
def generate_preview: () -> bool
|
||||||
|
|
||||||
def delete_preview: -> void
|
def force_regenerate: () -> bool
|
||||||
|
|
||||||
|
def delete_preview: () -> void
|
||||||
|
|
||||||
|
def should_generate?: () -> bool
|
||||||
|
|
||||||
|
def generation_in_progress?: () -> bool
|
||||||
|
|
||||||
|
def create_preview_image: () -> MiniMagick::Image
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_preview_image: -> MiniMagick::Image
|
def setup_storage: () -> void
|
||||||
|
|
||||||
def add_job_icon: (MiniMagick::Image image, MiniMagick::Image job_icon) -> MiniMagick::Image
|
def add_job_icon: (image: MiniMagick::Image, job_icon: MiniMagick::Image) -> MiniMagick::Image
|
||||||
|
|
||||||
def organize_and_draw_weapons: (MiniMagick::Image image) -> MiniMagick::Image
|
def organize_and_draw_weapons: (image: MiniMagick::Image, grid_layout: Hash[Symbol, untyped]) -> MiniMagick::Image
|
||||||
|
|
||||||
def draw_mainhand_weapon: (MiniMagick::Image image, MiniMagick::Image weapon_image) -> MiniMagick::Image
|
def draw_mainhand_weapon: (image: MiniMagick::Image, weapon_image: MiniMagick::Image) -> MiniMagick::Image
|
||||||
|
|
||||||
def save_preview: (MiniMagick::Image image) -> void
|
def save_preview: (image: MiniMagick::Image) -> void
|
||||||
|
|
||||||
def setup_storage: -> void
|
def upload_to_s3: (image: MiniMagick::Image) -> void
|
||||||
|
|
||||||
def upload_to_s3: (MiniMagick::Image image) -> void
|
def save_to_local_storage: (image: MiniMagick::Image) -> void
|
||||||
|
|
||||||
def save_to_local_storage: (MiniMagick::Image image) -> void
|
def preview_filename: () -> String
|
||||||
|
|
||||||
def preview_filename: -> String
|
def local_preview_path: () -> Pathname
|
||||||
|
|
||||||
def local_preview_path: -> Pathname
|
def local_preview_url: () -> String
|
||||||
|
|
||||||
def local_preview_url: -> String
|
def preview_key: () -> String
|
||||||
|
|
||||||
def preview_key: -> String
|
def preview_exists?: () -> bool
|
||||||
|
|
||||||
def preview_exists?: -> bool
|
def generate_s3_url: () -> String
|
||||||
|
|
||||||
def generate_s3_url: -> String
|
def set_generation_in_progress: () -> void
|
||||||
|
|
||||||
def should_generate?: -> bool
|
def clear_generation_in_progress: () -> void
|
||||||
|
|
||||||
def generation_in_progress?: -> bool
|
def schedule_generation: () -> void
|
||||||
|
|
||||||
def set_generation_in_progress: -> void
|
def default_preview_url: () -> String
|
||||||
|
|
||||||
def clear_generation_in_progress: -> void
|
def delete_s3_preview: () -> void
|
||||||
|
|
||||||
def schedule_generation: -> void
|
def delete_local_previews: () -> void
|
||||||
|
|
||||||
def default_preview_url: -> String
|
def handle_preview_generation_error: (error: Exception) -> void
|
||||||
|
|
||||||
def delete_s3_preview: -> void
|
|
||||||
|
|
||||||
def delete_local_previews: -> void
|
|
||||||
|
|
||||||
def handle_preview_generation_error: (Exception error) -> void
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue