Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/core/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ func ReloadBackupManager() {
logger.Backup.Error("Failed to reload backup manager: " + err.Error())
return
}
logger.Backup.Info("Backup manager reloaded successfully")
}

func ReloadDiscordBot() {
Expand Down
33 changes: 23 additions & 10 deletions src/managers/backupmgr/backupinterface.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// backupinterface.go
package backupmgr

import (
"sync"
"time"

"github.com/JacksonTheMaster/StationeersServerUI/v5/src/config"
"github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger"
"github.com/google/uuid"
)

// GlobalBackupManager is the singleton instance of the backup manager
Expand All @@ -14,30 +15,39 @@ var GlobalBackupManager *BackupManager
// Track all HTTP handlers that need updating when manager changes
var activeHTTPHandlers []*HTTPHandler

// initMutex ensures thread-safe initialization of the global backup manager
var initMutex sync.Mutex

// InitGlobalBackupManager initializes the global backup manager instance
func InitGlobalBackupManager(config BackupConfig) error {
// Lock to prevent concurrent initialization
initMutex.Lock()
defer initMutex.Unlock()

// Shut down existing manager if it exists
if GlobalBackupManager != nil {
logger.Backup.Debug("Shutting down global backup manager")
logger.Backup.Debugf("%s Previous Backup manager found. Shutting it down.", config.Identifier)
GlobalBackupManager.Shutdown()
GlobalBackupManager = nil // Clear the manager to avoid stale references
}

logger.Backup.Debug("Initializing global backup manager")

GlobalBackupManager = NewBackupManager(config)
logger.Backup.Debugf("%s Creating a global backup manager with ID %s", config.Identifier, config.Identifier)
manager := NewBackupManager(config)
GlobalBackupManager = manager

// Update all active HTTP handlers with the new manager
for _, handler := range activeHTTPHandlers {
handler.manager = GlobalBackupManager
}

// Start the backup manager in a goroutine to avoid blocking
go func() {
if err := GlobalBackupManager.Start(); err != nil {
logger.Backup.Error("Failed to start global backup manager: " + err.Error())
go func(m *BackupManager) {
if err := m.Start(config.Identifier); err != nil {
logger.Backup.Warnf("%s Exited: "+err.Error(), config.Identifier)
}
}()
}(manager)

// Return immediately, initialization will complete in the background
logger.Backup.Infof("%s Backup manager reloaded successfully", config.Identifier)
return nil
}

Expand All @@ -49,6 +59,8 @@ func RegisterHTTPHandler(handler *HTTPHandler) {
// GetBackupConfig returns a properly configured BackupConfig
func GetBackupConfig() BackupConfig {

id := uuid.New()
bmIdentifier := "[BM" + id.String()[:6] + "]:"
return BackupConfig{
WorldName: config.GetSaveName(),
BackupDir: config.GetConfiguredBackupDir(),
Expand All @@ -61,6 +73,7 @@ func GetBackupConfig() BackupConfig {
KeepMonthlyFor: config.GetBackupKeepMonthlyFor(),
CleanupInterval: config.GetBackupCleanupInterval(),
},
Identifier: bmIdentifier,
}
}

Expand Down
68 changes: 42 additions & 26 deletions src/managers/backupmgr/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,55 @@ can coexist but may conflict if configured with overlapping directories.

// Initialize checks for BackupDir and waits until it exists, then ensures SafeBackupDir exists.
// It returns a channel that signals when initialization is complete or an error occurs.
func (m *BackupManager) Initialize() <-chan error {
func (m *BackupManager) Initialize(identifier string) <-chan error {
m.mu.Lock()
defer m.mu.Unlock()

result := make(chan error, 1)

go func() {
defer close(result)
const timeout = 90 * time.Minute
const pollInterval = 2500 * time.Millisecond
deadline := time.Now().Add(timeout)

// Wait for BackupDir to exist
for {
if _, err := os.Stat(m.config.BackupDir); err == nil {
// Directory exists, proceed
break
for time.Now().Before(deadline) {
if stat, err := os.Stat(m.config.BackupDir); err == nil {
if stat.IsDir() {
// Directory exists, proceed
logger.Backup.Debugf("%s found backup directory: %s", identifier, m.config.BackupDir)
break
}
result <- fmt.Errorf("%s backup path %s is not a directory", identifier, m.config.BackupDir)
return
} else if !os.IsNotExist(err) {
// An error other than "not exists" occurred
result <- fmt.Errorf("error checking backup directory %s: %v", m.config.BackupDir, err)
result <- fmt.Errorf("%s error checking backup directory %s: %v", identifier, m.config.BackupDir, err)
return
}

if time.Now().After(deadline) {
result <- fmt.Errorf("timeout waiting for backup directory %s to be created", m.config.BackupDir)
logger.Backup.Debugf("%s waiting for save folder "+m.config.BackupDir+" to be created by Stationeers...", identifier)
select {
case <-m.ctx.Done():
result <- fmt.Errorf("%s I have to go, the config was likely changed: %s", identifier, m.ctx.Err())
return
case <-time.After(pollInterval):
// Continue polling
}
logger.Backup.Debug("Backup manager waiting for save folder " + m.config.BackupDir + " to be created by Stationeers...")
}

// Wait before checking again
time.Sleep(pollInterval)
if time.Now().After(deadline) {
result <- fmt.Errorf("%s timeout waiting for backup directory %s to be created", identifier, m.config.BackupDir)
return
}

// Ensure SafeBackupDir exists, create it if it doesn't
if err := os.MkdirAll(m.config.SafeBackupDir, os.ModePerm); err != nil {
result <- fmt.Errorf("error creating safe backup directory %s: %v", m.config.SafeBackupDir, err)
result <- fmt.Errorf("%s error creating safe backup directory %s: %v", identifier, m.config.SafeBackupDir, err)
return
}
logger.Backup.Debug("Backup manager created safebackups dir successfully")
logger.Backup.Debugf("%s created safebackups at %s", identifier, m.config.SafeBackupDir)

result <- nil
}()
Expand All @@ -69,22 +80,23 @@ func (m *BackupManager) Initialize() <-chan error {
}

// Start begins the backup monitoring and cleanup routines
func (m *BackupManager) Start() error {
func (m *BackupManager) Start(identifier string) error {
// Wait for initialization to complete
logger.Backup.Debug("Backup manager is waiting for save folder initialization...")
initResult := <-m.Initialize()

logger.Backup.Debugf("%s is waiting for save folder initialization...", identifier)
initResult := <-m.Initialize(identifier)
if initResult != nil {
return fmt.Errorf("failed to initialize backup manager: %w", initResult)
return fmt.Errorf("%s failed to initialize backup manager : %w", identifier, initResult)
}
logger.Backup.Info("Backup manager started")
logger.Backup.Infof("%s Backup manager instance started", identifier)

// Start file watcher
watcher, err := newFsWatcher(m.config.BackupDir)
watcher, err := newFsWatcher(m.config.BackupDir, identifier)
if err != nil {
return fmt.Errorf("failed to create autosave watcher: %w", err)
}
m.watcher = watcher
go m.watchBackups()
go m.watchBackups(identifier)

if config.GetIsCleanupEnabled() {
go m.startCleanupRoutine()
Expand All @@ -94,30 +106,31 @@ func (m *BackupManager) Start() error {
}

// watchBackups monitors the backup directory for new files
func (m *BackupManager) watchBackups() {
func (m *BackupManager) watchBackups(identifier string) {
m.wg.Add(1)
defer m.wg.Done()

logger.Backup.Debug("Starting backup file watcher...")
defer logger.Backup.Debug("Backup file watcher stopped")
logger.Backup.Debugf("%s Starting backup file watcher...", identifier)
defer logger.Backup.Debugf("%s Backup file watcher stopped", identifier)

for {
select {
case <-m.ctx.Done():
logger.Backup.Debugf("%s WatchBackups stopped due to context cancellation", identifier)
return
case event, ok := <-m.watcher.events:
if !ok {
return
}
if event.Op&fsnotify.Create == fsnotify.Create {
logger.Backup.Info("New backup file detected: " + event.Name)
logger.Backup.Infof("%s New backup file detected: %s", identifier, event.Name)
m.handleNewBackup(event.Name)
}
case err, ok := <-m.watcher.errors:
if !ok {
return
}
logger.Backup.Error("Backup watcher error: " + err.Error())
logger.Backup.Errorf("%s Backup watcher error: %s", identifier, err.Error())
}
}
}
Expand Down Expand Up @@ -178,6 +191,7 @@ func (m *BackupManager) startCleanupRoutine() {
for {
select {
case <-m.ctx.Done():
logger.Backup.Debug("Cleanup routine stopped due to context cancellation")
return
case <-ticker.C:
if err := m.Cleanup(); err != nil {
Expand Down Expand Up @@ -212,17 +226,19 @@ func (m *BackupManager) ListBackups(limit int) ([]BackupGroup, error) {

// Shutdown stops all backup operations
func (m *BackupManager) Shutdown() {
logger.Backup.Debug("Shutting down backup manager...")
logger.Backup.Debug("Shutting down previous backup manager...")

m.mu.Lock()
if m.cancel != nil {
m.cancel()
m.cancel = nil
logger.Backup.Debug("Context canceled for previous backup manager")
}

if m.watcher != nil {
m.watcher.close()
m.watcher = nil
logger.Backup.Debug("File watcher closed")
}
m.mu.Unlock()

Expand Down
2 changes: 1 addition & 1 deletion src/managers/backupmgr/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
func (m *BackupManager) RestoreBackup(index int) error {
m.mu.Lock()
defer m.mu.Unlock()
logger.Backup.Info("Restoring backup with index " + fmt.Sprintf("%d", index))
logger.Backup.Infof("Restoring backup with index %d", index)

groups, err := m.getBackupGroups()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions src/managers/backupmgr/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type BackupConfig struct {
SafeBackupDir string
RetentionPolicy RetentionPolicy
WaitTime time.Duration
Identifier string
}

// RetentionPolicy defines backup retention rules
Expand Down
14 changes: 7 additions & 7 deletions src/managers/backupmgr/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ type fsWatcher struct {
}

// newFsWatcher creates a new file system watcher
func newFsWatcher(path string) (*fsWatcher, error) {
func newFsWatcher(path string, identifier string) (*fsWatcher, error) {
// Normalize path
normalizedPath := filepath.Clean(path)
logger.Backup.Debug("Creating watcher for path: " + normalizedPath)
logger.Backup.Debugf("%s Creating watcher for path: %s", identifier, normalizedPath)

watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, fmt.Errorf("failed to create watcher: %w", err)
return nil, fmt.Errorf("%s failed to create watcher: %w", identifier, err)
}
logger.Backup.Debug("Watcher created successfully")
logger.Backup.Debugf("%s Watcher created successfully", identifier)

// Watch the root save path and all subdirectories
err = filepath.WalkDir(normalizedPath, func(subPath string, d os.DirEntry, err error) error {
Expand All @@ -37,16 +37,16 @@ func newFsWatcher(path string) (*fsWatcher, error) {
}
if d.IsDir() {
if err := watcher.Add(subPath); err != nil {
logger.Backup.Error("Failed to add subdir to watcher: " + subPath + ": " + err.Error())
logger.Backup.Errorf("%s Failed to add subdir %s to watcher: %s", identifier, subPath, err.Error())
} else {
logger.Backup.Debug("Successfully watching subdir: " + subPath)
logger.Backup.Debugf("%s Added subdir %s to watcher", identifier, subPath)
}
}
return nil
})
if err != nil {
watcher.Close()
return nil, fmt.Errorf("failed to add paths to watcher: %w", err)
return nil, fmt.Errorf("%s failed to add subdirectories to watcher: %w", identifier, err)
}

w := &fsWatcher{
Expand Down