Inital build
Some checks failed
Automated Container Build / build-and-push (push) Failing after 12s

This commit is contained in:
Elijah 2026-05-22 12:29:43 -07:00
parent fa2be029a2
commit 724d70e58b
3339 changed files with 1075535 additions and 0 deletions

View 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
}