package main import ( "fmt" "github.com/d2fn/sumi/internal/ids" "github.com/d2fn/sumi/internal/ora" "github.com/go-git/go-git/v6" "log" //"github.com/go-git/go-git/v5/plumbing" "database/sql" "github.com/gen2brain/raylib-go/raylib" "github.com/go-git/go-git/v6/plumbing/object" _ "modernc.org/sqlite" // pure Go, Nix-friendly "os" "path/filepath" "time" ) type Storage struct { repoRoot string snapshotsDir string gen *ids.Generator db *sql.DB log *log.Logger } func NewStorage(snapshotsDir string) (*Storage, error) { log := log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) gen, _ := ids.NewGenerator(82) db, err := OpenDB(filepath.Join(snapshotsDir, "snapshots.db"), log) if err != nil { return nil, err } s := Storage{ repoRoot: ".", snapshotsDir: snapshotsDir, gen: gen, db: db, log: log, } return &s, nil } func OpenDB(path string, log *log.Logger) (*sql.DB, error) { log.Printf("Opening sqlite db") first := false if _, err := os.Stat(path); os.IsNotExist(err) { log.Printf("Database not found, initializing") first = true } db, err := sql.Open("sqlite", path) if err != nil { log.Printf("Error opening database at %s", path) return nil, err } if first { log.Printf("Initializing empty db with schema", path) if err := initSchema(db); err != nil { db.Close() return nil, err } log.Printf("Error initializing schema: %v", err) } return db, nil } func initSchema(db *sql.DB) error { schema, err := os.ReadFile("db/schema.sql") if err != nil { return err } tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec(string(schema)); err != nil { return err } return tx.Commit() } func (s *Storage) Save(capture *SketchCapture) (string, error) { id, _ := s.gen.Next() flakeId := ids.Base62Encode(id) path := filepath.Join(s.snapshotsDir, flakeId) os.MkdirAll(path, 0755) hash, branch, committed, err := s.SaveToGit(flakeId) if err != nil { s.log.Printf("Error getting working tree in a known clean state: %v", err) } else { if committed { s.log.Printf("Created commit %s on %s for snapshot %s", hash, branch, flakeId) } else { s.log.Printf("Referencing commit %s on %s for snapshot %s", hash, branch, flakeId) } } err = s.SaveToDb(id, flakeId, branch, hash, path, committed) if err != nil { s.log.Printf("Error writing to db: %v\n", err) } // wysiwyg at screen res img := rl.LoadImageFromScreen() defer rl.UnloadImage(img) snapshotPng := filepath.Join(path, fmt.Sprintf("%s-screen.png", flakeId)) rl.ExportImage(*img, snapshotPng) // capture full res compsite compositePng := filepath.Join(path, fmt.Sprintf("%s-final.png", flakeId)) rl.ExportImage(*capture.compositeImage, compositePng) // capture full res layer oraLayers := make([]ora.ORALayer, len(capture.layerToolsOrdered)) for i, layerTools := range capture.layerToolsOrdered { filename := fmt.Sprintf("%s-%03d.png", flakeId, i) layerPng := filepath.Join(path, "data", filename) rl.ExportImage(*layerTools.capture, layerPng) opacity := float32(layerTools.config.a) / 255.0 oraLayers[i] = ora.ORALayer{ Name: layerTools.name, Filename: filename, Visible: layerTools.config.visible, Opacity: opacity, Blend: "svg:src-over", } } oraPath := filepath.Join(path, fmt.Sprintf("%s-layers.ora", flakeId)) ora.WriteORA(oraPath, int(capture.width), int(capture.height), oraLayers, func(name string) ([]byte, error) { return os.ReadFile(filepath.Join(path, "data", name)) }) s.log.Printf("Saved snapshot to %s\n", path) return path, nil } func (s *Storage) SaveToGit(flakeId string) (string, string, bool, error) { s.log.Printf("Checking git status...\n") return CommitAllIfDirty(s.repoRoot, "automated snapshot", s.log) } func (s *Storage) SaveToDb(id uint64, flakeId string, branch string, hash string, path string, committed bool) error { _, err := s.db.Exec(` INSERT INTO snapshots (id, sid, created_at, branch, git_hash, committed, path) VALUES (?, ?, ?, ?, ?, ?, ?) `, id, flakeId, time.Now().UnixMilli(), branch, hash, committed, path, ) if err != nil { s.log.Printf("Error inserting snapshot row into db: %v\n", err) } return err } func HeadHash(repoPath string) (string, error) { r, err := git.PlainOpen(repoPath) if err != nil { return "", err } ref, err := r.Head() if err != nil { return "", err } return ref.Hash().String(), nil } func IsDirty(repoPath string) (bool, error) { r, err := git.PlainOpen(repoPath) if err != nil { return false, err } wt, err := r.Worktree() if err != nil { return false, err } status, err := wt.Status() if err != nil { return false, err } return !status.IsClean(), nil } func CommitAllIfDirty(repoPath, message string, log *log.Logger) (commitHash string, branch string, committed bool, err error) { r, err := git.PlainOpen(repoPath) if err != nil { log.Printf("Error opening git repo") return } // Determine branch (may be empty if detached) ref, err := r.Head() if err != nil { log.Printf("Error determining head commit") return } if ref.Name().IsBranch() { branch = ref.Name().Short() } wt, err := r.Worktree() if err != nil { log.Printf("Error getting worktree state") return } status, err := wt.Status() if err != nil { log.Printf("Error getting worktree status") return } if status.IsClean() { hash, err := HeadHash(repoPath) if err != nil { log.Printf("Repo was clean but there was an error checking the commit for HEAD: %v", err) } return hash, branch, false, nil } // Stage everything (git add -A) if err = wt.AddWithOptions(&git.AddOptions{All: true}); err != nil { log.Printf("Error adding git changes to index") return } hash, err := wt.Commit(message, &git.CommitOptions{ Author: &object.Signature{ Name: "sumi", Email: "sumi@local", When: time.Now(), }, }) if err != nil { log.Printf("Error creating commit") return } return hash.String(), branch, true, nil }