Files
2025-10-25 13:30:07 -05:00

112 lines
2.6 KiB
Go

// Package readme provides README fetching, rendering, and caching functionality
// for container repositories. It fetches markdown content from URLs, renders it
// to sanitized HTML using GitHub-flavored markdown, and caches the results in
// a database with configurable TTL.
package readme
import (
"context"
"database/sql"
"log/slog"
"time"
)
// Cache stores rendered README HTML in the database
type Cache struct {
db *sql.DB
fetcher *Fetcher
ttl time.Duration
}
// NewCache creates a new README cache
func NewCache(db *sql.DB, ttl time.Duration) *Cache {
if ttl == 0 {
ttl = 1 * time.Hour // Default TTL
}
return &Cache{
db: db,
fetcher: NewFetcher(),
ttl: ttl,
}
}
// Get retrieves a README from cache or fetches it
func (c *Cache) Get(ctx context.Context, readmeURL string) (string, error) {
// Try to get from cache
html, fetchedAt, err := c.getFromDB(readmeURL)
if err == nil {
// Check if cache is still valid
if time.Since(fetchedAt) < c.ttl {
return html, nil
}
}
// Cache miss or expired, fetch fresh content
html, err = c.fetcher.FetchAndRender(ctx, readmeURL)
if err != nil {
// If fetch fails but we have stale cache, return it
if html != "" {
return html, nil
}
return "", err
}
// Store in cache
if err := c.storeInDB(readmeURL, html); err != nil {
// Log error but don't fail - we have the content
slog.Warn("Failed to cache README", "error", err)
}
return html, nil
}
// getFromDB retrieves cached README from database
func (c *Cache) getFromDB(readmeURL string) (string, time.Time, error) {
var html string
var fetchedAt time.Time
err := c.db.QueryRow(`
SELECT html, fetched_at
FROM readme_cache
WHERE url = ?
`, readmeURL).Scan(&html, &fetchedAt)
if err != nil {
return "", time.Time{}, err
}
return html, fetchedAt, nil
}
// storeInDB stores rendered README in database
func (c *Cache) storeInDB(readmeURL, html string) error {
_, err := c.db.Exec(`
INSERT INTO readme_cache (url, html, fetched_at)
VALUES (?, ?, ?)
ON CONFLICT(url) DO UPDATE SET
html = excluded.html,
fetched_at = excluded.fetched_at
`, readmeURL, html, time.Now())
return err
}
// Invalidate removes a README from the cache
func (c *Cache) Invalidate(readmeURL string) error {
_, err := c.db.Exec(`
DELETE FROM readme_cache
WHERE url = ?
`, readmeURL)
return err
}
// Cleanup removes expired entries from the cache
func (c *Cache) Cleanup() error {
cutoff := time.Now().Add(-c.ttl * 2) // Keep for 2x TTL
_, err := c.db.Exec(`
DELETE FROM readme_cache
WHERE fetched_at < ?
`, cutoff)
return err
}