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
453 lines
12 KiB
Go
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()
|
|
}
|
|
}
|