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

This commit is contained in:
2026-05-30 12:07:11 +00:00
commit 616c6b1c62
381 changed files with 55145 additions and 0 deletions
+31
View File
@@ -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
+212
View File
@@ -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()
}
+207
View File
@@ -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)
}
}
+175
View File
@@ -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}
}
+192
View File
@@ -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))
}
}
+431
View File
@@ -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
}
+452
View File
@@ -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()
}
}
+86
View File
@@ -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()
}
+10
View File
@@ -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)
}
+98
View File
@@ -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{}
}
+129
View File
@@ -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))
}
}