Files
ss/internal/storage/storage.go
2026-07-19 20:01:38 +08:00

157 lines
4.0 KiB
Go

// Package storage handles cross-platform persistence of configuration and
// subscription data, using YAML files under a per-user config directory.
package storage
import (
"fmt"
"os"
"path/filepath"
"runtime"
"gopkg.in/yaml.v3"
"gitea.epss.net.cn/klesh/ss/internal/model"
"gitea.epss.net.cn/klesh/ss/internal/yamlutil"
)
// Manager manages cross-platform data storage for subscriptions and configuration.
type Manager struct {
ConfigDir string
ConfigFile string
SubscriptionsFile string
}
// New creates a Manager, resolving the config directory (SF_CONFIG_DIR env
// var, falling back to ~/basicfiles/cli/ss) and ensuring it exists.
func New() (*Manager, error) {
configDir, err := getConfigDir()
if err != nil {
return nil, err
}
m := &Manager{
ConfigDir: configDir,
ConfigFile: filepath.Join(configDir, "config.yaml"),
SubscriptionsFile: filepath.Join(configDir, "subscriptions.yaml"),
}
if err := os.MkdirAll(m.ConfigDir, 0o755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %w", err)
}
return m, nil
}
func getConfigDir() (string, error) {
if dir := os.Getenv("SF_CONFIG_DIR"); dir != "" {
return dir, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to determine home directory: %w", err)
}
return filepath.Join(home, "basicfiles", "cli", "ss"), nil
}
// LoadSubscriptions loads subscriptions from the YAML file, returning an
// empty collection if the file doesn't exist or fails to parse.
func (m *Manager) LoadSubscriptions() *model.SubscriptionsData {
data := model.NewSubscriptionsData()
raw, err := os.ReadFile(m.SubscriptionsFile)
if err != nil {
return data
}
if err := yamlutil.Unmarshal(raw, data); err != nil {
fmt.Printf("Warning: Failed to load subscriptions: %v\n", err)
return model.NewSubscriptionsData()
}
if data.Subscriptions == nil {
data.Subscriptions = make(map[string]*model.Subscription)
}
return data
}
// SaveSubscriptions writes subscriptions to the YAML file.
func (m *Manager) SaveSubscriptions(data *model.SubscriptionsData) bool {
out, err := yaml.Marshal(data)
if err != nil {
fmt.Printf("Error: Failed to save subscriptions: %v\n", err)
return false
}
if err := os.WriteFile(m.SubscriptionsFile, out, 0o644); err != nil {
fmt.Printf("Error: Failed to save subscriptions: %v\n", err)
return false
}
return true
}
// LoadConfig loads the raw application config as a generic map, matching the
// Python version's dict-based (non-strict) handling of config.yaml.
func (m *Manager) LoadConfig() map[string]interface{} {
raw, err := os.ReadFile(m.ConfigFile)
if err != nil {
return map[string]interface{}{}
}
var data map[string]interface{}
if err := yamlutil.Unmarshal(raw, &data); err != nil {
fmt.Printf("Warning: Failed to load config: %v\n", err)
return map[string]interface{}{}
}
if data == nil {
return map[string]interface{}{}
}
return data
}
// SaveConfig writes the application config to the YAML file.
func (m *Manager) SaveConfig(config map[string]interface{}) bool {
out, err := yaml.Marshal(config)
if err != nil {
fmt.Printf("Error: Failed to save config: %v\n", err)
return false
}
if err := os.WriteFile(m.ConfigFile, out, 0o644); err != nil {
fmt.Printf("Error: Failed to save config: %v\n", err)
return false
}
return true
}
// Info describes the storage location, mirroring get_storage_info().
type Info struct {
ConfigDir string
ConfigFile string
SubscriptionsFile string
Platform string
Exists bool
}
// GetStorageInfo returns information about the storage location.
func (m *Manager) GetStorageInfo() Info {
_, err := os.Stat(m.ConfigDir)
return Info{
ConfigDir: m.ConfigDir,
ConfigFile: m.ConfigFile,
SubscriptionsFile: m.SubscriptionsFile,
Platform: platformName(),
Exists: err == nil,
}
}
func platformName() string {
switch runtime.GOOS {
case "darwin":
return "Darwin"
case "linux":
return "Linux"
case "windows":
return "Windows"
default:
return runtime.GOOS
}
}