270 lines
9.7 KiB
Go
270 lines
9.7 KiB
Go
// Package config provides a unified API for managing application configuration
|
|
// Location: monorepo/native/desktop/maplefile/internal/config/config.go
|
|
package config
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// AppNameBase is the base name of the desktop application
|
|
AppNameBase = "maplefile"
|
|
// AppNameDev is the app name used in development mode
|
|
AppNameDev = "maplefile-dev"
|
|
)
|
|
|
|
// BuildMode is set at compile time via -ldflags
|
|
// Example: go build -ldflags "-X codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/config.BuildMode=dev"
|
|
// This must be set alongside the app.BuildMode for consistent behavior.
|
|
var BuildMode string
|
|
|
|
// GetAppName returns the appropriate app name based on the current mode.
|
|
// In dev mode, returns "maplefile-dev" to keep dev and production data separate.
|
|
// In production mode (or when mode is not set), returns "maplefile".
|
|
func GetAppName() string {
|
|
mode := GetBuildMode()
|
|
if mode == "dev" || mode == "development" {
|
|
return AppNameDev
|
|
}
|
|
return AppNameBase
|
|
}
|
|
|
|
// GetBuildMode returns the current build mode from environment or compile-time variable.
|
|
// Priority: 1) Environment variable, 2) Compile-time variable, 3) Default to production
|
|
// This is used early in initialization before the full app is set up.
|
|
func GetBuildMode() string {
|
|
// Check environment variable first
|
|
if mode := os.Getenv("MAPLEFILE_MODE"); mode != "" {
|
|
return mode
|
|
}
|
|
// Check compile-time variable
|
|
if BuildMode != "" {
|
|
return BuildMode
|
|
}
|
|
// Default to production (secure default)
|
|
return "production"
|
|
}
|
|
|
|
// Config holds all application configuration in a flat structure
|
|
type Config struct {
|
|
// CloudProviderAddress is the URI backend to make all calls to from this application for E2EE cloud operations.
|
|
CloudProviderAddress string `json:"cloud_provider_address"`
|
|
Credentials *Credentials `json:"credentials"`
|
|
|
|
// Desktop-specific settings
|
|
WindowWidth int `json:"window_width"`
|
|
WindowHeight int `json:"window_height"`
|
|
Theme string `json:"theme"` // light, dark, auto
|
|
Language string `json:"language"` // en, es, fr, etc.
|
|
SyncMode string `json:"sync_mode"` // encrypted_only, hybrid, decrypted_only
|
|
AutoSync bool `json:"auto_sync"` // Enable automatic synchronization
|
|
SyncIntervalMinutes int `json:"sync_interval_minutes"` // Sync interval in minutes
|
|
ShowHiddenFiles bool `json:"show_hidden_files"` // Show hidden files in file manager
|
|
DefaultView string `json:"default_view"` // list, grid
|
|
SortBy string `json:"sort_by"` // name, date, size, type
|
|
SortOrder string `json:"sort_order"` // asc, desc
|
|
}
|
|
|
|
// Credentials holds all user credentials for authentication and authorization.
|
|
// Values are decrypted for convenience purposes as we assume threat actor cannot access the decrypted values on the user's device.
|
|
type Credentials struct {
|
|
// Email is the unique registered email of the user whom successfully logged into the system.
|
|
Email string `json:"email"`
|
|
AccessToken string `json:"access_token"`
|
|
AccessTokenExpiryTime *time.Time `json:"access_token_expiry_time"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
RefreshTokenExpiryTime *time.Time `json:"refresh_token_expiry_time"`
|
|
}
|
|
|
|
// ConfigService defines the unified interface for all configuration operations
|
|
type ConfigService interface {
|
|
GetConfig(ctx context.Context) (*Config, error)
|
|
GetAppDataDirPath(ctx context.Context) (string, error)
|
|
GetCloudProviderAddress(ctx context.Context) (string, error)
|
|
SetCloudProviderAddress(ctx context.Context, address string) error
|
|
GetLoggedInUserCredentials(ctx context.Context) (*Credentials, error)
|
|
SetLoggedInUserCredentials(
|
|
ctx context.Context,
|
|
email string,
|
|
accessToken string,
|
|
accessTokenExpiryTime *time.Time,
|
|
refreshToken string,
|
|
refreshTokenExpiryTime *time.Time,
|
|
) error
|
|
ClearLoggedInUserCredentials(ctx context.Context) error
|
|
|
|
// User-specific storage methods
|
|
// These return paths that are isolated per user and per environment (dev/production)
|
|
GetUserDataDirPath(ctx context.Context, userEmail string) (string, error)
|
|
GetUserFilesDirPath(ctx context.Context, userEmail string) (string, error)
|
|
GetUserSearchIndexDir(ctx context.Context, userEmail string) (string, error)
|
|
GetLoggedInUserEmail(ctx context.Context) (string, error)
|
|
|
|
// Desktop-specific methods
|
|
GetWindowSize(ctx context.Context) (width int, height int, err error)
|
|
SetWindowSize(ctx context.Context, width int, height int) error
|
|
GetTheme(ctx context.Context) (string, error)
|
|
SetTheme(ctx context.Context, theme string) error
|
|
GetLanguage(ctx context.Context) (string, error)
|
|
SetLanguage(ctx context.Context, language string) error
|
|
GetSyncMode(ctx context.Context) (string, error)
|
|
SetSyncMode(ctx context.Context, mode string) error
|
|
GetAutoSync(ctx context.Context) (bool, error)
|
|
SetAutoSync(ctx context.Context, enabled bool) error
|
|
GetSyncInterval(ctx context.Context) (int, error)
|
|
SetSyncInterval(ctx context.Context, minutes int) error
|
|
GetShowHiddenFiles(ctx context.Context) (bool, error)
|
|
SetShowHiddenFiles(ctx context.Context, show bool) error
|
|
GetDefaultView(ctx context.Context) (string, error)
|
|
SetDefaultView(ctx context.Context, view string) error
|
|
GetSortPreferences(ctx context.Context) (sortBy string, sortOrder string, err error)
|
|
SetSortPreferences(ctx context.Context, sortBy string, sortOrder string) error
|
|
}
|
|
|
|
// repository defines the interface for loading and saving configuration
|
|
type repository interface {
|
|
// LoadConfig loads the configuration, returning defaults if file doesn't exist
|
|
LoadConfig(ctx context.Context) (*Config, error)
|
|
|
|
// SaveConfig saves the configuration to persistent storage
|
|
SaveConfig(ctx context.Context, config *Config) error
|
|
}
|
|
|
|
// configService implements the ConfigService interface
|
|
type configService struct {
|
|
repo repository
|
|
mu sync.RWMutex // Thread safety
|
|
}
|
|
|
|
// fileRepository implements the repository interface with file-based storage
|
|
type fileRepository struct {
|
|
configPath string
|
|
appName string
|
|
}
|
|
|
|
// New creates a new configuration service with default settings
|
|
// This is the Wire provider function
|
|
func New() (ConfigService, error) {
|
|
appName := GetAppName()
|
|
repo, err := newFileRepository(appName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Wrap with integrity checking
|
|
fileRepo := repo.(*fileRepository)
|
|
integrityRepo, err := NewIntegrityAwareRepository(repo, appName, fileRepo.configPath)
|
|
if err != nil {
|
|
// Fall back to basic repository if integrity service fails
|
|
// This allows the app to still function
|
|
return &configService{
|
|
repo: repo,
|
|
}, nil
|
|
}
|
|
|
|
return &configService{
|
|
repo: integrityRepo,
|
|
}, nil
|
|
}
|
|
|
|
// NewForTesting creates a configuration service with the specified repository (for testing)
|
|
func NewForTesting(repo repository) ConfigService {
|
|
return &configService{
|
|
repo: repo,
|
|
}
|
|
}
|
|
|
|
// newFileRepository creates a new instance of repository
|
|
func newFileRepository(appName string) (repository, error) {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create app-specific config directory with restrictive permissions (owner only)
|
|
// 0700 = owner read/write/execute, no access for group or others
|
|
appConfigDir := filepath.Join(configDir, appName)
|
|
if err := os.MkdirAll(appConfigDir, 0700); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configPath := filepath.Join(appConfigDir, "config.json")
|
|
|
|
return &fileRepository{
|
|
configPath: configPath,
|
|
appName: appName,
|
|
}, nil
|
|
}
|
|
|
|
// LoadConfig loads the configuration from file, or returns defaults if file doesn't exist
|
|
func (r *fileRepository) LoadConfig(ctx context.Context) (*Config, error) {
|
|
// Check if the config file exists
|
|
if _, err := os.Stat(r.configPath); os.IsNotExist(err) {
|
|
// Return default config if file doesn't exist
|
|
defaults := getDefaultConfig()
|
|
|
|
// Save the defaults for future use
|
|
if err := r.SaveConfig(ctx, defaults); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return defaults, nil
|
|
}
|
|
|
|
// Read config from file
|
|
data, err := os.ReadFile(r.configPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var config Config
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// SaveConfig saves the configuration to file with restrictive permissions
|
|
func (r *fileRepository) SaveConfig(ctx context.Context, config *Config) error {
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use 0600 permissions (owner read/write only) for security
|
|
return os.WriteFile(r.configPath, data, 0600)
|
|
}
|
|
|
|
// getDefaultConfig returns the default configuration values
|
|
// Note: This function is only called from LoadConfig after the config directory
|
|
// has already been created by newFileRepository, so no directory creation needed here.
|
|
func getDefaultConfig() *Config {
|
|
return &Config{
|
|
CloudProviderAddress: "http://localhost:8000",
|
|
Credentials: &Credentials{
|
|
Email: "", // Leave blank because no user was authenticated.
|
|
AccessToken: "", // Leave blank because no user was authenticated.
|
|
AccessTokenExpiryTime: nil, // Leave blank because no user was authenticated.
|
|
RefreshToken: "", // Leave blank because no user was authenticated.
|
|
RefreshTokenExpiryTime: nil, // Leave blank because no user was authenticated.
|
|
},
|
|
// Desktop-specific defaults
|
|
WindowWidth: 1440,
|
|
WindowHeight: 900,
|
|
Theme: "auto",
|
|
Language: "en",
|
|
SyncMode: "hybrid",
|
|
AutoSync: true,
|
|
SyncIntervalMinutes: 30,
|
|
ShowHiddenFiles: false,
|
|
DefaultView: "list",
|
|
SortBy: "name",
|
|
SortOrder: "asc",
|
|
}
|
|
}
|