This repository has been archived on 2026-07-15. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
drive/backend/database/database.go
Elijah 71ba029fbc
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m18s
feat: implement trash auto-purge, settings layout update, larger folder icons, and legacy version cleanup
2026-05-22 18:41:16 -07:00

184 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package database
import (
"database/sql"
"fmt"
"os"
"path/filepath"
_ "github.com/mattn/go-sqlite3"
)
type DB struct {
*sql.DB
}
// New opens (or creates) the SQLite database, enables WAL mode, and runs migrations.
func New(dbPath string) (*DB, error) {
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("create db directory: %w", err)
}
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on")
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("ping database: %w", err)
}
// Enforce WAL mode explicitly
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
return nil, fmt.Errorf("set WAL mode: %w", err)
}
wrapped := &DB{db}
if err := wrapped.migrate(); err != nil {
return nil, fmt.Errorf("run migrations: %w", err)
}
return wrapped, nil
}
func (db *DB) migrate() error {
migrations := []string{
// Users table single user now, designed for future multi-user
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
totp_secret TEXT DEFAULT '',
totp_enabled INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// Files metadata table
`CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
is_dir INTEGER DEFAULT 0,
size INTEGER DEFAULT 0,
mime_type TEXT DEFAULT '',
checksum TEXT DEFAULT '',
is_pinned INTEGER DEFAULT 0,
is_trashed INTEGER DEFAULT 0,
trashed_at DATETIME,
original_path TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// Index for fast trash and pin queries
`CREATE INDEX IF NOT EXISTS idx_files_trashed ON files(is_trashed)`,
`CREATE INDEX IF NOT EXISTS idx_files_pinned ON files(is_pinned)`,
`CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)`,
// File versions table
`CREATE TABLE IF NOT EXISTS file_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL,
version_number INTEGER NOT NULL,
version_path TEXT NOT NULL,
size INTEGER DEFAULT 0,
checksum TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// Share links table
`CREATE TABLE IF NOT EXISTS share_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
target_path TEXT NOT NULL,
is_dir INTEGER DEFAULT 0,
password_hash TEXT DEFAULT '',
expires_at DATETIME,
max_downloads INTEGER DEFAULT 0,
download_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_share_links_token ON share_links(token)`,
// Audit log table
`CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action TEXT NOT NULL,
details TEXT DEFAULT '',
ip_address TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// Settings table (key-value)
`CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT DEFAULT ''
)`,
// FTS5 virtual table for file search
`CREATE VIRTUAL TABLE IF NOT EXISTS files_fts USING fts5(
name,
path,
content=files,
content_rowid=id
)`,
// Triggers to keep FTS5 in sync with the files table
`CREATE TRIGGER IF NOT EXISTS files_ai AFTER INSERT ON files BEGIN
INSERT INTO files_fts(rowid, name, path) VALUES (new.id, new.name, new.path);
END`,
`CREATE TRIGGER IF NOT EXISTS files_ad AFTER DELETE ON files BEGIN
INSERT INTO files_fts(files_fts, rowid, name, path) VALUES('delete', old.id, old.name, old.path);
END`,
`CREATE TRIGGER IF NOT EXISTS files_au AFTER UPDATE ON files BEGIN
INSERT INTO files_fts(files_fts, rowid, name, path) VALUES('delete', old.id, old.name, old.path);
INSERT INTO files_fts(rowid, name, path) VALUES (new.id, new.name, new.path);
END`,
`INSERT OR IGNORE INTO settings (key, value) VALUES ('theme', 'dark')`,
`INSERT OR IGNORE INTO settings (key, value) VALUES ('thumbnail_images', 'true')`,
`INSERT OR IGNORE INTO settings (key, value) VALUES ('thumbnail_videos', 'true')`,
`INSERT OR IGNORE INTO settings (key, value) VALUES ('trash_auto_purge_days', '30')`,
}
for _, m := range migrations {
if _, err := db.Exec(m); err != nil {
return fmt.Errorf("migration error: %w\nSQL: %s", err, m)
}
}
return nil
}
// IsSetupComplete checks if any user account exists.
func (db *DB) IsSetupComplete() (bool, error) {
var count int
err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
// GetSetting retrieves a setting value by key.
func (db *DB) GetSetting(key string) (string, error) {
var value string
err := db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value)
return value, err
}
// SetSetting upserts a setting.
func (db *DB) SetSetting(key, value string) error {
_, err := db.Exec("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", key, value)
return err
}
// AddAuditLog appends an entry to the audit log.
func (db *DB) AddAuditLog(action, details, ip string) error {
_, err := db.Exec("INSERT INTO audit_log (action, details, ip_address) VALUES (?, ?, ?)", action, details, ip)
return err
}