Files
torrserver/server/settings/settings.go
T
nkozobrod 616c6b1c62
Release Docker multi arch / docker (push) Has been cancelled
Test Install Script / Test Script Syntax (push) Has been cancelled
Test Install Script / Test on almalinux-10 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-10 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (root) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (default) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (root) (push) Has been cancelled
Test Install Script / Test on debian-11 (default) (push) Has been cancelled
Test Install Script / Test on debian-11 (root) (push) Has been cancelled
Test Install Script / Test on debian-12 (default) (push) Has been cancelled
Test Install Script / Test on debian-12 (root) (push) Has been cancelled
Test Install Script / Test on debian-13 (default) (push) Has been cancelled
Test Install Script / Test on debian-13 (root) (push) Has been cancelled
Test Install Script / Test on fedora-latest (default) (push) Has been cancelled
Test Install Script / Test on fedora-latest (root) (push) Has been cancelled
Test Install Script / Test on rocky-10 (default) (push) Has been cancelled
Test Install Script / Test on rocky-10 (root) (push) Has been cancelled
Test Install Script / Test on rocky-8 (default) (push) Has been cancelled
Test Install Script / Test on rocky-8 (root) (push) Has been cancelled
Test Install Script / Test on rocky-9 (default) (push) Has been cancelled
Test Install Script / Test on rocky-9 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (root) (push) Has been cancelled
Initial commit: docker compose config
2026-05-30 12:07:11 +00:00

453 lines
12 KiB
Go

package settings
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"server/log"
)
// Add a global lock for database operations during migration
var dbMigrationLock sync.RWMutex
func IsDebug() bool {
if BTsets != nil {
return BTsets.EnableDebug
}
return false
}
var (
tdb TorrServerDB
Path string
IP string
Port string
Ssl bool
SslPort string
ReadOnly bool
HttpAuth bool
SearchWA bool
PubIPv4 string
PubIPv6 string
TorAddr string
MaxSize int64
)
func InitSets(readOnly, searchWA bool) {
ReadOnly = readOnly
SearchWA = searchWA
bboltDB := NewTDB()
if bboltDB == nil {
log.TLogln("Error open bboltDB:", filepath.Join(Path, "config.db"))
os.Exit(1)
}
jsonDB := NewJsonDB()
if jsonDB == nil {
log.TLogln("Error open jsonDB")
os.Exit(1)
}
// Optional forced migration (for manual control)
if migrationMode := os.Getenv("TS_MIGRATION_MODE"); migrationMode != "" {
log.TLogln(fmt.Sprintf("Executing forced migration: %s", migrationMode))
if err := SmartMigrate(bboltDB, jsonDB, migrationMode); err != nil {
log.TLogln("Migration warning:", err)
}
}
// Determine storage preferences
settingsStoragePref, viewedStoragePref := determineStoragePreferences(bboltDB, jsonDB)
// Apply migrations (clean, one-way)
applyCleanMigrations(bboltDB, jsonDB, settingsStoragePref, viewedStoragePref)
// Setup routing
setupDatabaseRouting(bboltDB, jsonDB, settingsStoragePref, viewedStoragePref)
// Load settings
loadBTSets()
// Update preferences if they changed
if BTsets != nil && (BTsets.StoreSettingsInJson != settingsStoragePref || BTsets.StoreViewedInJson != viewedStoragePref) {
BTsets.StoreSettingsInJson = settingsStoragePref
BTsets.StoreViewedInJson = viewedStoragePref
SetBTSets(BTsets)
}
// Migrate old torrents
MigrateTorrents()
logConfiguration(settingsStoragePref, viewedStoragePref)
}
func determineStoragePreferences(bboltDB, jsonDB TorrServerDB) (settingsInJson, viewedInJson bool) {
// Try to load existing settings first
if existing := loadExistingSettings(bboltDB, jsonDB); existing != nil {
if IsDebug() {
log.TLogln(fmt.Sprintf("Found settings: StoreSettingsInJson=%v, StoreViewedInJson=%v",
existing.StoreSettingsInJson, existing.StoreViewedInJson))
}
// Check if these are actually set or just default zero values
// For now, trust the stored values
return existing.StoreSettingsInJson, existing.StoreViewedInJson
}
// Defaults (if not set by user)
settingsInJson = true // JSON for settings (easy editable)
viewedInJson = false // BBolt for viewed (performance)
// Environment overrides
if env := os.Getenv("TS_SETTINGS_STORAGE"); env != "" {
settingsInJson = (env == "json")
}
if env := os.Getenv("TS_VIEWED_STORAGE"); env != "" {
viewedInJson = (env == "json")
}
if IsDebug() {
log.TLogln(fmt.Sprintf("Using flags: settingsInJson=%v, viewedInJson=%v",
settingsInJson, viewedInJson))
}
return settingsInJson, viewedInJson
}
func loadExistingSettings(bboltDB, jsonDB TorrServerDB) *BTSets {
// Try JSON first
if buf := jsonDB.Get("Settings", "BitTorr"); buf != nil {
var sets BTSets
if err := json.Unmarshal(buf, &sets); err == nil {
return &sets
}
}
// Try BBolt
if buf := bboltDB.Get("Settings", "BitTorr"); buf != nil {
var sets BTSets
if err := json.Unmarshal(buf, &sets); err == nil {
return &sets
}
}
return nil
}
// func loadExistingSettingsDebug(bboltDB, jsonDB TorrServerDB) *BTSets {
// // Try JSON first
// if buf := jsonDB.Get("Settings", "BitTorr"); buf != nil {
// log.TLogln(fmt.Sprintf("Found settings in JSON, size: %d bytes", len(buf)))
// var sets BTSets
// if err := json.Unmarshal(buf, &sets); err == nil {
// log.TLogln(fmt.Sprintf("Parsed from JSON: StoreSettingsInJson=%v, StoreViewedInJson=%v",
// sets.StoreSettingsInJson, sets.StoreViewedInJson))
// return &sets
// } else {
// log.TLogln(fmt.Sprintf("Failed to parse JSON settings: %v", err))
// }
// } else {
// log.TLogln("No settings found in JSON")
// }
// // Try BBolt
// if buf := bboltDB.Get("Settings", "BitTorr"); buf != nil {
// log.TLogln(fmt.Sprintf("Found settings in BBolt, size: %d bytes", len(buf)))
// var sets BTSets
// if err := json.Unmarshal(buf, &sets); err == nil {
// log.TLogln(fmt.Sprintf("Parsed from BBolt: StoreSettingsInJson=%v, StoreViewedInJson=%v",
// sets.StoreSettingsInJson, sets.StoreViewedInJson))
// return &sets
// } else {
// log.TLogln(fmt.Sprintf("Failed to parse BBolt settings: %v", err))
// }
// } else {
// log.TLogln("No settings found in BBolt")
// }
// log.TLogln("No existing storage settings found")
// return nil
// }
func applyCleanMigrations(bboltDB, jsonDB TorrServerDB, settingsInJson, viewedInJson bool) {
// Settings migration
if settingsInJson {
safeMigrate(bboltDB, jsonDB, "Settings", "BitTorr", "JSON", true)
} else {
safeMigrate(jsonDB, bboltDB, "Settings", "BitTorr", "BBolt", true)
}
// Viewed migration
if viewedInJson {
safeMigrateAll(bboltDB, jsonDB, "Viewed", "JSON", true)
} else {
safeMigrateAll(jsonDB, bboltDB, "Viewed", "BBolt", true)
}
}
func safeMigrate(source, target TorrServerDB, xpath, name, targetName string, clearSource bool) {
if IsDebug() {
log.TLogln(fmt.Sprintf("Checking migration of %s/%s to %s", xpath, name, targetName))
}
migrated, err := MigrateSingle(source, target, xpath, name)
if err != nil {
log.TLogln(fmt.Sprintf("Migration error for %s/%s: %v", xpath, name, err))
return
}
if migrated {
log.TLogln(fmt.Sprintf("Successfully migrated %s/%s to %s", xpath, name, targetName))
// Clear source if requested
if clearSource {
source.Rem(xpath, name)
if IsDebug() {
log.TLogln(fmt.Sprintf("Cleared %s/%s from source", xpath, name))
}
}
} else {
log.TLogln(fmt.Sprintf("No migration needed for %s/%s (already exists or no data)",
xpath, name))
}
}
func safeMigrateAll(source, target TorrServerDB, xpath, targetName string, clearSource bool) {
if IsDebug() {
log.TLogln(fmt.Sprintf("Starting migration of all %s entries to %s", xpath, targetName))
}
migrated, skipped, err := MigrateAll(source, target, xpath)
log.TLogln(fmt.Sprintf("%s migration result: %d migrated, %d skipped", xpath, migrated, skipped))
if err != nil {
log.TLogln(fmt.Sprintf("Migration had errors: %v", err))
}
// Clear source if requested and we successfully migrated entries
if clearSource && migrated > 0 {
sourceCount := len(source.List(xpath))
// Only clear if we migrated at least as many as were in source
// (accounting for possible duplicates)
if migrated >= sourceCount {
source.Clear(xpath)
if IsDebug() {
log.TLogln(fmt.Sprintf("Cleared all %s entries from source", xpath))
}
} else {
log.TLogln(fmt.Sprintf("Not clearing %s: only migrated %d of %d entries",
xpath, migrated, sourceCount))
}
}
}
func setupDatabaseRouting(bboltDB, jsonDB TorrServerDB, settingsInJson, viewedInJson bool) {
dbRouter := NewXPathDBRouter()
if settingsInJson {
dbRouter.RegisterRoute(jsonDB, "Settings")
} else {
dbRouter.RegisterRoute(bboltDB, "Settings")
}
if viewedInJson {
dbRouter.RegisterRoute(jsonDB, "Viewed")
} else {
dbRouter.RegisterRoute(bboltDB, "Viewed")
}
dbRouter.RegisterRoute(bboltDB, "Torrents")
tdb = NewDBReadCache(dbRouter)
}
func logConfiguration(settingsInJson, viewedInJson bool) {
settingsLoc := "JSON"
if !settingsInJson {
settingsLoc = "BBolt"
}
viewedLoc := "JSON"
if !viewedInJson {
viewedLoc = "BBolt"
}
log.TLogln(fmt.Sprintf("Storage: Settings->%s, Viewed->%s, Torrents->BBolt",
settingsLoc, viewedLoc))
}
// SwitchSettingsStorage - simplified version
func SwitchSettingsStorage(useJson bool) error {
if ReadOnly {
return errors.New("read-only mode")
}
// Acquire exclusive lock for migration
dbMigrationLock.Lock()
defer dbMigrationLock.Unlock()
bboltDB := NewTDB()
if bboltDB == nil {
return errors.New("failed to open BBolt DB")
}
// DON'T CLOSE! They're still in use by tdb
// defer bboltDB.CloseDB()
jsonDB := NewJsonDB()
if jsonDB == nil {
return errors.New("failed to open JSON DB")
}
// DON'T CLOSE! They're still in use by tdb
// defer jsonDB.CloseDB()
log.TLogln(fmt.Sprintf("Switching Settings storage to %s",
map[bool]string{true: "JSON", false: "BBolt"}[useJson]))
// Update storage preference (must be called before migrate as this setting migrate too)
if BTsets != nil {
BTsets.StoreSettingsInJson = useJson
SetBTSets(BTsets)
}
var err error
if useJson {
err = MigrateSettingsToJson(bboltDB, jsonDB)
} else {
err = MigrateSettingsFromJson(jsonDB, bboltDB)
}
if err != nil {
return err
}
log.TLogln("Settings storage switched. Restart required for routing changes.")
return nil
}
// SwitchViewedStorage - simplified version
func SwitchViewedStorage(useJson bool) error {
if ReadOnly {
return errors.New("read-only mode")
}
// Acquire exclusive lock for migration
dbMigrationLock.Lock()
defer dbMigrationLock.Unlock()
bboltDB := NewTDB()
if bboltDB == nil {
return errors.New("failed to open BBolt DB")
}
// DON'T CLOSE! They're still in use by tdb
// defer bboltDB.CloseDB()
jsonDB := NewJsonDB()
if jsonDB == nil {
return errors.New("failed to open JSON DB")
}
// DON'T CLOSE! They're still in use by tdb
// defer jsonDB.CloseDB()
log.TLogln(fmt.Sprintf("Switching Viewed storage to %s",
map[bool]string{true: "JSON", false: "BBolt"}[useJson]))
var err error
if useJson {
err = MigrateViewedToJson(bboltDB, jsonDB)
if err == nil {
bboltDB.Clear("Viewed")
}
} else {
err = MigrateViewedFromJson(jsonDB, bboltDB)
if err == nil {
jsonDB.Clear("Viewed")
}
}
if err != nil {
return err
}
// Update preference
if BTsets != nil {
BTsets.StoreViewedInJson = useJson
SetBTSets(BTsets)
}
log.TLogln("Viewed storage switched. Restart required for routing changes.")
return nil
}
// Used in /storage/settings web API
func GetStoragePreferences() map[string]interface{} {
prefs := map[string]interface{}{
"settings": "json", // Default fallback
"viewed": "bbolt", // Default fallback
}
if BTsets != nil {
// Convert boolean preferences to string values
if BTsets.StoreSettingsInJson {
prefs["settings"] = "json"
} else {
prefs["settings"] = "bbolt"
}
if BTsets.StoreViewedInJson {
prefs["viewed"] = "json"
} else {
prefs["viewed"] = "bbolt"
}
}
if IsDebug() {
log.TLogln(fmt.Sprintf("GetStoragePreferences: settings=%s, viewed=%s",
prefs["settings"], prefs["viewed"]))
}
if tdb != nil {
prefs["viewedCount"] = len(tdb.List("Viewed"))
}
return prefs
}
// Used in /storage/settings web API
func SetStoragePreferences(prefs map[string]interface{}) error {
if ReadOnly || BTsets == nil {
return errors.New("cannot change storage preferences. Read-only mode")
}
if IsDebug() {
log.TLogln(fmt.Sprintf("SetStoragePreferences received: %v", prefs))
}
// Apply changes
if settingsPref, ok := prefs["settings"].(string); ok && settingsPref != "" {
useJson := (settingsPref == "json")
if IsDebug() {
log.TLogln(fmt.Sprintf("Changing settings storage to useJson=%v (was %v)",
useJson, BTsets.StoreSettingsInJson))
}
if BTsets.StoreSettingsInJson != useJson {
if err := SwitchSettingsStorage(useJson); err != nil {
return fmt.Errorf("failed to switch settings storage: %w", err)
}
}
}
if viewedPref, ok := prefs["viewed"].(string); ok && viewedPref != "" {
useJson := (viewedPref == "json")
if IsDebug() {
log.TLogln(fmt.Sprintf("Changing viewed storage to useJson=%v (was %v)",
useJson, BTsets.StoreViewedInJson))
}
if BTsets.StoreViewedInJson != useJson {
if err := SwitchViewedStorage(useJson); err != nil {
return fmt.Errorf("failed to switch viewed storage: %w", err)
}
}
}
return nil
}
func CloseDB() {
if tdb != nil {
tdb.CloseDB()
}
}