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
270
native/desktop/maplefile/internal/config/config.go
Normal file
270
native/desktop/maplefile/internal/config/config.go
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
// 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",
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue