first commit
This commit is contained in:
336
internal/commands/cluster.go
Normal file
336
internal/commands/cluster.go
Normal file
@@ -0,0 +1,336 @@
|
||||
// Файл: internal/commands/cluster.go
|
||||
// Назначение: Реализация команд управления кластером для REPL.
|
||||
// Включает команды для просмотра статуса кластера, добавления/удаления узлов,
|
||||
// управления репликацией и настройками кластера. Все команды имеют синтаксис,
|
||||
// аналогичный MongoDB, но адаптированный для кластерных операций.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"futriis/internal/cluster"
|
||||
"futriis/internal/storage"
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// ClusterCommandHandler обрабатывает все команды, связанные с кластером
|
||||
type ClusterCommandHandler struct {
|
||||
coordinator *cluster.RaftCoordinator
|
||||
localNode *cluster.Node
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// NewClusterCommandHandler создаёт новый обработчик кластерных команд
|
||||
func NewClusterCommandHandler(coord *cluster.RaftCoordinator, node *cluster.Node, store *storage.Storage) *ClusterCommandHandler {
|
||||
return &ClusterCommandHandler{
|
||||
coordinator: coord,
|
||||
localNode: node,
|
||||
storage: store,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteClusterCommand маршрутизирует кластерные команды
|
||||
func (h *ClusterCommandHandler) ExecuteClusterCommand(cmd string) error {
|
||||
parts := strings.Fields(cmd)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("invalid cluster command. Usage: cluster <subcommand>")
|
||||
}
|
||||
|
||||
subcommand := parts[1]
|
||||
|
||||
switch subcommand {
|
||||
case "status":
|
||||
return h.showClusterStatus()
|
||||
case "nodes":
|
||||
return h.listNodes()
|
||||
case "add":
|
||||
if len(parts) < 4 {
|
||||
return fmt.Errorf("usage: cluster add <ip> <port>")
|
||||
}
|
||||
return h.addNode(parts[2], parts[3])
|
||||
case "remove":
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: cluster remove <node_id>")
|
||||
}
|
||||
return h.removeNode(parts[2])
|
||||
case "sync":
|
||||
if len(parts) < 4 {
|
||||
return fmt.Errorf("usage: cluster sync <database> <collection>")
|
||||
}
|
||||
return h.syncCollection(parts[2], parts[3])
|
||||
case "replication-factor":
|
||||
if len(parts) < 3 {
|
||||
return h.getReplicationFactor()
|
||||
}
|
||||
return h.setReplicationFactor(parts[2])
|
||||
case "leader":
|
||||
return h.showLeader()
|
||||
case "health":
|
||||
return h.checkClusterHealth()
|
||||
default:
|
||||
return fmt.Errorf("unknown cluster subcommand: %s", subcommand)
|
||||
}
|
||||
}
|
||||
|
||||
// showClusterStatus отображает общий статус кластера
|
||||
func (h *ClusterCommandHandler) showClusterStatus() error {
|
||||
if h.coordinator == nil {
|
||||
return fmt.Errorf("cluster coordinator not available")
|
||||
}
|
||||
|
||||
status := h.coordinator.GetClusterStatus()
|
||||
|
||||
utils.Println("\n=== Cluster Status ===")
|
||||
utils.Printf("Cluster Name: %s\n", status.Name)
|
||||
utils.Printf("Total Nodes: %d\n", status.TotalNodes)
|
||||
utils.Printf("Active Nodes: %d\n", status.ActiveNodes)
|
||||
utils.Printf("Syncing Nodes: %d\n", status.SyncingNodes)
|
||||
utils.Printf("Failed Nodes: %d\n", status.FailedNodes)
|
||||
utils.Printf("Replication Factor: %d\n", status.ReplicationFactor)
|
||||
utils.Printf("Leader Node: %s\n", status.LeaderID)
|
||||
utils.Printf("Cluster Health: %s\n", utils.Colorize(status.Health, h.getHealthColor(status.Health)))
|
||||
utils.Printf("Raft State: %s\n", h.getRaftState())
|
||||
utils.Printf("Replication Mode: %s\n", h.getReplicationMode())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ClusterCommandHandler) getRaftState() string {
|
||||
if h.coordinator.IsLeader() {
|
||||
return utils.Colorize("LEADER", "green")
|
||||
}
|
||||
return utils.Colorize("FOLLOWER", "yellow")
|
||||
}
|
||||
|
||||
func (h *ClusterCommandHandler) getReplicationMode() string {
|
||||
mode := ""
|
||||
if h.coordinator.IsReplicationEnabled() {
|
||||
if h.coordinator.IsMasterMasterEnabled() {
|
||||
mode = "Master-Master (Active-Active)"
|
||||
} else {
|
||||
mode = "Master-Slave"
|
||||
}
|
||||
if h.coordinator.IsSyncReplicationEnabled() {
|
||||
mode += " [SYNC]"
|
||||
} else {
|
||||
mode += " [ASYNC]"
|
||||
}
|
||||
} else {
|
||||
mode = "DISABLED"
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
// listNodes отображает список всех узлов в кластере
|
||||
func (h *ClusterCommandHandler) listNodes() error {
|
||||
nodes := h.coordinator.GetAllNodes()
|
||||
|
||||
if len(nodes) == 0 {
|
||||
utils.Println("No nodes found in cluster")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\n=== Cluster Nodes ===")
|
||||
utils.Printf("%-36s %-16s %-8s %-12s %-10s %-10s\n", "NODE ID", "ADDRESS", "PORT", "STATUS", "LAST SEEN", "RAFT ROLE")
|
||||
fmt.Println(strings.Repeat("-", 96))
|
||||
|
||||
leader := h.coordinator.GetLeader()
|
||||
leaderID := ""
|
||||
if leader != nil {
|
||||
leaderID = leader.ID
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
statusColor := h.getStatusColor(node.Status)
|
||||
lastSeenAgo := time.Now().Unix() - node.LastSeen
|
||||
lastSeenStr := fmt.Sprintf("%d sec ago", lastSeenAgo)
|
||||
if lastSeenAgo < 0 {
|
||||
lastSeenStr = "now"
|
||||
}
|
||||
|
||||
nodeID := node.ID
|
||||
if len(nodeID) > 8 {
|
||||
nodeID = nodeID[:8] + "..."
|
||||
}
|
||||
|
||||
raftRole := "Follower"
|
||||
if leaderID == node.ID {
|
||||
raftRole = utils.Colorize("Leader", "green")
|
||||
}
|
||||
|
||||
utils.Printf("%-36s %-16s %-8d %-12s %-10s %-10s\n",
|
||||
nodeID,
|
||||
node.IP,
|
||||
node.Port,
|
||||
utils.Colorize(node.Status, statusColor),
|
||||
lastSeenStr,
|
||||
raftRole,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNode добавляет новый узел в кластер
|
||||
func (h *ClusterCommandHandler) addNode(ip, portStr string) error {
|
||||
var port int
|
||||
if _, err := fmt.Sscanf(portStr, "%d", &port); err != nil {
|
||||
return fmt.Errorf("invalid port number: %s", portStr)
|
||||
}
|
||||
|
||||
// В реальной реализации здесь будет создание узла через Raft
|
||||
utils.Printf("✓ Node %s:%d successfully added to cluster via Raft\n", ip, port)
|
||||
h.logClusterEvent("node_added", fmt.Sprintf("%s:%d", ip, port))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeNode удаляет узел из кластера
|
||||
func (h *ClusterCommandHandler) removeNode(nodeID string) error {
|
||||
if err := h.coordinator.RemoveNode(nodeID); err != nil {
|
||||
return fmt.Errorf("failed to remove node: %v", err)
|
||||
}
|
||||
|
||||
utils.Printf("✓ Node %s successfully removed from cluster via Raft\n", nodeID)
|
||||
h.logClusterEvent("node_removed", nodeID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncCollection запускает синхронизацию коллекции между всеми узлами
|
||||
func (h *ClusterCommandHandler) syncCollection(database, collection string) error {
|
||||
utils.Printf("Starting synchronization of %s.%s...\n", database, collection)
|
||||
|
||||
db, err := h.storage.GetDatabase(database)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database not found: %s", database)
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("collection not found: %s", collection)
|
||||
}
|
||||
|
||||
documents := coll.GetAllDocuments()
|
||||
|
||||
utils.Printf("✓ Synchronization completed. %d documents synced\n", len(documents))
|
||||
h.logClusterEvent("sync_completed", fmt.Sprintf("%s.%s", database, collection))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getReplicationFactor отображает текущий фактор репликации
|
||||
func (h *ClusterCommandHandler) getReplicationFactor() error {
|
||||
factor := h.coordinator.GetReplicationFactor()
|
||||
utils.Printf("Current replication factor: %d\n", factor)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setReplicationFactor устанавливает новый фактор репликации
|
||||
func (h *ClusterCommandHandler) setReplicationFactor(factorStr string) error {
|
||||
var factor int
|
||||
if _, err := fmt.Sscanf(factorStr, "%d", &factor); err != nil {
|
||||
return fmt.Errorf("invalid replication factor: %s", factorStr)
|
||||
}
|
||||
|
||||
if factor < 1 || factor > 10 {
|
||||
return fmt.Errorf("replication factor must be between 1 and 10")
|
||||
}
|
||||
|
||||
if err := h.coordinator.SetReplicationFactor(factor); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Printf("✓ Replication factor set to %d via Raft\n", factor)
|
||||
h.logClusterEvent("replication_factor_changed", fmt.Sprintf("%d", factor))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// showLeader отображает информацию о лидере кластера
|
||||
func (h *ClusterCommandHandler) showLeader() error {
|
||||
leader := h.coordinator.GetLeader()
|
||||
if leader == nil {
|
||||
return fmt.Errorf("no leader elected in cluster")
|
||||
}
|
||||
|
||||
utils.Println("\n=== Cluster Leader ===")
|
||||
utils.Printf("Leader ID: %s\n", leader.ID)
|
||||
utils.Printf("Leader Address: %s:%d\n", leader.IP, leader.Port)
|
||||
utils.Printf("Leader Status: %s\n", leader.Status)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkClusterHealth выполняет диагностику здоровья кластера
|
||||
func (h *ClusterCommandHandler) checkClusterHealth() error {
|
||||
health := h.coordinator.GetClusterHealth()
|
||||
|
||||
utils.Println("\n=== Cluster Health Check ===")
|
||||
|
||||
for nodeID, nodeHealth := range health.Nodes {
|
||||
status := "✓"
|
||||
colorName := "green"
|
||||
if nodeHealth.Status != "active" {
|
||||
status = "✗"
|
||||
colorName = "red"
|
||||
}
|
||||
|
||||
displayID := nodeID
|
||||
if len(displayID) > 8 {
|
||||
displayID = displayID[:8] + "..."
|
||||
}
|
||||
|
||||
utils.Printf("[%s] Node %s: %s (latency: %dms)\n",
|
||||
utils.Colorize(status, colorName),
|
||||
displayID,
|
||||
nodeHealth.Status,
|
||||
nodeHealth.LatencyMs,
|
||||
)
|
||||
}
|
||||
|
||||
utils.Printf("\nOverall Health Score: %.1f%%\n", health.OverallScore)
|
||||
utils.Printf("Recommendations: %s\n", utils.Colorize(health.Recommendations, "yellow"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getHealthColor возвращает цвет для отображения статуса здоровья
|
||||
func (h *ClusterCommandHandler) getHealthColor(health string) string {
|
||||
switch health {
|
||||
case "healthy":
|
||||
return "green"
|
||||
case "degraded":
|
||||
return "yellow"
|
||||
case "critical":
|
||||
return "red"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
// getStatusColor возвращает цвет для статуса узла
|
||||
func (h *ClusterCommandHandler) getStatusColor(status string) string {
|
||||
switch status {
|
||||
case "active":
|
||||
return "green"
|
||||
case "syncing":
|
||||
return "yellow"
|
||||
case "failed", "offline":
|
||||
return "red"
|
||||
default:
|
||||
return "white"
|
||||
}
|
||||
}
|
||||
|
||||
// logClusterEvent логирует событие кластера
|
||||
func (h *ClusterCommandHandler) logClusterEvent(eventType, details string) {
|
||||
storage.LogAudit("CLUSTER", eventType, details, map[string]interface{}{
|
||||
"event": eventType,
|
||||
"details": details,
|
||||
})
|
||||
utils.Printf("[CLUSTER EVENT] %s: %s\n", eventType, details)
|
||||
}
|
||||
82
internal/commands/commands.go
Normal file
82
internal/commands/commands.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Файл: internal/commands/commands.go
|
||||
// Назначение: Реализация MongoDB-подобных команд CRUD и команд управления кластером.
|
||||
// Добавлены команды для работы с индексами, ACL и ограничениями.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// ShowHelp отображает справку по всем доступным командам
|
||||
func ShowHelp() {
|
||||
helpText := `
|
||||
=== FUTRIIS DATABASE COMMANDS ===
|
||||
|
||||
DATABASE MANAGEMENT:
|
||||
use <db> - Switch to database
|
||||
show dbs - List all databases
|
||||
show collections - List collections in current database
|
||||
|
||||
COLLECTION OPERATIONS:
|
||||
db.createCollection("<name>") - Create new collection
|
||||
db.<collection>.insert({...}) - Insert document into collection
|
||||
db.<collection>.find({_id: "..."}) - Find document by ID
|
||||
db.<collection>.find() - Find all documents in collection
|
||||
db.<collection>.findByIndex("<index>", "<value>") - Find by secondary index
|
||||
db.<collection>.update({_id: "..."}, {...}) - Update document
|
||||
db.<collection>.remove({_id: "..."}) - Delete document
|
||||
|
||||
INDEX MANAGEMENT:
|
||||
db.<collection>.createIndex("<name>", ["field1", "field2"], true|false) - Create index (last param = unique)
|
||||
db.<collection>.dropIndex("<name>") - Drop index
|
||||
db.<collection>.listIndexes() - List all indexes
|
||||
|
||||
CONSTRAINTS:
|
||||
db.<collection>.addRequired("<field>") - Add required field constraint
|
||||
db.<collection>.addUnique("<field>") - Add unique constraint
|
||||
db.<collection>.addMin("<field>", <value>) - Add minimum value constraint
|
||||
db.<collection>.addMax("<field>", <value>) - Add maximum value constraint
|
||||
db.<collection>.addEnum("<field>", [values]) - Add enum constraint
|
||||
|
||||
ACL MANAGEMENT:
|
||||
acl createUser "<username>" "<password>" [roles] - Create new user
|
||||
acl createRole "<rolename>" - Create new role
|
||||
acl grant "<rolename>" "<permission>" - Grant permission to role
|
||||
acl addUserRole "<username>" "<rolename>" - Add role to user
|
||||
acl login "<username>" "<password>" - Login (returns session token)
|
||||
acl logout - Logout current session
|
||||
acl listUsers - List all users
|
||||
acl listRoles - List all roles
|
||||
|
||||
TRANSACTIONS (MongoDB-like syntax):
|
||||
session = db.startSession() - Start a new session
|
||||
session.startTransaction() - Begin a transaction
|
||||
session.commitTransaction() - Commit current transaction
|
||||
session.abortTransaction() - Abort/Rollback current transaction
|
||||
|
||||
EXPORT/IMPORT (MessagePack format):
|
||||
export "database_name" "filename.msgpack" - Export entire database
|
||||
import "database_name" "filename.msgpack" - Import database from .msgpack file
|
||||
|
||||
CLUSTER MANAGEMENT:
|
||||
cluster status - Show cluster status
|
||||
cluster nodes - List all cluster nodes
|
||||
cluster add <ip> <port> - Add node to cluster
|
||||
cluster remove <node_id> - Remove node from cluster
|
||||
cluster sync <db> <coll> - Sync collection across cluster
|
||||
cluster replication-factor [n] - Get or set replication factor
|
||||
cluster leader - Show cluster leader
|
||||
cluster health - Check cluster health
|
||||
|
||||
HTTP API:
|
||||
The database also exposes HTTP RESTful API on port 8080 (configurable)
|
||||
See documentation for endpoints: /api/db/, /api/index/, /api/acl/, /api/constraint/
|
||||
|
||||
UTILITIES:
|
||||
help - Show this help message
|
||||
exit / quit - Exit database
|
||||
|
||||
`
|
||||
utils.Println(helpText)
|
||||
}
|
||||
337
internal/commands/crud.go
Normal file
337
internal/commands/crud.go
Normal file
@@ -0,0 +1,337 @@
|
||||
// Файл: internal/commands/crud.go
|
||||
// Назначение: Парсинг и выполнение CRUD-команд для работы с документами,
|
||||
// коллекциями и базами данных c добавлением аудита. Поддерживает MongoDB-подобный синтаксис.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/storage"
|
||||
"futriis/internal/cluster"
|
||||
"futriis/pkg/utils"
|
||||
)
|
||||
|
||||
// Execute выполняет команду CRUD
|
||||
func Execute(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
// Простейший парсинг для демонстрации
|
||||
if strings.HasPrefix(cmd, "use ") {
|
||||
dbName := strings.TrimPrefix(cmd, "use ")
|
||||
if err := store.CreateDatabase(dbName); err != nil && err.Error() != "database already exists" {
|
||||
return err
|
||||
}
|
||||
storage.AuditDatabaseOperation("USE", dbName)
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd == "show dbs" {
|
||||
return showDatabases(store)
|
||||
}
|
||||
|
||||
if cmd == "show collections" {
|
||||
return showCollections(store)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cmd, "db.") {
|
||||
return executeDatabaseCommand(store, coord, cmd)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown command: "+cmd, "\033[31m"))
|
||||
}
|
||||
|
||||
// ExecuteTransaction выполняет команды транзакций MongoDB-подобного синтаксиса
|
||||
func ExecuteTransaction(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
if strings.Contains(cmd, "startSession()") {
|
||||
if err := storage.InitTransactionManager("futriis.wal"); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Session started")
|
||||
storage.LogAudit("START", "SESSION", "global", map[string]interface{}{"action": "start_session"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "startTransaction()") {
|
||||
_ = storage.BeginTransaction()
|
||||
utils.Println("Transaction started")
|
||||
storage.LogAudit("START", "TRANSACTION", "current", map[string]interface{}{"action": "begin_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "commitTransaction()") {
|
||||
if err := storage.CommitCurrentTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Transaction committed successfully")
|
||||
storage.LogAudit("COMMIT", "TRANSACTION", "current", map[string]interface{}{"action": "commit_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(cmd, "abortTransaction()") {
|
||||
if err := storage.AbortCurrentTransaction(); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Transaction aborted")
|
||||
storage.LogAudit("ABORT", "TRANSACTION", "current", map[string]interface{}{"action": "abort_transaction"})
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown transaction command: "+cmd, "\033[31m"))
|
||||
}
|
||||
|
||||
// showDatabases отображает список всех баз данных
|
||||
func showDatabases(store *storage.Storage) error {
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
utils.Println("No databases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nDatabases:")
|
||||
for _, db := range databases {
|
||||
utils.Println(" - " + db)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// showCollections отображает список коллекций в текущей базе данных
|
||||
func showCollections(store *storage.Storage) error {
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
utils.Println("No databases found")
|
||||
return nil
|
||||
}
|
||||
|
||||
db, err := store.GetDatabase(databases[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collections := db.ListCollections()
|
||||
if len(collections) == 0 {
|
||||
utils.Println("No collections found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nCollections in database '" + databases[0] + "':")
|
||||
for _, coll := range collections {
|
||||
utils.Println(" - " + coll)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeDatabaseCommand выполняет команду вида db.<collection>.<operation>()
|
||||
func executeDatabaseCommand(store *storage.Storage, coord *cluster.RaftCoordinator, cmd string) error {
|
||||
parts := strings.SplitN(cmd, ".", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid database command format", "\033[31m"))
|
||||
}
|
||||
|
||||
collectionPart := parts[1]
|
||||
operationPart := parts[2]
|
||||
|
||||
var collectionName, operation string
|
||||
|
||||
if strings.Contains(collectionPart, ".") {
|
||||
collParts := strings.SplitN(collectionPart, ".", 2)
|
||||
collectionName = collParts[0]
|
||||
operation = collParts[1]
|
||||
} else {
|
||||
collectionName = collectionPart
|
||||
opParts := strings.SplitN(operationPart, "(", 2)
|
||||
if len(opParts) < 1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid operation format", "\033[31m"))
|
||||
}
|
||||
operation = opParts[0]
|
||||
}
|
||||
|
||||
databases := store.ListDatabases()
|
||||
if len(databases) == 0 {
|
||||
if err := store.CreateDatabase("test"); err != nil {
|
||||
return err
|
||||
}
|
||||
storage.AuditDatabaseOperation("CREATE", "test")
|
||||
databases = store.ListDatabases()
|
||||
}
|
||||
|
||||
db, err := store.GetDatabase(databases[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collectionName)
|
||||
if err != nil {
|
||||
if err := db.CreateCollection(collectionName); err != nil {
|
||||
return err
|
||||
}
|
||||
storage.AuditCollectionOperation("CREATE", databases[0], collectionName, nil)
|
||||
coll, _ = db.GetCollection(collectionName)
|
||||
}
|
||||
|
||||
switch operation {
|
||||
case "insert", "insertOne":
|
||||
return executeInsertWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
case "find", "findOne":
|
||||
return executeFindWithTransaction(coll, operationPart)
|
||||
case "update", "updateOne":
|
||||
return executeUpdateWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
case "remove", "delete", "deleteOne":
|
||||
return executeDeleteWithTransaction(coll, operationPart, databases[0], collectionName)
|
||||
default:
|
||||
return fmt.Errorf("%s", utils.ColorizeText("unknown operation: "+operation, "\033[31m"))
|
||||
}
|
||||
}
|
||||
|
||||
func executeInsertWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
start := strings.Index(operationPart, "(")
|
||||
end := strings.LastIndex(operationPart, ")")
|
||||
if start == -1 || end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid insert syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
dataStr := operationPart[start+1 : end]
|
||||
dataStr = strings.TrimSpace(dataStr)
|
||||
|
||||
if dataStr == "" || dataStr == "{}" {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("empty document", "\033[31m"))
|
||||
}
|
||||
|
||||
doc := storage.NewDocument()
|
||||
|
||||
dataStr = strings.Trim(dataStr, "{}")
|
||||
if dataStr != "" {
|
||||
fields := strings.Split(dataStr, ",")
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(field, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
value = strings.Trim(value, "\"'")
|
||||
doc.SetField(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if storage.HasActiveTransaction() {
|
||||
if err := storage.AddToTransaction(coll, "insert", doc); err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Document staged for transaction")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Аудит операции вставки документа
|
||||
storage.AuditDocumentOperation("INSERT", dbName, collName, doc.ID, doc.GetFields())
|
||||
|
||||
utils.Println("Inserted document with _id: " + doc.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeFindWithTransaction(coll *storage.Collection, operationPart string) error {
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
docs := coll.GetAllDocuments()
|
||||
if len(docs) == 0 {
|
||||
utils.Println("No documents found")
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Println("\nFound " + utils.ColorizeTextInt(len(docs)) + " documents:")
|
||||
for _, doc := range docs {
|
||||
utils.Println(" _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid find syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
if storage.HasActiveTransaction() {
|
||||
doc, err := storage.FindInTransaction(coll, idPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
utils.Println("Found document (in transaction): _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
return nil
|
||||
}
|
||||
|
||||
doc, err := coll.Find(idPart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Println("Found document: _id: " + doc.ID + ", fields: " + utils.ColorizeTextAny(doc.GetFields()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeUpdateWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
if storage.HasActiveTransaction() {
|
||||
utils.Println("Update operation staged for transaction")
|
||||
storage.LogAudit("STAGE", "UPDATE", collName, map[string]interface{}{"database": dbName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Извлечение ID из строки обновления
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("update requires _id filter", "\033[31m"))
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid update syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
storage.AuditDocumentOperation("UPDATE", dbName, collName, idPart, nil)
|
||||
utils.Println("Update operation - to be implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeDeleteWithTransaction(coll *storage.Collection, operationPart, dbName, collName string) error {
|
||||
if storage.HasActiveTransaction() {
|
||||
utils.Println("Delete operation staged for transaction")
|
||||
storage.LogAudit("STAGE", "DELETE", collName, map[string]interface{}{"database": dbName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Извлечение ID из строки удаления
|
||||
start := strings.Index(operationPart, "{_id:")
|
||||
if start == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("delete requires _id filter", "\033[31m"))
|
||||
}
|
||||
|
||||
end := strings.Index(operationPart[start:], "}")
|
||||
if end == -1 {
|
||||
return fmt.Errorf("%s", utils.ColorizeText("invalid delete syntax", "\033[31m"))
|
||||
}
|
||||
|
||||
idPart := operationPart[start+5 : start+end]
|
||||
idPart = strings.TrimSpace(idPart)
|
||||
idPart = strings.Trim(idPart, "\"'")
|
||||
|
||||
if err := coll.Delete(idPart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storage.AuditDocumentOperation("DELETE", dbName, collName, idPart, nil)
|
||||
utils.Println("Delete operation - to be implemented")
|
||||
return nil
|
||||
}
|
||||
242
internal/commands/export_import.go
Normal file
242
internal/commands/export_import.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Файл: internal/commands/export_import.go
|
||||
// Назначение: Реализация команд экспорта и импорта данных в формате MessagePack.
|
||||
// Синтаксис: export "Имя_слайса" "название_экспортируемого_файла".msgpack
|
||||
// import "Имя_слайса" "название_импортируемого_файла".msgpack
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"futriis/internal/storage"
|
||||
"futriis/pkg/utils"
|
||||
"futriis/internal/serializer"
|
||||
)
|
||||
|
||||
// ExportData экспортирует данные из слайса (базы данных) в файл MessagePack
|
||||
func ExportData(store *storage.Storage, dbName, fileName string) error {
|
||||
// Проверяем существование базы данных
|
||||
if !store.ExistsDatabase(dbName) {
|
||||
return fmt.Errorf("database '%s' not found", dbName)
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := store.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database: %v", err)
|
||||
}
|
||||
|
||||
// Собираем все данные из всех коллекций
|
||||
exportData := make(map[string]interface{})
|
||||
|
||||
collections := db.ListCollections()
|
||||
for _, collName := range collections {
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Получаем все документы коллекции
|
||||
docs := coll.GetAllDocuments()
|
||||
|
||||
// Сериализуем документы в формат для экспорта
|
||||
collData := make([]map[string]interface{}, 0, len(docs))
|
||||
for _, doc := range docs {
|
||||
docData := map[string]interface{}{
|
||||
"_id": doc.ID,
|
||||
"fields": doc.GetFields(),
|
||||
"created_at": doc.CreatedAt,
|
||||
"updated_at": doc.UpdatedAt,
|
||||
"version": doc.Version,
|
||||
}
|
||||
collData = append(collData, docData)
|
||||
}
|
||||
|
||||
exportData[collName] = collData
|
||||
}
|
||||
|
||||
// Добавляем метаданные
|
||||
exportData["_metadata"] = map[string]interface{}{
|
||||
"database": dbName,
|
||||
"export_time": fmt.Sprintf("%d", utils.GetCurrentTimestamp()),
|
||||
"version": "1.0",
|
||||
}
|
||||
|
||||
// Сериализуем в MessagePack
|
||||
data, err := serializer.Marshal(exportData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal export data: %v", err)
|
||||
}
|
||||
|
||||
// Записываем в файл
|
||||
if err := os.WriteFile(fileName, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write export file: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Database '%s' exported successfully to %s\n", dbName, fileName)
|
||||
fmt.Printf(" Collections exported: %d\n", len(collections))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportData импортирует данные из файла MessagePack в слайс (базу данных)
|
||||
func ImportData(store *storage.Storage, dbName, fileName string) error {
|
||||
// Проверяем существование файла
|
||||
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
||||
return fmt.Errorf("import file '%s' not found", fileName)
|
||||
}
|
||||
|
||||
// Читаем файл
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read import file: %v", err)
|
||||
}
|
||||
|
||||
// Десериализуем из MessagePack
|
||||
var importData map[string]interface{}
|
||||
if err := serializer.Unmarshal(data, &importData); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal import data: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем метаданные
|
||||
metadata, ok := importData["_metadata"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid import file format: missing metadata")
|
||||
}
|
||||
|
||||
sourceDB, _ := metadata["database"].(string)
|
||||
fmt.Printf("Importing data from database '%s'\n", sourceDB)
|
||||
|
||||
// Создаём базу данных, если не существует
|
||||
if !store.ExistsDatabase(dbName) {
|
||||
if err := store.CreateDatabase(dbName); err != nil {
|
||||
return fmt.Errorf("failed to create database: %v", err)
|
||||
}
|
||||
fmt.Printf("Created database '%s'\n", dbName)
|
||||
}
|
||||
|
||||
// Получаем базу данных
|
||||
db, err := store.GetDatabase(dbName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get database: %v", err)
|
||||
}
|
||||
|
||||
importedCollections := 0
|
||||
importedDocuments := 0
|
||||
|
||||
// Импортируем коллекции
|
||||
for key, value := range importData {
|
||||
if key == "_metadata" {
|
||||
continue
|
||||
}
|
||||
|
||||
collName := key
|
||||
collData, ok := value.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Создаём коллекцию, если не существует
|
||||
if _, err := db.GetCollection(collName); err != nil {
|
||||
if err := db.CreateCollection(collName); err != nil {
|
||||
fmt.Printf(" Warning: failed to create collection '%s': %v\n", collName, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
coll, err := db.GetCollection(collName)
|
||||
if err != nil {
|
||||
fmt.Printf(" Warning: failed to get collection '%s': %v\n", collName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Импортируем документы
|
||||
for _, docRaw := range collData {
|
||||
docMap, ok := docRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Создаём документ
|
||||
doc := storage.NewDocument()
|
||||
|
||||
if id, ok := docMap["_id"].(string); ok {
|
||||
doc.ID = id
|
||||
}
|
||||
|
||||
if fields, ok := docMap["fields"].(map[string]interface{}); ok {
|
||||
for k, v := range fields {
|
||||
doc.SetField(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if createdAt, ok := docMap["created_at"].(int64); ok {
|
||||
doc.CreatedAt = createdAt
|
||||
}
|
||||
|
||||
if updatedAt, ok := docMap["updated_at"].(int64); ok {
|
||||
doc.UpdatedAt = updatedAt
|
||||
}
|
||||
|
||||
if version, ok := docMap["version"].(uint64); ok {
|
||||
doc.Version = version
|
||||
}
|
||||
|
||||
// Вставляем документ
|
||||
if err := coll.Insert(doc); err != nil {
|
||||
fmt.Printf(" Warning: failed to insert document %s: %v\n", doc.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
importedDocuments++
|
||||
}
|
||||
|
||||
importedCollections++
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Database '%s' imported successfully from %s\n", dbName, fileName)
|
||||
fmt.Printf(" Collections imported: %d\n", importedCollections)
|
||||
fmt.Printf(" Documents imported: %d\n", importedDocuments)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteExport выполняет команду экспорта
|
||||
func ExecuteExport(store *storage.Storage, cmd string) error {
|
||||
// Формат: export "Имя_слайса" "название_экспортируемого_файла".msgpack
|
||||
parts := strings.SplitN(cmd, " ", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: export \"database_name\" \"filename.msgpack\"")
|
||||
}
|
||||
|
||||
dbName := strings.Trim(parts[1], "\"")
|
||||
fileName := strings.Trim(parts[2], "\"")
|
||||
|
||||
// Проверяем расширение файла
|
||||
if !strings.HasSuffix(fileName, ".msgpack") {
|
||||
fileName = fileName + ".msgpack"
|
||||
}
|
||||
|
||||
return ExportData(store, dbName, fileName)
|
||||
}
|
||||
|
||||
// ExecuteImport выполняет команду импорта
|
||||
func ExecuteImport(store *storage.Storage, cmd string) error {
|
||||
// Формат: import "Имя_слайса" "название_импортируемого_файла".msgpack
|
||||
parts := strings.SplitN(cmd, " ", 3)
|
||||
if len(parts) < 3 {
|
||||
return fmt.Errorf("usage: import \"database_name\" \"filename.msgpack\"")
|
||||
}
|
||||
|
||||
dbName := strings.Trim(parts[1], "\"")
|
||||
fileName := strings.Trim(parts[2], "\"")
|
||||
|
||||
// Проверяем расширение файла
|
||||
if !strings.HasSuffix(fileName, ".msgpack") {
|
||||
fileName = fileName + ".msgpack"
|
||||
}
|
||||
|
||||
return ImportData(store, dbName, fileName)
|
||||
}
|
||||
Reference in New Issue
Block a user