move Settings and Viewed to separate json files

This commit is contained in:
Alexey D. Filimonov
2024-02-01 20:35:54 +03:00
parent cd830f67c9
commit 83c7ed1f95
7 changed files with 465 additions and 9 deletions

View File

@@ -15,7 +15,7 @@ type TDB struct {
db *bolt.DB db *bolt.DB
} }
func NewTDB() *TDB { func NewTDB() TorrServerDB {
db, err := bolt.Open(filepath.Join(Path, "config.db"), 0o666, &bolt.Options{Timeout: 5 * time.Second}) db, err := bolt.Open(filepath.Join(Path, "config.db"), 0o666, &bolt.Options{Timeout: 5 * time.Second})
if err != nil { if err != nil {
log.TLogln(err) log.TLogln(err)

View File

@@ -0,0 +1,60 @@
package settings
type DBReadCache struct {
db TorrServerDB
listCache map[string][]string
dataCache map[[2]string][]byte
}
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 {
cacheKey := v.makeDataCacheKey(xPath, name)
if data, ok := v.dataCache[cacheKey]; ok {
return data
}
data := v.db.Get(xPath, name)
v.dataCache[cacheKey] = data
return data
}
func (v *DBReadCache) Set(xPath, name string, value []byte) {
cacheKey := v.makeDataCacheKey(xPath, name)
v.dataCache[cacheKey] = value
delete(v.listCache, xPath)
v.db.Set(xPath, name, value)
}
func (v *DBReadCache) List(xPath string) []string {
if names, ok := v.listCache[xPath]; ok {
return names
}
names := v.db.List(xPath)
v.listCache[xPath] = names
return names
}
func (v *DBReadCache) Rem(xPath, name string) {
cacheKey := v.makeDataCacheKey(xPath, name)
delete(v.dataCache, cacheKey)
delete(v.listCache, xPath)
v.db.Rem(xPath, name)
}
func (v *DBReadCache) makeDataCacheKey(xPath, name string) [2]string {
return [2]string{xPath, name}
}

159
server/settings/jsondb.go Normal file
View File

@@ -0,0 +1,159 @@
package settings
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"server/log"
"strings"
"sync"
)
type JsonDB struct {
Path string
filenameDelimiter string
filenameExtension string
fileMode fs.FileMode
xPathDelimeter string
}
var jsonDbLocks = make(map[string]*sync.Mutex)
func NewJsonDB() TorrServerDB {
settingsDB := &JsonDB{
Path: Path,
filenameDelimiter: ".",
filenameExtension: ".json",
fileMode: fs.FileMode(0o666),
xPathDelimeter: "/",
}
return settingsDB
}
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 byteData
}
} 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)
v.writeMapAsJsonFile(filename, root)
return
}
}
v.log(fmt.Sprintf("Rem: error removing entry %s->%s", xPath, name), err)
}
func (v *JsonDB) lock(filename string) {
var mtx sync.Mutex
if mtx, ok := jsonDbLocks[filename]; !ok {
mtx = new(sync.Mutex)
jsonDbLocks[v.Path] = mtx
}
mtx.Lock()
}
func (v *JsonDB) unlock(filename string) {
if mtx, ok := jsonDbLocks[filename]; ok {
mtx.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 {
err = json.Unmarshal(fileData, &jsonData)
}
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 {
err = os.WriteFile(path, fileData, v.fileMode)
}
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))
}
}

View File

@@ -2,13 +2,16 @@ package settings
import ( import (
"encoding/binary" "encoding/binary"
"encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
bolt "go.etcd.io/bbolt"
"server/log" "server/log"
"server/web/api/utils" "server/web/api/utils"
bolt "go.etcd.io/bbolt"
) )
var dbTorrentsName = []byte("Torrents") var dbTorrentsName = []byte("Torrents")
@@ -22,7 +25,8 @@ type torrentOldDB struct {
Timestamp int64 Timestamp int64
} }
func Migrate() { // Migrate from torrserver.db to config.db
func Migrate1() {
if _, err := os.Lstat(filepath.Join(Path, "torrserver.db")); os.IsNotExist(err) { if _, err := os.Lstat(filepath.Join(Path, "torrserver.db")); os.IsNotExist(err) {
return return
} }
@@ -100,3 +104,87 @@ func Migrate() {
func b2i(v []byte) int64 { func b2i(v []byte) int64 {
return int64(binary.BigEndian.Uint64(v)) return int64(binary.BigEndian.Uint64(v))
} }
/*
=== Migrate 2 ===
Migrate 'Settings' and 'Viewed' buckets from BBolt ('config.db')
to separate JSON files ('settings.json' and 'viewed.json')
'Torrents' data continues to remain in the BBolt database ('config.db')
due to the fact that BLOBs are stored there
To make user be able to roll settings back, no data is deleted from 'config.db' file.
*/
func Migrate2(bboltDB, jsonDB TorrServerDB) error {
var err error = nil
const XPATH_SETTINGS = "Settings"
const NAME_BITTORR = "BitTorr"
const XPATH_VIEWED = "Viewed"
if BTsets != nil {
msg := "Migrate0002 MUST be called before initializing BTSets"
log.TLogln(msg)
os.Exit(1)
}
isByteArraysEqualJson := func(a, b []byte) (bool, error) {
var objectA interface{}
var objectB interface{}
var err error = nil
if err = json.Unmarshal(a, &objectA); err == nil {
if err = json.Unmarshal(b, &objectB); err == nil {
return reflect.DeepEqual(objectA, objectB), nil
} else {
err = fmt.Errorf("Error unmashalling B: %s", err.Error())
}
} else {
err = fmt.Errorf("Error unmashalling A: %s", err.Error())
}
return false, err
}
migrateXPath := func(xPath, name string) error {
if jsonDB.Get(xPath, name) == nil {
bboltDBBlob := bboltDB.Get(xPath, name)
if bboltDBBlob != nil {
log.TLogln(fmt.Sprintf("Attempting to migrate %s->%s from TDB to JsonDB", xPath, name))
jsonDB.Set(xPath, name, bboltDBBlob)
jsonDBBlob := jsonDB.Get(xPath, name)
if isEqual, err := isByteArraysEqualJson(bboltDBBlob, jsonDBBlob); err == nil {
if isEqual {
log.TLogln(fmt.Sprintf("Migrated %s->%s successful", xPath, name))
} else {
msg := fmt.Sprintf("Failed to migrate %s->%s TDB to JsonDB: equality check failed", xPath, name)
log.TLogln(msg)
return errors.New(msg)
}
} else {
msg := fmt.Sprintf("Failed to migrate %s->%s TDB to JsonDB: %s", xPath, name, err)
log.TLogln(msg)
return errors.New(msg)
}
}
}
return nil
}
if err = migrateXPath(XPATH_SETTINGS, NAME_BITTORR); err != nil {
return err
}
jsonDBViewedNames := jsonDB.List(XPATH_VIEWED)
if len(jsonDBViewedNames) <= 0 {
bboltDBViewedNames := bboltDB.List(XPATH_VIEWED)
if len(bboltDBViewedNames) > 0 {
for _, name := range bboltDBViewedNames {
err = migrateXPath(XPATH_VIEWED, name)
if err != nil {
break
}
}
}
}
return err
}

View File

@@ -1,6 +1,7 @@
package settings package settings
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -8,7 +9,7 @@ import (
) )
var ( var (
tdb *TDB tdb TorrServerDB
Path string Path string
Port string Port string
Ssl bool Ssl bool
@@ -24,13 +25,35 @@ var (
func InitSets(readOnly, searchWA bool) { func InitSets(readOnly, searchWA bool) {
ReadOnly = readOnly ReadOnly = readOnly
SearchWA = searchWA SearchWA = searchWA
tdb = NewTDB()
if tdb == nil { bboltDB := NewTDB()
log.TLogln("Error open db:", filepath.Join(Path, "config.db")) 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)
}
dbRouter := NewXPathDBRouter()
// First registered DB becomes default route
dbRouter.RegisterRoute(jsonDB, "Settings")
dbRouter.RegisterRoute(jsonDB, "Viewed")
dbRouter.RegisterRoute(bboltDB, "Torrents")
tdb = NewDBReadCache(dbRouter)
// We migrate settings here, it must be done before loadBTSets()
if err := Migrate2(bboltDB, jsonDB); err != nil {
log.TLogln(fmt.Sprintf("Migrate2 failed"))
os.Exit(1) os.Exit(1)
} }
loadBTSets() loadBTSets()
Migrate() Migrate1()
} }
func CloseDB() { func CloseDB() {

View File

@@ -0,0 +1,9 @@
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)
}

View File

@@ -0,0 +1,117 @@
package settings
import (
"errors"
"fmt"
"reflect"
"server/log"
"slices"
"sort"
"strings"
)
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 errors.New(fmt.Sprintf("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", newRoute, v.getDBName(db), len(v.routes)))
return nil
}
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) 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))
}
}