175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
// 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
|
|
}
|