Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,284 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue