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
change
commit 4f95cf0f4899485af14c7bbeaf9ce41471f102bb
author Claude <noreply@anthropic.com>
date
parent 1763df3b
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