mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-29 13:35:35 +00:00
174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"strings"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
const schema = `
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
did TEXT PRIMARY KEY,
|
|
handle TEXT NOT NULL,
|
|
pds_endpoint TEXT NOT NULL,
|
|
avatar TEXT,
|
|
last_seen TIMESTAMP NOT NULL,
|
|
UNIQUE(handle)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_users_handle ON users(handle);
|
|
|
|
CREATE TABLE IF NOT EXISTS manifests (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
did TEXT NOT NULL,
|
|
repository TEXT NOT NULL,
|
|
digest TEXT NOT NULL,
|
|
hold_endpoint TEXT NOT NULL,
|
|
schema_version INTEGER NOT NULL,
|
|
media_type TEXT NOT NULL,
|
|
config_digest TEXT,
|
|
config_size INTEGER,
|
|
raw_manifest TEXT NOT NULL,
|
|
created_at TIMESTAMP NOT NULL,
|
|
UNIQUE(did, repository, digest),
|
|
FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_manifests_did_repo ON manifests(did, repository);
|
|
CREATE INDEX IF NOT EXISTS idx_manifests_created_at ON manifests(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_manifests_digest ON manifests(digest);
|
|
|
|
CREATE TABLE IF NOT EXISTS layers (
|
|
manifest_id INTEGER NOT NULL,
|
|
digest TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
media_type TEXT NOT NULL,
|
|
layer_index INTEGER NOT NULL,
|
|
PRIMARY KEY(manifest_id, layer_index),
|
|
FOREIGN KEY(manifest_id) REFERENCES manifests(id) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_layers_digest ON layers(digest);
|
|
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
did TEXT NOT NULL,
|
|
repository TEXT NOT NULL,
|
|
tag TEXT NOT NULL,
|
|
digest TEXT NOT NULL,
|
|
created_at TIMESTAMP NOT NULL,
|
|
UNIQUE(did, repository, tag),
|
|
FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_tags_did_repo ON tags(did, repository);
|
|
|
|
CREATE TABLE IF NOT EXISTS firehose_cursor (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
cursor INTEGER NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS backfill_state (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
start_cursor INTEGER NOT NULL,
|
|
current_cursor INTEGER NOT NULL,
|
|
completed BOOLEAN NOT NULL DEFAULT 0,
|
|
updated_at TIMESTAMP NOT NULL
|
|
);
|
|
`
|
|
|
|
// InitDB initializes the SQLite database with the schema
|
|
func InitDB(path string) (*sql.DB, error) {
|
|
db, err := sql.Open("sqlite3", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Enable foreign keys
|
|
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create schema
|
|
if _, err := db.Exec(schema); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Migration: Add avatar column if it doesn't exist
|
|
_, err = db.Exec(`ALTER TABLE users ADD COLUMN avatar TEXT`)
|
|
// Ignore error if column already exists
|
|
if err != nil && !strings.Contains(err.Error(), "duplicate column") {
|
|
// Log but don't fail - column might already exist
|
|
}
|
|
|
|
// Migration: Convert old cdn.bsky.app avatar URLs to imgs.blue
|
|
if err := migrateCDNURLs(db); err != nil {
|
|
// Log but don't fail - not critical
|
|
println("Warning: Failed to migrate CDN URLs:", err.Error())
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// migrateCDNURLs converts old cdn.bsky.app avatar URLs to imgs.blue format
|
|
// Old format: https://cdn.bsky.app/img/avatar/plain/did:plc:abc123/bafkreibxuy73...@jpeg
|
|
// New format: https://imgs.blue/did:plc:abc123/bafkreibxuy73...
|
|
func migrateCDNURLs(db *sql.DB) error {
|
|
// Find all users with cdn.bsky.app avatars
|
|
rows, err := db.Query(`SELECT did, avatar FROM users WHERE avatar LIKE 'https://cdn.bsky.app/%'`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
updates := []struct {
|
|
did string
|
|
newURL string
|
|
}{}
|
|
|
|
for rows.Next() {
|
|
var did, oldURL string
|
|
if err := rows.Scan(&did, &oldURL); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Extract CID from old URL
|
|
// Format: https://cdn.bsky.app/img/avatar/plain/did:plc:abc123/bafkreibxuy73...@jpeg
|
|
parts := strings.Split(oldURL, "/")
|
|
if len(parts) < 7 {
|
|
continue
|
|
}
|
|
|
|
// Get the last part which contains CID@format
|
|
cidPart := parts[len(parts)-1]
|
|
// Strip off @jpeg or @png suffix
|
|
cid := strings.Split(cidPart, "@")[0]
|
|
|
|
// Construct new imgs.blue URL
|
|
newURL := "https://imgs.blue/" + did + "/" + cid
|
|
|
|
updates = append(updates, struct {
|
|
did string
|
|
newURL string
|
|
}{did, newURL})
|
|
}
|
|
|
|
// Update all users
|
|
stmt, err := db.Prepare(`UPDATE users SET avatar = ? WHERE did = ?`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, update := range updates {
|
|
if _, err := stmt.Exec(update.newURL, update.did); err != nil {
|
|
// Log but continue
|
|
println("Warning: Failed to update avatar for", update.did, ":", err.Error())
|
|
}
|
|
}
|
|
|
|
if len(updates) > 0 {
|
|
println("Migrated", len(updates), "avatar URLs from cdn.bsky.app to imgs.blue")
|
|
}
|
|
|
|
return nil
|
|
}
|