Serve uploads directly with Rack::Files
Replace the hand-rolled file route (DB lookup + File.binread + manual
headers) with Rack::Files mounted under /files, serving stored uploads
straight off disk. It handles content-type, range requests, conditional
GETs and 404s for free; the immutable cache header is passed through.
Image URLs now carry the extension (/files/{id}{ext}).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N92HLTepkg3AtRpDUyZ54o
diff --git a/lib/app.rb b/lib/app.rb
index 3aee12c..1f6c224 100644
--- a/lib/app.rb
+++ b/lib/app.rb
@@ -20,7 +20,9 @@ module Domus
# : (Hash[Symbol, untyped]) -> Pathname
def file_path(record)
- config.storage_path / "files" / "#{record[:id]}#{record[:extension]}"
+ files_root / "#{record[:id]}#{record[:extension]}"
end
+
+ def files_root = config.storage_path / "files"
end
end
diff --git a/lib/views/asset.rb b/lib/views/asset.rb
index eb5aba1..29730a7 100644
--- a/lib/views/asset.rb
+++ b/lib/views/asset.rb
@@ -53,7 +53,7 @@ module Domus
div(class: "photos") do
@images.each do |file|
div(class: "shot") do
- img(src: "/files/#{file[:id]}", alt: "Photo of #{@asset[:name]}", loading: "lazy")
+ img(src: "/files/#{file[:id]}#{file[:extension]}", alt: "Photo of #{@asset[:name]}", loading: "lazy")
end
end
end
diff --git a/lib/web.rb b/lib/web.rb
index 0db8b6c..3516dcf 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -2,7 +2,7 @@
require "roda"
require "fileutils"
-require "rack/mime"
+require "rack/files"
require_relative "views/layout"
require_relative "views/home"
require_relative "views/asset"
@@ -61,20 +61,11 @@ module Domus
r.redirect "/"
end
- # GET /files/:id — stream a stored upload. .sole raises
- # Sequel::NoMatchingRow (→ 404) for an unknown id.
- r.is Integer do |id|
- r.get do
- file = db[:files].where(id:).sole
- path = app.file_path(file)
- raise ClientError.new("File not found.", status: 404) unless ::File.exist?(path)
-
- response["Content-Type"] = Rack::Mime.mime_type(file[:extension], "application/octet-stream")
- # Uploads are immutable once stored, so they cache indefinitely.
- response["Cache-Control"] = "private, max-age=31536000, immutable"
- ::File.binread(path)
- end
- end
+ # GET /files/:filename — serve a stored upload straight off disk.
+ # Rack::Files handles content-type, range requests, conditional GETs
+ # and a 404 for unknown files; uploads are named {id}{ext} and are
+ # immutable once stored, so they cache indefinitely.
+ r.run Rack::Files.new(app.files_root, { "cache-control" => "private, max-age=31536000, immutable" })
end
r.on "assets" do
diff --git a/test/test_app.rb b/test/test_app.rb
index 5924363..496c75e 100644
--- a/test/test_app.rb
+++ b/test/test_app.rb
@@ -107,7 +107,7 @@ class TestApp < Minitest::Test
get "/assets/#{asset_id}"
assert_equal 200, last_response.status
- assert_includes last_response.body, %(src="/files/#{file_id}")
+ assert_includes last_response.body, %(src="/files/#{file_id}.png")
end
def test_asset_detail_without_images_omits_photos
@@ -120,16 +120,16 @@ class TestApp < Minitest::Test
def test_get_file_serves_stored_image
post "/files", "file" => upload("photo.png", "image/png", "fake-png-bytes")
- file_id = domus.db[:files].first[:id]
+ file = domus.db[:files].first
- get "/files/#{file_id}"
+ get "/files/#{file[:id]}#{file[:extension]}"
assert_equal 200, last_response.status
assert_equal "image/png", last_response.headers["Content-Type"]
assert_equal "fake-png-bytes", last_response.body
end
def test_get_missing_file_is_404
- get "/files/999999"
+ get "/files/999999.png"
assert_equal 404, last_response.status
end