Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
bundler-cache: true

- name: Lint code for consistent style
run: bin/rubocop -f github
run: bundle exec rubocop -f github

test:
runs-on: ubuntu-latest
Expand All @@ -44,10 +44,7 @@ jobs:
bundler-cache: true

- name: Run tests
env:
RAILS_ENV: test
# REDIS_URL: redis://localhost:6379/0
run: bin/rails db:test:prepare test
run: bundle exec rspec

- name: Keep screenshots from failed system tests
uses: actions/upload-artifact@v4
Expand Down
7 changes: 1 addition & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,5 @@
/doc/
/log/*.log
/pkg/
/tmp/
/test/dummy/db/*.sqlite3
/test/dummy/db/*.sqlite3-*
/test/dummy/log/*.log
/test/dummy/storage/
/test/dummy/tmp/
/spec/internal/tmp/
Claude.md
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--color
--require spec_helper
--format documentation
42 changes: 39 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
PATH
remote: .
specs:
fileboost (0.1.0)
rails (>= 8.0.2.1)
fileboost (0.1.1)
activestorage (>= 6.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -83,10 +83,15 @@ GEM
benchmark (0.4.1)
bigdecimal (3.2.3)
builder (3.3.0)
combustion (1.5.0)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
thor (>= 0.14.6)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
crass (1.0.6)
date (3.4.1)
diff-lcs (1.6.2)
drb (2.2.3)
erb (5.0.2)
erubi (1.13.1)
Expand Down Expand Up @@ -126,6 +131,8 @@ GEM
nio4r (2.7.4)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
parallel (1.27.0)
parser (3.3.9.0)
ast (~> 2.4.1)
Expand Down Expand Up @@ -189,6 +196,27 @@ GEM
regexp_parser (2.11.2)
reline (0.6.2)
io-console (~> 0.5)
rspec (3.13.1)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.5)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.5)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.5)
rubocop (1.80.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
Expand Down Expand Up @@ -220,6 +248,7 @@ GEM
ruby-progressbar (1.13.0)
securerandom (0.4.1)
sqlite3 (2.7.3-arm64-darwin)
sqlite3 (2.7.3-x86_64-linux-gnu)
stringio (3.1.7)
thor (1.4.0)
timeout (0.4.3)
Expand All @@ -238,13 +267,20 @@ GEM

PLATFORMS
arm64-darwin
x86_64-linux

DEPENDENCIES
bundler (~> 2)
combustion (~> 1)
fileboost!
propshaft
puma
rails (~> 8.0.0)
rake (~> 13.0)
rspec (~> 3.0)
rspec-rails (~> 6.0)
rubocop-rails-omakase
sqlite3
sqlite3 (~> 2.0, >= 0)

BUNDLED WITH
2.7.1
9 changes: 4 additions & 5 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
require "bundler/setup"
require "bundler/gem_tasks"
require "rspec/core/rake_task"

APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
load "rails/tasks/engine.rake"

load "rails/tasks/statistics.rake"
RSpec::Core::RakeTask.new(:spec)

require "bundler/gem_tasks"
task default: :spec
7 changes: 5 additions & 2 deletions fileboost.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ Gem::Specification.new do |spec|

spec.add_runtime_dependency "activestorage", ">= 6.0"

spec.add_development_dependency "bundler", "~> 1.15"
spec.add_development_dependency "combustion", "~> 1.1"
spec.add_development_dependency "bundler", "~> 2"
spec.add_development_dependency "combustion", "~> 1"
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rails", "~> 8.0.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rspec-rails", "~> 6.0"
spec.add_development_dependency "sqlite3", "~> 2.0"
end
4 changes: 2 additions & 2 deletions lib/fileboost/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def initialize
end

def valid?
project_id.present? && token.present?
!project_id.empty? && !token.empty?
end

def base_url
Expand All @@ -26,4 +26,4 @@ def self.config
def self.configure
yield(config) if block_given?
end
end
end
6 changes: 0 additions & 6 deletions lib/fileboost/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,5 @@ class Engine < ::Rails::Engine
include Fileboost::Helpers
end
end

initializer "fileboost.active_storage" do
ActiveSupport.on_load :active_storage_blob do
# Extend ActiveStorage::Blob with fileboost-specific methods if needed
end
end
end
end
52 changes: 1 addition & 51 deletions lib/fileboost/error_handler.rb
Original file line number Diff line number Diff line change
@@ -1,57 +1,7 @@
module Fileboost
class ErrorHandler
class << self
def handle_with_fallback(error_context, &block)
begin
yield
rescue StandardError => e
log_error(error_context, e)

if Fileboost.config.fallback_to_rails
yield_fallback if block_given?
else
nil
end
end
end

def handle_gracefully(error_context, default_value = nil, &block)
begin
yield
rescue StandardError => e
log_error(error_context, e)
default_value
end
end

private

def log_error(context, error)
return unless defined?(Rails) && Rails.logger

Rails.logger.warn(
"[Fileboost] Error in #{context}: #{error.class}: #{error.message}"
)

# Log backtrace in development for debugging
if Rails.env.development?
Rails.logger.debug(
"[Fileboost] Backtrace:\n#{error.backtrace.take(5).join("\n")}"
)
end
end

def yield_fallback
# This would be implemented by the calling code
# The pattern is to pass a fallback block when needed
nil
end
end
end

# Specific exception classes for better error handling
class ConfigurationError < StandardError; end
class SignatureGenerationError < StandardError; end
class UrlBuildError < StandardError; end
class AssetPathExtractionError < StandardError; end
end
end
32 changes: 7 additions & 25 deletions lib/fileboost/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ def fileboost_image_tag(asset, **options)
# Generate the optimized URL
optimized_url = fileboost_url_for(asset, resize: resize_options)

# Return empty string if no URL could be generated
return "" if optimized_url.blank?

# Use the optimized URL with Rails image_tag for consistency
image_tag(optimized_url, **options)
end
Expand All @@ -36,16 +33,10 @@ def fileboost_image_tag(asset, **options)
# fileboost_url_for(user.avatar.blob, resize: { w: 1200, h: 400, q: 85 })
def fileboost_url_for(asset, **options)
# Validate that asset is an ActiveStorage object
unless valid_activestorage_asset?(asset)
Rails.logger.error("[Fileboost] Invalid asset type #{asset.class}. Only ActiveStorage objects are supported.") if defined?(Rails)
return nil
end
raise ArgumentError, "Invalid asset type #{asset.class}. Only ActiveStorage objects are supported." unless valid_activestorage_asset?(asset)

# Validate configuration
unless Fileboost.config.valid?
log_configuration_warning
return nil
end
raise Fileboost::ConfigurationError, "Invalid Fileboost configuration" unless Fileboost.config.valid?

# Build the optimized URL
Fileboost::UrlBuilder.build_url(asset, **options)
Expand All @@ -71,10 +62,13 @@ def fileboost_responsive_urls(asset, sizes, **base_options)
sizes.each do |size_config|
suffix = size_config[:suffix] || size_config["suffix"]
size_options = size_config.except(:suffix, "suffix")
combined_options = base_options.merge(size_options)

# Merge size options into base resize options
merged_resize_options = (base_options[:resize] || {}).merge(size_options)
combined_options = base_options.merge(resize: merged_resize_options)

url = fileboost_url_for(asset, **combined_options)
urls[suffix] = url if url.present?
urls[suffix] = url if !url.nil? && !url.empty?
end

urls
Expand All @@ -91,17 +85,5 @@ def valid_activestorage_asset?(asset)

false
end

# Log configuration warnings
def log_configuration_warning
missing_configs = []
missing_configs << "project_id" if Fileboost.config.project_id.blank?
missing_configs << "token" if Fileboost.config.token.blank?

Rails.logger.warn(
"[Fileboost] Configuration incomplete. Missing: #{missing_configs.join(', ')}. " \
"Set FILEBOOST_PROJECT_ID and FILEBOOST_TOKEN environment variables or configure them in your initializer."
) if defined?(Rails)
end
end
end
29 changes: 2 additions & 27 deletions lib/fileboost/signature_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,17 @@

module Fileboost
class SignatureGenerator
def self.generate(project_id:, asset_path:, params: {})
return nil unless project_id.present? && asset_path.present? && Fileboost.config.token.present?

def self.generate(asset_path:, params: {})
# Sort parameters for consistent signature generation
sorted_params = params.sort.to_h
query_string = sorted_params.map { |k, v| "#{k}=#{v}" }.join("&")

# Create the signing string: project_id:asset_path:sorted_query_params
signing_string = [project_id, asset_path, query_string].join(":")
Rails.logger.debug("signature payload #{project_id}, #{asset_path}, #{query_string}, #{Fileboost.config.token}")
signing_string = [ Fileboost.config.project_id, asset_path, query_string ].join(":")
# Generate HMAC-SHA256 signature for secure authentication with Fileboost.dev
digest = OpenSSL::HMAC.digest("SHA256", Fileboost.config.token, signing_string)
# Use URL-safe base64 encoding and remove padding for maximum URL compatibility
Base64.urlsafe_encode64(digest, padding: false)
rescue StandardError => e
if defined?(Rails) && Rails.env.development?
raise e
else
Rails.logger.warn("[Fileboost] Failed to generate signature: #{e.message}") if defined?(Rails)
nil
end
end

def self.verify_signature(project_id:, asset_path:, params: {}, signature:)
expected_signature = generate(project_id: project_id, asset_path: asset_path, params: params)
return false if expected_signature.nil? || signature.nil?

# Use secure comparison to prevent timing attacks
ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
rescue StandardError => e
if defined?(Rails) && Rails.env.development?
raise e
else
Rails.logger.warn("[Fileboost] Failed to verify signature: #{e.message}") if defined?(Rails)
false
end
end
end
end
Loading