fix(flatpak): ensure favicon cache directory exists before saving
- commit
- 2e2d70290b122183584dfb4fd19097584cbd0513
- author
- bnema · · 2026-01-06 18:31 UTC
- committer
- bnema · · 2026-01-06 19:04 UTC
- parent
- 5bc9fcf
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +48 -44
M internal/infrastructure/config/xdg.go +1 -0
| @@ -185,6 +185,7 @@ func EnsureDirectories() error { | |||
| 185 | 185 | dirs.ConfigHome, | |
| 186 | 186 | dirs.DataHome, | |
| 187 | 187 | dirs.StateHome, | |
| 188 | + | dirs.CacheHome, | |
| 188 | 189 | } | |
| 189 | 190 | ||
| 190 | 191 | for _, dir := range directories { | |
| @@ -185,6 +185,7 @@ func EnsureDirectories() error { | |||||
| 185 | dirs.ConfigHome, | 185 | dirs.ConfigHome, | ||
| 186 | dirs.DataHome, | 186 | dirs.DataHome, | ||
| 187 | dirs.StateHome, | 187 | dirs.StateHome, | ||
| 188 | + | dirs.CacheHome, | |||
| 188 | } | 189 | } | ||
| 189 | 190 | ||||
| 190 | for _, dir := range directories { | 191 | for _, dir := range directories { | ||
M internal/infrastructure/favicon/cache.go +9 -0
| @@ -218,6 +218,15 @@ func (c *Cache) Close() { | |||
| 218 | 218 | }) | |
| 219 | 219 | } | |
| 220 | 220 | ||
| 221 | + | // EnsureDir ensures the cache directory exists on disk. | |
| 222 | + | // Call this before using DiskPathPNG with external save functions. | |
| 223 | + | func (c *Cache) EnsureDir() error { | |
| 224 | + | if c.diskDir == "" { | |
| 225 | + | return nil | |
| 226 | + | } | |
| 227 | + | return os.MkdirAll(c.diskDir, diskCacheDirPerm) | |
| 228 | + | } | |
| 229 | + | ||
| 221 | 230 | // Clear removes all entries from the in-memory cache. | |
| 222 | 231 | func (c *Cache) Clear() { | |
| 223 | 232 | c.mu.Lock() | |
| @@ -218,6 +218,15 @@ func (c *Cache) Close() { | |||||
| 218 | }) | 218 | }) | ||
| 219 | } | 219 | } | ||
| 220 | 220 | ||||
| 221 | + | // EnsureDir ensures the cache directory exists on disk. | |||
| 222 | + | // Call this before using DiskPathPNG with external save functions. | |||
| 223 | + | func (c *Cache) EnsureDir() error { | |||
| 224 | + | if c.diskDir == "" { | |||
| 225 | + | return nil | |||
| 226 | + | } | |||
| 227 | + | return os.MkdirAll(c.diskDir, diskCacheDirPerm) | |||
| 228 | + | } | |||
| 229 | + | ||||
| 221 | // Clear removes all entries from the in-memory cache. | 230 | // Clear removes all entries from the in-memory cache. | ||
| 222 | func (c *Cache) Clear() { | 231 | func (c *Cache) Clear() { | ||
| 223 | c.mu.Lock() | 232 | c.mu.Lock() | ||
M internal/infrastructure/favicon/service.go +6 -0
| @@ -185,3 +185,9 @@ func (s *Service) EnsureInternalFaviconPNG(pngData []byte, size int) string { | |||
| 185 | 185 | s.cache.WritePNGSized(InternalDomain, pngData, size) | |
| 186 | 186 | return s.DiskPathPNGSized(InternalDomain, size) | |
| 187 | 187 | } | |
| 188 | + | ||
| 189 | + | // EnsureCacheDir ensures the favicon cache directory exists. | |
| 190 | + | // Call this before using DiskPathPNG with external save functions like GTK's SaveToPng. | |
| 191 | + | func (s *Service) EnsureCacheDir() error { | |
| 192 | + | return s.cache.EnsureDir() | |
| 193 | + | } | |
| @@ -185,3 +185,9 @@ func (s *Service) EnsureInternalFaviconPNG(pngData []byte, size int) string { | |||||
| 185 | s.cache.WritePNGSized(InternalDomain, pngData, size) | 185 | s.cache.WritePNGSized(InternalDomain, pngData, size) | ||
| 186 | return s.DiskPathPNGSized(InternalDomain, size) | 186 | return s.DiskPathPNGSized(InternalDomain, size) | ||
| 187 | } | 187 | } | ||
| 188 | + | ||||
| 189 | + | // EnsureCacheDir ensures the favicon cache directory exists. | |||
| 190 | + | // Call this before using DiskPathPNG with external save functions like GTK's SaveToPng. | |||
| 191 | + | func (s *Service) EnsureCacheDir() error { | |||
| 192 | + | return s.cache.EnsureDir() | |||
| 193 | + | } | |||
M internal/ui/adapter/favicon.go +32 -44
| @@ -166,34 +166,11 @@ func (a *FaviconAdapter) StoreFromWebKit(ctx context.Context, pageURL string, te | |||
| 166 | 166 | return | |
| 167 | 167 | } | |
| 168 | 168 | ||
| 169 | - | log := logging.FromContext(ctx) | |
| 170 | - | ||
| 171 | 169 | // Store in texture cache | |
| 172 | 170 | a.setTexture(domain, texture) | |
| 173 | 171 | ||
| 174 | - | // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread | |
| 175 | - | // since GTK texture operations need to happen there | |
| 176 | - | pngPath := a.service.DiskPathPNG(domain) | |
| 177 | - | if pngPath != "" && !a.service.HasPNGOnDisk(domain) { | |
| 178 | - | if ok := texture.SaveToPng(pngPath); !ok { | |
| 179 | - | log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG") | |
| 180 | - | } else { | |
| 181 | - | log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG") | |
| 182 | - | ||
| 183 | - | // Create normalized sized copy for dmenu/fuzzel (async) | |
| 184 | - | // The original PNG is now on disk, so this should succeed | |
| 185 | - | go func() { | |
| 186 | - | if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil { | |
| 187 | - | log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG after save") | |
| 188 | - | } else { | |
| 189 | - | log.Debug().Str("domain", domain).Msg("created sized PNG after save") | |
| 190 | - | } | |
| 191 | - | }() | |
| 192 | - | } | |
| 193 | - | } | |
| 194 | - | ||
| 195 | - | // Ensure disk cache is populated (async, in background) | |
| 196 | - | go a.service.EnsureDiskCache(ctx, domain) | |
| 172 | + | // Export as PNG and ensure disk cache | |
| 173 | + | a.saveFaviconToDisk(ctx, domain, texture) | |
| 197 | 174 | } | |
| 198 | 175 | ||
| 199 | 176 | // StoreFromWebKitWithOrigin stores a favicon for both current URL and original URL. | |
| @@ -205,34 +182,45 @@ func (a *FaviconAdapter) StoreFromWebKitWithOrigin( | |||
| 205 | 182 | ||
| 206 | 183 | // Also store under original URL domain if different | |
| 207 | 184 | if originURL != "" && originURL != currentURL { | |
| 208 | - | log := logging.FromContext(ctx) | |
| 209 | 185 | originDomain := domainurl.ExtractDomain(originURL) | |
| 210 | 186 | currentDomain := domainurl.ExtractDomain(currentURL) | |
| 211 | 187 | if originDomain != "" && originDomain != currentDomain { | |
| 212 | 188 | a.setTexture(originDomain, texture) | |
| 189 | + | a.saveFaviconToDisk(ctx, originDomain, texture) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // saveFaviconToDisk exports a favicon texture to PNG and ensures disk cache is populated. | |
| 195 | + | func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) { | |
| 196 | + | log := logging.FromContext(ctx) | |
| 197 | + | ||
| 198 | + | // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread | |
| 199 | + | // since GTK texture operations need to happen there | |
| 200 | + | pngPath := a.service.DiskPathPNG(domain) | |
| 201 | + | if pngPath != "" && !a.service.HasPNGOnDisk(domain) { | |
| 202 | + | // Ensure cache directory exists before SaveToPng (GTK won't create it) | |
| 203 | + | if err := a.service.EnsureCacheDir(); err != nil { | |
| 204 | + | log.Warn().Err(err).Str("domain", domain).Msg("failed to create favicon cache dir") | |
| 205 | + | } else if ok := texture.SaveToPng(pngPath); !ok { | |
| 206 | + | log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG") | |
| 207 | + | } else { | |
| 208 | + | log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG") | |
| 213 | 209 | ||
| 214 | - | // Export PNG for origin domain too | |
| 215 | - | pngPath := a.service.DiskPathPNG(originDomain) | |
| 216 | - | if pngPath != "" && !a.service.HasPNGOnDisk(originDomain) { | |
| 217 | - | if ok := texture.SaveToPng(pngPath); !ok { | |
| 218 | - | log.Warn().Str("domain", originDomain).Str("path", pngPath).Msg("failed to save origin favicon PNG") | |
| 210 | + | // Create normalized sized copy for dmenu/fuzzel (async) | |
| 211 | + | // The original PNG is now on disk, so this should succeed | |
| 212 | + | go func() { | |
| 213 | + | if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil { | |
| 214 | + | log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG") | |
| 219 | 215 | } else { | |
| 220 | - | log.Debug().Str("domain", originDomain).Str("path", pngPath).Msg("saved origin favicon PNG") | |
| 221 | - | ||
| 222 | - | // Create normalized sized copy for dmenu/fuzzel (async) | |
| 223 | - | go func() { | |
| 224 | - | if err := a.service.EnsureSizedPNG(ctx, originDomain, favicon.NormalizedIconSize); err != nil { | |
| 225 | - | log.Warn().Err(err).Str("domain", originDomain).Msg("failed to create sized PNG for origin") | |
| 226 | - | } else { | |
| 227 | - | log.Debug().Str("domain", originDomain).Msg("created sized PNG for origin") | |
| 228 | - | } | |
| 229 | - | }() | |
| 216 | + | log.Debug().Str("domain", domain).Msg("created sized PNG") | |
| 230 | 217 | } | |
| 231 | - | } | |
| 232 | - | ||
| 233 | - | go a.service.EnsureDiskCache(ctx, originDomain) | |
| 218 | + | }() | |
| 234 | 219 | } | |
| 235 | 220 | } | |
| 221 | + | ||
| 222 | + | // Ensure disk cache is populated (async, in background) | |
| 223 | + | go a.service.EnsureDiskCache(ctx, domain) | |
| 236 | 224 | } | |
| 237 | 225 | ||
| 238 | 226 | // PreloadFromCache attempts to load a favicon from cache without fetching. | |
| @@ -166,34 +166,11 @@ func (a *FaviconAdapter) StoreFromWebKit(ctx context.Context, pageURL string, te | |||||
| 166 | return | 166 | return | ||
| 167 | } | 167 | } | ||
| 168 | 168 | ||||
| 169 | - | log := logging.FromContext(ctx) | |||
| 170 | - | ||||
| 171 | // Store in texture cache | 169 | // Store in texture cache | ||
| 172 | a.setTexture(domain, texture) | 170 | a.setTexture(domain, texture) | ||
| 173 | 171 | ||||
| 174 | - | // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread | 172 | + | // Export as PNG and ensure disk cache |
| 175 | - | // since GTK texture operations need to happen there | 173 | + | a.saveFaviconToDisk(ctx, domain, texture) |
| 176 | - | pngPath := a.service.DiskPathPNG(domain) | |||
| 177 | - | if pngPath != "" && !a.service.HasPNGOnDisk(domain) { | |||
| 178 | - | if ok := texture.SaveToPng(pngPath); !ok { | |||
| 179 | - | log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG") | |||
| 180 | - | } else { | |||
| 181 | - | log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG") | |||
| 182 | - | ||||
| 183 | - | // Create normalized sized copy for dmenu/fuzzel (async) | |||
| 184 | - | // The original PNG is now on disk, so this should succeed | |||
| 185 | - | go func() { | |||
| 186 | - | if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil { | |||
| 187 | - | log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG after save") | |||
| 188 | - | } else { | |||
| 189 | - | log.Debug().Str("domain", domain).Msg("created sized PNG after save") | |||
| 190 | - | } | |||
| 191 | - | }() | |||
| 192 | - | } | |||
| 193 | - | } | |||
| 194 | - | ||||
| 195 | - | // Ensure disk cache is populated (async, in background) | |||
| 196 | - | go a.service.EnsureDiskCache(ctx, domain) | |||
| 197 | } | 174 | } | ||
| 198 | 175 | ||||
| 199 | // StoreFromWebKitWithOrigin stores a favicon for both current URL and original URL. | 176 | // StoreFromWebKitWithOrigin stores a favicon for both current URL and original URL. | ||
| @@ -205,34 +182,45 @@ func (a *FaviconAdapter) StoreFromWebKitWithOrigin( | |||||
| 205 | 182 | ||||
| 206 | // Also store under original URL domain if different | 183 | // Also store under original URL domain if different | ||
| 207 | if originURL != "" && originURL != currentURL { | 184 | if originURL != "" && originURL != currentURL { | ||
| 208 | - | log := logging.FromContext(ctx) | |||
| 209 | originDomain := domainurl.ExtractDomain(originURL) | 185 | originDomain := domainurl.ExtractDomain(originURL) | ||
| 210 | currentDomain := domainurl.ExtractDomain(currentURL) | 186 | currentDomain := domainurl.ExtractDomain(currentURL) | ||
| 211 | if originDomain != "" && originDomain != currentDomain { | 187 | if originDomain != "" && originDomain != currentDomain { | ||
| 212 | a.setTexture(originDomain, texture) | 188 | a.setTexture(originDomain, texture) | ||
| 189 | + | a.saveFaviconToDisk(ctx, originDomain, texture) | |||
| 190 | + | } | |||
| 191 | + | } | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | // saveFaviconToDisk exports a favicon texture to PNG and ensures disk cache is populated. | |||
| 195 | + | func (a *FaviconAdapter) saveFaviconToDisk(ctx context.Context, domain string, texture *gdk.Texture) { | |||
| 196 | + | log := logging.FromContext(ctx) | |||
| 197 | + | ||||
| 198 | + | // Export as PNG for CLI tools (rofi/fuzzel) - do this on main thread | |||
| 199 | + | // since GTK texture operations need to happen there | |||
| 200 | + | pngPath := a.service.DiskPathPNG(domain) | |||
| 201 | + | if pngPath != "" && !a.service.HasPNGOnDisk(domain) { | |||
| 202 | + | // Ensure cache directory exists before SaveToPng (GTK won't create it) | |||
| 203 | + | if err := a.service.EnsureCacheDir(); err != nil { | |||
| 204 | + | log.Warn().Err(err).Str("domain", domain).Msg("failed to create favicon cache dir") | |||
| 205 | + | } else if ok := texture.SaveToPng(pngPath); !ok { | |||
| 206 | + | log.Warn().Str("domain", domain).Str("path", pngPath).Msg("failed to save favicon PNG") | |||
| 207 | + | } else { | |||
| 208 | + | log.Debug().Str("domain", domain).Str("path", pngPath).Msg("saved favicon PNG") | |||
| 213 | 209 | ||||
| 214 | - | // Export PNG for origin domain too | 210 | + | // Create normalized sized copy for dmenu/fuzzel (async) |
| 215 | - | pngPath := a.service.DiskPathPNG(originDomain) | 211 | + | // The original PNG is now on disk, so this should succeed |
| 216 | - | if pngPath != "" && !a.service.HasPNGOnDisk(originDomain) { | 212 | + | go func() { |
| 217 | - | if ok := texture.SaveToPng(pngPath); !ok { | 213 | + | if err := a.service.EnsureSizedPNG(ctx, domain, favicon.NormalizedIconSize); err != nil { |
| 218 | - | log.Warn().Str("domain", originDomain).Str("path", pngPath).Msg("failed to save origin favicon PNG") | 214 | + | log.Warn().Err(err).Str("domain", domain).Msg("failed to create sized PNG") |
| 219 | } else { | 215 | } else { | ||
| 220 | - | log.Debug().Str("domain", originDomain).Str("path", pngPath).Msg("saved origin favicon PNG") | 216 | + | log.Debug().Str("domain", domain).Msg("created sized PNG") |
| 221 | - | ||||
| 222 | - | // Create normalized sized copy for dmenu/fuzzel (async) | |||
| 223 | - | go func() { | |||
| 224 | - | if err := a.service.EnsureSizedPNG(ctx, originDomain, favicon.NormalizedIconSize); err != nil { | |||
| 225 | - | log.Warn().Err(err).Str("domain", originDomain).Msg("failed to create sized PNG for origin") | |||
| 226 | - | } else { | |||
| 227 | - | log.Debug().Str("domain", originDomain).Msg("created sized PNG for origin") | |||
| 228 | - | } | |||
| 229 | - | }() | |||
| 230 | } | 217 | } | ||
| 231 | - | } | 218 | + | }() |
| 232 | - | ||||
| 233 | - | go a.service.EnsureDiskCache(ctx, originDomain) | |||
| 234 | } | 219 | } | ||
| 235 | } | 220 | } | ||
| 221 | + | ||||
| 222 | + | // Ensure disk cache is populated (async, in background) | |||
| 223 | + | go a.service.EnsureDiskCache(ctx, domain) | |||
| 236 | } | 224 | } | ||
| 237 | 225 | ||||
| 238 | // PreloadFromCache attempts to load a favicon from cache without fetching. | 226 | // PreloadFromCache attempts to load a favicon from cache without fetching. | ||