Configure static upload serving inside Web
The :static plugin needs the storage root at load, which forced its
config into config.ru (and a duplicate in the test helper). A shared
Domus.config lets Web read the path in its own class body.

Assisted-by: Claude Opus 4.8 via Claude Code
change mvzvrkmlqskrptmmsstnslwzssynmnqu
commit 56f47548935435c5e56315a49cd01ddbecb0372b
author Alpha Chen <alpha@kejadlen.dev>
date
parent nmkoppuo
diff --git a/config.ru b/config.ru
index eef2d7e..1031a7d 100644
--- a/config.ru
+++ b/config.ru
@@ -10,10 +10,4 @@ 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/domus/app.rb b/lib/domus/app.rb
index 7419a30..bcc3ee7 100644
--- a/lib/domus/app.rb
+++ b/lib/domus/app.rb
@@ -8,7 +8,7 @@ module Domus
     attr_reader :config, :db
 
     # : (Config) -> void
-    def initialize(config = Config.env)
+    def initialize(config = Domus.config)
       @config = config
       @db = Sequel.sqlite(config.database_url)
       @db.extension(:sole)
diff --git a/lib/domus/config.rb b/lib/domus/config.rb
index 88169c1..3c68f29 100644
--- a/lib/domus/config.rb
+++ b/lib/domus/config.rb
@@ -15,4 +15,15 @@ module Domus
       storage_path: Pathname(ENV.fetch("STORAGE_PATH") { "storage" }),
     )
   end
+
+  class << self
+    # The process-wide config, resolved from the environment the first time
+    # it's read. Loaded code reads this single instance: Web's :static plugin
+    # needs the storage path at load, and App stores files there. Tests assign
+    # a Config here before requiring the app to point it at a temp dir.
+    attr_writer :config
+
+    #: () -> Config
+    def config = @config ||= Config.env
+  end
 end
diff --git a/lib/domus/web.rb b/lib/domus/web.rb
index 4e9b455..2cc81b5 100644
--- a/lib/domus/web.rb
+++ b/lib/domus/web.rb
@@ -2,6 +2,7 @@
 
 require "roda"
 require "fileutils"
+require_relative "config"
 require_relative "views/layout"
 require_relative "views/home"
 require_relative "views/asset"
@@ -21,6 +22,15 @@ module Domus
 
   class Web < Roda
     plugin :public
+
+    # Serve stored uploads (storage/files/{id}{ext}) at /files/ straight off
+    # disk. The :static plugin needs a concrete root at load time, so it reads
+    # the shared Domus.config (the same instance App uses). Uploads are
+    # immutable once stored, so they cache indefinitely.
+    plugin :static, ["/files/"],
+      root: Domus.config.storage_path.to_s,
+      cache_control: "private, max-age=31536000, immutable"
+
     plugin :all_verbs
     plugin :error_handler do |e|
       case e
@@ -55,8 +65,8 @@ module Domus
       # 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.
+      # by the :static middleware configured above. POST /files stays on the
+      # route.
       r.on "files" do
         r.post do
           save_file(r.params)
diff --git a/sig/generated/domus/config.rbs b/sig/generated/domus/config.rbs
index 381cf90..7a54d3c 100644
--- a/sig/generated/domus/config.rbs
+++ b/sig/generated/domus/config.rbs
@@ -18,4 +18,13 @@ module Domus
     # : () -> Config
     def self.env: () -> Config
   end
+
+  # The process-wide config, resolved from the environment the first time
+  # it's read. Loaded code reads this single instance: Web's :static plugin
+  # needs the storage path at load, and App stores files there. Tests assign
+  # a Config here before requiring the app to point it at a temp dir.
+  attr_writer config: untyped
+
+  # : () -> Config
+  def self.config: () -> Config
 end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 6323e8c..2437373 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -5,16 +5,18 @@ require "fileutils"
 require "pathname"
 
 require "domus/app"
-require "domus/web"
-require "domus/seeds"
 
 storage = Dir.mktmpdir("domus-test")
 at_exit { FileUtils.rm_rf(storage) }
 
-app = Domus::App.new(Domus::Config.new(database_url: ":memory:", storage_path: Pathname(storage)))
+# Web reads Domus.config at load to configure static upload serving, so inject
+# a temp-dir config before requiring it.
+Domus.config = Domus::Config.new(database_url: ":memory:", storage_path: Pathname(storage))
+
+require "domus/web"
+require "domus/seeds"
+
+app = Domus::App.new
 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"