package main import ( "crypto/rand" "encoding/base64" "fmt" "log" "os" "path/filepath" "runtime" ) // AppEnvironment holds the resolved paths for the running application. type AppEnvironment struct { // DataDir is the persistent data directory (survives app updates). DataDir string // LaravelRoot is the extracted Laravel app in the temp directory. LaravelRoot string // DatabasePath is the full path to the SQLite database file. DatabasePath string } // PrepareEnvironment creates data directories, generates .env, and symlinks // storage so Laravel can write to persistent locations. func PrepareEnvironment(laravelRoot string) (*AppEnvironment, error) { dataDir, err := resolveDataDir() if err != nil { return nil, fmt.Errorf("resolve data dir: %w", err) } env := &AppEnvironment{ DataDir: dataDir, LaravelRoot: laravelRoot, DatabasePath: filepath.Join(dataDir, "core-app.sqlite"), } // Create persistent directories dirs := []string{ dataDir, filepath.Join(dataDir, "storage", "app"), filepath.Join(dataDir, "storage", "framework", "cache", "data"), filepath.Join(dataDir, "storage", "framework", "sessions"), filepath.Join(dataDir, "storage", "framework", "views"), filepath.Join(dataDir, "storage", "logs"), } for _, dir := range dirs { if err := os.MkdirAll(dir, 0o755); err != nil { return nil, fmt.Errorf("create dir %s: %w", dir, err) } } // Create empty SQLite database if it doesn't exist if _, err := os.Stat(env.DatabasePath); os.IsNotExist(err) { if err := os.WriteFile(env.DatabasePath, nil, 0o644); err != nil { return nil, fmt.Errorf("create database: %w", err) } log.Printf("Created new database: %s", env.DatabasePath) } // Replace the extracted storage/ with a symlink to the persistent one extractedStorage := filepath.Join(laravelRoot, "storage") os.RemoveAll(extractedStorage) persistentStorage := filepath.Join(dataDir, "storage") if err := os.Symlink(persistentStorage, extractedStorage); err != nil { return nil, fmt.Errorf("symlink storage: %w", err) } // Generate .env file with resolved paths if err := writeEnvFile(laravelRoot, env); err != nil { return nil, fmt.Errorf("write .env: %w", err) } return env, nil } // resolveDataDir returns the OS-appropriate persistent data directory. func resolveDataDir() (string, error) { var base string switch runtime.GOOS { case "darwin": home, err := os.UserHomeDir() if err != nil { return "", err } base = filepath.Join(home, "Library", "Application Support", "core-app") case "linux": if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" { base = filepath.Join(xdg, "core-app") } else { home, err := os.UserHomeDir() if err != nil { return "", err } base = filepath.Join(home, ".local", "share", "core-app") } default: home, err := os.UserHomeDir() if err != nil { return "", err } base = filepath.Join(home, ".core-app") } return base, nil } // writeEnvFile generates the Laravel .env with resolved runtime paths. func writeEnvFile(laravelRoot string, env *AppEnvironment) error { appKey, err := loadOrGenerateAppKey(env.DataDir) if err != nil { return fmt.Errorf("app key: %w", err) } content := fmt.Sprintf(`APP_NAME="Core App" APP_ENV=production APP_KEY=%s APP_DEBUG=false APP_URL=http://localhost DB_CONNECTION=sqlite DB_DATABASE="%s" CACHE_STORE=file SESSION_DRIVER=file LOG_CHANNEL=single LOG_LEVEL=warning `, appKey, env.DatabasePath) return os.WriteFile(filepath.Join(laravelRoot, ".env"), []byte(content), 0o644) } // loadOrGenerateAppKey loads an existing APP_KEY from the data dir, // or generates a new one and persists it. func loadOrGenerateAppKey(dataDir string) (string, error) { keyFile := filepath.Join(dataDir, ".app-key") data, err := os.ReadFile(keyFile) if err == nil && len(data) > 0 { return string(data), nil } // Generate a new 32-byte key key := make([]byte, 32) if _, err := rand.Read(key); err != nil { return "", fmt.Errorf("generate key: %w", err) } appKey := "base64:" + base64.StdEncoding.EncodeToString(key) if err := os.WriteFile(keyFile, []byte(appKey), 0o600); err != nil { return "", fmt.Errorf("save key: %w", err) } log.Printf("Generated new APP_KEY (saved to %s)", keyFile) return appKey, nil } // appendEnv appends a key=value pair to the Laravel .env file. func appendEnv(laravelRoot, key, value string) error { envFile := filepath.Join(laravelRoot, ".env") f, err := os.OpenFile(envFile, os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return err } defer f.Close() _, err = fmt.Fprintf(f, "%s=\"%s\"\n", key, value) return err }