284 lines
9.3 KiB
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
|
|
}
|