network/pkg/gateway/handlers/sqlite/create_handler.go
anonpenguin23 84c9b9ab9b fixes
2026-01-24 11:12:00 +02:00

222 lines
6.4 KiB
Go

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"github.com/DeBrosOfficial/network/pkg/rqlite"
"github.com/google/uuid"
"go.uber.org/zap"
_ "github.com/mattn/go-sqlite3"
)
// SQLiteHandler handles namespace SQLite database operations
type SQLiteHandler struct {
db rqlite.Client
homeNodeManager *deployments.HomeNodeManager
logger *zap.Logger
basePath string
currentNodeID string // The node's peer ID for affinity checks
}
// NewSQLiteHandler creates a new SQLite handler
// dataDir: Base directory for node-local data (if empty, defaults to ~/.orama)
// nodeID: The node's peer ID for affinity checks (can be empty for single-node setups)
func NewSQLiteHandler(db rqlite.Client, homeNodeManager *deployments.HomeNodeManager, logger *zap.Logger, dataDir string, nodeID string) *SQLiteHandler {
var basePath string
if dataDir != "" {
basePath = filepath.Join(dataDir, "sqlite")
} else {
// Use user's home directory for cross-platform compatibility
homeDir, err := os.UserHomeDir()
if err != nil {
logger.Error("Failed to get user home directory", zap.Error(err))
homeDir = os.Getenv("HOME")
}
basePath = filepath.Join(homeDir, ".orama", "sqlite")
}
return &SQLiteHandler{
db: db,
homeNodeManager: homeNodeManager,
logger: logger,
basePath: basePath,
currentNodeID: nodeID,
}
}
// CreateDatabase creates a new SQLite database for a namespace
func (h *SQLiteHandler) CreateDatabase(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string)
if !ok || namespace == "" {
http.Error(w, "Namespace not found in context", http.StatusUnauthorized)
return
}
var req struct {
DatabaseName string `json:"database_name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.DatabaseName == "" {
http.Error(w, "database_name is required", http.StatusBadRequest)
return
}
// Validate database name (alphanumeric, underscore, hyphen only)
if !isValidDatabaseName(req.DatabaseName) {
http.Error(w, "Invalid database name. Use only alphanumeric characters, underscores, and hyphens", http.StatusBadRequest)
return
}
h.logger.Info("Creating SQLite database",
zap.String("namespace", namespace),
zap.String("database", req.DatabaseName),
)
// Assign home node for namespace
homeNodeID, err := h.homeNodeManager.AssignHomeNode(ctx, namespace)
if err != nil {
h.logger.Error("Failed to assign home node", zap.Error(err))
http.Error(w, "Failed to assign home node", http.StatusInternalServerError)
return
}
// Check if database already exists
existing, err := h.getDatabaseRecord(ctx, namespace, req.DatabaseName)
if err == nil && existing != nil {
http.Error(w, "Database already exists", http.StatusConflict)
return
}
// Create database file path
dbID := uuid.New().String()
dbPath := filepath.Join(h.basePath, namespace, req.DatabaseName+".db")
// Create directory if needed
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
h.logger.Error("Failed to create directory", zap.Error(err))
http.Error(w, "Failed to create database directory", http.StatusInternalServerError)
return
}
// Create SQLite database
sqliteDB, err := sql.Open("sqlite3", dbPath)
if err != nil {
h.logger.Error("Failed to create SQLite database", zap.Error(err))
http.Error(w, "Failed to create database", http.StatusInternalServerError)
return
}
// Enable WAL mode for better concurrency
if _, err := sqliteDB.Exec("PRAGMA journal_mode=WAL"); err != nil {
h.logger.Warn("Failed to enable WAL mode", zap.Error(err))
}
sqliteDB.Close()
// Record in RQLite
query := `
INSERT INTO namespace_sqlite_databases (
id, namespace, database_name, home_node_id, file_path, size_bytes, created_at, updated_at, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`
now := time.Now()
_, err = h.db.Exec(ctx, query, dbID, namespace, req.DatabaseName, homeNodeID, dbPath, 0, now, now, namespace)
if err != nil {
h.logger.Error("Failed to record database", zap.Error(err))
os.Remove(dbPath) // Cleanup
http.Error(w, "Failed to record database", http.StatusInternalServerError)
return
}
h.logger.Info("SQLite database created",
zap.String("id", dbID),
zap.String("namespace", namespace),
zap.String("database", req.DatabaseName),
zap.String("path", dbPath),
)
// Return response
resp := map[string]interface{}{
"id": dbID,
"namespace": namespace,
"database_name": req.DatabaseName,
"home_node_id": homeNodeID,
"file_path": dbPath,
"created_at": now,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(resp)
}
// getDatabaseRecord retrieves database metadata from RQLite
func (h *SQLiteHandler) getDatabaseRecord(ctx context.Context, namespace, databaseName string) (map[string]interface{}, error) {
type dbRow struct {
ID string `db:"id"`
Namespace string `db:"namespace"`
DatabaseName string `db:"database_name"`
HomeNodeID string `db:"home_node_id"`
FilePath string `db:"file_path"`
SizeBytes int64 `db:"size_bytes"`
BackupCID string `db:"backup_cid"`
CreatedAt time.Time `db:"created_at"`
}
var rows []dbRow
query := `SELECT * FROM namespace_sqlite_databases WHERE namespace = ? AND database_name = ? LIMIT 1`
err := h.db.Query(ctx, &rows, query, namespace, databaseName)
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, fmt.Errorf("database not found")
}
row := rows[0]
return map[string]interface{}{
"id": row.ID,
"namespace": row.Namespace,
"database_name": row.DatabaseName,
"home_node_id": row.HomeNodeID,
"file_path": row.FilePath,
"size_bytes": row.SizeBytes,
"backup_cid": row.BackupCID,
"created_at": row.CreatedAt,
}, nil
}
// isValidDatabaseName validates database name
func isValidDatabaseName(name string) bool {
if len(name) == 0 || len(name) > 64 {
return false
}
for _, ch := range name {
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '_' || ch == '-') {
return false
}
}
return true
}