Skip to content

Commit

Permalink
Add engine for handling uploads domain
Browse files Browse the repository at this point in the history
The serving is only used in development - in production it hits CloudFront.
  • Loading branch information
pixeltrix committed Oct 9, 2024
1 parent 8c5f4e9 commit 41a8381
Show file tree
Hide file tree
Showing 21 changed files with 237 additions and 2 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ OTP_SECRET_ENCRYPTION_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTIFY_API_KEY=xxxxx
GROUPED_PROPOSAL_DETAILS_FEATURE=enabled
GOOGLE_TAG_MANAGER_ID=GTM-XXXXXXXX
UPLOADS_BASE_URL=http://uploads.bops.localhost:3000
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ STAGING_API_URL="bops-staging.services"
STAGING_API_BEARER="fjisdfjsdiofjdsoi"
OS_VECTOR_TILES_API_KEY="testtest"
NOTIFY_LETTER_API_KEY='testtest'
UPLOADS_BASE_URL=http://uploads.example.com
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ jobs:
- { group: "bops_api", module: "engines" }
- { group: "bops_config", module: "engines" }
- { group: "bops_core", module: "engines" }
- { group: "bops_uploads", module: "engines" }
fail-fast: false
with:
name: "${{matrix.specs.group}}"
Expand Down
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
-I engines/bops_admin/spec
-I engines/bops_api/spec
-I engines/bops_config/spec
-I engines/bops_uploads/spec
--require spec_helper
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ gem "bops_core", path: "engines/bops_core"
gem "bops_admin", path: "engines/bops_admin"
gem "bops_api", path: "engines/bops_api"
gem "bops_config", path: "engines/bops_config"
gem "bops_uploads", path: "engines/bops_uploads"

group :development, :test do
gem "brakeman", require: false
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ PATH
pagy
rails (>= 7.1.3, < 7.2)

PATH
remote: engines/bops_uploads
specs:
bops_uploads (0.1.0)
bops_core (= 0.1.0)

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -668,6 +674,7 @@ DEPENDENCIES
bops_api!
bops_config!
bops_core!
bops_uploads!
brakeman
bullet
business_time
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ config-specs:
core-specs:
$(DOCKER-RUN) console rspec engines/bops_core/spec

engine-specs: api-specs admin-specs config-specs core-specs
uploads-specs:
$(DOCKER-RUN) console rspec engines/bops_uploads/spec

engine-specs: api-specs admin-specs config-specs core-specs uploads-specs

rspec:
$(DOCKER-RUN) console rspec
Expand Down
2 changes: 1 addition & 1 deletion app/models/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class NotArchiveableError < StandardError; end
inverse_of: false

delegate :audits, to: :planning_application
delegate :representable?, to: :file
delegate :blob, :representable?, to: :file

include Auditable

Expand Down
1 change: 1 addition & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class Application < Rails::Application
config.planx_file_production_api_key = ENV["PLANX_FILE_PRODUCTION_API_KEY"]
config.staging_api_bearer = ENV["STAGING_API_BEARER"]
config.staging_api_url = ENV["STAGING_API_URL"]
config.uploads_base_url = ENV["UPLOADS_BASE_URL"]
end

def self.env
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@
mount BopsConfig::Engine, at: "/", as: :bops_config
end

constraints Constraints::UploadsSubdomain do
mount BopsUploads::Engine, at: "/", as: :bops_uploads
end

direct :rails_public_blob do |blob|
if (cdn_host = ENV["CDN_HOST"])
# Use an environment variable instead of hard-coding the CDN host
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module BopsUploads
class ApplicationController < ActionController::Base
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

module BopsUploads
class FilesController < ApplicationController
before_action :set_service
before_action :set_blob

def show
serve_file(blob_path, content_type:, disposition:)
rescue Errno::ENOENT
head :not_found
end

private

def set_service
@service = ActiveStorage::Blob.service
end

def set_blob
@blob = ActiveStorage::Blob.find_by!(key: params[:key])
end

def blob_path
@service.path_for(@blob.key)
end

def forcibly_serve_as_binary?
ActiveStorage.content_types_to_serve_as_binary.include?(@blob.content_type)
end

def allowed_inline?
ActiveStorage.content_types_allowed_inline.include?(@blob.content_type)
end

def content_type
forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : @blob.content_type
end

def disposition
if forcibly_serve_as_binary? || !allowed_inline?
:attachment
else
:inline
end
end

def serve_file(path, content_type:, disposition:)
::Rack::Files.new(nil).serving(request, path).tap do |(status, headers, body)|
self.status = status
self.response_body = body

headers.each do |name, value|
response.headers[name] = value
end

response.headers.except!("X-Cascade", "x-cascade") if status == 416
response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
end
end
end
end
19 changes: 19 additions & 0 deletions engines/bops_uploads/bin/rails
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails gems
# installed from the root of your application.

ENGINE_ROOT = File.expand_path("..", __dir__)
ENGINE_PATH = File.expand_path("../lib/bops_uploads/engine", __dir__)
APP_PATH = File.expand_path("../../../config/application", __dir__)

# Set up gems listed in the Gemfile.
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])

require "rails"

require "active_model/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_view/railtie"
require "rails/engine/commands"
16 changes: 16 additions & 0 deletions engines/bops_uploads/bops_uploads.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

Gem::Specification.new do |spec|
spec.name = "bops_uploads"
spec.version = "0.1.0"
spec.authors = ["Unboxed Consulting Ltd"]
spec.email = ["[email protected]"]
spec.homepage = "https://unboxed.co/"
spec.summary = "Provides the uploads facility for the BOPS system"

spec.files = Dir.chdir(File.expand_path(__dir__)) do
Dir["{app,config,db,lib}/**/*"]
end

spec.add_dependency "bops_core", "0.1.0"
end
19 changes: 19 additions & 0 deletions engines/bops_uploads/config/routes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

BopsUploads::Engine.routes.draw do
get "/:key", to: "files#show", as: "file"
end

Rails.application.routes.draw do
direct :uploaded_file do |blob, options|
next "" if blob.blank?

bops_uploads.file_url(blob.key, host: Rails.configuration.uploads_base_url)
end

resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:uploaded_file, attachment.blob, options) }
resolve("ActiveStorage::Blob") { |blob, options| route_for(:uploaded_file, blob, options) }
resolve("ActiveStorage::Preview") { |preview, options| route_for(:uploaded_file, preview, options) }
resolve("ActiveStorage::VariantWithRecord") { |variant, options| route_for(:uploaded_file, variant, options) }
resolve("ActiveStorage::Variant") { |variant, options| route_for(:uploaded_file, variant, options) }
end
11 changes: 11 additions & 0 deletions engines/bops_uploads/lib/bops_uploads.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

require "bops_uploads/engine"

module BopsUploads
class << self
def env
ActiveSupport::StringInquirer.new(ENV.fetch("BOPS_ENVIRONMENT", "development"))
end
end
end
7 changes: 7 additions & 0 deletions engines/bops_uploads/lib/bops_uploads/engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

module BopsUploads
class Engine < ::Rails::Engine
isolate_namespace BopsUploads
end
end
9 changes: 9 additions & 0 deletions engines/bops_uploads/spec/bops_uploads_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.configure do |config|
config.before type: :request do
host!("uploads.bops.services")
end
end
38 changes: 38 additions & 0 deletions engines/bops_uploads/spec/requests/files_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

require "bops_uploads_helper"

RSpec.describe "Downloading files", show_exceptions: true do
context "when a blob exists" do
let(:document) { create(:document) }
let(:blob) { document.file }
let(:key) { blob.key }
let(:path) { blob.service.path_for(blob.key) }

it "returns 200 OK" do
get "/#{key}"
expect(response).to have_http_status(:ok)
expect(response.content_type).to eq("image/png")
end

context "but the file is missing" do
before do
File.unlink(path)
end

it "returns 404 Not Found" do
get "/#{key}"
expect(response).to have_http_status(:not_found)
end
end
end

context "when a blob doesn't exist" do
let(:key) { SecureRandom.base36(28) }

it "returns 404 Not Found" do
get "/#{key}"
expect(response).to have_http_status(:not_found)
end
end
end
8 changes: 8 additions & 0 deletions lib/constraints/subdomain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ def matches?(request)
end
end
end

class UploadsSubdomain
class << self
def matches?(request)
request.subdomain == "uploads"
end
end
end
end
19 changes: 19 additions & 0 deletions spec/support/show_exceptions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

RSpec.configure do |config|
config.around(:each, type: :request) do |example|
if example.metadata.key?(:show_exceptions)
begin
env_config = Rails.application.env_config
show_exceptions = env_config["action_dispatch.show_exceptions"]
env_config["action_dispatch.show_exceptions"] = example.metadata[:show_exceptions] ? :all : :none

example.run
ensure
env_config["action_dispatch.show_exceptions"] = show_exceptions
end
else
example.run
end
end
end

0 comments on commit 41a8381

Please sign in to comment.