// 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" "fmt" "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 // In production, you'd use proper logging here fmt.Printf("Failed to cache README: %v\n", 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 }