small refactor*
This commit is contained in:
parent
b6b541e050
commit
24a4d30275
232 changed files with 2164 additions and 1906 deletions
daemons/qbittorrent
|
@ -1,413 +0,0 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/pkg/qbittorrent"
|
||||
"git.kmsign.ru/royalcat/tstor/pkg/rlog"
|
||||
"git.kmsign.ru/royalcat/tstor/src/vfs"
|
||||
"github.com/iceber/iouring-go"
|
||||
)
|
||||
|
||||
type FS struct {
|
||||
mu sync.Mutex
|
||||
client *cacheClient
|
||||
name string
|
||||
hash string
|
||||
dataDir string // directory where torrent files are stored
|
||||
modTime time.Time
|
||||
|
||||
ur *iouring.IOURing
|
||||
|
||||
entries map[string]fileEntry
|
||||
|
||||
log *rlog.Logger
|
||||
|
||||
vfs.FilesystemPrototype
|
||||
}
|
||||
|
||||
type fileEntry struct {
|
||||
fs.FileInfo
|
||||
Content *qbittorrent.TorrentContent
|
||||
}
|
||||
|
||||
var _ vfs.Filesystem = (*FS)(nil)
|
||||
|
||||
func newTorrentFS(ctx context.Context, client *cacheClient, name string, hash string, modTime time.Time, dataDir string) (*FS, error) {
|
||||
ctx, span := trace.Start(ctx, "newTorrentFS")
|
||||
defer span.End()
|
||||
|
||||
cnts, err := client.listContent(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list content for hash %s: %w", hash, err)
|
||||
}
|
||||
|
||||
entries := make(map[string]fileEntry, len(cnts))
|
||||
for _, cnt := range cnts {
|
||||
if cnt.Priority == qbittorrent.PriorityDoNotDownload {
|
||||
continue
|
||||
}
|
||||
|
||||
entries[vfs.AbsPath(cnt.Name)] = fileEntry{
|
||||
Content: cnt,
|
||||
FileInfo: vfs.NewFileInfo(cnt.Name, cnt.Size, modTime),
|
||||
}
|
||||
}
|
||||
|
||||
return &FS{
|
||||
client: client,
|
||||
name: name,
|
||||
hash: hash,
|
||||
modTime: modTime,
|
||||
|
||||
dataDir: dataDir,
|
||||
|
||||
entries: entries,
|
||||
|
||||
log: rlog.Component("qbittorrent", "fs"),
|
||||
|
||||
FilesystemPrototype: vfs.FilesystemPrototype(name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Open implements vfs.Filesystem.
|
||||
func (f *FS) Open(ctx context.Context, name string) (vfs.File, error) {
|
||||
if name == vfs.Separator {
|
||||
return vfs.NewDirFile(name), nil
|
||||
}
|
||||
|
||||
if entry, ok := f.entries[name]; ok {
|
||||
return openFile(ctx, f.ur, f.client, f.dataDir, f.hash, entry.Content)
|
||||
}
|
||||
|
||||
for p := range f.entries {
|
||||
if strings.HasPrefix(p, name) {
|
||||
return vfs.NewDirFile(name), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, vfs.ErrNotExist
|
||||
}
|
||||
|
||||
// ReadDir implements vfs.Filesystem.
|
||||
func (f *FS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
|
||||
infos := make(map[string]fs.FileInfo, len(f.entries))
|
||||
for k, v := range f.entries {
|
||||
infos[k] = v.FileInfo
|
||||
}
|
||||
|
||||
return vfs.ListDirFromInfo(infos, name)
|
||||
}
|
||||
|
||||
// Stat implements vfs.Filesystem.
|
||||
func (f *FS) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
|
||||
name = vfs.AbsPath(path.Clean(name))
|
||||
|
||||
if vfs.IsRoot(name) {
|
||||
return vfs.NewDirInfo(f.name, f.modTime), nil
|
||||
}
|
||||
|
||||
if entry, ok := f.entries[name]; ok {
|
||||
return entry.FileInfo, nil
|
||||
}
|
||||
|
||||
for p := range f.entries {
|
||||
if strings.HasPrefix(p, name) {
|
||||
return vfs.NewDirInfo(name, f.modTime), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, vfs.ErrNotExist
|
||||
}
|
||||
|
||||
// Unlink implements vfs.Filesystem.
|
||||
func (f *FS) Unlink(ctx context.Context, filename string) error {
|
||||
filename = vfs.AbsPath(path.Clean(filename))
|
||||
|
||||
// we cannot delete a torrent itself, cause it will be added on next source scan and all delited files will be restored
|
||||
|
||||
if entry, ok := f.entries[filename]; ok {
|
||||
return f.removeFile(ctx, f.hash, entry.Content)
|
||||
}
|
||||
|
||||
for p, entry := range f.entries {
|
||||
if strings.HasPrefix(p, filename) {
|
||||
return f.removeFile(ctx, f.hash, entry.Content)
|
||||
}
|
||||
}
|
||||
|
||||
return vfs.ErrNotExist
|
||||
}
|
||||
|
||||
func (f *FS) Rename(ctx context.Context, oldpath string, newpath string) error {
|
||||
oldpath = vfs.AbsPath(path.Clean(oldpath))
|
||||
newpath = vfs.AbsPath(path.Clean(newpath))
|
||||
|
||||
if _, ok := f.entries[oldpath]; ok {
|
||||
err := f.client.qb.Torrent().RenameFile(ctx, f.hash, vfs.RelPath(oldpath), vfs.RelPath(newpath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename file %s to %s: %w", oldpath, newpath, err)
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
f.entries[newpath] = f.entries[oldpath]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return vfs.ErrNotExist
|
||||
}
|
||||
|
||||
func (f *FS) removeFile(ctx context.Context, hash string, content *qbittorrent.TorrentContent) error {
|
||||
log := f.log.With(slog.String("hash", hash), slog.String("file", content.Name))
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
fpath := vfs.AbsPath(content.Name)
|
||||
|
||||
if _, ok := f.entries[fpath]; !ok {
|
||||
return fmt.Errorf("file %s is does not found", fpath)
|
||||
}
|
||||
delete(f.entries, fpath)
|
||||
|
||||
err := f.client.qb.Torrent().SetFilePriority(ctx, f.hash, content.Index, qbittorrent.PriorityDoNotDownload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set priority for torrent %s for file %s: %w", hash, content.Name, err)
|
||||
}
|
||||
|
||||
err = os.Remove(path.Join(f.dataDir, vfs.RelPath(content.Name)))
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
log.Warn(ctx, "failed to remove file", rlog.Error(err))
|
||||
return fmt.Errorf("failed to remove file %s: %w", content.Name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func openFile(ctx context.Context, ur *iouring.IOURing, client *cacheClient, torrentDir string, hash string, content *qbittorrent.TorrentContent) (*File, error) {
|
||||
props, err := client.getProperties(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME error when file not started downloading
|
||||
file, err := os.OpenFile(path.Join(torrentDir, content.Name), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &File{
|
||||
client: client,
|
||||
hash: hash,
|
||||
torrentDir: torrentDir,
|
||||
|
||||
filePath: content.Name,
|
||||
contentIndex: content.Index,
|
||||
pieceSize: props.PieceSize,
|
||||
fileSize: content.Size,
|
||||
|
||||
file: file,
|
||||
|
||||
offset: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type File struct {
|
||||
client *cacheClient
|
||||
hash string
|
||||
torrentModTime time.Time
|
||||
torrentDir string
|
||||
filePath string // path inside a torrent directory
|
||||
contentIndex int
|
||||
pieceSize int
|
||||
fileSize int64
|
||||
|
||||
mu sync.Mutex
|
||||
file *os.File
|
||||
offset int64
|
||||
}
|
||||
|
||||
var _ vfs.File = (*File)(nil)
|
||||
|
||||
// Info implements vfs.File.
|
||||
func (f *File) Info() (fs.FileInfo, error) {
|
||||
return vfs.NewFileInfo(path.Base(f.filePath), f.fileSize, f.torrentModTime), nil
|
||||
}
|
||||
|
||||
// IsDir implements vfs.File.
|
||||
func (f *File) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Seek implements vfs.File.
|
||||
func (f *File) Seek(offset int64, whence int) (int64, error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
f.offset = offset
|
||||
case io.SeekCurrent:
|
||||
f.offset += offset
|
||||
case io.SeekEnd:
|
||||
f.offset = f.fileSize + offset
|
||||
}
|
||||
return f.offset, nil
|
||||
}
|
||||
|
||||
// Name implements vfs.File.
|
||||
func (f *File) Name() string {
|
||||
return path.Base(f.filePath)
|
||||
}
|
||||
|
||||
func (f *File) canExpectSoon(ctx context.Context) (bool, error) {
|
||||
info, err := f.client.getInfo(ctx, f.hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if info == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return info.Completed == info.Size || info.State == qbittorrent.TorrentStateCheckingUP || info.State == qbittorrent.TorrentStateDownloading || info.State == qbittorrent.TorrentStateForcedDL, nil
|
||||
}
|
||||
|
||||
func (f *File) isRangeComplete(ctx context.Context, offset int64, size int) (bool, error) {
|
||||
startPieceIndex := int(offset / int64(f.pieceSize))
|
||||
pieceCount := (size + f.pieceSize - 1) / f.pieceSize // rouding up
|
||||
|
||||
for i := range pieceCount {
|
||||
ok, err := f.client.isPieceComplete(ctx, f.hash, startPieceIndex+i)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (f *File) waitRangeAvailable(ctx context.Context, offset int64, size int) error {
|
||||
complete, err := f.isRangeComplete(ctx, offset, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if complete {
|
||||
return nil
|
||||
}
|
||||
|
||||
canExpectSoon, err := f.canExpectSoon(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canExpectSoon {
|
||||
return fmt.Errorf("torrent is not downloading")
|
||||
}
|
||||
|
||||
const checkingInterval = 1 * time.Second
|
||||
|
||||
ticker := time.NewTicker(checkingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
complete, err := f.isRangeComplete(ctx, offset, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if complete {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read implements vfs.File.
|
||||
func (f *File) Read(ctx context.Context, p []byte) (int, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if err := f.waitRangeAvailable(ctx, f.offset, len(p)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := f.file.ReadAt(p, f.offset)
|
||||
f.offset += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ReadAt implements vfs.File.
|
||||
func (f *File) ReadAt(ctx context.Context, p []byte, off int64) (int, error) {
|
||||
if err := f.waitRangeAvailable(ctx, f.offset, len(p)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return f.file.ReadAt(p, off)
|
||||
}
|
||||
|
||||
// Size implements vfs.File.
|
||||
func (f *File) Size() int64 {
|
||||
return f.fileSize
|
||||
}
|
||||
|
||||
// Type implements vfs.File.
|
||||
func (f *File) Type() fs.FileMode {
|
||||
return fs.ModeDir
|
||||
}
|
||||
|
||||
// Close implements vfs.File.
|
||||
func (f *File) Close(ctx context.Context) error {
|
||||
return f.file.Close()
|
||||
}
|
||||
|
||||
// type fileInfo struct {
|
||||
// name string
|
||||
// size int64
|
||||
// modTime time.Time
|
||||
// }
|
||||
|
||||
// var _ fs.FileInfo = (*fileInfo)(nil)
|
||||
|
||||
// // IsDir implements fs.FileInfo.
|
||||
// func (f *fileInfo) IsDir() bool {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// // ModTime implements fs.FileInfo.
|
||||
// func (f *fileInfo) ModTime() time.Time {
|
||||
// return f.modTime
|
||||
// }
|
||||
|
||||
// // Mode implements fs.FileInfo.
|
||||
// func (f *fileInfo) Mode() fs.FileMode {
|
||||
// return vfs.ModeFileRO
|
||||
// }
|
||||
|
||||
// // Name implements fs.FileInfo.
|
||||
// func (f *fileInfo) Name() string {
|
||||
// return f.name
|
||||
// }
|
||||
|
||||
// // Size implements fs.FileInfo.
|
||||
// func (f *fileInfo) Size() int64 {
|
||||
// return f.size
|
||||
// }
|
||||
|
||||
// // Sys implements fs.FileInfo.
|
||||
// func (f *fileInfo) Sys() any {
|
||||
// return nil
|
||||
// }
|
Loading…
Add table
Add a link
Reference in a new issue