Files
forever-files/db/db.go
2024-06-06 20:14:18 -06:00

243 lines
6.4 KiB
Go

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
StoreFilePartition(fileMetadata types.FileMetadata) error
GetTotalSize() (int64, error)
RemovePartitionAssignment() 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,
partitionId TEXT DEFAULT ''
)`,
},
{
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 DeleteDB(appName string) error {
configPath := configdir.LocalConfig(appName)
dbPath := path.Join(configPath, "db", fmt.Sprintf("%v.db", appName))
err := os.Remove(dbPath)
if err != nil {
return fmt.Errorf("error deleting db | %w", err)
}
return 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) StoreFilePartition(fileMetadata types.FileMetadata) error {
query := `UPDATE files SET partitionId = ? WHERE name = ? AND path = ? AND hash = ?`
_, err := d.db.Exec(
query,
fileMetadata.PartitionId,
fileMetadata.Name,
fileMetadata.Path,
fileMetadata.Hash,
)
if err != nil {
return fmt.Errorf("error storing file's partiition | %w", err)
}
return nil
}
func (d *store) RemovePartitionAssignment() error {
query := `UPDATE files SET partitionId = ''`
_, err := d.db.Exec(query)
if err != nil {
return fmt.Errorf("error removing partition assignment | %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, partitionId 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, &file.PartitionId)
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 {
return "", fmt.Errorf("error creating config directory: %w", 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 {
return "", fmt.Errorf("error creating db directory: %w", 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
}