798 lines
30 KiB
Go
798 lines
30 KiB
Go
/*
|
||
* Copyright 2026 Safronov Grigorii
|
||
*
|
||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
*
|
||
* You may obtain a copy of the License at
|
||
* https://opensource.org/licenses/CDDL-1.0
|
||
*/
|
||
|
||
// Файл: internal/storage/engine.go
|
||
// Назначение: In-memory движок хранения документов с поддержкой коллекций,
|
||
// слайсов (аналог БД), тапплов (аналог таблиц), полей и кортежей.
|
||
// Полностью wait-free с использованием sync.Map и атомарных операций.
|
||
|
||
package storage
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"futriis/internal/log"
|
||
"futriis/internal/serializer"
|
||
)
|
||
|
||
// Storage представляет основное хранилище баз данных
|
||
type Storage struct {
|
||
databases sync.Map // map[string]*Database
|
||
pageSize int64
|
||
logger *log.Logger
|
||
totalDocs atomic.Int64
|
||
createdAt int64
|
||
}
|
||
|
||
// Database представляет базу данных (аналог слайса в реляционных СУБД)
|
||
type Database struct {
|
||
name string
|
||
collections sync.Map // map[string]*Collection
|
||
createdAt int64
|
||
updatedAt int64
|
||
deletedAt int64
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// ExportMetadata содержит метаданные экспорта (добавлено)
|
||
type ExportMetadata struct {
|
||
DatabaseName string `msgpack:"database_name"`
|
||
ExportTime int64 `msgpack:"export_time"`
|
||
ExportTimeStr string `msgpack:"export_time_str"`
|
||
Version string `msgpack:"version"`
|
||
CollectionsCount int `msgpack:"collections_count"`
|
||
DocumentsCount int64 `msgpack:"documents_count"`
|
||
ExportedBy string `msgpack:"exported_by,omitempty"`
|
||
Additional map[string]interface{} `msgpack:"additional,omitempty"`
|
||
}
|
||
|
||
// ImportMetadata содержит метаданные импорта (добавлено)
|
||
type ImportMetadata struct {
|
||
DatabaseName string `msgpack:"database_name"`
|
||
ImportTime int64 `msgpack:"import_time"`
|
||
ImportTimeStr string `msgpack:"import_time_str"`
|
||
SourceFile string `msgpack:"source_file"`
|
||
SourceExportTime int64 `msgpack:"source_export_time,omitempty"`
|
||
CollectionsCount int `msgpack:"collections_count"`
|
||
DocumentsCount int64 `msgpack:"documents_count"`
|
||
SkippedDocuments int64 `msgpack:"skipped_documents"`
|
||
FailedDocuments int64 `msgpack:"failed_documents"`
|
||
ImportedBy string `msgpack:"imported_by,omitempty"`
|
||
}
|
||
|
||
// NewStorage создаёт новый экземпляр хранилища
|
||
func NewStorage(pageSizeMB int, logger *log.Logger) *Storage {
|
||
return &Storage{
|
||
pageSize: int64(pageSizeMB) * 1024 * 1024,
|
||
logger: logger,
|
||
createdAt: time.Now().UnixMilli(),
|
||
}
|
||
}
|
||
|
||
// CreateDatabase создаёт новую базу данных
|
||
func (s *Storage) CreateDatabase(name string) error {
|
||
now := time.Now().UnixMilli()
|
||
db := &Database{
|
||
name: name,
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
deletedAt: 0,
|
||
}
|
||
|
||
if _, exists := s.databases.LoadOrStore(name, db); exists {
|
||
return fmt.Errorf("database already exists")
|
||
}
|
||
AuditDatabaseOperation("CREATE", name)
|
||
if s.logger != nil {
|
||
s.logger.Info("Database created: " + name)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetDatabase возвращает базу данных по имени
|
||
func (s *Storage) GetDatabase(name string) (*Database, error) {
|
||
if val, ok := s.databases.Load(name); ok {
|
||
return val.(*Database), nil
|
||
}
|
||
return nil, fmt.Errorf("database not found")
|
||
}
|
||
|
||
// DropDatabase удаляет базу данных
|
||
func (s *Storage) DropDatabase(name string) error {
|
||
if val, ok := s.databases.Load(name); ok {
|
||
db := val.(*Database)
|
||
db.mu.Lock()
|
||
db.deletedAt = time.Now().UnixMilli()
|
||
db.mu.Unlock()
|
||
}
|
||
|
||
if _, ok := s.databases.LoadAndDelete(name); !ok {
|
||
return fmt.Errorf("database not found")
|
||
}
|
||
AuditDatabaseOperation("DROP", name)
|
||
if s.logger != nil {
|
||
s.logger.Info("Database dropped: " + name)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ListDatabases возвращает список всех баз данных
|
||
func (s *Storage) ListDatabases() []string {
|
||
databases := make([]string, 0)
|
||
s.databases.Range(func(key, value interface{}) bool {
|
||
databases = append(databases, key.(string))
|
||
return true
|
||
})
|
||
return databases
|
||
}
|
||
|
||
// Name возвращает имя базы данных
|
||
func (db *Database) Name() string {
|
||
return db.name
|
||
}
|
||
|
||
// GetCreatedAt возвращает время создания базы данных
|
||
func (db *Database) GetCreatedAt() int64 {
|
||
db.mu.RLock()
|
||
defer db.mu.RUnlock()
|
||
return db.createdAt
|
||
}
|
||
|
||
// GetUpdatedAt возвращает время последнего обновления базы данных
|
||
func (db *Database) GetUpdatedAt() int64 {
|
||
db.mu.RLock()
|
||
defer db.mu.RUnlock()
|
||
return db.updatedAt
|
||
}
|
||
|
||
// GetDeletedAt возвращает время удаления базы данных (0 = не удалена)
|
||
func (db *Database) GetDeletedAt() int64 {
|
||
db.mu.RLock()
|
||
defer db.mu.RUnlock()
|
||
return db.deletedAt
|
||
}
|
||
|
||
// UpdateTimestamp обновляет временную метку базы данных
|
||
func (db *Database) UpdateTimestamp() {
|
||
db.mu.Lock()
|
||
defer db.mu.Unlock()
|
||
db.updatedAt = time.Now().UnixMilli()
|
||
}
|
||
|
||
// CreateCollection создаёт новую коллекцию в базе данных
|
||
func (db *Database) CreateCollection(name string) error {
|
||
if _, exists := db.collections.LoadOrStore(name, NewCollection(db.name, name, nil)); exists {
|
||
return fmt.Errorf("collection already exists")
|
||
}
|
||
db.UpdateTimestamp()
|
||
AuditCollectionOperation("CREATE", db.name, name, nil)
|
||
return nil
|
||
}
|
||
|
||
// CreateCollectionWithSettings создаёт коллекцию с настройками
|
||
func (db *Database) CreateCollectionWithSettings(name string, settings *CollectionSettings) error {
|
||
if _, exists := db.collections.LoadOrStore(name, NewCollection(db.name, name, settings)); exists {
|
||
return fmt.Errorf("collection already exists")
|
||
}
|
||
db.UpdateTimestamp()
|
||
AuditCollectionOperation("CREATE", db.name, name, settings)
|
||
return nil
|
||
}
|
||
|
||
// GetCollection возвращает коллекцию по имени
|
||
func (db *Database) GetCollection(name string) (*Collection, error) {
|
||
if val, ok := db.collections.Load(name); ok {
|
||
return val.(*Collection), nil
|
||
}
|
||
return nil, fmt.Errorf("collection not found")
|
||
}
|
||
|
||
// DropCollection удаляет коллекцию
|
||
func (db *Database) DropCollection(name string) error {
|
||
if _, ok := db.collections.LoadAndDelete(name); !ok {
|
||
return fmt.Errorf("collection not found")
|
||
}
|
||
db.UpdateTimestamp()
|
||
AuditCollectionOperation("DROP", db.name, name, nil)
|
||
return nil
|
||
}
|
||
|
||
// ListCollections возвращает список всех коллекций в базе данных
|
||
func (db *Database) ListCollections() []string {
|
||
collections := make([]string, 0)
|
||
db.collections.Range(func(key, value interface{}) bool {
|
||
collections = append(collections, key.(string))
|
||
return true
|
||
})
|
||
return collections
|
||
}
|
||
|
||
// GetTotalDocuments возвращает общее количество документов во всех коллекциях
|
||
func (s *Storage) GetTotalDocuments() int64 {
|
||
return s.totalDocs.Load()
|
||
}
|
||
|
||
// GetPageSize возвращает размер страницы памяти
|
||
func (s *Storage) GetPageSize() int64 {
|
||
return s.pageSize
|
||
}
|
||
|
||
// GetCreatedAt возвращает время создания хранилища
|
||
func (s *Storage) GetCreatedAt() int64 {
|
||
return s.createdAt
|
||
}
|
||
|
||
// SerializeDatabaseWithMetadata сериализует базу данных с метаданными экспорта (модифицировано)
|
||
func (db *Database) SerializeDatabaseWithMetadata(exportedBy string) ([]byte, *ExportMetadata, error) {
|
||
dbData := make(map[string]interface{})
|
||
|
||
totalDocuments := int64(0)
|
||
|
||
db.collections.Range(func(key, value interface{}) bool {
|
||
coll := value.(*Collection)
|
||
collData := make(map[string]interface{})
|
||
|
||
docs := coll.GetAllDocuments()
|
||
totalDocuments += int64(len(docs))
|
||
|
||
collDocs := make([]*Document, 0, len(docs))
|
||
for _, doc := range docs {
|
||
collDocs = append(collDocs, doc)
|
||
}
|
||
collData["documents"] = collDocs
|
||
collData["metadata"] = coll.GetMetadata()
|
||
collData["timestamps"] = coll.GetTimestamps()
|
||
|
||
// Добавляем информацию об индексах при экспорте
|
||
collData["indexes"] = coll.GetIndexesInfo()
|
||
|
||
// Добавляем ограничения при экспорте
|
||
collData["constraints"] = coll.GetConstraints()
|
||
collData["constraint_timestamps"] = coll.GetConstraintTimestamps()
|
||
|
||
// Добавляем ACL при экспорте
|
||
collData["acl_timestamps"] = coll.GetACLTimestamps()
|
||
|
||
dbData[key.(string)] = collData
|
||
return true
|
||
})
|
||
|
||
nowMs, nowStr := GetCurrentTimestamp()
|
||
metadata := &ExportMetadata{
|
||
DatabaseName: db.name,
|
||
ExportTime: nowMs,
|
||
ExportTimeStr: nowStr,
|
||
Version: "1.0",
|
||
CollectionsCount: len(db.ListCollections()),
|
||
DocumentsCount: totalDocuments,
|
||
ExportedBy: exportedBy,
|
||
Additional: map[string]interface{}{
|
||
"storage_created_at": time.UnixMilli(db.createdAt).Format("2006-01-02 15:04:05.000"),
|
||
},
|
||
}
|
||
|
||
// Добавляем метаданные базы данных
|
||
dbData["_metadata"] = map[string]interface{}{
|
||
"name": db.name,
|
||
"created_at": db.GetCreatedAt(),
|
||
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
|
||
"updated_at": db.GetUpdatedAt(),
|
||
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
|
||
"export_metadata": metadata,
|
||
}
|
||
|
||
data, err := serializer.Marshal(dbData)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
|
||
return data, metadata, nil
|
||
}
|
||
|
||
// SerializeDatabase сериализует всю базу данных в MessagePack (сохранён для совместимости)
|
||
func (db *Database) SerializeDatabase() ([]byte, error) {
|
||
data, _, err := db.SerializeDatabaseWithMetadata("")
|
||
return data, err
|
||
}
|
||
|
||
// DeserializeDatabaseWithMetadata десериализует базу данных с сохранением метаданных импорта (модифицировано)
|
||
func (db *Database) DeserializeDatabaseWithMetadata(data []byte, sourceFile string, importedBy string) (*ImportMetadata, error) {
|
||
var dbData map[string]interface{}
|
||
if err := serializer.Unmarshal(data, &dbData); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var sourceExportTime int64
|
||
var collectionsCount int
|
||
var documentsCount int64
|
||
|
||
// Извлекаем метаданные
|
||
if metaRaw, ok := dbData["_metadata"]; ok {
|
||
if meta, ok := metaRaw.(map[string]interface{}); ok {
|
||
if createdAt, ok := meta["created_at"]; ok {
|
||
if v, ok := createdAt.(int64); ok {
|
||
db.createdAt = v
|
||
}
|
||
}
|
||
if exportMetaRaw, ok := meta["export_metadata"]; ok {
|
||
if exportMeta, ok := exportMetaRaw.(map[string]interface{}); ok {
|
||
if sourceTime, ok := exportMeta["export_time"]; ok {
|
||
if v, ok := sourceTime.(int64); ok {
|
||
sourceExportTime = v
|
||
} else if v, ok := sourceTime.(float64); ok {
|
||
sourceExportTime = int64(v)
|
||
}
|
||
}
|
||
if collCount, ok := exportMeta["collections_count"]; ok {
|
||
if v, ok := collCount.(int); ok {
|
||
collectionsCount = v
|
||
} else if v, ok := collCount.(int64); ok {
|
||
collectionsCount = int(v)
|
||
} else if v, ok := collCount.(float64); ok {
|
||
collectionsCount = int(v)
|
||
}
|
||
}
|
||
if docCount, ok := exportMeta["documents_count"]; ok {
|
||
if v, ok := docCount.(int64); ok {
|
||
documentsCount = v
|
||
} else if v, ok := docCount.(float64); ok {
|
||
documentsCount = int64(v)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
delete(dbData, "_metadata")
|
||
}
|
||
|
||
for collName, collDataRaw := range dbData {
|
||
collData, ok := collDataRaw.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
settings := &CollectionSettings{
|
||
MaxDocuments: 0,
|
||
ValidateSchema: false,
|
||
AutoIndexID: true,
|
||
TTLSeconds: 0,
|
||
SoftDelete: false,
|
||
}
|
||
|
||
if metaRaw, ok := collData["metadata"]; ok {
|
||
if meta, ok := metaRaw.(map[string]interface{}); ok {
|
||
if settingsRaw, ok := meta["settings"]; ok {
|
||
if settingsMap, ok := settingsRaw.(map[string]interface{}); ok {
|
||
if maxDocs, ok := settingsMap["max_documents"]; ok {
|
||
if v, ok := maxDocs.(int); ok {
|
||
settings.MaxDocuments = v
|
||
} else if v, ok := maxDocs.(int64); ok {
|
||
settings.MaxDocuments = int(v)
|
||
} else if v, ok := maxDocs.(float64); ok {
|
||
settings.MaxDocuments = int(v)
|
||
}
|
||
}
|
||
if validateSchema, ok := settingsMap["validate_schema"]; ok {
|
||
if v, ok := validateSchema.(bool); ok {
|
||
settings.ValidateSchema = v
|
||
}
|
||
}
|
||
if autoIndexID, ok := settingsMap["auto_index_id"]; ok {
|
||
if v, ok := autoIndexID.(bool); ok {
|
||
settings.AutoIndexID = v
|
||
}
|
||
}
|
||
if ttlSeconds, ok := settingsMap["ttl_seconds"]; ok {
|
||
if v, ok := ttlSeconds.(int); ok {
|
||
settings.TTLSeconds = v
|
||
} else if v, ok := ttlSeconds.(int64); ok {
|
||
settings.TTLSeconds = int(v)
|
||
} else if v, ok := ttlSeconds.(float64); ok {
|
||
settings.TTLSeconds = int(v)
|
||
}
|
||
}
|
||
if softDelete, ok := settingsMap["soft_delete"]; ok {
|
||
if v, ok := softDelete.(bool); ok {
|
||
settings.SoftDelete = v
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if meta, ok := metaRaw.(*CollectionMetadata); ok {
|
||
if meta.Settings != nil {
|
||
settings = meta.Settings
|
||
}
|
||
}
|
||
}
|
||
|
||
coll := NewCollection(db.name, collName, settings)
|
||
|
||
// Восстанавливаем временные метки коллекции если они есть
|
||
if timestampsRaw, ok := collData["timestamps"]; ok {
|
||
if timestamps, ok := timestampsRaw.(map[string]interface{}); ok {
|
||
if createdAt, ok := timestamps["created_at"]; ok {
|
||
if v, ok := createdAt.(int64); ok {
|
||
coll.metadata.CreatedAt = v
|
||
} else if v, ok := createdAt.(float64); ok {
|
||
coll.metadata.CreatedAt = int64(v)
|
||
}
|
||
}
|
||
if updatedAt, ok := timestamps["updated_at"]; ok {
|
||
if v, ok := updatedAt.(int64); ok {
|
||
coll.metadata.UpdatedAt = v
|
||
} else if v, ok := updatedAt.(float64); ok {
|
||
coll.metadata.UpdatedAt = int64(v)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Восстанавливаем индексы
|
||
if indexesRaw, ok := collData["indexes"]; ok {
|
||
if indexesList, ok := indexesRaw.([]interface{}); ok {
|
||
for _, idxRaw := range indexesList {
|
||
if idxMap, ok := idxRaw.(map[string]interface{}); ok {
|
||
idxName, _ := idxMap["name"].(string)
|
||
idxFieldsRaw, _ := idxMap["fields"].([]interface{})
|
||
idxFields := make([]string, len(idxFieldsRaw))
|
||
for i, f := range idxFieldsRaw {
|
||
idxFields[i] = fmt.Sprintf("%v", f)
|
||
}
|
||
idxUnique, _ := idxMap["unique"].(bool)
|
||
if idxName != "" && idxName != "_id_" {
|
||
coll.CreateIndex(idxName, idxFields, idxUnique)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if docsRaw, ok := collData["documents"]; ok {
|
||
if docs, ok := docsRaw.([]interface{}); ok {
|
||
for _, docRaw := range docs {
|
||
if doc, ok := docRaw.(*Document); ok {
|
||
coll.Insert(doc)
|
||
} else if docMap, ok := docRaw.(map[string]interface{}); ok {
|
||
doc := NewDocument()
|
||
if id, ok := docMap["ID"].(string); ok {
|
||
doc.ID = id
|
||
} else if id, ok := docMap["id"].(string); ok {
|
||
doc.ID = id
|
||
}
|
||
if fields, ok := docMap["fields"]; ok {
|
||
if fieldsMap, ok := fields.(map[string]interface{}); ok {
|
||
for k, v := range fieldsMap {
|
||
doc.SetField(k, v)
|
||
}
|
||
}
|
||
}
|
||
if createdAt, ok := docMap["created_at"]; ok {
|
||
if v, ok := createdAt.(int64); ok {
|
||
doc.CreatedAt = v
|
||
} else if v, ok := createdAt.(float64); ok {
|
||
doc.CreatedAt = int64(v)
|
||
}
|
||
}
|
||
if updatedAt, ok := docMap["updated_at"]; ok {
|
||
if v, ok := updatedAt.(int64); ok {
|
||
doc.UpdatedAt = v
|
||
} else if v, ok := updatedAt.(float64); ok {
|
||
doc.UpdatedAt = int64(v)
|
||
}
|
||
}
|
||
if deletedAt, ok := docMap["deleted_at"]; ok {
|
||
if v, ok := deletedAt.(int64); ok {
|
||
doc.DeletedAt = v
|
||
} else if v, ok := deletedAt.(float64); ok {
|
||
doc.DeletedAt = int64(v)
|
||
}
|
||
}
|
||
if version, ok := docMap["version"]; ok {
|
||
if v, ok := version.(uint64); ok {
|
||
doc.Version = v
|
||
} else if v, ok := version.(int64); ok {
|
||
doc.Version = uint64(v)
|
||
} else if v, ok := version.(float64); ok {
|
||
doc.Version = uint64(v)
|
||
}
|
||
}
|
||
coll.Insert(doc)
|
||
}
|
||
}
|
||
} else if docs, ok := docsRaw.([]*Document); ok {
|
||
for _, doc := range docs {
|
||
coll.Insert(doc)
|
||
}
|
||
}
|
||
}
|
||
|
||
db.collections.Store(collName, coll)
|
||
AuditCollectionOperation("RESTORE", db.name, collName, settings)
|
||
}
|
||
|
||
nowMs, nowStr := GetCurrentTimestamp()
|
||
importMetadata := &ImportMetadata{
|
||
DatabaseName: db.name,
|
||
ImportTime: nowMs,
|
||
ImportTimeStr: nowStr,
|
||
SourceFile: sourceFile,
|
||
SourceExportTime: sourceExportTime,
|
||
CollectionsCount: collectionsCount,
|
||
DocumentsCount: documentsCount,
|
||
SkippedDocuments: 0,
|
||
FailedDocuments: 0,
|
||
ImportedBy: importedBy,
|
||
}
|
||
|
||
db.UpdateTimestamp()
|
||
|
||
// Аудит импорта
|
||
LogAudit("IMPORT", "DATABASE", db.name, map[string]interface{}{
|
||
"source_file": sourceFile,
|
||
"collections_count": collectionsCount,
|
||
"documents_count": documentsCount,
|
||
"source_export_time": sourceExportTime,
|
||
"imported_by": importedBy,
|
||
})
|
||
|
||
return importMetadata, nil
|
||
}
|
||
|
||
// DeserializeDatabase десериализует базу данных из MessagePack (сохранён для совместимости)
|
||
func (db *Database) DeserializeDatabase(data []byte) error {
|
||
_, err := db.DeserializeDatabaseWithMetadata(data, "", "")
|
||
return err
|
||
}
|
||
|
||
// ExportDatabaseWithMetadata экспортирует базу данных с метаданными (добавлено)
|
||
func (s *Storage) ExportDatabaseWithMetadata(dbName, filePath, exportedBy string) (*ExportMetadata, error) {
|
||
if !s.ExistsDatabase(dbName) {
|
||
return nil, fmt.Errorf("database '%s' not found", dbName)
|
||
}
|
||
|
||
db, err := s.GetDatabase(dbName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
data, metadata, err := db.SerializeDatabaseWithMetadata(exportedBy)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||
return nil, fmt.Errorf("failed to write export file: %v", err)
|
||
}
|
||
|
||
if s.logger != nil {
|
||
s.logger.Info(fmt.Sprintf("Database '%s' exported to %s at %s", dbName, filePath, metadata.ExportTimeStr))
|
||
}
|
||
|
||
return metadata, nil
|
||
}
|
||
|
||
// ImportDatabaseWithMetadata импортирует базу данных с метаданными (добавлено)
|
||
func (s *Storage) ImportDatabaseWithMetadata(dbName, filePath, importedBy string) (*ImportMetadata, error) {
|
||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||
return nil, fmt.Errorf("import file '%s' not found", filePath)
|
||
}
|
||
|
||
data, err := os.ReadFile(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to read import file: %v", err)
|
||
}
|
||
|
||
if !s.ExistsDatabase(dbName) {
|
||
if err := s.CreateDatabase(dbName); err != nil {
|
||
return nil, fmt.Errorf("failed to create database: %v", err)
|
||
}
|
||
}
|
||
|
||
db, err := s.GetDatabase(dbName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
metadata, err := db.DeserializeDatabaseWithMetadata(data, filePath, importedBy)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if s.logger != nil {
|
||
s.logger.Info(fmt.Sprintf("Database '%s' imported from %s at %s", dbName, filePath, metadata.ImportTimeStr))
|
||
}
|
||
|
||
return metadata, nil
|
||
}
|
||
|
||
// GetDatabaseNames возвращает имена всех баз данных
|
||
func (s *Storage) GetDatabaseNames() []string {
|
||
return s.ListDatabases()
|
||
}
|
||
|
||
// ExistsDatabase проверяет существование базы данных
|
||
func (s *Storage) ExistsDatabase(name string) bool {
|
||
_, ok := s.databases.Load(name)
|
||
return ok
|
||
}
|
||
|
||
// GetDatabaseCount возвращает количество баз данных
|
||
func (s *Storage) GetDatabaseCount() int {
|
||
count := 0
|
||
s.databases.Range(func(key, value interface{}) bool {
|
||
count++
|
||
return true
|
||
})
|
||
return count
|
||
}
|
||
|
||
// ========== Дополнительные методы для управления хранилищем ==========
|
||
|
||
// Backup создаёт резервную копию всех данных
|
||
func (s *Storage) Backup(backupPath string) error {
|
||
if s.logger != nil {
|
||
s.logger.Info(fmt.Sprintf("Starting backup to %s", backupPath))
|
||
}
|
||
|
||
backup := make(map[string]interface{})
|
||
backup["created_at"] = s.createdAt
|
||
backup["backup_time"] = time.Now().UnixMilli()
|
||
backup["backup_time_str"] = time.Now().Format("2006-01-02 15:04:05.000")
|
||
|
||
databases := make(map[string][]byte)
|
||
databasesMetadata := make(map[string]interface{})
|
||
|
||
s.databases.Range(func(key, value interface{}) bool {
|
||
dbName := key.(string)
|
||
db := value.(*Database)
|
||
|
||
dbData, err := db.SerializeDatabase()
|
||
if err != nil {
|
||
if s.logger != nil {
|
||
s.logger.Error(fmt.Sprintf("Failed to serialize database %s: %v", dbName, err))
|
||
}
|
||
return false
|
||
}
|
||
|
||
databases[dbName] = dbData
|
||
databasesMetadata[dbName] = map[string]interface{}{
|
||
"created_at": db.GetCreatedAt(),
|
||
"updated_at": db.GetUpdatedAt(),
|
||
"collections": len(db.ListCollections()),
|
||
}
|
||
return true
|
||
})
|
||
|
||
backup["databases"] = databases
|
||
backup["databases_metadata"] = databasesMetadata
|
||
backup["total_databases"] = s.GetDatabaseCount()
|
||
|
||
data, err := serializer.Marshal(backup)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to marshal backup: %v", err)
|
||
}
|
||
|
||
if err := os.WriteFile(backupPath, data, 0644); err != nil {
|
||
return fmt.Errorf("failed to write backup: %v", err)
|
||
}
|
||
|
||
if s.logger != nil {
|
||
s.logger.Info(fmt.Sprintf("Backup completed: %s", backupPath))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Restore восстанавливает данные из резервной копии
|
||
func (s *Storage) Restore(backupPath string) error {
|
||
if s.logger != nil {
|
||
s.logger.Info(fmt.Sprintf("Starting restore from %s", backupPath))
|
||
}
|
||
|
||
data, err := os.ReadFile(backupPath)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read backup file: %v", err)
|
||
}
|
||
|
||
var backup map[string]interface{}
|
||
if err := serializer.Unmarshal(data, &backup); err != nil {
|
||
return fmt.Errorf("failed to unmarshal backup: %v", err)
|
||
}
|
||
|
||
// Восстанавливаем временную метку хранилища
|
||
if createdAt, ok := backup["created_at"]; ok {
|
||
if v, ok := createdAt.(int64); ok {
|
||
s.createdAt = v
|
||
}
|
||
}
|
||
|
||
databasesRaw, ok := backup["databases"]
|
||
if !ok {
|
||
return fmt.Errorf("invalid backup format: missing databases")
|
||
}
|
||
|
||
databases, ok := databasesRaw.(map[string]interface{})
|
||
if !ok {
|
||
return fmt.Errorf("invalid backup format: databases is not a map")
|
||
}
|
||
|
||
for dbName, dbDataRaw := range databases {
|
||
dbData, ok := dbDataRaw.([]byte)
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
if !s.ExistsDatabase(dbName) {
|
||
if err := s.CreateDatabase(dbName); err != nil {
|
||
return fmt.Errorf("failed to create database %s: %v", dbName, err)
|
||
}
|
||
}
|
||
|
||
db, err := s.GetDatabase(dbName)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to get database %s: %v", dbName, err)
|
||
}
|
||
|
||
if err := db.DeserializeDatabase(dbData); err != nil {
|
||
return fmt.Errorf("failed to restore database %s: %v", dbName, err)
|
||
}
|
||
}
|
||
|
||
if s.logger != nil {
|
||
s.logger.Info("Restore completed")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetStats возвращает статистику хранилища
|
||
func (s *Storage) GetStats() map[string]interface{} {
|
||
stats := map[string]interface{}{
|
||
"total_databases": s.GetDatabaseCount(),
|
||
"total_documents": s.GetTotalDocuments(),
|
||
"page_size_bytes": s.pageSize,
|
||
"created_at": s.createdAt,
|
||
"created_at_str": time.UnixMilli(s.createdAt).Format("2006-01-02 15:04:05.000"),
|
||
}
|
||
|
||
databases := make([]map[string]interface{}, 0)
|
||
s.databases.Range(func(key, value interface{}) bool {
|
||
db := value.(*Database)
|
||
dbStats := map[string]interface{}{
|
||
"name": db.name,
|
||
"collections": len(db.ListCollections()),
|
||
"created_at": db.GetCreatedAt(),
|
||
"created_at_str": time.UnixMilli(db.GetCreatedAt()).Format("2006-01-02 15:04:05.000"),
|
||
"updated_at": db.GetUpdatedAt(),
|
||
"updated_at_str": time.UnixMilli(db.GetUpdatedAt()).Format("2006-01-02 15:04:05.000"),
|
||
"deleted_at": db.GetDeletedAt(),
|
||
}
|
||
|
||
totalDocs := int64(0)
|
||
totalSize := int64(0)
|
||
db.collections.Range(func(k, v interface{}) bool {
|
||
coll := v.(*Collection)
|
||
totalDocs += coll.Count()
|
||
totalSize += coll.Size()
|
||
return true
|
||
})
|
||
|
||
dbStats["documents"] = totalDocs
|
||
dbStats["size_bytes"] = totalSize
|
||
databases = append(databases, dbStats)
|
||
return true
|
||
})
|
||
|
||
stats["databases"] = databases
|
||
return stats
|
||
}
|