// internal/config/userdata.go package config import ( "crypto/sha256" "encoding/hex" "os" "path/filepath" "runtime" "strings" ) // GetUserDataDir returns the appropriate directory for storing application data // following platform-specific conventions: // - Windows: %LOCALAPPDATA%\{appName} // - macOS: ~/Library/Application Support/{appName} // - Linux: ~/.local/share/{appName} (or $XDG_DATA_HOME/{appName}) func GetUserDataDir(appName string) (string, error) { var baseDir string var err error switch runtime.GOOS { case "windows": // Use LOCALAPPDATA for application data on Windows baseDir = os.Getenv("LOCALAPPDATA") if baseDir == "" { // Fallback to APPDATA if LOCALAPPDATA is not set baseDir = os.Getenv("APPDATA") if baseDir == "" { // Last resort: use UserConfigDir baseDir, err = os.UserConfigDir() if err != nil { return "", err } } } case "darwin": // Use ~/Library/Application Support on macOS home, err := os.UserHomeDir() if err != nil { return "", err } baseDir = filepath.Join(home, "Library", "Application Support") default: // Linux and other Unix-like systems // Follow XDG Base Directory Specification if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" { baseDir = xdgData } else { home, err := os.UserHomeDir() if err != nil { return "", err } baseDir = filepath.Join(home, ".local", "share") } } // Combine with app name appDataDir := filepath.Join(baseDir, appName) // Create the directory if it doesn't exist with restrictive permissions if err := os.MkdirAll(appDataDir, 0700); err != nil { return "", err } return appDataDir, nil } // GetUserSpecificDataDir returns the data directory for a specific user. // User data is isolated by hashing the email to create a unique directory name. // This ensures: // 1. Different users have completely separate storage // 2. Email addresses are not exposed in directory names // 3. The same user always gets the same directory // // Directory structure: // // {appDataDir}/users/{emailHash}/ // ├── local_files/ # File and collection metadata (LevelDB) // ├── sync_state/ # Sync state (LevelDB) // ├── cache/ # Application cache (LevelDB) // └── files/ # Downloaded decrypted files // └── {collectionId}/ // └── {filename} func GetUserSpecificDataDir(appName, userEmail string) (string, error) { if userEmail == "" { return "", nil // No user logged in, return empty } appDataDir, err := GetUserDataDir(appName) if err != nil { return "", err } // Hash the email to create a privacy-preserving directory name emailHash := hashEmail(userEmail) // Create user-specific directory userDir := filepath.Join(appDataDir, "users", emailHash) // Create the directory with restrictive permissions (owner only) if err := os.MkdirAll(userDir, 0700); err != nil { return "", err } return userDir, nil } // GetUserFilesDir returns the directory where decrypted files are stored for a user. // Files are organized by collection: {userDir}/files/{collectionId}/{filename} func GetUserFilesDir(appName, userEmail string) (string, error) { userDir, err := GetUserSpecificDataDir(appName, userEmail) if err != nil { return "", err } if userDir == "" { return "", nil // No user logged in } filesDir := filepath.Join(userDir, "files") // Create with restrictive permissions if err := os.MkdirAll(filesDir, 0700); err != nil { return "", err } return filesDir, nil } // hashEmail creates a SHA256 hash of the email address (lowercase, trimmed). // Returns a shortened hash (first 16 characters) for more readable directory names // while still maintaining uniqueness. func hashEmail(email string) string { // Normalize email: lowercase and trim whitespace normalizedEmail := strings.ToLower(strings.TrimSpace(email)) // Create SHA256 hash hash := sha256.Sum256([]byte(normalizedEmail)) // Return first 16 characters of hex representation (64 bits of entropy is sufficient) return hex.EncodeToString(hash[:])[:16] } // GetEmailHashForPath returns the hash that would be used for a user's directory. // This can be used to check if a user's data exists without revealing the email. func GetEmailHashForPath(userEmail string) string { if userEmail == "" { return "" } return hashEmail(userEmail) } // GetUserSearchIndexDir returns the directory where the Bleve search index is stored. // Returns: {userDir}/search/index.bleve func GetUserSearchIndexDir(appName, userEmail string) (string, error) { userDir, err := GetUserSpecificDataDir(appName, userEmail) if err != nil { return "", err } if userDir == "" { return "", nil // No user logged in } searchIndexPath := filepath.Join(userDir, "search", "index.bleve") // Create parent directory with restrictive permissions searchDir := filepath.Join(userDir, "search") if err := os.MkdirAll(searchDir, 0700); err != nil { return "", err } return searchIndexPath, nil }