add admin panel

This commit is contained in:
2026-02-13 19:44:00 +01:00
parent fb9bb2fc82
commit c672fccce3
11 changed files with 2185 additions and 255 deletions

1212
internal/server/admin.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,7 @@ type Config struct {
S3UseSSL bool
S3Enabled bool // Enable/disable S3 backend
BaseDataset string
ConfigFile string
MetadataFile string
DatabasePath string // Path to SQLite database
Port string
}
@@ -42,8 +41,7 @@ func LoadConfig() *Config {
S3UseSSL: getEnv("S3_USE_SSL", "true") != "false",
S3Enabled: s3Enabled,
BaseDataset: getEnv("ZFS_BASE_DATASET", "backup"),
ConfigFile: getEnv("CONFIG_FILE", "clients.json"),
MetadataFile: getEnv("METADATA_FILE", "metadata.json"),
DatabasePath: getEnv("DATABASE_PATH", "zfs-backup.db"),
Port: getEnv("PORT", "8080"),
}
}

654
internal/server/database.go Normal file
View File

@@ -0,0 +1,654 @@
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
}

View File

@@ -9,131 +9,60 @@ import (
"io"
"log"
"net/http"
"os"
"sort"
"strings"
"sync"
"time"
)
// Server manages snapshots from multiple clients with S3 support
type Server struct {
clients map[string]*ClientConfig
snapshots map[string][]*SnapshotMetadata
mu sync.RWMutex
db *Database
s3Backend *S3Backend
localBackend *LocalBackend
metadataFile string
configFile string
}
// New creates a new snapshot server
func New(configFile, metadataFile string, s3Backend *S3Backend, localBackend *LocalBackend) *Server {
// New creates a new snapshot server with SQLite database
func New(dbPath string, s3Backend *S3Backend, localBackend *LocalBackend) (*Server, error) {
db, err := NewDatabase(dbPath)
if err != nil {
return nil, fmt.Errorf("failed to initialize database: %v", err)
}
// Create default client if none exists
if err := db.CreateDefaultClient(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to create default client: %v", err)
}
// Create default admin if none exists
if err := db.CreateDefaultAdmin(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to create default admin: %v", err)
}
// Clean expired sessions
db.CleanExpiredSessions()
s := &Server{
clients: make(map[string]*ClientConfig),
snapshots: make(map[string][]*SnapshotMetadata),
db: db,
s3Backend: s3Backend,
localBackend: localBackend,
metadataFile: metadataFile,
configFile: configFile,
}
s.loadConfig()
s.loadMetadata()
return s
return s, nil
}
func (s *Server) loadConfig() {
data, err := os.ReadFile(s.configFile)
if err != nil {
log.Printf("Warning: Could not read config file: %v", err)
// Create default config
s.clients["client1"] = &ClientConfig{
ClientID: "client1",
APIKey: hashAPIKey("secret123"),
MaxSizeBytes: 100 * 1024 * 1024 * 1024,
Dataset: "backup/client1",
Enabled: true,
StorageType: "s3",
}
s.saveConfig()
return
}
var clients []*ClientConfig
if err := json.Unmarshal(data, &clients); err != nil {
log.Printf("Error parsing config: %v", err)
return
}
for _, client := range clients {
s.clients[client.ClientID] = client
}
log.Printf("Loaded %d client configurations", len(s.clients))
}
func (s *Server) saveConfig() {
s.mu.RLock()
defer s.mu.RUnlock()
var clients []*ClientConfig
for _, client := range s.clients {
clients = append(clients, client)
}
data, err := json.MarshalIndent(clients, "", " ")
if err != nil {
log.Printf("Error marshaling config: %v", err)
return
}
if err := os.WriteFile(s.configFile, data, 0600); err != nil {
log.Printf("Error writing config: %v", err)
}
}
func (s *Server) loadMetadata() {
data, err := os.ReadFile(s.metadataFile)
if err != nil {
log.Printf("No existing metadata file, starting fresh")
return
}
if err := json.Unmarshal(data, &s.snapshots); err != nil {
log.Printf("Error parsing metadata: %v", err)
return
}
totalSnapshots := 0
for _, snaps := range s.snapshots {
totalSnapshots += len(snaps)
}
log.Printf("Loaded metadata for %d snapshots", totalSnapshots)
}
func (s *Server) saveMetadata() {
s.mu.RLock()
defer s.mu.RUnlock()
data, err := json.MarshalIndent(s.snapshots, "", " ")
if err != nil {
log.Printf("Error marshaling metadata: %v", err)
return
}
if err := os.WriteFile(s.metadataFile, data, 0600); err != nil {
log.Printf("Error writing metadata: %v", err)
}
// Close closes the database connection
func (s *Server) Close() error {
return s.db.Close()
}
func (s *Server) authenticate(clientID, apiKey string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
client, err := s.db.GetClient(clientID)
if err != nil || client == nil {
return false
}
client, exists := s.clients[clientID]
if !exists || !client.Enabled {
if !client.Enabled {
return false
}
@@ -141,22 +70,13 @@ func (s *Server) authenticate(clientID, apiKey string) bool {
}
func (s *Server) getClientUsage(clientID string) int64 {
s.mu.RLock()
defer s.mu.RUnlock()
var total int64
for _, snap := range s.snapshots[clientID] {
total += snap.SizeBytes
}
return total
usage, _ := s.db.GetClientUsage(clientID)
return usage
}
func (s *Server) canAcceptSnapshot(clientID string, estimatedSize int64) (bool, string) {
s.mu.RLock()
defer s.mu.RUnlock()
client, exists := s.clients[clientID]
if !exists {
client, err := s.db.GetClient(clientID)
if err != nil || client == nil {
return false, "Client not found"
}
@@ -171,74 +91,69 @@ func (s *Server) canAcceptSnapshot(clientID string, estimatedSize int64) (bool,
}
func (s *Server) rotateSnapshots(clientID string) (int, int64) {
// First pass: collect snapshots to delete while holding lock
s.mu.Lock()
client, exists := s.clients[clientID]
if !exists {
s.mu.Unlock()
client, err := s.db.GetClient(clientID)
if err != nil || client == nil {
return 0, 0
}
snapshots := s.snapshots[clientID]
if len(snapshots) == 0 {
s.mu.Unlock()
currentUsage := s.getClientUsage(clientID)
if currentUsage <= client.MaxSizeBytes {
return 0, 0
}
// Sort by timestamp (oldest first)
sort.Slice(snapshots, func(i, j int) bool {
return snapshots[i].Timestamp.Before(snapshots[j].Timestamp)
})
// Calculate how many bytes we need to free
bytesToFree := currentUsage - client.MaxSizeBytes
var deletedCount int
var reclaimedBytes int64
currentUsage := int64(0)
for _, snap := range snapshots {
currentUsage += snap.SizeBytes
// Get oldest snapshots and delete until we're under quota
snapshots, err := s.db.GetOldestSnapshots(clientID, 100) // Get up to 100 oldest
if err != nil {
log.Printf("Error getting oldest snapshots: %v", err)
return 0, 0
}
// Collect snapshots to delete
var toDelete []*SnapshotMetadata
for currentUsage > client.MaxSizeBytes && len(snapshots) > 1 {
oldest := snapshots[0]
toDelete = append(toDelete, oldest)
currentUsage -= oldest.SizeBytes
snapshots = snapshots[1:]
for _, snap := range snapshots {
if reclaimedBytes >= bytesToFree {
break
}
toDelete = append(toDelete, snap)
reclaimedBytes += snap.SizeBytes
}
// Update state before I/O
s.snapshots[clientID] = snapshots
s.mu.Unlock()
if len(toDelete) == 0 {
return 0, 0
}
// Select appropriate backend
var backend StorageBackend
if client.StorageType == "s3" {
if s.s3Backend != nil {
backend = s.s3Backend
} else {
} else if s.localBackend != nil {
backend = s.localBackend
} else {
log.Printf("No storage backend available for rotation")
return 0, 0
}
// Second pass: delete without holding lock
deletedCount := 0
reclaimedBytes := int64(0)
// Delete snapshots
ctx := context.Background()
for _, snap := range toDelete {
if err := backend.Delete(ctx, snap.StorageKey); err != nil {
log.Printf("Error deleting snapshot %s: %v", snap.StorageKey, err)
continue
}
if err := s.db.DeleteSnapshot(clientID, snap.SnapshotID); err != nil {
log.Printf("Error deleting snapshot record %s: %v", snap.SnapshotID, err)
continue
}
log.Printf("Rotated out snapshot: %s (freed %d bytes)", snap.StorageKey, snap.SizeBytes)
reclaimedBytes += snap.SizeBytes
deletedCount++
}
// Save metadata after deletions
s.saveMetadata()
return deletedCount, reclaimedBytes
}
@@ -283,9 +198,14 @@ func (s *Server) HandleUpload(w http.ResponseWriter, r *http.Request) {
return
}
s.mu.RLock()
client := s.clients[req.ClientID]
s.mu.RUnlock()
client, err := s.db.GetClient(req.ClientID)
if err != nil || client == nil {
respondJSON(w, http.StatusInternalServerError, UploadResponse{
Success: false,
Message: "Failed to get client configuration",
})
return
}
timestamp := time.Now().Format("2006-01-02_15:04:05")
@@ -379,8 +299,7 @@ func (s *Server) HandleUploadStream(w http.ResponseWriter, r *http.Request) {
actualSize = size
}
// Save metadata
s.mu.Lock()
// Save metadata to database
metadata := &SnapshotMetadata{
ClientID: clientID,
SnapshotID: storageKey,
@@ -393,10 +312,10 @@ func (s *Server) HandleUploadStream(w http.ResponseWriter, r *http.Request) {
Incremental: incrementalStr == "true",
BaseSnapshot: baseSnapshot,
}
s.snapshots[clientID] = append(s.snapshots[clientID], metadata)
s.mu.Unlock()
s.saveMetadata()
if err := s.db.SaveSnapshot(metadata); err != nil {
log.Printf("Error saving snapshot metadata: %v", err)
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
@@ -415,10 +334,17 @@ func (s *Server) HandleStatus(w http.ResponseWriter, r *http.Request) {
return
}
s.mu.RLock()
client := s.clients[clientID]
snapshots := s.snapshots[clientID]
s.mu.RUnlock()
client, err := s.db.GetClient(clientID)
if err != nil || client == nil {
respondJSON(w, http.StatusInternalServerError, StatusResponse{Success: false})
return
}
snapshots, err := s.db.GetSnapshotsByClient(clientID)
if err != nil {
log.Printf("Error getting snapshots: %v", err)
snapshots = []*SnapshotMetadata{}
}
usedBytes := s.getClientUsage(clientID)
percentUsed := float64(usedBytes) / float64(client.MaxSizeBytes) * 100
@@ -477,18 +403,14 @@ func (s *Server) HandleDownload(w http.ResponseWriter, r *http.Request) {
}
// Find snapshot metadata
s.mu.RLock()
client := s.clients[clientID]
var targetSnapshot *SnapshotMetadata
for _, snap := range s.snapshots[clientID] {
if snap.SnapshotID == snapshotID {
targetSnapshot = snap
break
}
client, err := s.db.GetClient(clientID)
if err != nil || client == nil {
http.Error(w, "Client not found", http.StatusNotFound)
return
}
s.mu.RUnlock()
if targetSnapshot == nil {
targetSnapshot, err := s.db.GetSnapshotByID(clientID, snapshotID)
if err != nil || targetSnapshot == nil {
http.Error(w, "Snapshot not found", http.StatusNotFound)
return
}
@@ -496,10 +418,13 @@ func (s *Server) HandleDownload(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
var backend StorageBackend
if client.StorageType == "s3" {
if client.StorageType == "s3" && s.s3Backend != nil {
backend = s.s3Backend
} else {
} else if s.localBackend != nil {
backend = s.localBackend
} else {
http.Error(w, "No storage backend available", http.StatusInternalServerError)
return
}
// Download from storage
@@ -543,11 +468,8 @@ func (s *Server) HandleRotationPolicy(w http.ResponseWriter, r *http.Request) {
return
}
s.mu.RLock()
client, exists := s.clients[clientID]
s.mu.RUnlock()
if !exists {
client, err := s.db.GetClient(clientID)
if err != nil || client == nil {
respondJSON(w, http.StatusNotFound, RotationPolicyResponse{
Success: false,
Message: "Client not found",
@@ -577,6 +499,7 @@ func (s *Server) HandleRotationPolicy(w http.ResponseWriter, r *http.Request) {
// RegisterRoutes registers all HTTP routes
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
// Client API routes
mux.HandleFunc("/upload", s.HandleUpload)
mux.HandleFunc("/upload-stream/", s.HandleUploadStream)
mux.HandleFunc("/status", s.HandleStatus)
@@ -584,6 +507,25 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/download", s.HandleDownload)
mux.HandleFunc("/health", s.HandleHealth)
mux.HandleFunc("/rotation-policy", s.HandleRotationPolicy)
// Admin API routes
mux.HandleFunc("/admin/login", s.handleAdminLogin)
mux.HandleFunc("/admin/logout", s.handleAdminLogout)
mux.HandleFunc("/admin/check", s.handleAdminCheck)
mux.HandleFunc("/admin/clients", s.handleAdminGetClients)
mux.HandleFunc("/admin/client", s.handleAdminGetClient)
mux.HandleFunc("/admin/client/create", s.handleAdminCreateClient)
mux.HandleFunc("/admin/client/update", s.handleAdminUpdateClient)
mux.HandleFunc("/admin/client/delete", s.handleAdminDeleteClient)
mux.HandleFunc("/admin/snapshots", s.handleAdminGetSnapshots)
mux.HandleFunc("/admin/snapshot/delete", s.handleAdminDeleteSnapshot)
mux.HandleFunc("/admin/stats", s.handleAdminGetStats)
mux.HandleFunc("/admin/admins", s.handleAdminGetAdmins)
mux.HandleFunc("/admin/admin/create", s.handleAdminCreateAdmin)
mux.HandleFunc("/admin/admin/delete", s.handleAdminDeleteAdmin)
// Admin UI (static files served from /admin/)
mux.HandleFunc("/admin/", s.handleAdminUI)
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {