112 lines
2.6 KiB
Go
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
|
|
}
|