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 GetTotalSize() (int64, error) GetFileCount() (int64, error) GetFiles() ([]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) GetTotalSize() (int64, error) { var size int64 query := `SELECT SUM(size) FROM files` err := d.db.QueryRow(query).Scan(&size) if err != nil { return 0, fmt.Errorf("error getting size | %w", err) } return size, nil } func (d *store) GetFileCount() (int64, error) { var count int64 query := `SELECT COUNT(*) FROM files` err := d.db.QueryRow(query).Scan(&count) if err != nil { return 0, fmt.Errorf("error getting count | %w", err) } return count, nil } func (d *store) GetFiles() ([]types.FileMetadata, error) { var files []types.FileMetadata query := `SELECT name, path, size, hash, modifiedDate, backedUp FROM files order by path, name` rows, err := d.db.Query(query) if err != nil { return nil, fmt.Errorf("error getting files | %w", err) } defer rows.Close() for rows.Next() { var file types.FileMetadata err := rows.Scan(&file.Name, &file.Path, &file.Size, &file.Hash, &file.ModifiedDate, &file.BackedUp) if err != nil { return nil, fmt.Errorf("error scanning file | %w", err) } files = append(files, file) } return files, 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 }