This commit is contained in:
parent
fa2be029a2
commit
724d70e58b
3339 changed files with 1075535 additions and 0 deletions
186
backend/database/database.go
Normal file
186
backend/database/database.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
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`,
|
||||
|
||||
// Default settings
|
||||
`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 ('max_versions', '5')`,
|
||||
`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
|
||||
}
|
||||
Reference in a new issue