refactor: implement configurable directory structure with XDG support
Separate configuration from data storage and make paths configurable via environment variables and command-line flags. This improves Unix/Linux compliance and supports both development and production deployments. Key changes: - Separate config dir (opal.yml) from data dir (database, logs) - Support XDG Base Directory specification - Add --config-dir and --data-dir flags - Environment variables: OPAL_CONFIG_DIR, OPAL_DATA_DIR, OPAL_DB_PATH - Smart fallback: /etc/opal, /var/lib/opal -> ~/.config/opal, ~/.local/share/opal - Server mode validates required OAuth/JWT environment variables - Update naming from 'jade' to 'opal' throughout - Update systemd service name to 'opal.service' - Add migration guide in README Default paths: - Config: /etc/opal (fallback: ~/.config/opal) - Data: /var/lib/opal (fallback: ~/.local/share/opal) Files modified: - internal/engine/config.go: New directory resolution logic - internal/engine/database.go: Auto-create data directory - cmd/root.go: Add global flags for directory overrides - cmd/server.go: Add configuration validation - cmd/sync.go, internal/sync/*: Use new path helper functions - tests: Update to use directory overrides - docs: Update deployment guide and README
This commit is contained in:
@@ -45,22 +45,151 @@ type Config struct {
|
||||
|
||||
var globalConfig *Config
|
||||
|
||||
// Global variables for flag/programmatic overrides
|
||||
var (
|
||||
configDirOverride string
|
||||
dataDirOverride string
|
||||
)
|
||||
|
||||
// SetConfigDirOverride sets the config directory override (typically from --config-dir flag)
|
||||
func SetConfigDirOverride(dir string) {
|
||||
configDirOverride = dir
|
||||
}
|
||||
|
||||
// SetDataDirOverride sets the data directory override (typically from --data-dir flag)
|
||||
func SetDataDirOverride(dir string) {
|
||||
dataDirOverride = dir
|
||||
}
|
||||
|
||||
// GetConfigDir returns the configuration directory path
|
||||
// Resolution priority:
|
||||
// 1. Flag override (via SetConfigDirOverride)
|
||||
// 2. OPAL_CONFIG_DIR environment variable
|
||||
// 3. XDG_CONFIG_HOME/opal
|
||||
// 4. /etc/opal (with fallback to ~/.config/opal if not writable)
|
||||
func GetConfigDir() (string, error) {
|
||||
// Priority 1: Flag override
|
||||
if configDirOverride != "" {
|
||||
return configDirOverride, nil
|
||||
}
|
||||
|
||||
// Priority 2: Environment variable
|
||||
if dir := os.Getenv("OPAL_CONFIG_DIR"); dir != "" {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// Priority 3: XDG_CONFIG_HOME
|
||||
if xdgConfig := os.Getenv("XDG_CONFIG_HOME"); xdgConfig != "" {
|
||||
return filepath.Join(xdgConfig, "opal"), nil
|
||||
}
|
||||
|
||||
// Priority 4: Try /etc/opal, fallback to ~/.config/opal if not writable
|
||||
etcOpal := "/etc/opal"
|
||||
if isWritable(etcOpal) {
|
||||
return etcOpal, nil
|
||||
}
|
||||
|
||||
// Fallback to user config directory
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".config", "jade"), nil
|
||||
return filepath.Join(home, ".config", "opal"), nil
|
||||
}
|
||||
|
||||
// GetDataDir returns the data directory path
|
||||
// Resolution priority:
|
||||
// 1. Flag override (via SetDataDirOverride)
|
||||
// 2. OPAL_DATA_DIR environment variable
|
||||
// 3. XDG_DATA_HOME/opal
|
||||
// 4. /var/lib/opal (with fallback to ~/.local/share/opal if not writable)
|
||||
func GetDataDir() (string, error) {
|
||||
// Priority 1: Flag override
|
||||
if dataDirOverride != "" {
|
||||
return dataDirOverride, nil
|
||||
}
|
||||
|
||||
// Priority 2: Environment variable
|
||||
if dir := os.Getenv("OPAL_DATA_DIR"); dir != "" {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// Priority 3: XDG_DATA_HOME
|
||||
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
|
||||
return filepath.Join(xdgData, "opal"), nil
|
||||
}
|
||||
|
||||
// Priority 4: Try /var/lib/opal, fallback to ~/.local/share/opal if not writable
|
||||
varLibOpal := "/var/lib/opal"
|
||||
if isWritable(varLibOpal) {
|
||||
return varLibOpal, nil
|
||||
}
|
||||
|
||||
// Fallback to user data directory
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
return filepath.Join(home, ".local", "share", "opal"), nil
|
||||
}
|
||||
|
||||
// isWritable checks if a directory exists and is writable, or if parent exists and is writable
|
||||
func isWritable(path string) bool {
|
||||
// Check if path exists
|
||||
info, err := os.Stat(path)
|
||||
if err == nil {
|
||||
// Path exists, check if it's a directory and writable
|
||||
if !info.IsDir() {
|
||||
return false
|
||||
}
|
||||
// Test write permission by trying to create a temp file
|
||||
testFile := filepath.Join(path, ".write-test")
|
||||
f, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
f.Close()
|
||||
os.Remove(testFile)
|
||||
return true
|
||||
}
|
||||
|
||||
// Path doesn't exist, check if parent is writable
|
||||
parent := filepath.Dir(path)
|
||||
parentInfo, err := os.Stat(parent)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !parentInfo.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Test if we can create in parent
|
||||
testFile := filepath.Join(parent, ".write-test")
|
||||
f, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
f.Close()
|
||||
os.Remove(testFile)
|
||||
return true
|
||||
}
|
||||
|
||||
// GetDBPath returns the path to the SQLite database
|
||||
// Resolution priority:
|
||||
// 1. OPAL_DB_PATH environment variable
|
||||
// 2. {data-dir}/opal.db
|
||||
func GetDBPath() (string, error) {
|
||||
configDir, err := GetConfigDir()
|
||||
// Priority 1: OPAL_DB_PATH override
|
||||
if dbPath := os.Getenv("OPAL_DB_PATH"); dbPath != "" {
|
||||
return dbPath, nil
|
||||
}
|
||||
|
||||
// Priority 2: Data directory + opal.db
|
||||
dataDir, err := GetDataDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(configDir, "opal.db"), nil
|
||||
return filepath.Join(dataDir, "opal.db"), nil
|
||||
}
|
||||
|
||||
// GetConfigPath returns the path to the config file
|
||||
@@ -72,6 +201,24 @@ func GetConfigPath() (string, error) {
|
||||
return filepath.Join(configDir, "opal.yml"), nil
|
||||
}
|
||||
|
||||
// GetSyncQueuePath returns the path to the sync queue file
|
||||
func GetSyncQueuePath() (string, error) {
|
||||
dataDir, err := GetDataDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dataDir, "sync_queue.json"), nil
|
||||
}
|
||||
|
||||
// GetSyncConflictLogPath returns the path to the sync conflict log
|
||||
func GetSyncConflictLogPath() (string, error) {
|
||||
dataDir, err := GetDataDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dataDir, "sync_conflicts.log"), nil
|
||||
}
|
||||
|
||||
// LoadConfig loads the configuration from file or creates default
|
||||
func LoadConfig() (*Config, error) {
|
||||
if globalConfig != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package engine
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
@@ -20,6 +21,15 @@ func InitDB() error {
|
||||
return fmt.Errorf("failed to get database path: %w", err)
|
||||
}
|
||||
|
||||
// Ensure data directory exists
|
||||
dataDir, err := GetDataDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get data directory: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
// Open database connection
|
||||
database, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,12 +9,15 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Setup test database
|
||||
os.Setenv("HOME", "/tmp/opal-test")
|
||||
// Setup test database with explicit directory overrides
|
||||
testDir := "/tmp/opal-test"
|
||||
|
||||
// Ensure config directory exists
|
||||
configDir := "/tmp/opal-test/.config/jade"
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
// Set directory overrides to use test directory
|
||||
SetConfigDirOverride(testDir)
|
||||
SetDataDirOverride(testDir)
|
||||
|
||||
// Ensure test directory exists
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -27,7 +30,7 @@ func TestMain(m *testing.M) {
|
||||
code := m.Run()
|
||||
|
||||
// Cleanup
|
||||
os.RemoveAll("/tmp/opal-test/.config")
|
||||
os.RemoveAll(testDir)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user