Let Photo fetch and cache itself
Assisted-by: Claude Opus 4.8 via Claude Code
change snqwtuzyksnskvtmtwxtnzvnqkxnnqvt
commit 73bc2f2e2b50457beccdf093e0c7c45530513992
author Alpha Chen <alpha@kejadlen.dev>
date
parent ssnlxyrw
diff --git a/lib/seeds.rb b/lib/seeds.rb
index c2fa000..3a55285 100644
--- a/lib/seeds.rb
+++ b/lib/seeds.rb
@@ -13,11 +13,25 @@ module Domus
   module Seeds
     USER_AGENT = "domus-seed/1.0 (https://github.com/kejadlen/domus)"
 
-    # A seed photo. +key+ names the cache file; +url+ is the source download.
+    # A seed photo that knows how to fetch and cache itself. +key+ names the
+    # cache file; +url+ is the source download.
     Photo = Data.define(
       :key, #: Symbol
       :url, #: String
-    )
+    ) do
+      # Downloads the photo into the cache on first use, returning its path.
+      # : () -> Pathname
+      def fetch
+        return cache_path if cache_path.exist?
+
+        cache_path.dirname.mkpath
+        cache_path.binwrite(URI.parse(url).open("User-Agent" => USER_AGENT, &:read))
+        cache_path
+      end
+
+      # : () -> Pathname
+      def cache_path = Seeds.cache_dir / "#{key}.jpg"
+    end
 
     # An asset to seed, with zero or more attached photos.
     Asset = Data.define(
@@ -65,15 +79,15 @@ module Domus
 
     # Seeds the database when it's empty, returning true when it inserted data
     # and false when assets already exist. The empty check keeps repeated dev
-    # runs and CI idempotent. Photos download (and cache) outside the
-    # transaction so a slow network doesn't hold the write lock open.
+    # runs and CI idempotent.
     # : (App) -> bool
     def self.call(app)
       db = app.db
       return false unless db[:assets].empty?
 
-      photos = ASSETS.flat_map(&:photos).uniq #: Array[Photo]
-      sources = photos.to_h { |photo| [photo, fetch(photo)] } #: Hash[Photo, Pathname]
+      # Warm the cache outside the transaction so a slow download doesn't hold
+      # the write lock open.
+      ASSETS.flat_map(&:photos).uniq.each(&:fetch)
 
       db.transaction do
         now = Time.now
@@ -83,7 +97,7 @@ module Domus
             file_id = db[:files].insert(extension: ".jpg", created_at: now)
             dest = app.file_path(id: file_id, extension: ".jpg")
             FileUtils.mkdir_p(dest.dirname)
-            FileUtils.cp(sources.fetch(photo), dest)
+            FileUtils.cp(photo.cache_path, dest)
             db[:asset_attachments].insert(asset_id:, file_id:, created_at: now)
           end
         end
@@ -91,18 +105,6 @@ module Domus
       true
     end
 
-    # Returns the cached path to a seed photo, downloading it on first use.
-    # : (Photo) -> Pathname
-    def self.fetch(photo)
-      path = cache_dir / "#{photo.key}.jpg"
-      return path if path.exist?
-
-      cache_dir.mkpath
-      data = URI.parse(photo.url).open("User-Agent" => USER_AGENT, &:read) #: String
-      path.binwrite(data)
-      path
-    end
-
     # The XDG cache directory for downloaded seed photos.
     # : () -> Pathname
     def self.cache_dir
diff --git a/sig/generated/seeds.rbs b/sig/generated/seeds.rbs
index f83014c..604f032 100644
--- a/sig/generated/seeds.rbs
+++ b/sig/generated/seeds.rbs
@@ -9,7 +9,8 @@ module Domus
   module Seeds
     USER_AGENT: ::String
 
-    # A seed photo. +key+ names the cache file; +url+ is the source download.
+    # A seed photo that knows how to fetch and cache itself. +key+ names the
+    # cache file; +url+ is the source download.
     class Photo < Data
       attr_reader key(): Symbol
 
@@ -60,15 +61,10 @@ module Domus
 
     # Seeds the database when it's empty, returning true when it inserted data
     # and false when assets already exist. The empty check keeps repeated dev
-    # runs and CI idempotent. Photos download (and cache) outside the
-    # transaction so a slow network doesn't hold the write lock open.
+    # runs and CI idempotent.
     # : (App) -> bool
     def self.call: (untyped app) -> untyped
 
-    # Returns the cached path to a seed photo, downloading it on first use.
-    # : (Photo) -> Pathname
-    def self.fetch: (untyped photo) -> untyped
-
     # The XDG cache directory for downloaded seed photos.
     # : () -> Pathname
     def self.cache_dir: () -> untyped