tstor/daemons/qbittorrent/daemon.go

290 lines
7 KiB
Go
Raw Permalink Normal View History

package qbittorrent
import (
"context"
2024-08-31 23:00:13 +00:00
"errors"
"fmt"
"io"
"log/slog"
"os"
"path"
2024-08-31 23:00:13 +00:00
"path/filepath"
2024-10-19 01:24:14 +00:00
"sync"
2024-08-31 23:00:13 +00:00
"time"
2024-08-31 23:00:13 +00:00
"git.kmsign.ru/royalcat/tstor/pkg/qbittorrent"
"git.kmsign.ru/royalcat/tstor/pkg/rlog"
"git.kmsign.ru/royalcat/tstor/src/config"
"git.kmsign.ru/royalcat/tstor/src/logwrap"
"git.kmsign.ru/royalcat/tstor/src/vfs"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/types/infohash"
2024-08-31 23:00:13 +00:00
infohash_v2 "github.com/anacrolix/torrent/types/infohash-v2"
2024-11-15 13:39:56 +00:00
mapset "github.com/deckarep/golang-set/v2"
2024-10-14 00:58:42 +00:00
"github.com/iceber/iouring-go"
"github.com/royalcat/ctxio"
2024-09-24 13:26:15 +00:00
"go.opentelemetry.io/otel"
)
2024-11-24 17:33:44 +00:00
var trace = otel.Tracer("git.kmsign.ru/royalcat/tstor/daemons/qbittorrent")
2024-09-24 13:26:15 +00:00
type Daemon struct {
2024-10-19 01:24:14 +00:00
proc *os.Process
qb qbittorrent.Client
client *cacheClient
sourceFilesMu sync.Mutex
sourceFiles map[string]string // [sourcePath]infohash
2024-11-15 13:39:56 +00:00
registeredTorrents mapset.Set[string] // infohash list
dataDir string
2024-10-14 00:58:42 +00:00
ur *iouring.IOURing
2024-08-31 23:00:13 +00:00
log *rlog.Logger
}
2024-08-31 23:00:13 +00:00
const defaultConf = `
[LegalNotice]
Accepted=true
2024-08-31 23:00:13 +00:00
[Preferences]
WebUI\LocalHostAuth=false
WebUI\Password_PBKDF2="@ByteArray(qef5I4wZBkDG+PP6/5mQwA==:LoTmorQM/QM5RHI4+dOiu6xfAz9xak6fhR4ZGpRtJF3JNCGG081Yrtva4G71kXz//ODUuWQKTLlrZPuIDvzqUQ==)"
`
func NewDaemon(conf config.QBittorrent) (*Daemon, error) {
ctx := context.Background()
log := rlog.Component("qbittorrent")
binPath := conf.MetadataFolder + "/qbittorrent-nox"
err := downloadLatestQbitRelease(ctx, binPath)
if err != nil {
return nil, err
}
daemonLog := log.WithComponent("process")
outLog := logwrap.NewSlogWriter(ctx, slog.LevelInfo, daemonLog.Slog())
errLog := logwrap.NewSlogWriter(ctx, slog.LevelError, daemonLog.Slog())
_, err = os.Stat(conf.MetadataFolder + "/profile/qBittorrent/config/qBittorrent.conf")
if errors.Is(err, os.ErrNotExist) {
err = os.MkdirAll(conf.MetadataFolder+"/profile/qBittorrent/config", 0744)
if err != nil {
return nil, err
}
err = os.WriteFile(conf.MetadataFolder+"/profile/qBittorrent/config/qBittorrent.conf", []byte(defaultConf), 0644)
if err != nil {
return nil, err
}
}
err = os.MkdirAll(conf.DataFolder, 0744)
if err != nil {
return nil, err
}
const port = 25436
proc, err := runQBittorrent(binPath, conf.MetadataFolder+"/profile", port, outLog, errLog)
if err != nil {
return nil, err
}
time.Sleep(time.Second)
qb, err := qbittorrent.NewClient(ctx, &qbittorrent.Config{
Address: fmt.Sprintf("http://localhost:%d", port),
})
if err != nil {
return nil, err
}
for { // wait for qbittorrent to start
2024-10-14 00:58:42 +00:00
ver, err := qb.Application().Version(ctx)
log.Info(ctx, "qbittorrent started", slog.String("version", ver))
2024-08-31 23:00:13 +00:00
if err == nil {
break
}
log.Warn(ctx, "waiting for qbittorrent to start", rlog.Error(err))
time.Sleep(time.Second)
}
dataDir, err := filepath.Abs(conf.DataFolder)
if err != nil {
return nil, err
}
err = qb.Application().SetPreferences(ctx, &qbittorrent.Preferences{
SavePath: dataDir,
})
if err != nil {
return nil, err
}
2024-10-14 00:58:42 +00:00
ur, err := iouring.New(8, iouring.WithAsync())
if err != nil {
return nil, err
}
return &Daemon{
2024-11-15 13:39:56 +00:00
qb: qb,
proc: proc,
dataDir: conf.DataFolder,
ur: ur,
sourceFiles: make(map[string]string),
registeredTorrents: mapset.NewSet[string](),
client: wrapClient(qb),
log: rlog.Component("qbittorrent"),
}, nil
}
2024-08-31 23:00:13 +00:00
func (d *Daemon) Close(ctx context.Context) error {
err := d.proc.Signal(os.Interrupt)
if err != nil {
return err
}
2024-08-31 23:00:13 +00:00
_, err = d.proc.Wait()
if err != nil {
return err
}
return nil
}
2024-09-24 13:26:15 +00:00
func torrentDataPath(dataDir string, ih string) (string, error) {
return filepath.Abs(path.Join(dataDir, ih))
2024-08-31 23:00:13 +00:00
}
2024-10-19 01:24:14 +00:00
func (fs *Daemon) GetTorrentFS(ctx context.Context, sourcePath string, file vfs.File) (vfs.Filesystem, error) {
2024-09-24 13:26:15 +00:00
ctx, span := trace.Start(ctx, "GetTorrentFS")
defer span.End()
2024-12-17 09:51:10 +00:00
stat, err := file.Info()
if err != nil {
return nil, err
}
2024-08-31 23:00:13 +00:00
log := fs.log.With(slog.String("file", file.Name()))
ih, err := readInfoHash(ctx, file)
if err != nil {
return nil, err
}
2024-08-31 23:00:13 +00:00
log = log.With(slog.String("infohash", ih.HexString()))
2024-09-24 13:26:15 +00:00
torrentPath, err := torrentDataPath(fs.dataDir, ih.HexString())
2024-08-31 23:00:13 +00:00
if err != nil {
return nil, fmt.Errorf("error getting torrent path: %w", err)
}
log = log.With(slog.String("torrentPath", torrentPath))
log.Debug(ctx, "creating fs for torrent")
err = fs.syncTorrentState(ctx, file, ih, torrentPath)
if err != nil {
return nil, fmt.Errorf("error syncing torrent state: %w", err)
}
2024-10-19 01:24:14 +00:00
fs.sourceFilesMu.Lock()
fs.sourceFiles[sourcePath] = ih.HexString()
fs.sourceFilesMu.Unlock()
2024-12-17 09:51:10 +00:00
return newTorrentFS(ctx, fs.client, file.Name(), ih.HexString(), stat.ModTime(), torrentPath)
2024-08-31 23:00:13 +00:00
}
func (d *Daemon) syncTorrentState(ctx context.Context, file vfs.File, ih metainfo.Hash, torrentPath string) error {
2024-09-24 13:26:15 +00:00
ctx, span := trace.Start(ctx, "syncTorrentState")
defer span.End()
2024-08-31 23:00:13 +00:00
log := d.log.With(slog.String("file", file.Name()), slog.String("infohash", ih.HexString()))
2024-09-24 13:26:15 +00:00
info, err := d.client.getInfo(ctx, ih.HexString())
if err != nil {
2024-09-24 13:26:15 +00:00
return err
}
2024-08-31 23:00:13 +00:00
log = log.With(slog.String("torrentPath", torrentPath))
2024-09-24 13:26:15 +00:00
if info == nil {
2024-08-31 23:00:13 +00:00
_, err := file.Seek(0, io.SeekStart)
if err != nil {
2024-08-31 23:00:13 +00:00
return err
}
data, err := ctxio.ReadAll(ctx, file)
if err != nil {
return err
}
err = d.qb.Torrent().AddNewTorrent(ctx, &qbittorrent.TorrentAddOption{
Torrents: []*qbittorrent.TorrentAddFileMetadata{
{
Data: data,
},
},
SavePath: torrentPath,
// SequentialDownload: "true",
FirstLastPiecePrio: "true",
})
if err != nil {
2024-11-15 13:39:56 +00:00
d.log.Error(ctx, "error adding torrent", rlog.Error(err))
2024-12-09 20:46:53 +00:00
return fmt.Errorf("error adding torrent: %w", err)
2024-08-31 23:00:13 +00:00
}
2024-11-15 13:39:56 +00:00
var props *qbittorrent.TorrentProperties
2024-08-31 23:00:13 +00:00
for {
2024-11-15 13:39:56 +00:00
props, err = d.client.getProperties(ctx, ih.HexString())
2024-08-31 23:00:13 +00:00
if err == nil {
break
}
2024-11-24 17:33:44 +00:00
if errors.Is(err, context.DeadlineExceeded) {
return err
}
2024-08-31 23:00:13 +00:00
log.Error(ctx, "waiting for torrent to be added", rlog.Error(err))
time.Sleep(time.Millisecond * 15)
}
log.Info(ctx, "added torrent", slog.String("infohash", ih.HexString()))
2024-11-15 13:39:56 +00:00
d.registeredTorrents.Add(props.Hash)
2024-08-31 23:00:13 +00:00
return nil
2024-09-24 13:26:15 +00:00
} else {
2024-08-31 23:00:13 +00:00
// info := existing[0]
2024-09-24 13:26:15 +00:00
props, err := d.client.getProperties(ctx, ih.HexString())
2024-08-31 23:00:13 +00:00
if err != nil {
2024-12-09 20:46:53 +00:00
return fmt.Errorf("error getting torrent properties: %w for infohash: %s", err, ih.HexString())
}
2024-08-31 23:00:13 +00:00
2024-11-15 13:39:56 +00:00
d.registeredTorrents.Add(props.Hash)
2024-08-31 23:00:13 +00:00
if props.SavePath != torrentPath {
log.Info(ctx, "moving torrent to correct location", slog.String("oldPath", props.SavePath))
err = d.qb.Torrent().SetLocation(ctx, []string{ih.HexString()}, torrentPath)
if err != nil {
2024-12-09 20:46:53 +00:00
return fmt.Errorf("error moving torrent: %w", err)
2024-08-31 23:00:13 +00:00
}
}
return nil
}
}
// TODO caching
func readInfoHash(ctx context.Context, file vfs.File) (infohash.T, error) {
mi, err := metainfo.Load(ctxio.IoReader(ctx, file))
if err != nil {
return infohash.T{}, err
}
2024-08-31 23:00:13 +00:00
info, err := mi.UnmarshalInfo()
if err != nil {
return infohash.T{}, err
}
if info.HasV2() {
ih := infohash_v2.HashBytes(mi.InfoBytes)
return *(&ih).ToShort(), nil
}
return infohash.HashBytes(mi.InfoBytes), nil
}