mirror of
https://github.com/DeBrosOfficial/network.git
synced 2026-01-30 23:03:03 +00:00
237 lines
7.1 KiB
Go
237 lines
7.1 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,
|
|
}
|
|
}
|
|
|
|
// writeCreateError writes an error response as JSON for consistency
|
|
func writeCreateError(w http.ResponseWriter, status int, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
|
}
|
|
|
|
// 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 == "" {
|
|
writeCreateError(w, http.StatusUnauthorized, "Namespace not found in context")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
DatabaseName string `json:"database_name"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeCreateError(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.DatabaseName == "" {
|
|
writeCreateError(w, http.StatusBadRequest, "database_name is required")
|
|
return
|
|
}
|
|
|
|
// Validate database name (alphanumeric, underscore, hyphen only)
|
|
if !isValidDatabaseName(req.DatabaseName) {
|
|
writeCreateError(w, http.StatusBadRequest, "Invalid database name. Use only alphanumeric characters, underscores, and hyphens")
|
|
return
|
|
}
|
|
|
|
h.logger.Info("Creating SQLite database",
|
|
zap.String("namespace", namespace),
|
|
zap.String("database", req.DatabaseName),
|
|
)
|
|
|
|
// For SQLite databases, the home node is ALWAYS the current node
|
|
// because the database file is stored locally on this node's filesystem.
|
|
// This is different from deployments which can be load-balanced across nodes.
|
|
homeNodeID := h.currentNodeID
|
|
if homeNodeID == "" {
|
|
// Fallback: if node ID not configured, try to get from HomeNodeManager
|
|
// This provides backward compatibility for single-node setups
|
|
var err error
|
|
homeNodeID, err = h.homeNodeManager.AssignHomeNode(ctx, namespace)
|
|
if err != nil {
|
|
h.logger.Error("Failed to assign home node", zap.Error(err))
|
|
writeCreateError(w, http.StatusInternalServerError, "Failed to assign home node")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check if database already exists
|
|
existing, err := h.getDatabaseRecord(ctx, namespace, req.DatabaseName)
|
|
if err == nil && existing != nil {
|
|
writeCreateError(w, http.StatusConflict, "Database already exists")
|
|
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))
|
|
writeCreateError(w, http.StatusInternalServerError, "Failed to create database directory")
|
|
return
|
|
}
|
|
|
|
// Create SQLite database
|
|
sqliteDB, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
h.logger.Error("Failed to create SQLite database", zap.Error(err))
|
|
writeCreateError(w, http.StatusInternalServerError, "Failed to create database")
|
|
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
|
|
writeCreateError(w, http.StatusInternalServerError, "Failed to record database")
|
|
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
|
|
}
|