package db import ( "database/sql" "fmt" "os" "path" "forever-files/types" "github.com/kirsle/configdir" _ "github.com/mattn/go-sqlite3" ) type DB interface { Close() error Migrate() error StoreFile(fileMetadata types.FileMetadata) error RemoveFile(fileMetadata types.FileMetadata) error } type store struct { db *sql.DB } type Migrations struct { name string query string } var migrations = []Migrations{ { name: "001-sourceFiles", query: `CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, path TEXT NOT NULL, size INTEGER NOT NULL, hash TEXT NOT NULL, modifiedDate TIMESTAMP NOT NULL, backedUp BOOLEAN NOT NULL )`, }, { name: "002-fileUniqueConstraint", query: `CREATE UNIQUE INDEX IF NOT EXISTS file_unique ON files (name, path, hash)`, }, } func NewDB(appName string) (DB, error) { dbPath, err := createDBFileIfNotExist(appName) if err != nil { return nil, fmt.Errorf("error creating db file %w", err) } dbSQL, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, fmt.Errorf("error opening db | %w", err) } return &store{ db: dbSQL, }, nil } func (d *store) StoreFile(fileMetadata types.FileMetadata) error { query := `INSERT INTO files (name, path, size, hash, modifiedDate, backedUp) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (name, path, hash) DO UPDATE SET size = ?, modifiedDate = ?, backedUp = ?` _, err := d.db.Exec( query, fileMetadata.Name, fileMetadata.Path, fileMetadata.Size, fileMetadata.Hash, fileMetadata.ModifiedDate, fileMetadata.BackedUp, fileMetadata.Size, fileMetadata.ModifiedDate, fileMetadata.BackedUp, ) if err != nil { return fmt.Errorf("error storing file metadata | %w", err) } return nil } func (d *store) RemoveFile(fileMetadata types.FileMetadata) error { query := `DELETE FROM files WHERE name = ? AND path = ? AND hash = ?` _, err := d.db.Exec(query, fileMetadata.Name, fileMetadata.Path, fileMetadata.Hash) if err != nil { return fmt.Errorf("error removing file metadata | %w", err) } return nil } func (d *store) Close() error { return d.db.Close() } func (d *store) Migrate() error { // check if migration table exists var migrationsCheck string //goland:noinspection SqlResolve err := d.db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'").Scan(&migrationsCheck) if err != nil { if err == sql.ErrNoRows { _, err := d.db.Exec("CREATE TABLE migrations (name TEXT NOT NULL)") if err != nil { return fmt.Errorf("error creating migrations table | %w", err) } } else { return fmt.Errorf("error checking if migrations table exists | %w", err) } } for _, migration := range migrations { var migrationInHistory string err = d.db.QueryRow("SELECT name FROM migrations WHERE name = ?", migration.name).Scan(&migrationInHistory) if err != nil { if err == sql.ErrNoRows { _, err := d.db.Exec(migration.query) if err != nil { return fmt.Errorf("error running migration: %s | %w", migration.name, err) } _, err = d.db.Exec("INSERT INTO migrations (name) VALUES (?)", migration.name) if err != nil { return fmt.Errorf("error inserting migration: %s into migrations table | %w", migration.name, err) } } else { return fmt.Errorf("error checking if migration: %s has been run | %w", migration.name, err) } } } return nil } func createDBFileIfNotExist(appName string) (string, error) { configPath := configdir.LocalConfig(appName) // set up the config directory err := configdir.MakePath(configPath) if err != nil { panic(err) } dbDirectoryPath := path.Join(configPath, "db") dbPath := path.Join(configPath, "db", fmt.Sprintf("%v.db", appName)) // Set up the database err = configdir.MakePath(dbDirectoryPath) if err != nil { panic(err) } // If the file doesn't exist, create it, or append to the file f, err := os.OpenFile(dbPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return "", fmt.Errorf("error opening file: %v", err) } defer func(f *os.File) { err := f.Close() if err != nil { fmt.Println("error closing file") } }(f) return dbPath, nil }