diff --git a/app/models/signbank/asset.rb b/app/models/signbank/asset.rb index 9de160f3e..cd0b1bac1 100644 --- a/app/models/signbank/asset.rb +++ b/app/models/signbank/asset.rb @@ -7,5 +7,11 @@ class Asset < Signbank::Record default_scope -> { order(display_order: :asc) } scope :image, -> { where("filename LIKE '%.png'") } + + def url + return unless super.presence + + AssetURL.new(super).url.to_s + end end end diff --git a/app/models/signbank/asset_url.rb b/app/models/signbank/asset_url.rb new file mode 100644 index 000000000..ed053fd1e --- /dev/null +++ b/app/models/signbank/asset_url.rb @@ -0,0 +1,69 @@ +module Signbank + class AssetURL + attr_reader :asset_url + + class S3Adapter + cattr_accessor :region, :access_key_id, :secret_access_key, :endpoint + self.region = ENV.fetch('DICTIONARY_AWS_REGION', ENV.fetch('AWS_REGION', nil)) + self.access_key_id = ENV.fetch('DICTIONARY_AWS_ACCESS_KEY_ID', nil) + self.secret_access_key = ENV.fetch('DICTIONARY_AWS_SECRET_ACCESS_KEY', nil) + self.endpoint = 's3.amazonaws.com' + + def initialize(asset) + @asset = asset + end + + def self.configured? + region && access_key_id && secret_access_key && client + end + + def self.client + @client ||= Aws::S3::Client.new(region:, access_key_id:, + secret_access_key:) + rescue Aws::Errors::MissingCredentialsError, Aws::Errors::MissingRegionError + nil + end + + def bucket_name + bucket_name, hostname = @asset.asset_url.host.split('.', 2) + raise ArgumentError, "Invalid hostname #{@asset.asset_url.host}" unless hostname == endpoint + + bucket_name + end + + def url(expires_in: 1.hour) + return unless self.class.configured? + + object_key = @asset.asset_url.path[1..] + + URI.parse( + Aws::S3::Object.new(bucket_name, object_key, client: self.class.client) + .presigned_url(:get, expires_in: expires_in.to_i) + ) + end + end + + class PassthroughUrlAdapter + def initialize(asset) + @asset = asset + end + + def self.configured? + true + end + + def url(*) + return unless self.class.configured? + + @asset.asset_url + end + end + + delegate :url, to: :@adapter + + def initialize(asset_url, adapter: nil) + @asset_url = URI.parse(asset_url) + @adapter = (adapter || [S3Adapter, PassthroughUrlAdapter].find(&:configured?)).new(self) + end + end +end diff --git a/app/models/signbank/example.rb b/app/models/signbank/example.rb index 5618d1142..1c0a5e13d 100644 --- a/app/models/signbank/example.rb +++ b/app/models/signbank/example.rb @@ -7,5 +7,11 @@ class Example < Signbank::Record foreign_key: :word_id, inverse_of: :examples default_scope -> { order(display_order: :asc).where.not(video: nil) } + + def video + return unless super.presence + + AssetURL.new(super).url.to_s + end end end diff --git a/app/models/signbank/sign.rb b/app/models/signbank/sign.rb index 6f4053224..847b3c6be 100644 --- a/app/models/signbank/sign.rb +++ b/app/models/signbank/sign.rb @@ -43,6 +43,12 @@ def picture_url picture&.url end + def video + return unless super.presence + + AssetURL.new(super).url.to_s + end + ## # These are all aliases for the object shape that # existing code is expecting diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index aa7435fbc..34f00586c 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections @@ -11,7 +12,6 @@ # inflect.uncountable %w( fish sheep ) # end -# These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' -# end +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym 'URL' +end diff --git a/spec/models/signbank/asset_spec.rb b/spec/models/signbank/asset_spec.rb index ab7113fe6..93fc0e120 100644 --- a/spec/models/signbank/asset_spec.rb +++ b/spec/models/signbank/asset_spec.rb @@ -17,6 +17,25 @@ end end + describe '#url' do + it 'uses Signbank::AssetURL' do + double = instance_double(Signbank::AssetURL, url: URI.parse('/test.png')) + allow(Signbank::AssetURL).to receive(:new).and_return(double) + asset = Signbank::Asset.new(url: 'test.png') + expect(asset.url).to eq '/test.png' + end + + it 'is nil when the URL is nil' do + asset = Signbank::Asset.new(url: nil) + expect(asset.url).to be_nil + end + + it 'is nil when the URL is blank' do + asset = Signbank::Asset.new(url: '') + expect(asset.url).to be_nil + end + end + describe '.scoped' do it 'is ordered by display_order' do sign = Signbank::Sign.create!(id: SecureRandom.uuid) diff --git a/spec/models/signbank/asset_url_spec.rb b/spec/models/signbank/asset_url_spec.rb new file mode 100644 index 000000000..0934f2d0f --- /dev/null +++ b/spec/models/signbank/asset_url_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' + +RSpec.describe Signbank::AssetURL do + describe '#url' do + context 'when using S3Adapter' do + let(:asset_url) { 'https://example.s3.amazonaws.com/assets/asset.mp4' } + let(:adapter) { Signbank::AssetURL::S3Adapter } + + it 'returns the presigned URL for the asset' do + asset = Signbank::AssetURL.new(asset_url, adapter:) + presigned_url = 'https://s3.amazonaws.com/bucket-name/asset.mp4?expires=1234567890' + + allow(adapter).to receive(:configured?).and_return(true) + allow(adapter).to receive(:client).and_return(instance_double(Aws::S3::Client)) + allow_any_instance_of(Aws::S3::Object).to receive(:presigned_url).and_return(presigned_url) + + expect(asset.url).to eq(URI.parse(presigned_url)) + end + + it 'returns nil if S3Adapter is not configured' do + asset = Signbank::AssetURL.new(asset_url, adapter:) + + allow(adapter).to receive(:configured?).and_return(false) + + expect(asset.url).to be_nil + end + + it 'raises an error if the URL does not have the expected hostname' do + asset_url = 'https://example.com/assets/asset.mp4' + asset = Signbank::AssetURL.new(asset_url, adapter:) + allow(adapter).to receive(:configured?).and_return(true) + + expect { asset.url }.to raise_error(ArgumentError) + end + end + + context 'when using PassthroughUrlAdapter' do + let(:asset_url) { URI.parse('https://example.com/assets/asset.mp4') } + let(:adapter) { Signbank::AssetURL::PassthroughUrlAdapter } + + it 'returns the original asset URL' do + asset = Signbank::AssetURL.new(asset_url.to_s, adapter:) + + allow(adapter).to receive(:configured?).and_return(true) + + expect(asset.url).to eq(asset_url) + end + end + + context 'when no adapter is specified' do + let(:asset_url) { URI.parse('https://example.com/assets/asset.mp4') } + + it 'uses the first configured adapter' do + asset = Signbank::AssetURL.new(asset_url.to_s) + + allow(Signbank::AssetURL::S3Adapter).to receive(:configured?).and_return(false) + allow(Signbank::AssetURL::PassthroughUrlAdapter).to receive(:configured?).and_return(true) + + expect(asset.url).to eq(asset_url) + end + + it 'returns nil if no adapter is configured' do + asset = Signbank::AssetURL.new(asset_url.to_s) + + allow(Signbank::AssetURL::S3Adapter).to receive(:configured?).and_return(false) + allow(Signbank::AssetURL::PassthroughUrlAdapter).to receive(:configured?).and_return(false) + + expect(asset.url).to be_nil + end + end + end +end diff --git a/spec/models/signbank/example_spec.rb b/spec/models/signbank/example_spec.rb index 7f2e57aad..03fcade03 100644 --- a/spec/models/signbank/example_spec.rb +++ b/spec/models/signbank/example_spec.rb @@ -18,4 +18,23 @@ expect(sign.examples).to be_empty end end + + describe '#video' do + it 'uses Signbank::AssetURL' do + double = instance_double(Signbank::AssetURL, url: URI.parse('/test.png')) + allow(Signbank::AssetURL).to receive(:new).and_return(double) + example = Signbank::Example.new(video: 'test.png') + expect(example.video).to eq '/test.png' + end + + it 'is nil when the URL is nil' do + example = Signbank::Example.new(video: nil) + expect(example.video).to be_nil + end + + it 'is nil when the URL is blank' do + example = Signbank::Example.new(video: '') + expect(example.video).to be_nil + end + end end diff --git a/spec/models/signbank/sign_spec.rb b/spec/models/signbank/sign_spec.rb index 60201852c..8aaf62750 100644 --- a/spec/models/signbank/sign_spec.rb +++ b/spec/models/signbank/sign_spec.rb @@ -101,4 +101,23 @@ module Signbank end end end + + describe '#url' do + it 'uses Signbank::AssetURL' do + double = instance_double(Signbank::AssetURL, url: URI.parse('/test.png')) + allow(Signbank::AssetURL).to receive(:new).and_return(double) + sign = Signbank::Sign.new(video: 'test.png') + expect(sign.video).to eq '/test.png' + end + + it 'is nil when the URL is nil' do + sign = Signbank::Sign.new(video: nil) + expect(sign.video).to be_nil + end + + it 'is nil when the URL is blank' do + sign = Signbank::Sign.new(video: '') + expect(sign.video).to be_nil + end + end end