Files
Artifacto/internal/store/store.go
dtoro c7d7ee287c initial commit: Artifacto v0.1
Self-hosted HTML artifact publisher. Single Go binary, SQLite metadata,
HTML on disk. Features: password-protected artifacts, per-artifact expiration,
admin dashboard with view metrics (privacy-preserving daily-salt visitor hash,
sparklines, 30-day SVG chart, top referrers), rate-limited unlock endpoint.

Packaged as a Docker image (two-stage, CGO_ENABLED=0, pure-Go SQLite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:45:57 +02:00

81 lines
1.8 KiB
Go

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
}