monorepo/native/desktop/maplefile/internal/service/storagemanager/manager.go

284 lines
9.3 KiB
Go

// 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
}