' +
'| ' + s.client_id + ' | ' +
+ '' + (s.dataset_name || '-') + ' | ' +
'' + s.snapshot_id + ' | ' +
'' + new Date(s.timestamp).toLocaleString() + ' | ' +
'' + sizeGB + ' GB | ' +
+ '' + s.storage_type + ' | ' +
'' +
- (s.incremental ? 'Incremental' : 'Full') +
- (s.compressed ? ' Compressed' : '') +
+ (s.incremental ? 'Inc' : 'Full') +
+ (s.compressed ? ' LZ4' : '') +
' | ' +
' | ' +
'
';
diff --git a/internal/server/database.go b/internal/server/database.go
index 45c7d64..ea65761 100644
--- a/internal/server/database.go
+++ b/internal/server/database.go
@@ -89,6 +89,23 @@ func (d *Database) initTables() error {
return fmt.Errorf("failed to create clients table: %v", err)
}
+ // Datasets table - multiple datasets per client
+ _, err = d.db.Exec(`
+ CREATE TABLE IF NOT EXISTS datasets (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ client_id TEXT NOT NULL,
+ dataset_name TEXT NOT NULL,
+ storage_type TEXT NOT NULL DEFAULT 's3',
+ enabled INTEGER NOT NULL DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE,
+ UNIQUE(client_id, dataset_name)
+ )
+ `)
+ if err != nil {
+ return fmt.Errorf("failed to create datasets table: %v", err)
+ }
+
// Snapshots table
_, err = d.db.Exec(`
CREATE TABLE IF NOT EXISTS snapshots (
@@ -400,6 +417,138 @@ func (d *Database) CreateDefaultClient() error {
return d.SaveClient(defaultClient)
}
+// CreateDefaultDataset creates a default dataset for a client if none exists
+func (d *Database) CreateDefaultDataset(clientID, datasetName string) error {
+ datasets, err := d.GetDatasetsByClient(clientID)
+ if err != nil {
+ return err
+ }
+
+ if len(datasets) > 0 {
+ return nil
+ }
+
+ // Create default dataset
+ dataset := &DatasetConfig{
+ ClientID: clientID,
+ DatasetName: datasetName,
+ StorageType: "s3",
+ Enabled: true,
+ }
+
+ return d.SaveDataset(dataset)
+}
+
+// DatasetConfig represents a dataset configuration
+type DatasetConfig struct {
+ ID int64 `json:"id"`
+ ClientID string `json:"client_id"`
+ DatasetName string `json:"dataset_name"`
+ StorageType string `json:"storage_type"`
+ Enabled bool `json:"enabled"`
+}
+
+// GetDatasetsByClient gets all datasets for a client
+func (d *Database) GetDatasetsByClient(clientID string) ([]*DatasetConfig, error) {
+ query := `SELECT id, client_id, dataset_name, storage_type, enabled FROM datasets WHERE client_id = ?`
+
+ rows, err := d.db.Query(query, clientID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var datasets []*DatasetConfig
+ for rows.Next() {
+ dataset := &DatasetConfig{}
+ var enabled int
+
+ err := rows.Scan(&dataset.ID, &dataset.ClientID, &dataset.DatasetName, &dataset.StorageType, &enabled)
+ if err != nil {
+ return nil, err
+ }
+
+ dataset.Enabled = enabled == 1
+ datasets = append(datasets, dataset)
+ }
+
+ return datasets, nil
+}
+
+// GetDatasetByName gets a dataset by client and dataset name
+func (d *Database) GetDatasetByName(clientID, datasetName string) (*DatasetConfig, error) {
+ query := `SELECT id, client_id, dataset_name, storage_type, enabled FROM datasets WHERE client_id = ? AND dataset_name = ?`
+
+ row := d.db.QueryRow(query, clientID, datasetName)
+
+ dataset := &DatasetConfig{}
+ var enabled int
+
+ err := row.Scan(&dataset.ID, &dataset.ClientID, &dataset.DatasetName, &dataset.StorageType, &enabled)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ dataset.Enabled = enabled == 1
+ return dataset, nil
+}
+
+// SaveDataset saves or updates a dataset
+func (d *Database) SaveDataset(dataset *DatasetConfig) error {
+ enabled := 0
+ if dataset.Enabled {
+ enabled = 1
+ }
+
+ if dataset.ID == 0 {
+ // Insert new
+ _, err := d.db.Exec(`INSERT INTO datasets (client_id, dataset_name, storage_type, enabled) VALUES (?, ?, ?, ?)`,
+ dataset.ClientID, dataset.DatasetName, dataset.StorageType, enabled)
+ return err
+ }
+
+ // Update existing
+ _, err := d.db.Exec(`UPDATE datasets SET storage_type = ?, enabled = ? WHERE id = ?`,
+ dataset.StorageType, enabled, dataset.ID)
+ return err
+}
+
+// DeleteDataset deletes a dataset
+func (d *Database) DeleteDataset(id int64) error {
+ _, err := d.db.Exec(`DELETE FROM datasets WHERE id = ?`, id)
+ return err
+}
+
+// GetAllDatasets gets all datasets
+func (d *Database) GetAllDatasets() ([]*DatasetConfig, error) {
+ query := `SELECT id, client_id, dataset_name, storage_type, enabled FROM datasets`
+
+ rows, err := d.db.Query(query)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var datasets []*DatasetConfig
+ for rows.Next() {
+ dataset := &DatasetConfig{}
+ var enabled int
+
+ err := rows.Scan(&dataset.ID, &dataset.ClientID, &dataset.DatasetName, &dataset.StorageType, &enabled)
+ if err != nil {
+ return nil, err
+ }
+
+ dataset.Enabled = enabled == 1
+ datasets = append(datasets, dataset)
+ }
+
+ return datasets, nil
+}
+
// GetSnapshotByID retrieves a specific snapshot
func (d *Database) GetSnapshotByID(clientID, snapshotID string) (*SnapshotMetadata, error) {
snap := &SnapshotMetadata{}
diff --git a/internal/server/server.go b/internal/server/server.go
index a85c7dd..61601bf 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -271,6 +271,25 @@ func (s *Server) HandleUploadStream(w http.ResponseWriter, r *http.Request) {
return
}
+ // Check if dataset is allowed for this client
+ dataset, err := s.db.GetDatasetByName(clientID, datasetName)
+ if err != nil || dataset == nil {
+ log.Printf("Dataset %s not found for client %s", datasetName, clientID)
+ respondJSON(w, http.StatusForbidden, UploadResponse{
+ Success: false,
+ Message: "Dataset not configured for this client",
+ })
+ return
+ }
+
+ if !dataset.Enabled {
+ respondJSON(w, http.StatusForbidden, UploadResponse{
+ Success: false,
+ Message: "Dataset is disabled",
+ })
+ return
+ }
+
ctx := context.Background()
// Upload to S3
@@ -438,13 +457,6 @@ func (s *Server) HandleDownload(w http.ResponseWriter, r *http.Request) {
return
}
- // Find snapshot metadata
- client, err := s.db.GetClient(clientID)
- if err != nil || client == nil {
- http.Error(w, "Client not found", http.StatusNotFound)
- return
- }
-
targetSnapshot, err := s.db.GetSnapshotByID(clientID, snapshotID)
if err != nil || targetSnapshot == nil {
http.Error(w, "Snapshot not found", http.StatusNotFound)