Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,175 @@
// 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
}