Initial commit: docker compose config
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
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
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package settings
|
||||
|
||||
type ExecArgs struct {
|
||||
Port string
|
||||
IP string
|
||||
Ssl bool
|
||||
SslPort string
|
||||
SslCert string
|
||||
SslKey string
|
||||
Path string
|
||||
LogPath string
|
||||
WebLogPath string
|
||||
RDB bool
|
||||
HttpAuth bool
|
||||
DontKill bool
|
||||
UI bool
|
||||
TorrentsDir string
|
||||
TorrentAddr string
|
||||
PubIPv4 string
|
||||
PubIPv6 string
|
||||
SearchWA bool
|
||||
MaxSize string
|
||||
TGToken string
|
||||
FusePath string
|
||||
WebDAV bool
|
||||
ProxyURL string
|
||||
ProxyMode string
|
||||
ForceHTTPS bool
|
||||
}
|
||||
|
||||
var Args *ExecArgs
|
||||
@@ -0,0 +1,212 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"server/log"
|
||||
)
|
||||
|
||||
type TorznabConfig struct {
|
||||
Host string
|
||||
Key string
|
||||
Name string
|
||||
}
|
||||
|
||||
type TMDBConfig struct {
|
||||
APIKey string // TMDB API Key
|
||||
APIURL string // Base API URL (default: https://api.themoviedb.org)
|
||||
ImageURL string // Image URL (default: https://image.tmdb.org)
|
||||
ImageURLRu string // Image URL for Russian users (default: https://imagetmdb.com)
|
||||
}
|
||||
|
||||
type BTSets struct {
|
||||
// Cache
|
||||
CacheSize int64 // in byte, def 64 MB
|
||||
ReaderReadAHead int // in percent, 5%-100%, [...S__X__E...] [S-E] not clean
|
||||
PreloadCache int // in percent
|
||||
|
||||
// Disk
|
||||
UseDisk bool
|
||||
TorrentsSavePath string
|
||||
RemoveCacheOnDrop bool
|
||||
|
||||
// Torrent
|
||||
ForceEncrypt bool
|
||||
RetrackersMode int // 0 - don`t add, 1 - add retrackers (def), 2 - remove retrackers 3 - replace retrackers
|
||||
TorrentDisconnectTimeout int // in seconds
|
||||
EnableDebug bool // debug logs
|
||||
|
||||
// DLNA
|
||||
EnableDLNA bool
|
||||
FriendlyName string
|
||||
|
||||
// Rutor
|
||||
EnableRutorSearch bool
|
||||
|
||||
// Torznab
|
||||
EnableTorznabSearch bool
|
||||
TorznabUrls []TorznabConfig
|
||||
|
||||
// TMDB
|
||||
TMDBSettings TMDBConfig
|
||||
|
||||
// BT Config
|
||||
EnableIPv6 bool
|
||||
DisableTCP bool
|
||||
DisableUTP bool
|
||||
DisableUPNP bool
|
||||
DisableDHT bool
|
||||
DisablePEX bool
|
||||
DisableUpload bool
|
||||
DownloadRateLimit int // in kb, 0 - inf
|
||||
UploadRateLimit int // in kb, 0 - inf
|
||||
ConnectionsLimit int
|
||||
PeersListenPort int
|
||||
|
||||
// HTTPS
|
||||
SslPort int
|
||||
SslCert string
|
||||
SslKey string
|
||||
|
||||
// Reader
|
||||
ResponsiveMode bool // enable Responsive reader (don't wait pieceComplete)
|
||||
|
||||
// FS
|
||||
ShowFSActiveTorr bool
|
||||
|
||||
// Storage preferences
|
||||
StoreSettingsInJson bool
|
||||
StoreViewedInJson bool
|
||||
|
||||
// P2P Proxy
|
||||
EnableProxy bool
|
||||
ProxyHosts []string
|
||||
}
|
||||
|
||||
func (v *BTSets) String() string {
|
||||
buf, _ := json.Marshal(v)
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
var BTsets *BTSets
|
||||
|
||||
func SetBTSets(sets *BTSets) {
|
||||
if ReadOnly {
|
||||
return
|
||||
}
|
||||
// failsafe checks (use defaults)
|
||||
if sets.CacheSize == 0 {
|
||||
sets.CacheSize = 64 * 1024 * 1024
|
||||
}
|
||||
if sets.ConnectionsLimit == 0 {
|
||||
sets.ConnectionsLimit = 25
|
||||
}
|
||||
if sets.TorrentDisconnectTimeout == 0 {
|
||||
sets.TorrentDisconnectTimeout = 30
|
||||
}
|
||||
|
||||
if sets.ReaderReadAHead < 5 {
|
||||
sets.ReaderReadAHead = 5
|
||||
}
|
||||
if sets.ReaderReadAHead > 100 {
|
||||
sets.ReaderReadAHead = 100
|
||||
}
|
||||
|
||||
if sets.PreloadCache < 0 {
|
||||
sets.PreloadCache = 0
|
||||
}
|
||||
if sets.PreloadCache > 100 {
|
||||
sets.PreloadCache = 100
|
||||
}
|
||||
|
||||
if sets.TorrentsSavePath == "" {
|
||||
sets.UseDisk = false
|
||||
} else if sets.UseDisk {
|
||||
BTsets = sets
|
||||
|
||||
go filepath.WalkDir(sets.TorrentsSavePath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() && strings.ToLower(d.Name()) == ".tsc" {
|
||||
BTsets.TorrentsSavePath = path
|
||||
log.TLogln("Find directory \"" + BTsets.TorrentsSavePath + "\", use as cache dir")
|
||||
return io.EOF
|
||||
}
|
||||
if d.IsDir() && strings.HasPrefix(d.Name(), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
BTsets = sets
|
||||
buf, err := json.Marshal(BTsets)
|
||||
if err != nil {
|
||||
log.TLogln("Error marshal btsets", err)
|
||||
return
|
||||
}
|
||||
tdb.Set("Settings", "BitTorr", buf)
|
||||
}
|
||||
|
||||
func SetDefaultConfig() {
|
||||
sets := new(BTSets)
|
||||
sets.CacheSize = 64 * 1024 * 1024 // 64 MB
|
||||
sets.PreloadCache = 50
|
||||
sets.ConnectionsLimit = 25
|
||||
sets.RetrackersMode = 1
|
||||
sets.TorrentDisconnectTimeout = 30
|
||||
sets.ReaderReadAHead = 95 // 95%
|
||||
sets.ResponsiveMode = true
|
||||
sets.ShowFSActiveTorr = true
|
||||
sets.StoreSettingsInJson = true
|
||||
// Set default TMDB settings
|
||||
sets.TMDBSettings = TMDBConfig{
|
||||
APIKey: "",
|
||||
APIURL: "https://api.themoviedb.org",
|
||||
ImageURL: "https://image.tmdb.org",
|
||||
ImageURLRu: "https://imagetmdb.com",
|
||||
}
|
||||
BTsets = sets
|
||||
if !ReadOnly {
|
||||
buf, err := json.Marshal(BTsets)
|
||||
if err != nil {
|
||||
log.TLogln("Error marshal btsets", err)
|
||||
return
|
||||
}
|
||||
tdb.Set("Settings", "BitTorr", buf)
|
||||
}
|
||||
//Proxy
|
||||
sets.EnableProxy = false
|
||||
sets.ProxyHosts = []string{"*themoviedb.org", "*tmdb.org", "rutor.info"}
|
||||
}
|
||||
|
||||
func loadBTSets() {
|
||||
buf := tdb.Get("Settings", "BitTorr")
|
||||
if len(buf) > 0 {
|
||||
err := json.Unmarshal(buf, &BTsets)
|
||||
if err == nil {
|
||||
if BTsets.ReaderReadAHead < 5 {
|
||||
BTsets.ReaderReadAHead = 5
|
||||
}
|
||||
// Set default TMDB settings if missing (for existing configs)
|
||||
if BTsets.TMDBSettings.APIURL == "" {
|
||||
BTsets.TMDBSettings = TMDBConfig{
|
||||
APIKey: "",
|
||||
APIURL: "https://api.themoviedb.org",
|
||||
ImageURL: "https://image.tmdb.org",
|
||||
ImageURLRu: "https://imagetmdb.com",
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
log.TLogln("Error unmarshal btsets", err)
|
||||
}
|
||||
// initialize defaults on error
|
||||
SetDefaultConfig()
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/log"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type TDB struct {
|
||||
Path string
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
var globalBboltDB TorrServerDB
|
||||
|
||||
func NewTDB() TorrServerDB {
|
||||
if globalBboltDB != nil {
|
||||
return globalBboltDB // Return existing instance
|
||||
}
|
||||
db, err := bolt.Open(filepath.Join(Path, "config.db"), 0o666, &bolt.Options{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
log.TLogln(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
tdb := new(TDB)
|
||||
tdb.db = db
|
||||
tdb.Path = Path
|
||||
globalBboltDB = tdb
|
||||
return globalBboltDB
|
||||
}
|
||||
|
||||
func (v *TDB) CloseDB() {
|
||||
if v.db != nil {
|
||||
v.db.Close()
|
||||
v.db = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TDB) Get(xpath, name string) []byte {
|
||||
spath := strings.Split(xpath, "/")
|
||||
if len(spath) == 0 {
|
||||
return nil
|
||||
}
|
||||
var ret []byte
|
||||
err := v.db.View(func(tx *bolt.Tx) error {
|
||||
buckt := tx.Bucket([]byte(spath[0]))
|
||||
if buckt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, p := range spath {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
buckt = buckt.Bucket([]byte(p))
|
||||
if buckt == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
data := buckt.Get([]byte(name))
|
||||
if data != nil {
|
||||
// CRITICAL: Copy the data before returning
|
||||
ret = make([]byte, len(data))
|
||||
copy(ret, data)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.TLogln("Error get sets", xpath+"/"+name, ", error:", err)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (v *TDB) Set(xpath, name string, value []byte) {
|
||||
spath := strings.Split(xpath, "/")
|
||||
if len(spath) == 0 {
|
||||
return
|
||||
}
|
||||
err := v.db.Update(func(tx *bolt.Tx) error {
|
||||
buckt, err := tx.CreateBucketIfNotExists([]byte(spath[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, p := range spath {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
buckt, err = buckt.CreateBucketIfNotExists([]byte(p))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return buckt.Put([]byte(name), value)
|
||||
})
|
||||
if err != nil {
|
||||
log.TLogln("Error put sets", xpath+"/"+name, ", error:", err)
|
||||
log.TLogln("value:", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TDB) List(xpath string) []string {
|
||||
spath := strings.Split(xpath, "/")
|
||||
if len(spath) == 0 {
|
||||
return nil
|
||||
}
|
||||
var ret []string
|
||||
err := v.db.View(func(tx *bolt.Tx) error {
|
||||
buckt := tx.Bucket([]byte(spath[0]))
|
||||
if buckt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, p := range spath {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
buckt = buckt.Bucket([]byte(p))
|
||||
if buckt == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
buckt.ForEach(func(k, _ []byte) error {
|
||||
if len(k) > 0 {
|
||||
ret = append(ret, string(k))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.TLogln("Error list sets", xpath, ", error:", err)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (v *TDB) Rem(xpath, name string) {
|
||||
spath := strings.Split(xpath, "/")
|
||||
if len(spath) == 0 {
|
||||
return
|
||||
}
|
||||
err := v.db.Update(func(tx *bolt.Tx) error {
|
||||
buckt := tx.Bucket([]byte(spath[0]))
|
||||
if buckt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, p := range spath {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
buckt = buckt.Bucket([]byte(p))
|
||||
if buckt == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return buckt.Delete([]byte(name))
|
||||
})
|
||||
if err != nil {
|
||||
log.TLogln("Error rem sets", xpath+"/"+name, ", error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TDB) Clear(xPath string) {
|
||||
spath := strings.Split(xPath, "/")
|
||||
if len(spath) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
err := v.db.Update(func(tx *bolt.Tx) error {
|
||||
buckt := tx.Bucket([]byte(spath[0]))
|
||||
if buckt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, p := range spath {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
buckt = buckt.Bucket([]byte(p))
|
||||
if buckt == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all entries in this bucket
|
||||
return buckt.ForEach(func(k, _ []byte) error {
|
||||
return buckt.Delete(k)
|
||||
})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.TLogln("Error clear xPath", xPath, ", error:", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"server/log"
|
||||
)
|
||||
|
||||
type DBReadCache struct {
|
||||
db TorrServerDB
|
||||
listCache map[string][]string
|
||||
listCacheMutex sync.RWMutex
|
||||
dataCache map[[2]string][]byte
|
||||
dataCacheMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDBReadCache(db TorrServerDB) TorrServerDB {
|
||||
cdb := &DBReadCache{
|
||||
db: db,
|
||||
listCache: map[string][]string{},
|
||||
dataCache: map[[2]string][]byte{},
|
||||
}
|
||||
return cdb
|
||||
}
|
||||
|
||||
func (v *DBReadCache) CloseDB() {
|
||||
v.db.CloseDB()
|
||||
v.db = nil
|
||||
v.listCache = nil
|
||||
v.dataCache = nil
|
||||
}
|
||||
|
||||
func (v *DBReadCache) Get(xPath, name string) []byte {
|
||||
if v.dataCache == nil {
|
||||
return nil // или panic, или возвращаем ошибку
|
||||
}
|
||||
cacheKey := v.makeDataCacheKey(xPath, name)
|
||||
|
||||
v.dataCacheMutex.RLock()
|
||||
if data, ok := v.dataCache[cacheKey]; ok {
|
||||
defer v.dataCacheMutex.RUnlock()
|
||||
return data
|
||||
}
|
||||
v.dataCacheMutex.RUnlock()
|
||||
|
||||
// Если база данных закрыта, не пытаемся к ней обращаться
|
||||
if v.db == nil {
|
||||
return nil
|
||||
}
|
||||
data := v.db.Get(xPath, name)
|
||||
|
||||
v.dataCacheMutex.Lock()
|
||||
if v.dataCache != nil { // Двойная проверка
|
||||
v.dataCache[cacheKey] = data
|
||||
}
|
||||
v.dataCacheMutex.Unlock()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (v *DBReadCache) Set(xPath, name string, value []byte) {
|
||||
if ReadOnly {
|
||||
if IsDebug() {
|
||||
log.TLogln("DBReadCache.Set: Read-only DB mode!", name)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Проверяем, не закрыта ли база
|
||||
if v.dataCache == nil || v.db == nil {
|
||||
log.TLogln("DBReadCache.Set: no dataCache or DB is closed, cannot set", name)
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := v.makeDataCacheKey(xPath, name)
|
||||
|
||||
v.dataCacheMutex.Lock()
|
||||
if v.dataCache != nil { // Двойная проверка
|
||||
v.dataCache[cacheKey] = value
|
||||
}
|
||||
v.dataCacheMutex.Unlock()
|
||||
|
||||
if v.listCache != nil {
|
||||
delete(v.listCache, xPath)
|
||||
}
|
||||
|
||||
v.db.Set(xPath, name, value)
|
||||
}
|
||||
|
||||
func (v *DBReadCache) List(xPath string) []string {
|
||||
if v.listCache == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v.listCacheMutex.RLock()
|
||||
if names, ok := v.listCache[xPath]; ok {
|
||||
defer v.listCacheMutex.RUnlock()
|
||||
return names
|
||||
}
|
||||
v.listCacheMutex.RUnlock()
|
||||
|
||||
// Проверяем, не закрыта ли база
|
||||
if v.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := v.db.List(xPath)
|
||||
|
||||
v.listCacheMutex.Lock()
|
||||
if v.listCache != nil { // Двойная проверка
|
||||
v.listCache[xPath] = names
|
||||
}
|
||||
v.listCacheMutex.Unlock()
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func (v *DBReadCache) Rem(xPath, name string) {
|
||||
if ReadOnly {
|
||||
if IsDebug() {
|
||||
log.TLogln("DBReadCache.Rem: Read-only DB mode!", name)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Проверяем, не закрыта ли база
|
||||
if v.dataCache == nil || v.db == nil {
|
||||
log.TLogln("DBReadCache.Rem: no dataCache or DB is closed, cannot remove", name)
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := v.makeDataCacheKey(xPath, name)
|
||||
|
||||
v.dataCacheMutex.Lock()
|
||||
if v.dataCache != nil {
|
||||
delete(v.dataCache, cacheKey)
|
||||
}
|
||||
v.dataCacheMutex.Unlock()
|
||||
|
||||
if v.listCache != nil {
|
||||
delete(v.listCache, xPath)
|
||||
}
|
||||
|
||||
v.db.Rem(xPath, name)
|
||||
}
|
||||
|
||||
func (v *DBReadCache) Clear(xPath string) {
|
||||
if ReadOnly {
|
||||
if IsDebug() {
|
||||
log.TLogln("DBReadCache.Clear: Read-only DB mode!", xPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Clear from underlying DB first
|
||||
if v.db != nil {
|
||||
v.db.Clear(xPath)
|
||||
}
|
||||
|
||||
// Clear cache
|
||||
v.listCacheMutex.Lock()
|
||||
delete(v.listCache, xPath)
|
||||
v.listCacheMutex.Unlock()
|
||||
|
||||
// Clear data cache entries for this xPath
|
||||
v.dataCacheMutex.Lock()
|
||||
for key := range v.dataCache {
|
||||
if key[0] == xPath {
|
||||
delete(v.dataCache, key)
|
||||
}
|
||||
}
|
||||
v.dataCacheMutex.Unlock()
|
||||
}
|
||||
|
||||
func (v *DBReadCache) makeDataCacheKey(xPath, name string) [2]string {
|
||||
return [2]string{xPath, name}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"server/log"
|
||||
)
|
||||
|
||||
type JsonDB struct {
|
||||
Path string
|
||||
filenameDelimiter string
|
||||
filenameExtension string
|
||||
fileMode fs.FileMode
|
||||
xPathDelimeter string
|
||||
}
|
||||
|
||||
var globalJsonDB TorrServerDB
|
||||
var jsonDbLocks = make(map[string]*sync.Mutex)
|
||||
var jsonDbLocksMutex sync.Mutex
|
||||
|
||||
func NewJsonDB() TorrServerDB {
|
||||
if globalJsonDB != nil {
|
||||
return globalJsonDB
|
||||
}
|
||||
globalJsonDB := &JsonDB{
|
||||
Path: Path,
|
||||
filenameDelimiter: ".",
|
||||
filenameExtension: ".json",
|
||||
fileMode: fs.FileMode(0o666),
|
||||
xPathDelimeter: "/",
|
||||
}
|
||||
return globalJsonDB
|
||||
}
|
||||
|
||||
func (v *JsonDB) CloseDB() {
|
||||
// Not necessary
|
||||
}
|
||||
|
||||
func (v *JsonDB) Set(xPath, name string, value []byte) {
|
||||
var err error = nil
|
||||
jsonObj := map[string]interface{}{}
|
||||
if err := json.Unmarshal(value, &jsonObj); err == nil {
|
||||
if filename, err := v.xPathToFilename(xPath); err == nil {
|
||||
v.lock(filename)
|
||||
defer v.unlock(filename)
|
||||
if root, err := v.readJsonFileAsMap(filename); err == nil {
|
||||
root[name] = jsonObj
|
||||
if err = v.writeMapAsJsonFile(filename, root); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v.log(fmt.Sprintf("Set: error writing entry %s->%s", xPath, name), err)
|
||||
}
|
||||
|
||||
func (v *JsonDB) Get(xPath, name string) []byte {
|
||||
var err error = nil
|
||||
if filename, err := v.xPathToFilename(xPath); err == nil {
|
||||
v.lock(filename)
|
||||
defer v.unlock(filename)
|
||||
if root, err := v.readJsonFileAsMap(filename); err == nil {
|
||||
if jsonData, ok := root[name]; ok {
|
||||
if byteData, err := json.Marshal(jsonData); err == nil {
|
||||
// Return a copy to be safe
|
||||
data := make([]byte, len(byteData))
|
||||
copy(data, byteData)
|
||||
return data
|
||||
}
|
||||
} else {
|
||||
// We assume this is not 'error' but 'no entry' which is normal
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
v.log(fmt.Sprintf("Get: error reading entry %s->%s", xPath, name), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *JsonDB) List(xPath string) []string {
|
||||
var err error = nil
|
||||
if filename, err := v.xPathToFilename(xPath); err == nil {
|
||||
v.lock(filename)
|
||||
defer v.unlock(filename)
|
||||
if root, err := v.readJsonFileAsMap(filename); err == nil {
|
||||
nameList := make([]string, 0, len(root))
|
||||
for k := range root {
|
||||
nameList = append(nameList, k)
|
||||
}
|
||||
return nameList
|
||||
}
|
||||
}
|
||||
v.log(fmt.Sprintf("List: error reading entries in xPath %s", xPath), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *JsonDB) Rem(xPath, name string) {
|
||||
var err error = nil
|
||||
if filename, err := v.xPathToFilename(xPath); err == nil {
|
||||
v.lock(filename)
|
||||
defer v.unlock(filename)
|
||||
if root, err := v.readJsonFileAsMap(filename); err == nil {
|
||||
delete(root, name)
|
||||
if err = v.writeMapAsJsonFile(filename, root); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
v.log(fmt.Sprintf("Rem: error removing entry %s->%s", xPath, name), err)
|
||||
}
|
||||
|
||||
func (v *JsonDB) Clear(xPath string) {
|
||||
filename, err := v.xPathToFilename(xPath)
|
||||
if err != nil {
|
||||
v.log(fmt.Sprintf("Clear: error converting xPath %s to filename: %v", xPath, err))
|
||||
return
|
||||
}
|
||||
|
||||
v.lock(filename)
|
||||
defer v.unlock(filename)
|
||||
|
||||
path := filepath.Join(v.Path, filename)
|
||||
emptyData := []byte("{}")
|
||||
|
||||
if err := os.WriteFile(path, emptyData, v.fileMode); err != nil {
|
||||
v.log(fmt.Sprintf("Clear: error writing empty file for xPath %s: %v", xPath, err))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *JsonDB) lock(filename string) {
|
||||
jsonDbLocksMutex.Lock()
|
||||
mtx, ok := jsonDbLocks[filename]
|
||||
if !ok {
|
||||
mtx = &sync.Mutex{}
|
||||
jsonDbLocks[filename] = mtx
|
||||
}
|
||||
jsonDbLocksMutex.Unlock()
|
||||
mtx.Lock()
|
||||
}
|
||||
|
||||
func (v *JsonDB) unlock(filename string) {
|
||||
jsonDbLocksMutex.Lock()
|
||||
if mtx, ok := jsonDbLocks[filename]; ok {
|
||||
mtx.Unlock()
|
||||
}
|
||||
jsonDbLocksMutex.Unlock()
|
||||
}
|
||||
|
||||
func (v *JsonDB) xPathToFilename(xPath string) (string, error) {
|
||||
if pathComponents := strings.Split(xPath, v.xPathDelimeter); len(pathComponents) > 0 {
|
||||
return strings.ToLower(strings.Join(pathComponents, v.filenameDelimiter) + v.filenameExtension), nil
|
||||
}
|
||||
return "", errors.New("xPath has no components")
|
||||
}
|
||||
|
||||
func (v *JsonDB) readJsonFileAsMap(filename string) (map[string]interface{}, error) {
|
||||
var err error = nil
|
||||
jsonData := map[string]interface{}{}
|
||||
path := filepath.Join(v.Path, filename)
|
||||
if fileData, err := os.ReadFile(path); err == nil {
|
||||
if err = json.Unmarshal(fileData, &jsonData); err != nil {
|
||||
v.log(fmt.Sprintf("readJsonFileAsMap(%s) fileData: %s error", filename, fileData), err)
|
||||
}
|
||||
}
|
||||
return jsonData, err
|
||||
}
|
||||
|
||||
func (v *JsonDB) writeMapAsJsonFile(filename string, o map[string]interface{}) error {
|
||||
var err error = nil
|
||||
path := filepath.Join(v.Path, filename)
|
||||
if fileData, err := json.MarshalIndent(o, "", " "); err == nil {
|
||||
if err = os.WriteFile(path, fileData, v.fileMode); err != nil {
|
||||
v.log(fmt.Sprintf("writeMapAsJsonFile path: %s, fileMode: %s, fileData: %s error", path, v.fileMode, fileData), err)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *JsonDB) log(s string, params ...interface{}) {
|
||||
if len(params) > 0 {
|
||||
log.TLogln(fmt.Sprintf("JsonDB: %s: %s", s, fmt.Sprint(params...)))
|
||||
} else {
|
||||
log.TLogln(fmt.Sprintf("JsonDB: %s", s))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"server/log"
|
||||
"server/web/api/utils"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var dbTorrentsName = []byte("Torrents")
|
||||
|
||||
type torrentBackupDB struct {
|
||||
Name string
|
||||
Magnet string
|
||||
InfoBytes []byte
|
||||
Hash string
|
||||
Size int64
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
// Migrate from torrserver.db to config.db
|
||||
// TODO: migrate categories and data too
|
||||
func MigrateTorrents() {
|
||||
if _, err := os.Lstat(filepath.Join(Path, "torrserver.db")); os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
db, err := bolt.Open(filepath.Join(Path, "torrserver.db"), 0o666, &bolt.Options{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
log.TLogln("MigrateTorrents", err)
|
||||
return
|
||||
}
|
||||
|
||||
torrs := make([]*torrentBackupDB, 0)
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
tdb := tx.Bucket(dbTorrentsName)
|
||||
if tdb == nil {
|
||||
return nil
|
||||
}
|
||||
c := tdb.Cursor()
|
||||
for h, _ := c.First(); h != nil; h, _ = c.Next() {
|
||||
hdb := tdb.Bucket(h)
|
||||
if hdb != nil {
|
||||
torr := new(torrentBackupDB)
|
||||
torr.Hash = string(h)
|
||||
tmp := hdb.Get([]byte("Name"))
|
||||
if tmp == nil {
|
||||
return fmt.Errorf("error load torrent")
|
||||
}
|
||||
torr.Name = string(tmp)
|
||||
|
||||
tmp = hdb.Get([]byte("Link"))
|
||||
if tmp == nil {
|
||||
return fmt.Errorf("error load torrent")
|
||||
}
|
||||
torr.Magnet = string(tmp)
|
||||
|
||||
tmp = hdb.Get([]byte("Size"))
|
||||
if tmp == nil {
|
||||
return fmt.Errorf("error load torrent")
|
||||
}
|
||||
torr.Size = b2i(tmp)
|
||||
|
||||
tmp = hdb.Get([]byte("Timestamp"))
|
||||
if tmp == nil {
|
||||
return fmt.Errorf("error load torrent")
|
||||
}
|
||||
torr.Timestamp = b2i(tmp)
|
||||
|
||||
torrs = append(torrs, torr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
db.Close()
|
||||
if err == nil && len(torrs) > 0 {
|
||||
for _, torr := range torrs {
|
||||
spec, err := utils.ParseLink(torr.Magnet)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
title := torr.Name
|
||||
if len(spec.DisplayName) > len(title) {
|
||||
title = spec.DisplayName
|
||||
}
|
||||
log.TLogln("Migrate torrent", torr.Name, torr.Hash, torr.Size)
|
||||
AddTorrent(&TorrentDB{
|
||||
TorrentSpec: spec,
|
||||
Title: title,
|
||||
Timestamp: torr.Timestamp,
|
||||
Size: torr.Size,
|
||||
})
|
||||
}
|
||||
}
|
||||
os.Remove(filepath.Join(Path, "torrserver.db"))
|
||||
}
|
||||
|
||||
// MigrateSettingsToJson migrates Settings from BBolt to JSON
|
||||
func MigrateSettingsToJson(bboltDB, jsonDB TorrServerDB) error {
|
||||
// if BTsets != nil {
|
||||
// return errors.New("migration must be called before initializing BTSets")
|
||||
// }
|
||||
migrated, err := MigrateSingle(bboltDB, jsonDB, "Settings", "BitTorr")
|
||||
if migrated {
|
||||
log.TLogln("Settings migrated from BBolt to JSON")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MigrateSettingsFromJson migrates Settings from JSON to BBolt
|
||||
func MigrateSettingsFromJson(jsonDB, bboltDB TorrServerDB) error {
|
||||
// if BTsets != nil {
|
||||
// return errors.New("migration must be called before initializing BTSets")
|
||||
// }
|
||||
migrated, err := MigrateSingle(jsonDB, bboltDB, "Settings", "BitTorr")
|
||||
if migrated {
|
||||
log.TLogln("Settings migrated from JSON to BBolt")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MigrateViewedToJson migrates Viewed data from BBolt to JSON
|
||||
func MigrateViewedToJson(bboltDB, jsonDB TorrServerDB) error {
|
||||
migrated, skipped, err := MigrateAll(bboltDB, jsonDB, "Viewed")
|
||||
log.TLogln(fmt.Sprintf("Viewed->JSON: %d migrated, %d skipped", migrated, skipped))
|
||||
return err
|
||||
}
|
||||
|
||||
// MigrateViewedFromJson migrates Viewed data from JSON to BBolt
|
||||
func MigrateViewedFromJson(jsonDB, bboltDB TorrServerDB) error {
|
||||
migrated, skipped, err := MigrateAll(jsonDB, bboltDB, "Viewed")
|
||||
log.TLogln(fmt.Sprintf("Viewed->BBolt: %d migrated, %d skipped", migrated, skipped))
|
||||
return err
|
||||
}
|
||||
|
||||
// MigrateSingle migrates a single entry with validation
|
||||
// Returns: (migrated bool, error)
|
||||
func MigrateSingle(source, target TorrServerDB, xpath, name string) (bool, error) {
|
||||
sourceData := source.Get(xpath, name)
|
||||
if sourceData == nil {
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("No data to migrate for %s/%s", xpath, name))
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
targetData := target.Get(xpath, name)
|
||||
if targetData != nil {
|
||||
// Check if already identical
|
||||
if equal, err := isByteArraysEqualJson(sourceData, targetData); err == nil && equal {
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("Skipping %s/%s (already identical)", xpath, name))
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Perform migration
|
||||
target.Set(xpath, name, sourceData)
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("Migrating %s/%s", xpath, name))
|
||||
}
|
||||
|
||||
// Verify migration
|
||||
if err := verifyMigration(source, target, xpath, name, sourceData); err != nil {
|
||||
return false, fmt.Errorf("migration verification failed for %s/%s: %w", xpath, name, err)
|
||||
}
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("Successfully migrated %s/%s", xpath, name))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// MigrateAll migrates all entries in an xpath with validation
|
||||
// Returns: (migratedCount, skippedCount, error)
|
||||
func MigrateAll(source, target TorrServerDB, xpath string) (int, int, error) {
|
||||
names := source.List(xpath)
|
||||
if len(names) == 0 {
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("No entries to migrate for %s", xpath))
|
||||
}
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
migratedCount := 0
|
||||
skippedCount := 0
|
||||
var firstError error
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("Starting migration of %d %s entries", len(names), xpath))
|
||||
}
|
||||
for i, name := range names {
|
||||
sourceData := source.Get(xpath, name)
|
||||
if sourceData == nil {
|
||||
skippedCount++
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("[%d/%d] Skipping %s/%s (no data in source)",
|
||||
i+1, len(names), xpath, name))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
targetData := target.Get(xpath, name)
|
||||
if targetData != nil {
|
||||
// Check if already identical
|
||||
if equal, err := isByteArraysEqualJson(sourceData, targetData); err == nil && equal {
|
||||
skippedCount++
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("[%d/%d] Skipping %s/%s (already identical)",
|
||||
i+1, len(names), xpath, name))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Perform migration
|
||||
target.Set(xpath, name, sourceData)
|
||||
|
||||
// Verify migration
|
||||
if err := verifyMigration(source, target, xpath, name, sourceData); err != nil {
|
||||
log.TLogln(fmt.Sprintf("[%d/%d] Migration failed for %s/%s: %v",
|
||||
i+1, len(names), xpath, name, err))
|
||||
if firstError == nil {
|
||||
firstError = err
|
||||
}
|
||||
} else {
|
||||
migratedCount++
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("[%d/%d] Successfully migrated %s/%s",
|
||||
i+1, len(names), xpath, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary := fmt.Sprintf("%s migration complete: %d migrated, %d skipped",
|
||||
xpath, migratedCount, skippedCount)
|
||||
if firstError != nil {
|
||||
summary += fmt.Sprintf(", 1+ errors (first: %v)", firstError)
|
||||
}
|
||||
if IsDebug() {
|
||||
log.TLogln(summary)
|
||||
}
|
||||
|
||||
return migratedCount, skippedCount, firstError
|
||||
}
|
||||
|
||||
// SmartMigrate - keep for manual/advanced use
|
||||
func SmartMigrate(bboltDB, jsonDB TorrServerDB, forceDirection string) error {
|
||||
// if BTsets != nil {
|
||||
// return errors.New("migration must be called before initializing BTSets")
|
||||
// }
|
||||
switch forceDirection {
|
||||
case "viewed_to_json":
|
||||
return MigrateViewedToJson(bboltDB, jsonDB)
|
||||
case "viewed_to_bbolt":
|
||||
return MigrateViewedFromJson(jsonDB, bboltDB)
|
||||
case "settings_to_json":
|
||||
return MigrateSettingsToJson(bboltDB, jsonDB)
|
||||
case "settings_to_bbolt":
|
||||
return MigrateSettingsFromJson(jsonDB, bboltDB)
|
||||
case "sync_both":
|
||||
// Simple sync: copy missing data both ways
|
||||
if err := migrateMissing(bboltDB, jsonDB, "Settings", "BitTorr"); err != nil {
|
||||
return err
|
||||
}
|
||||
return syncViewedSimple(bboltDB, jsonDB)
|
||||
default:
|
||||
return fmt.Errorf("unknown migration direction: %s", forceDirection)
|
||||
}
|
||||
}
|
||||
|
||||
func isByteArraysEqualJson(a, b []byte) (bool, error) {
|
||||
if len(a) == 0 && len(b) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
// Quick check: same length and byte equality
|
||||
if len(a) == len(b) {
|
||||
// Fast path: byte-by-byte comparison
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
break // Need to parse as JSON
|
||||
}
|
||||
}
|
||||
// If we get here, bytes are identical
|
||||
return true, nil
|
||||
}
|
||||
// Parse as JSON for structural comparison
|
||||
var objectA, objectB interface{}
|
||||
|
||||
if err := json.Unmarshal(a, &objectA); err != nil {
|
||||
return false, fmt.Errorf("error unmarshalling A: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &objectB); err != nil {
|
||||
return false, fmt.Errorf("error unmarshalling B: %w", err)
|
||||
}
|
||||
|
||||
return reflect.DeepEqual(objectA, objectB), nil
|
||||
}
|
||||
|
||||
// Optimized version for performance
|
||||
func isByteArraysEqualJsonOptimized(a, b []byte) (bool, error) {
|
||||
// Fast paths
|
||||
if a == nil && b == nil {
|
||||
return true, nil
|
||||
}
|
||||
if len(a) != len(b) {
|
||||
return false, nil
|
||||
}
|
||||
if len(a) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
// Byte equality (fastest check)
|
||||
equal := true
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
equal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if equal {
|
||||
return true, nil
|
||||
}
|
||||
// Parse as JSON (slower but accurate)
|
||||
return isByteArraysEqualJson(a, b)
|
||||
}
|
||||
|
||||
func verifyMigration(source, target TorrServerDB, xpath, name string, originalData []byte) error {
|
||||
// Get migrated data
|
||||
migratedData := target.Get(xpath, name)
|
||||
if migratedData == nil {
|
||||
return fmt.Errorf("migration failed: no data after migration for %s/%s", xpath, name)
|
||||
}
|
||||
// Compare with original
|
||||
if equal, err := isByteArraysEqualJsonOptimized(originalData, migratedData); err != nil {
|
||||
return fmt.Errorf("verification failed for %s/%s: %w", xpath, name, err)
|
||||
} else if !equal {
|
||||
return fmt.Errorf("data mismatch after migration for %s/%s", xpath, name)
|
||||
}
|
||||
if IsDebug() {
|
||||
log.TLogln(fmt.Sprintf("Verified migration of %s/%s", xpath, name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func b2i(v []byte) int64 {
|
||||
return int64(binary.BigEndian.Uint64(v))
|
||||
}
|
||||
|
||||
func migrateMissing(db1, db2 TorrServerDB, xpath, name string) error {
|
||||
// Copy from db1 to db2 if missing
|
||||
if db2.Get(xpath, name) == nil {
|
||||
if data := db1.Get(xpath, name); data != nil {
|
||||
db2.Set(xpath, name, data)
|
||||
}
|
||||
}
|
||||
// Copy from db2 to db1 if missing
|
||||
if db1.Get(xpath, name) == nil {
|
||||
if data := db2.Get(xpath, name); data != nil {
|
||||
db1.Set(xpath, name, data)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncViewedSimple(bboltDB, jsonDB TorrServerDB) error {
|
||||
// Get all hashes from both
|
||||
bboltHashes := bboltDB.List("Viewed")
|
||||
jsonHashes := jsonDB.List("Viewed")
|
||||
|
||||
allHashes := make(map[string]bool)
|
||||
for _, h := range bboltHashes {
|
||||
allHashes[h] = true
|
||||
}
|
||||
for _, h := range jsonHashes {
|
||||
allHashes[h] = true
|
||||
}
|
||||
|
||||
// For each hash, ensure it exists in both with merged data
|
||||
for hash := range allHashes {
|
||||
bboltData := bboltDB.Get("Viewed", hash)
|
||||
jsonData := jsonDB.Get("Viewed", hash)
|
||||
|
||||
merged := mergeViewedDataSimple(bboltData, jsonData)
|
||||
if merged != nil {
|
||||
bboltDB.Set("Viewed", hash, merged)
|
||||
jsonDB.Set("Viewed", hash, merged)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeViewedDataSimple(data1, data2 []byte) []byte {
|
||||
if data1 == nil && data2 == nil {
|
||||
return nil
|
||||
}
|
||||
if data1 == nil {
|
||||
return data2
|
||||
}
|
||||
if data2 == nil {
|
||||
return data1
|
||||
}
|
||||
|
||||
// Try to merge
|
||||
var indices1, indices2 map[int]struct{}
|
||||
json.Unmarshal(data1, &indices1)
|
||||
json.Unmarshal(data2, &indices2)
|
||||
|
||||
merged := make(map[int]struct{})
|
||||
for idx := range indices1 {
|
||||
merged[idx] = struct{}{}
|
||||
}
|
||||
for idx := range indices2 {
|
||||
merged[idx] = struct{}{}
|
||||
}
|
||||
|
||||
result, _ := json.Marshal(merged)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
)
|
||||
|
||||
type TorrentDB struct {
|
||||
*torrent.TorrentSpec
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Poster string `json:"poster,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Id int `json:"id,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func AddTorrent(torr *TorrentDB) {
|
||||
list := ListTorrent()
|
||||
mu.Lock()
|
||||
find := -1
|
||||
for i, db := range list {
|
||||
if db.InfoHash.HexString() == torr.InfoHash.HexString() {
|
||||
find = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if find != -1 {
|
||||
list[find] = torr
|
||||
} else {
|
||||
list = append(list, torr)
|
||||
}
|
||||
for _, db := range list {
|
||||
buf, err := json.Marshal(db)
|
||||
if err == nil {
|
||||
tdb.Set("Torrents", db.InfoHash.HexString(), buf)
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
func ListTorrent() []*TorrentDB {
|
||||
// Use read lock to prevent migration during read
|
||||
dbMigrationLock.RLock()
|
||||
defer dbMigrationLock.RUnlock()
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
var list []*TorrentDB
|
||||
keys := tdb.List("Torrents")
|
||||
for _, key := range keys {
|
||||
buf := tdb.Get("Torrents", key)
|
||||
if len(buf) > 0 {
|
||||
var torr *TorrentDB
|
||||
err := json.Unmarshal(buf, &torr)
|
||||
if err == nil {
|
||||
list = append(list, torr)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].Timestamp > list[j].Timestamp
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
func RemTorrent(hash metainfo.Hash) {
|
||||
mu.Lock()
|
||||
tdb.Rem("Torrents", hash.HexString())
|
||||
mu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package settings
|
||||
|
||||
type TorrServerDB interface {
|
||||
CloseDB()
|
||||
Get(xPath, name string) []byte
|
||||
Set(xPath, name string, value []byte)
|
||||
List(xPath string) []string
|
||||
Rem(xPath, name string)
|
||||
Clear(xPath string)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"server/log"
|
||||
)
|
||||
|
||||
type Viewed struct {
|
||||
Hash string `json:"hash"`
|
||||
FileIndex int `json:"file_index"`
|
||||
}
|
||||
|
||||
func SetViewed(vv *Viewed) {
|
||||
var indexes map[int]struct{}
|
||||
var err error
|
||||
|
||||
buf := tdb.Get("Viewed", vv.Hash)
|
||||
if len(buf) == 0 {
|
||||
indexes = make(map[int]struct{})
|
||||
indexes[vv.FileIndex] = struct{}{}
|
||||
buf, err = json.Marshal(indexes)
|
||||
if err == nil {
|
||||
tdb.Set("Viewed", vv.Hash, buf)
|
||||
}
|
||||
} else {
|
||||
err = json.Unmarshal(buf, &indexes)
|
||||
if err == nil {
|
||||
indexes[vv.FileIndex] = struct{}{}
|
||||
buf, err = json.Marshal(indexes)
|
||||
if err == nil {
|
||||
tdb.Set("Viewed", vv.Hash, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.TLogln("Error set viewed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func RemViewed(vv *Viewed) {
|
||||
buf := tdb.Get("Viewed", vv.Hash)
|
||||
var indeces map[int]struct{}
|
||||
err := json.Unmarshal(buf, &indeces)
|
||||
if err == nil {
|
||||
if vv.FileIndex != -1 {
|
||||
delete(indeces, vv.FileIndex)
|
||||
buf, err = json.Marshal(indeces)
|
||||
if err == nil {
|
||||
tdb.Set("Viewed", vv.Hash, buf)
|
||||
}
|
||||
} else {
|
||||
tdb.Rem("Viewed", vv.Hash)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.TLogln("Error rem viewed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ListViewed(hash string) []*Viewed {
|
||||
var err error
|
||||
if hash != "" {
|
||||
buf := tdb.Get("Viewed", hash)
|
||||
if len(buf) == 0 {
|
||||
return []*Viewed{}
|
||||
}
|
||||
var indeces map[int]struct{}
|
||||
err = json.Unmarshal(buf, &indeces)
|
||||
if err == nil {
|
||||
var ret []*Viewed
|
||||
for i := range indeces {
|
||||
ret = append(ret, &Viewed{hash, i})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
} else {
|
||||
var ret []*Viewed
|
||||
keys := tdb.List("Viewed")
|
||||
for _, key := range keys {
|
||||
buf := tdb.Get("Viewed", key)
|
||||
if len(buf) == 0 {
|
||||
return []*Viewed{}
|
||||
}
|
||||
var indeces map[int]struct{}
|
||||
err = json.Unmarshal(buf, &indeces)
|
||||
if err == nil {
|
||||
for i := range indeces {
|
||||
ret = append(ret, &Viewed{key, i})
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
log.TLogln("Error list viewed:", err)
|
||||
return []*Viewed{}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"server/log"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type XPathDBRouter struct {
|
||||
dbs []TorrServerDB
|
||||
routes []string
|
||||
route2db map[string]TorrServerDB
|
||||
dbNames map[TorrServerDB]string
|
||||
}
|
||||
|
||||
func NewXPathDBRouter() *XPathDBRouter {
|
||||
router := &XPathDBRouter{
|
||||
dbs: []TorrServerDB{},
|
||||
dbNames: map[TorrServerDB]string{},
|
||||
routes: []string{},
|
||||
route2db: map[string]TorrServerDB{},
|
||||
}
|
||||
return router
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) RegisterRoute(db TorrServerDB, xPath string) error {
|
||||
newRoute := v.xPathToRoute(xPath)
|
||||
|
||||
if slices.Contains(v.routes, newRoute) {
|
||||
return fmt.Errorf("route \"%s\" already in routing table", newRoute)
|
||||
}
|
||||
|
||||
// First DB becomes Default DB with default route
|
||||
if len(v.dbs) == 0 && len(newRoute) != 0 {
|
||||
v.RegisterRoute(db, "")
|
||||
}
|
||||
|
||||
if !slices.Contains(v.dbs, db) {
|
||||
v.dbs = append(v.dbs, db)
|
||||
v.dbNames[db] = reflect.TypeOf(db).Elem().Name()
|
||||
v.log(fmt.Sprintf("Registered new DB \"%s\", total %d DBs registered", v.getDBName(db), len(v.dbs)))
|
||||
}
|
||||
|
||||
v.route2db[newRoute] = db
|
||||
v.routes = append(v.routes, newRoute)
|
||||
|
||||
// Sort routes by length descending.
|
||||
// It is important later to help selecting
|
||||
// most suitable route in getDBForXPath(xPath)
|
||||
sort.Slice(v.routes, func(iLeft, iRight int) bool {
|
||||
return len(v.routes[iLeft]) > len(v.routes[iRight])
|
||||
})
|
||||
v.log(fmt.Sprintf("Registered new route \"%s\" for DB \"%s\", total %d routes", getDefaultRoureName(newRoute), v.getDBName(db), len(v.routes)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDefaultRoureName(route string) string {
|
||||
if len(route) > 0 {
|
||||
return route
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) xPathToRoute(xPath string) string {
|
||||
return strings.ToLower(strings.TrimSpace(xPath))
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) getDBForXPath(xPath string) TorrServerDB {
|
||||
if len(v.dbs) == 0 {
|
||||
return nil
|
||||
}
|
||||
lookup_route := v.xPathToRoute(xPath)
|
||||
var db TorrServerDB = nil
|
||||
// Expected v.routes sorted by length descending
|
||||
for _, route_prefix := range v.routes {
|
||||
if strings.HasPrefix(lookup_route, route_prefix) {
|
||||
db = v.route2db[route_prefix]
|
||||
break
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) Get(xPath, name string) []byte {
|
||||
return v.getDBForXPath(xPath).Get(xPath, name)
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) Set(xPath, name string, value []byte) {
|
||||
v.getDBForXPath(xPath).Set(xPath, name, value)
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) List(xPath string) []string {
|
||||
return v.getDBForXPath(xPath).List(xPath)
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) Rem(xPath, name string) {
|
||||
v.getDBForXPath(xPath).Rem(xPath, name)
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) Clear(xPath string) {
|
||||
v.getDBForXPath(xPath).Clear(xPath)
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) CloseDB() {
|
||||
for _, db := range v.dbs {
|
||||
db.CloseDB()
|
||||
}
|
||||
v.dbs = nil
|
||||
v.routes = nil
|
||||
v.route2db = nil
|
||||
v.dbNames = nil
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) getDBName(db TorrServerDB) string {
|
||||
return v.dbNames[db]
|
||||
}
|
||||
|
||||
func (v *XPathDBRouter) log(s string, params ...interface{}) {
|
||||
if len(params) > 0 {
|
||||
log.TLogln(fmt.Sprintf("XPathDBRouter: %s: %s", s, fmt.Sprint(params...)))
|
||||
} else {
|
||||
log.TLogln(fmt.Sprintf("XPathDBRouter: %s", s))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user