package server import ( "database/sql" "fmt" "log" "time" _ "modernc.org/sqlite" ) // Database handles SQLite operations for the server type Database struct { db *sql.DB } // NewDatabase creates a new database connection and initializes tables func NewDatabase(dbPath string) (*Database, error) { db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %v", err) } // Enable foreign keys if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { db.Close() return nil, fmt.Errorf("failed to enable foreign keys: %v", err) } database := &Database{db: db} if err := database.initTables(); err != nil { db.Close() return nil, fmt.Errorf("failed to initialize tables: %v", err) } return database, nil } // initTables creates the database tables if they don't exist func (d *Database) initTables() error { // Admins table _, err := d.db.Exec(` CREATE TABLE IF NOT EXISTS admins ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'admin', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) if err != nil { return fmt.Errorf("failed to create admins table: %v", err) } // Admin sessions table _, err = d.db.Exec(` CREATE TABLE IF NOT EXISTS admin_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, admin_id INTEGER NOT NULL, token TEXT UNIQUE NOT NULL, expires_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE ) `) if err != nil { return fmt.Errorf("failed to create admin_sessions table: %v", err) } // Clients table _, err = d.db.Exec(` CREATE TABLE IF NOT EXISTS clients ( client_id TEXT PRIMARY KEY, api_key TEXT NOT NULL, max_size_bytes INTEGER NOT NULL DEFAULT 0, dataset TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, storage_type TEXT NOT NULL DEFAULT 's3', keep_hourly INTEGER DEFAULT 24, keep_daily INTEGER DEFAULT 7, keep_weekly INTEGER DEFAULT 4, keep_monthly INTEGER DEFAULT 12, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) if err != nil { return fmt.Errorf("failed to create clients table: %v", err) } // Snapshots table _, err = d.db.Exec(` CREATE TABLE IF NOT EXISTS snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id TEXT NOT NULL, snapshot_id TEXT NOT NULL, timestamp DATETIME NOT NULL, size_bytes INTEGER NOT NULL DEFAULT 0, dataset_name TEXT NOT NULL, storage_key TEXT NOT NULL, storage_type TEXT NOT NULL, compressed INTEGER NOT NULL DEFAULT 0, incremental INTEGER NOT NULL DEFAULT 0, base_snapshot TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE, UNIQUE(client_id, snapshot_id) ) `) if err != nil { return fmt.Errorf("failed to create snapshots table: %v", err) } // Create indexes _, err = d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_snapshots_client_id ON snapshots(client_id)`) if err != nil { return fmt.Errorf("failed to create index: %v", err) } _, err = d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_snapshots_timestamp ON snapshots(timestamp)`) if err != nil { return fmt.Errorf("failed to create timestamp index: %v", err) } _, err = d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_admin_sessions_token ON admin_sessions(token)`) if err != nil { return fmt.Errorf("failed to create admin_sessions index: %v", err) } return nil } // Close closes the database connection func (d *Database) Close() error { return d.db.Close() } // Client operations // GetClient retrieves a client by ID func (d *Database) GetClient(clientID string) (*ClientConfig, error) { client := &ClientConfig{} var enabled int var keepHourly, keepDaily, keepWeekly, keepMonthly sql.NullInt64 query := `SELECT client_id, api_key, max_size_bytes, dataset, enabled, storage_type, keep_hourly, keep_daily, keep_weekly, keep_monthly FROM clients WHERE client_id = ?` err := d.db.QueryRow(query, clientID).Scan( &client.ClientID, &client.APIKey, &client.MaxSizeBytes, &client.Dataset, &enabled, &client.StorageType, &keepHourly, &keepDaily, &keepWeekly, &keepMonthly, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } client.Enabled = enabled == 1 // Set rotation policy if any values are set if keepHourly.Valid || keepDaily.Valid || keepWeekly.Valid || keepMonthly.Valid { client.RotationPolicy = &RotationPolicy{ KeepHourly: int(keepHourly.Int64), KeepDaily: int(keepDaily.Int64), KeepWeekly: int(keepWeekly.Int64), KeepMonthly: int(keepMonthly.Int64), } } return client, nil } // GetAllClients retrieves all clients func (d *Database) GetAllClients() ([]*ClientConfig, error) { query := `SELECT client_id, api_key, max_size_bytes, dataset, enabled, storage_type, keep_hourly, keep_daily, keep_weekly, keep_monthly FROM clients` rows, err := d.db.Query(query) if err != nil { return nil, err } defer rows.Close() var clients []*ClientConfig for rows.Next() { client := &ClientConfig{} var enabled int var keepHourly, keepDaily, keepWeekly, keepMonthly sql.NullInt64 err := rows.Scan( &client.ClientID, &client.APIKey, &client.MaxSizeBytes, &client.Dataset, &enabled, &client.StorageType, &keepHourly, &keepDaily, &keepWeekly, &keepMonthly, ) if err != nil { return nil, err } client.Enabled = enabled == 1 if keepHourly.Valid || keepDaily.Valid || keepWeekly.Valid || keepMonthly.Valid { client.RotationPolicy = &RotationPolicy{ KeepHourly: int(keepHourly.Int64), KeepDaily: int(keepDaily.Int64), KeepWeekly: int(keepWeekly.Int64), KeepMonthly: int(keepMonthly.Int64), } } clients = append(clients, client) } return clients, nil } // SaveClient saves or updates a client func (d *Database) SaveClient(client *ClientConfig) error { var keepHourly, keepDaily, keepWeekly, keepMonthly interface{} if client.RotationPolicy != nil { keepHourly = client.RotationPolicy.KeepHourly keepDaily = client.RotationPolicy.KeepDaily keepWeekly = client.RotationPolicy.KeepWeekly keepMonthly = client.RotationPolicy.KeepMonthly } enabled := 0 if client.Enabled { enabled = 1 } query := `INSERT OR REPLACE INTO clients (client_id, api_key, max_size_bytes, dataset, enabled, storage_type, keep_hourly, keep_daily, keep_weekly, keep_monthly, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)` _, err := d.db.Exec(query, client.ClientID, client.APIKey, client.MaxSizeBytes, client.Dataset, enabled, client.StorageType, keepHourly, keepDaily, keepWeekly, keepMonthly, ) return err } // Snapshot operations // SaveSnapshot saves a new snapshot record func (d *Database) SaveSnapshot(metadata *SnapshotMetadata) error { compressed := 0 if metadata.Compressed { compressed = 1 } incremental := 0 if metadata.Incremental { incremental = 1 } query := `INSERT INTO snapshots (client_id, snapshot_id, timestamp, size_bytes, dataset_name, storage_key, storage_type, compressed, incremental, base_snapshot) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` _, err := d.db.Exec(query, metadata.ClientID, metadata.SnapshotID, metadata.Timestamp, metadata.SizeBytes, metadata.DatasetName, metadata.StorageKey, metadata.StorageType, compressed, incremental, metadata.BaseSnapshot, ) return err } // GetSnapshotsByClient retrieves all snapshots for a client func (d *Database) GetSnapshotsByClient(clientID string) ([]*SnapshotMetadata, error) { query := `SELECT client_id, snapshot_id, timestamp, size_bytes, dataset_name, storage_key, storage_type, compressed, incremental, base_snapshot FROM snapshots WHERE client_id = ? ORDER BY timestamp DESC` rows, err := d.db.Query(query, clientID) if err != nil { return nil, err } defer rows.Close() var snapshots []*SnapshotMetadata for rows.Next() { snap := &SnapshotMetadata{} var compressed, incremental int var baseSnapshot sql.NullString err := rows.Scan( &snap.ClientID, &snap.SnapshotID, &snap.Timestamp, &snap.SizeBytes, &snap.DatasetName, &snap.StorageKey, &snap.StorageType, &compressed, &incremental, &baseSnapshot, ) if err != nil { return nil, err } snap.Compressed = compressed == 1 snap.Incremental = incremental == 1 if baseSnapshot.Valid { snap.BaseSnapshot = baseSnapshot.String } snapshots = append(snapshots, snap) } return snapshots, nil } // GetClientUsage calculates total storage used by a client func (d *Database) GetClientUsage(clientID string) (int64, error) { var total sql.NullInt64 err := d.db.QueryRow(`SELECT SUM(size_bytes) FROM snapshots WHERE client_id = ?`, clientID).Scan(&total) if err != nil { return 0, err } if !total.Valid { return 0, nil } return total.Int64, nil } // DeleteSnapshot deletes a snapshot record func (d *Database) DeleteSnapshot(clientID, snapshotID string) error { _, err := d.db.Exec(`DELETE FROM snapshots WHERE client_id = ? AND snapshot_id = ?`, clientID, snapshotID) return err } // GetOldestSnapshots gets the oldest snapshots for a client (for rotation) func (d *Database) GetOldestSnapshots(clientID string, limit int) ([]*SnapshotMetadata, error) { query := `SELECT client_id, snapshot_id, timestamp, size_bytes, dataset_name, storage_key, storage_type, compressed, incremental, base_snapshot FROM snapshots WHERE client_id = ? ORDER BY timestamp ASC LIMIT ?` rows, err := d.db.Query(query, clientID, limit) if err != nil { return nil, err } defer rows.Close() var snapshots []*SnapshotMetadata for rows.Next() { snap := &SnapshotMetadata{} var compressed, incremental int var baseSnapshot sql.NullString err := rows.Scan( &snap.ClientID, &snap.SnapshotID, &snap.Timestamp, &snap.SizeBytes, &snap.DatasetName, &snap.StorageKey, &snap.StorageType, &compressed, &incremental, &baseSnapshot, ) if err != nil { return nil, err } snap.Compressed = compressed == 1 snap.Incremental = incremental == 1 if baseSnapshot.Valid { snap.BaseSnapshot = baseSnapshot.String } snapshots = append(snapshots, snap) } return snapshots, nil } // CreateDefaultClient creates a default client if none exists func (d *Database) CreateDefaultClient() error { clients, err := d.GetAllClients() if err != nil { return err } if len(clients) > 0 { return nil } log.Println("No clients found, creating default client 'client1'") defaultClient := &ClientConfig{ ClientID: "client1", APIKey: hashAPIKey("secret123"), MaxSizeBytes: 100 * 1024 * 1024 * 1024, // 100GB Dataset: "backup/client1", Enabled: true, StorageType: "s3", RotationPolicy: &RotationPolicy{ KeepHourly: 24, KeepDaily: 7, KeepWeekly: 4, KeepMonthly: 12, }, } return d.SaveClient(defaultClient) } // GetSnapshotByID retrieves a specific snapshot func (d *Database) GetSnapshotByID(clientID, snapshotID string) (*SnapshotMetadata, error) { snap := &SnapshotMetadata{} var compressed, incremental int var baseSnapshot sql.NullString query := `SELECT client_id, snapshot_id, timestamp, size_bytes, dataset_name, storage_key, storage_type, compressed, incremental, base_snapshot FROM snapshots WHERE client_id = ? AND snapshot_id = ?` err := d.db.QueryRow(query, clientID, snapshotID).Scan( &snap.ClientID, &snap.SnapshotID, &snap.Timestamp, &snap.SizeBytes, &snap.DatasetName, &snap.StorageKey, &snap.StorageType, &compressed, &incremental, &baseSnapshot, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } snap.Compressed = compressed == 1 snap.Incremental = incremental == 1 if baseSnapshot.Valid { snap.BaseSnapshot = baseSnapshot.String } return snap, nil } // GetAllSnapshots retrieves all snapshots (for admin purposes) func (d *Database) GetAllSnapshots() ([]*SnapshotMetadata, error) { query := `SELECT client_id, snapshot_id, timestamp, size_bytes, dataset_name, storage_key, storage_type, compressed, incremental, base_snapshot FROM snapshots ORDER BY timestamp DESC` rows, err := d.db.Query(query) if err != nil { return nil, err } defer rows.Close() var snapshots []*SnapshotMetadata for rows.Next() { snap := &SnapshotMetadata{} var compressed, incremental int var baseSnapshot sql.NullString err := rows.Scan( &snap.ClientID, &snap.SnapshotID, &snap.Timestamp, &snap.SizeBytes, &snap.DatasetName, &snap.StorageKey, &snap.StorageType, &compressed, &incremental, &baseSnapshot, ) if err != nil { return nil, err } snap.Compressed = compressed == 1 snap.Incremental = incremental == 1 if baseSnapshot.Valid { snap.BaseSnapshot = baseSnapshot.String } snapshots = append(snapshots, snap) } return snapshots, nil } // GetTotalSnapshotCount returns the total number of snapshots func (d *Database) GetTotalSnapshotCount() (int, error) { var count int err := d.db.QueryRow(`SELECT COUNT(*) FROM snapshots`).Scan(&count) return count, err } // GetTotalStorageUsed returns the total storage used across all clients func (d *Database) GetTotalStorageUsed() (int64, error) { var total sql.NullInt64 err := d.db.QueryRow(`SELECT SUM(size_bytes) FROM snapshots`).Scan(&total) if err != nil { return 0, err } if !total.Valid { return 0, nil } return total.Int64, nil } // UpdateSnapshotTimestamp updates the timestamp of a snapshot (for rotation tracking) func (d *Database) UpdateSnapshotTimestamp(clientID, snapshotID string, timestamp time.Time) error { _, err := d.db.Exec(`UPDATE snapshots SET timestamp = ? WHERE client_id = ? AND snapshot_id = ?`, timestamp, clientID, snapshotID) return err } // Admin operations // Admin represents an admin user type Admin struct { ID int Username string PasswordHash string Role string CreatedAt time.Time UpdatedAt time.Time } // AdminSession represents an admin session type AdminSession struct { ID int AdminID int Token string ExpiresAt time.Time CreatedAt time.Time } // CreateAdmin creates a new admin user func (d *Database) CreateAdmin(username, passwordHash, role string) error { _, err := d.db.Exec(` INSERT INTO admins (username, password_hash, role) VALUES (?, ?, ?) `, username, passwordHash, role) return err } // GetAdminByUsername retrieves an admin by username func (d *Database) GetAdminByUsername(username string) (*Admin, error) { admin := &Admin{} err := d.db.QueryRow(` SELECT id, username, password_hash, role, created_at, updated_at FROM admins WHERE username = ? `, username).Scan(&admin.ID, &admin.Username, &admin.PasswordHash, &admin.Role, &admin.CreatedAt, &admin.UpdatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return admin, nil } // GetAdminByID retrieves an admin by ID func (d *Database) GetAdminByID(id int) (*Admin, error) { admin := &Admin{} err := d.db.QueryRow(` SELECT id, username, password_hash, role, created_at, updated_at FROM admins WHERE id = ? `, id).Scan(&admin.ID, &admin.Username, &admin.PasswordHash, &admin.Role, &admin.CreatedAt, &admin.UpdatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return admin, nil } // CreateSession creates a new admin session func (d *Database) CreateSession(adminID int, token string, expiresAt time.Time) error { _, err := d.db.Exec(` INSERT INTO admin_sessions (admin_id, token, expires_at) VALUES (?, ?, ?) `, adminID, token, expiresAt) return err } // GetSessionByToken retrieves a session by token func (d *Database) GetSessionByToken(token string) (*AdminSession, error) { session := &AdminSession{} err := d.db.QueryRow(` SELECT id, admin_id, token, expires_at, created_at FROM admin_sessions WHERE token = ? `, token).Scan(&session.ID, &session.AdminID, &session.Token, &session.ExpiresAt, &session.CreatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return session, nil } // DeleteSession deletes a session (logout) func (d *Database) DeleteSession(token string) error { _, err := d.db.Exec(`DELETE FROM admin_sessions WHERE token = ?`, token) return err } // CleanExpiredSessions removes all expired sessions func (d *Database) CleanExpiredSessions() error { _, err := d.db.Exec(`DELETE FROM admin_sessions WHERE expires_at < CURRENT_TIMESTAMP`) return err } // CreateDefaultAdmin creates a default admin if none exists func (d *Database) CreateDefaultAdmin() error { var count int err := d.db.QueryRow(`SELECT COUNT(*) FROM admins`).Scan(&count) if err != nil { return err } if count > 0 { return nil } log.Println("No admins found, creating default admin 'admin'") // Default password: admin123 defaultPasswordHash := hashAPIKey("admin123") return d.CreateAdmin("admin", defaultPasswordHash, "admin") } // DeleteClient deletes a client and all its snapshots func (d *Database) DeleteClient(clientID string) error { _, err := d.db.Exec(`DELETE FROM clients WHERE client_id = ?`, clientID) return err } // GetAllAdmins retrieves all admins func (d *Database) GetAllAdmins() ([]*Admin, error) { rows, err := d.db.Query(` SELECT id, username, password_hash, role, created_at, updated_at FROM admins ORDER BY id `) if err != nil { return nil, err } defer rows.Close() var admins []*Admin for rows.Next() { admin := &Admin{} err := rows.Scan(&admin.ID, &admin.Username, &admin.PasswordHash, &admin.Role, &admin.CreatedAt, &admin.UpdatedAt) if err != nil { return nil, err } admins = append(admins, admin) } return admins, nil } // DeleteAdmin deletes an admin by ID func (d *Database) DeleteAdmin(id int) error { _, err := d.db.Exec(`DELETE FROM admins WHERE id = ?`, id) return err } // UpdateAdminPassword updates an admin's password func (d *Database) UpdateAdminPassword(id int, passwordHash string) error { _, err := d.db.Exec(`UPDATE admins SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, passwordHash, id) return err }