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
+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
}