Serve uploads with the Roda :static plugin
Replace the in-route Rack::Files mount with Roda's :static plugin,
wired at boot (config.ru and the test setup) to the app's storage dir
since the path is injected via opts[:app]. The "/files/" prefix serves
stored uploads straight off disk with an immutable cache header, while
the bare POST /files upload route is left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N92HLTepkg3AtRpDUyZ54o
diff --git a/config.ru b/config.ru
index 042a8ea..5f0512f 100644
--- a/config.ru
+++ b/config.ru
@@ -11,4 +11,10 @@ app = Domus::App.new
Sequel::Migrator.run(app.db, "db/migrate") unless Dir.empty?("db/migrate")
Domus::Web.opts[:app] = app
+# Serve stored uploads (storage/files/{id}{ext}) at /files/ straight off disk.
+# Uploads are immutable once stored, so they cache indefinitely.
+Domus::Web.plugin :static, ["/files/"],
+ root: app.config.storage_path.to_s,
+ cache_control: "private, max-age=31536000, immutable"
+
run Domus::Web
diff --git a/lib/web.rb b/lib/web.rb
index 3516dcf..4e9b455 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -2,7 +2,6 @@
require "roda"
require "fileutils"
-require "rack/files"
require_relative "views/layout"
require_relative "views/home"
require_relative "views/asset"
@@ -55,17 +54,14 @@ module Domus
# credential a forged cross-site POST could ride on. If this app ever
# adopts cookie-based sessions, load the Roda :route_csrf plugin and
# verify the token here before accepting the upload.
+ # GET /files/:filename (the stored uploads) is served straight off disk
+ # by the :static middleware, wired to the app's storage dir at boot
+ # (see config.ru / the test setup). POST /files stays on the route.
r.on "files" do
r.post do
save_file(r.params)
r.redirect "/"
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 496c75e..3cea785 100644
--- a/test/test_app.rb
+++ b/test/test_app.rb
@@ -125,6 +125,7 @@ class TestApp < Minitest::Test
get "/files/#{file[:id]}#{file[:extension]}"
assert_equal 200, last_response.status
assert_equal "image/png", last_response.headers["Content-Type"]
+ assert_includes last_response.headers["Cache-Control"].to_s, "immutable"
assert_equal "fake-png-bytes", last_response.body
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 2e316a8..2485587 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -14,3 +14,6 @@ app = Domus::App.new(Domus::Config.new(database_url: ":memory:", storage_path: P
migrate_dir = File.expand_path("../db/migrate", __dir__)
Sequel::Migrator.run(app.db, migrate_dir) unless Dir.empty?(migrate_dir)
Domus::Web.opts[:app] = app
+Domus::Web.plugin :static, ["/files/"],
+ root: app.config.storage_path.to_s,
+ cache_control: "private, max-age=31536000, immutable"