package store import ( "database/sql" "embed" "fmt" "os" "path/filepath" _ "modernc.org/sqlite" ) //go:embed migrations/*.sql var migrationsFS embed.FS type Store struct { DB *sql.DB DataDir string } func Open(dataDir string) (*Store, error) { if err := os.MkdirAll(filepath.Join(dataDir, "artifacts"), 0o755); err != nil { return nil, fmt.Errorf("create data dir: %w", err) } dbPath := filepath.Join(dataDir, "artifacto.db") dsn := "file:" + dbPath + "?_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=busy_timeout(5000)" db, err := sql.Open("sqlite", dsn) if err != nil { return nil, fmt.Errorf("open db: %w", err) } db.SetMaxOpenConns(1) if err := db.Ping(); err != nil { return nil, fmt.Errorf("ping db: %w", err) } s := &Store{DB: db, DataDir: dataDir} if err := s.migrate(); err != nil { return nil, fmt.Errorf("migrate: %w", err) } return s, nil } func (s *Store) Close() error { return s.DB.Close() } func (s *Store) migrate() error { entries, err := migrationsFS.ReadDir("migrations") if err != nil { return err } for _, e := range entries { if e.IsDir() { continue } b, err := migrationsFS.ReadFile("migrations/" + e.Name()) if err != nil { return err } if _, err := s.DB.Exec(string(b)); err != nil { return fmt.Errorf("apply %s: %w", e.Name(), err) } } return nil } func (s *Store) ArtifactPath(slug string) string { return filepath.Join(s.DataDir, "artifacts", slug+".html") } func (s *Store) MetaGet(key string) (string, error) { var v string err := s.DB.QueryRow(`SELECT value FROM meta WHERE key = ?`, key).Scan(&v) if err == sql.ErrNoRows { return "", nil } return v, err } func (s *Store) MetaSet(key, value string) error { _, err := s.DB.Exec(`INSERT INTO meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`, key, value) return err }