Skip to content

Commit

Permalink
Merge pull request #1524 from ODNZSL/task/nzsl-167-presign-sign-asset…
Browse files Browse the repository at this point in the history
…-requests

NZSL-167: Presign asset requests
  • Loading branch information
joshmcarthur committed Jan 15, 2024
2 parents b5f9c74 + e3e1422 commit 228d5f1
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 4 deletions.
6 changes: 6 additions & 0 deletions app/models/signbank/asset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
69 changes: 69 additions & 0 deletions app/models/signbank/asset_url.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions app/models/signbank/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions app/models/signbank/sign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions config/initializers/inflections.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
19 changes: 19 additions & 0 deletions spec/models/signbank/asset_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 72 additions & 0 deletions spec/models/signbank/asset_url_spec.rb
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions spec/models/signbank/example_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 19 additions & 0 deletions spec/models/signbank/sign_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 228d5f1

Please sign in to comment.