ficsit-cli-flake/cli/localregistry/registry.go

198 lines
6 KiB
Go
Raw Normal View History

package localregistry
import (
"database/sql"
"fmt"
"log/slog"
"os"
"path/filepath"
"sync"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
// sqlite driver
_ "modernc.org/sqlite"
)
var db *sql.DB
var dbWriteMutex = sync.Mutex{}
func Init() error {
dbPath := filepath.Join(viper.GetString("cache-dir"), "registry.db")
err := os.MkdirAll(filepath.Dir(dbPath), 0o777)
if err != nil {
return fmt.Errorf("failed to create local registry directory: %w", err)
}
db, err = sql.Open("sqlite", dbPath)
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
// Set pragmas here because modernc.org/sqlite does not support them in the connection string
_, err = db.Exec(`
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
CREATE TABLE IF NOT EXISTS "versions" (
"id" TEXT NOT NULL PRIMARY KEY,
"mod_reference" TEXT NOT NULL,
"version" TEXT NOT NULL,
"game_version" TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS "mod_reference" ON "versions" ("mod_reference");
CREATE UNIQUE INDEX IF NOT EXISTS "mod_version" ON "versions" ("mod_reference", "version");
CREATE TABLE IF NOT EXISTS "dependencies" (
"version_id" TEXT NOT NULL,
"dependency" TEXT NOT NULL,
"condition" TEXT NOT NULL,
"optional" INT NOT NULL,
FOREIGN KEY ("version_id") REFERENCES "versions" ("id") ON DELETE CASCADE,
PRIMARY KEY ("version_id", "dependency")
);
CREATE TABLE IF NOT EXISTS "targets" (
"version_id" TEXT NOT NULL,
"target_name" TEXT NOT NULL,
"link" TEXT NOT NULL,
"hash" TEXT NOT NULL,
"size" INT NOT NULL,
FOREIGN KEY ("version_id") REFERENCES "versions" ("id") ON DELETE CASCADE,
PRIMARY KEY ("version_id", "target_name")
);
`)
if err != nil {
return fmt.Errorf("failed to setup tables: %w", err)
}
return nil
}
func Add(modReference string, modVersions []resolver.ModVersion) {
dbWriteMutex.Lock()
defer dbWriteMutex.Unlock()
tx, err := db.Begin()
if err != nil {
slog.Error("failed to start local registry transaction", slog.Any("err", err))
return
}
// In case the transaction is not committed, revert and release
defer tx.Rollback() //nolint:errcheck
_, err = tx.Exec("DELETE FROM versions WHERE mod_reference = ?", modReference)
if err != nil {
slog.Error("failed to delete existing mod versions from local registry", slog.Any("err", err))
return
}
for _, modVersion := range modVersions {
l := slog.With(slog.String("mod", modReference), slog.String("version", modVersion.Version))
_, err = tx.Exec("INSERT INTO versions (id, mod_reference, version, game_version) VALUES (?, ?, ?, ?)", modVersion.ID, modReference, modVersion.Version, modVersion.GameVersion)
if err != nil {
l.Error("failed to insert mod version into local registry", slog.Any("err", err))
return
}
for _, dependency := range modVersion.Dependencies {
_, err = tx.Exec("INSERT INTO dependencies (version_id, dependency, condition, optional) VALUES (?, ?, ?, ?)", modVersion.ID, dependency.ModID, dependency.Condition, dependency.Optional)
if err != nil {
l.Error("failed to insert dependency into local registry", slog.String("dependency", dependency.ModID), slog.Any("err", err))
return
}
}
for _, target := range modVersion.Targets {
_, err = tx.Exec("INSERT INTO targets (version_id, target_name, link, hash, size) VALUES (?, ?, ?, ?, ?)", modVersion.ID, target.TargetName, target.Link, target.Hash, target.Size)
if err != nil {
l.Error("failed to insert target into local registry", slog.Any("target", target.TargetName), slog.Any("err", err))
return
}
}
}
err = tx.Commit()
if err != nil {
slog.Error("failed to commit local registry transaction", slog.Any("err", err))
return
}
}
func GetModVersions(modReference string) ([]resolver.ModVersion, error) {
versionRows, err := db.Query("SELECT id, version, game_version FROM versions WHERE mod_reference = ?", modReference)
if err != nil {
return nil, fmt.Errorf("failed to fetch mod versions from local registry: %w", err)
}
defer versionRows.Close()
var versions []resolver.ModVersion
for versionRows.Next() {
var version resolver.ModVersion
err = versionRows.Scan(&version.ID, &version.Version, &version.GameVersion)
if err != nil {
return nil, fmt.Errorf("failed to scan version row: %w", err)
}
dependencies, err := getVersionDependencies(version.ID)
if err != nil {
return nil, err
}
version.Dependencies = dependencies
targets, err := getVersionTargets(version.ID)
if err != nil {
return nil, err
}
version.Targets = targets
versions = append(versions, version)
}
return versions, nil
}
func getVersionDependencies(versionID string) ([]resolver.Dependency, error) {
var dependencies []resolver.Dependency
dependencyRows, err := db.Query("SELECT dependency, condition, optional FROM dependencies WHERE version_id = ?", versionID)
if err != nil {
return nil, fmt.Errorf("failed to fetch dependencies from local registry: %w", err)
}
defer dependencyRows.Close()
for dependencyRows.Next() {
var dependency resolver.Dependency
err = dependencyRows.Scan(&dependency.ModID, &dependency.Condition, &dependency.Optional)
if err != nil {
return nil, fmt.Errorf("failed to scan dependency row: %w", err)
}
dependencies = append(dependencies, dependency)
}
return dependencies, nil
}
func getVersionTargets(versionID string) ([]resolver.Target, error) {
var targets []resolver.Target
targetRows, err := db.Query("SELECT target_name, link, hash, size FROM targets WHERE version_id = ?", versionID)
if err != nil {
return nil, fmt.Errorf("failed to fetch targets from local registry: %w", err)
}
defer targetRows.Close()
for targetRows.Next() {
var target resolver.Target
err = targetRows.Scan(&target.TargetName, &target.Link, &target.Hash, &target.Size)
if err != nil {
return nil, fmt.Errorf("failed to scan target row: %w", err)
}
targets = append(targets, target)
}
return targets, nil
}