// Package storagemanager provides a service for managing user-specific storage. // It handles the lifecycle of storage instances, creating new storage when a user // logs in and cleaning up when they log out. // // Storage is organized as follows: // - Global storage (session): {appDir}/session/ - stores current login session // - User-specific storage: {appDir}/users/{emailHash}/ - stores user data // // This ensures: // 1. Different users have completely isolated data // 2. Dev and production modes have separate directories // 3. Email addresses are not exposed in directory names (hashed) package storagemanager import ( "fmt" "os" "path/filepath" "sync" "go.uber.org/zap" "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/config" collectionDomain "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/domain/collection" fileDomain "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/domain/file" syncstateDomain "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/domain/syncstate" collectionRepo "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/repo/collection" fileRepo "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/repo/file" syncstateRepo "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/repo/syncstate" "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/pkg/storage" "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/pkg/storage/leveldb" ) // Manager manages user-specific storage instances. // It creates storage when a user logs in and cleans up when they log out. type Manager struct { logger *zap.Logger mu sync.RWMutex // Current user's email (empty if no user is logged in) currentUserEmail string // User-specific storage instances localFilesStorage storage.Storage syncStateStorage storage.Storage // User-specific repositories (built on top of storage) fileRepo fileDomain.Repository collectionRepo collectionDomain.Repository syncStateRepo syncstateDomain.Repository } // ProvideManager creates a new storage manager. func ProvideManager(logger *zap.Logger) *Manager { return &Manager{ logger: logger.Named("storage-manager"), } } // InitializeForUser initializes user-specific storage for the given user. // This should be called after a user successfully logs in. // If storage is already initialized for a different user, it will be cleaned up first. func (m *Manager) InitializeForUser(userEmail string) error { m.mu.Lock() defer m.mu.Unlock() if userEmail == "" { return fmt.Errorf("user email is required") } // If same user, no need to reinitialize if m.currentUserEmail == userEmail && m.localFilesStorage != nil { m.logger.Debug("Storage already initialized for user", zap.String("email_hash", config.GetEmailHashForPath(userEmail))) return nil } // Clean up existing storage if different user if m.currentUserEmail != "" && m.currentUserEmail != userEmail { m.logger.Info("Switching user storage", zap.String("old_user_hash", config.GetEmailHashForPath(m.currentUserEmail)), zap.String("new_user_hash", config.GetEmailHashForPath(userEmail))) m.cleanupStorageUnsafe() } m.logger.Info("Initializing storage for user", zap.String("email_hash", config.GetEmailHashForPath(userEmail))) // Initialize local files storage localFilesProvider, err := config.NewLevelDBConfigurationProviderForLocalFilesWithUser(userEmail) if err != nil { return fmt.Errorf("failed to create local files storage provider: %w", err) } m.localFilesStorage = leveldb.NewDiskStorage(localFilesProvider, m.logger.Named("local-files")) // Initialize sync state storage syncStateProvider, err := config.NewLevelDBConfigurationProviderForSyncStateWithUser(userEmail) if err != nil { m.cleanupStorageUnsafe() return fmt.Errorf("failed to create sync state storage provider: %w", err) } m.syncStateStorage = leveldb.NewDiskStorage(syncStateProvider, m.logger.Named("sync-state")) // Create repositories m.fileRepo = fileRepo.ProvideRepository(m.localFilesStorage) m.collectionRepo = collectionRepo.ProvideRepository(m.localFilesStorage) m.syncStateRepo = syncstateRepo.ProvideRepository(m.syncStateStorage) m.currentUserEmail = userEmail m.logger.Info("Storage initialized successfully", zap.String("email_hash", config.GetEmailHashForPath(userEmail))) return nil } // Cleanup cleans up all user-specific storage. // This should be called when a user logs out. func (m *Manager) Cleanup() { m.mu.Lock() defer m.mu.Unlock() m.cleanupStorageUnsafe() } // cleanupStorageUnsafe cleans up storage without acquiring the lock. // Caller must hold the lock. func (m *Manager) cleanupStorageUnsafe() { if m.localFilesStorage != nil { if closer, ok := m.localFilesStorage.(interface{ Close() error }); ok { if err := closer.Close(); err != nil { m.logger.Warn("Failed to close local files storage", zap.Error(err)) } } m.localFilesStorage = nil } if m.syncStateStorage != nil { if closer, ok := m.syncStateStorage.(interface{ Close() error }); ok { if err := closer.Close(); err != nil { m.logger.Warn("Failed to close sync state storage", zap.Error(err)) } } m.syncStateStorage = nil } m.fileRepo = nil m.collectionRepo = nil m.syncStateRepo = nil m.currentUserEmail = "" m.logger.Debug("Storage cleaned up") } // IsInitialized returns true if storage has been initialized for a user. func (m *Manager) IsInitialized() bool { m.mu.RLock() defer m.mu.RUnlock() return m.localFilesStorage != nil } // GetCurrentUserEmail returns the email of the user for whom storage is initialized. // Returns empty string if no user storage is initialized. func (m *Manager) GetCurrentUserEmail() string { m.mu.RLock() defer m.mu.RUnlock() return m.currentUserEmail } // GetFileRepository returns the file repository for the current user. // Returns nil if storage is not initialized. func (m *Manager) GetFileRepository() fileDomain.Repository { m.mu.RLock() defer m.mu.RUnlock() return m.fileRepo } // GetCollectionRepository returns the collection repository for the current user. // Returns nil if storage is not initialized. func (m *Manager) GetCollectionRepository() collectionDomain.Repository { m.mu.RLock() defer m.mu.RUnlock() return m.collectionRepo } // GetSyncStateRepository returns the sync state repository for the current user. // Returns nil if storage is not initialized. func (m *Manager) GetSyncStateRepository() syncstateDomain.Repository { m.mu.RLock() defer m.mu.RUnlock() return m.syncStateRepo } // GetLocalFilesStorage returns the raw local files storage for the current user. // Returns nil if storage is not initialized. func (m *Manager) GetLocalFilesStorage() storage.Storage { m.mu.RLock() defer m.mu.RUnlock() return m.localFilesStorage } // DeleteUserData permanently deletes all local data for the specified user. // This includes all files, metadata, and sync state stored on this device. // IMPORTANT: This is a destructive operation and cannot be undone. // The user will need to re-download all files from the cloud after this operation. func (m *Manager) DeleteUserData(userEmail string) error { m.mu.Lock() defer m.mu.Unlock() if userEmail == "" { return fmt.Errorf("user email is required") } emailHash := config.GetEmailHashForPath(userEmail) m.logger.Info("Deleting all local data for user", zap.String("email_hash", emailHash)) // If this is the current user, clean up storage first if m.currentUserEmail == userEmail { m.cleanupStorageUnsafe() } // Get the user's data directory userDir, err := config.GetUserSpecificDataDir("maplefile", userEmail) if err != nil { m.logger.Error("Failed to get user data directory", zap.Error(err)) return fmt.Errorf("failed to get user data directory: %w", err) } // Check if the directory exists if _, err := os.Stat(userDir); os.IsNotExist(err) { m.logger.Debug("User data directory does not exist, nothing to delete", zap.String("email_hash", emailHash)) return nil } // Remove the entire user directory and all its contents if err := os.RemoveAll(userDir); err != nil { m.logger.Error("Failed to delete user data directory", zap.Error(err), zap.String("path", userDir)) return fmt.Errorf("failed to delete user data: %w", err) } m.logger.Info("Successfully deleted all local data for user", zap.String("email_hash", emailHash)) return nil } // GetUserDataSize returns the total size of local data stored for the specified user in bytes. // Returns 0 if no data exists or if there's an error calculating the size. func (m *Manager) GetUserDataSize(userEmail string) (int64, error) { if userEmail == "" { return 0, fmt.Errorf("user email is required") } userDir, err := config.GetUserSpecificDataDir("maplefile", userEmail) if err != nil { return 0, fmt.Errorf("failed to get user data directory: %w", err) } // Check if the directory exists if _, err := os.Stat(userDir); os.IsNotExist(err) { return 0, nil } var totalSize int64 err = filepath.Walk(userDir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil // Ignore errors and continue } if !info.IsDir() { totalSize += info.Size() } return nil }) if err != nil { m.logger.Warn("Error calculating user data size", zap.Error(err)) return totalSize, nil // Return what we have } return totalSize, nil }