2024-08-22 22:16:16 +00:00
|
|
|
package qbittorrent
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-08-31 23:00:13 +00:00
|
|
|
"fmt"
|
2024-08-22 22:16:16 +00:00
|
|
|
"io"
|
|
|
|
"io/fs"
|
|
|
|
"os"
|
|
|
|
"path"
|
2024-08-31 23:00:13 +00:00
|
|
|
"strings"
|
2024-08-22 22:16:16 +00:00
|
|
|
"time"
|
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
"git.kmsign.ru/royalcat/tstor/pkg/qbittorrent"
|
2024-08-22 22:16:16 +00:00
|
|
|
"git.kmsign.ru/royalcat/tstor/src/vfs"
|
|
|
|
)
|
|
|
|
|
|
|
|
type FS struct {
|
2024-08-31 23:00:13 +00:00
|
|
|
client *cacheClient
|
2024-08-22 22:16:16 +00:00
|
|
|
name string
|
|
|
|
hash string
|
|
|
|
dataDir string
|
2024-08-31 23:00:13 +00:00
|
|
|
|
|
|
|
content map[string]*qbittorrent.TorrentContent
|
|
|
|
files map[string]fs.FileInfo
|
|
|
|
|
|
|
|
vfs.FilesystemPrototype
|
2024-08-22 22:16:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var _ vfs.Filesystem = (*FS)(nil)
|
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
func newTorrentFS(ctx context.Context, client *cacheClient, name string, hash string, dataDir string) (*FS, error) {
|
|
|
|
cnts, err := client.listContent(ctx, hash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to list content for hash %s: %w", hash, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
content := make(map[string]*qbittorrent.TorrentContent, len(cnts))
|
|
|
|
files := make(map[string]fs.FileInfo, len(cnts))
|
|
|
|
for _, cnt := range cnts {
|
|
|
|
path := vfs.AbsPath(cnt.Name)
|
|
|
|
files[path] = vfs.NewFileInfo(cnt.Name, cnt.Size)
|
|
|
|
content[path] = cnt
|
|
|
|
}
|
|
|
|
|
2024-08-22 22:16:16 +00:00
|
|
|
return &FS{
|
2024-08-31 23:00:13 +00:00
|
|
|
client: client,
|
|
|
|
name: name,
|
|
|
|
hash: hash,
|
|
|
|
|
2024-08-22 22:16:16 +00:00
|
|
|
dataDir: dataDir,
|
2024-08-31 23:00:13 +00:00
|
|
|
|
|
|
|
content: content,
|
|
|
|
files: files,
|
|
|
|
|
|
|
|
FilesystemPrototype: vfs.FilesystemPrototype(name),
|
2024-08-22 22:16:16 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
// 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
|
|
|
|
}
|
2024-08-22 22:16:16 +00:00
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
cnt, ok := f.content[name]
|
|
|
|
if ok {
|
|
|
|
return openFile(ctx, f.client, f.dataDir, f.hash, cnt)
|
|
|
|
}
|
2024-08-22 22:16:16 +00:00
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
for p := range f.content {
|
|
|
|
if strings.HasPrefix(p, name) {
|
|
|
|
return vfs.NewDirFile(name), nil
|
|
|
|
}
|
|
|
|
}
|
2024-08-22 22:16:16 +00:00
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
return nil, vfs.ErrNotExist
|
2024-08-22 22:16:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ReadDir implements vfs.Filesystem.
|
2024-08-31 23:00:13 +00:00
|
|
|
func (fs *FS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
|
|
|
|
return vfs.ListDirFromInfo(fs.files, name)
|
2024-08-22 22:16:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Stat implements vfs.Filesystem.
|
2024-08-31 23:00:13 +00:00
|
|
|
func (f *FS) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
|
|
|
|
info, ok := f.files[name]
|
|
|
|
if !ok {
|
|
|
|
return nil, vfs.ErrNotExist
|
|
|
|
}
|
|
|
|
return info, nil
|
2024-08-22 22:16:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Unlink implements vfs.Filesystem.
|
|
|
|
func (f *FS) Unlink(ctx context.Context, filename string) error {
|
2024-08-31 23:00:13 +00:00
|
|
|
return vfs.ErrNotImplemented
|
2024-08-22 22:16:16 +00:00
|
|
|
}
|
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
func openFile(ctx context.Context, client *cacheClient, torrentDir string, hash string, content *qbittorrent.TorrentContent) (*File, error) {
|
|
|
|
props, err := client.getProperties(ctx, hash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-08-22 22:16:16 +00:00
|
|
|
|
|
|
|
return &File{
|
2024-08-31 23:00:13 +00:00
|
|
|
client: client,
|
|
|
|
hash: hash,
|
|
|
|
torrentDir: torrentDir,
|
|
|
|
|
|
|
|
filePath: content.Name,
|
|
|
|
contentIndex: content.Index,
|
|
|
|
pieceSize: props.PieceSize,
|
|
|
|
fileSize: content.Size,
|
|
|
|
|
|
|
|
offset: 0,
|
|
|
|
}, nil
|
2024-08-22 22:16:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type File struct {
|
2024-08-31 23:00:13 +00:00
|
|
|
client *cacheClient
|
2024-08-22 22:16:16 +00:00
|
|
|
hash string
|
2024-08-31 23:00:13 +00:00
|
|
|
torrentDir string
|
2024-08-22 22:16:16 +00:00
|
|
|
filePath string // path inside a torrent directory
|
|
|
|
contentIndex int
|
|
|
|
pieceSize int
|
|
|
|
fileSize int64
|
|
|
|
|
|
|
|
offset int64
|
|
|
|
osfile *os.File
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ vfs.File = (*File)(nil)
|
|
|
|
|
|
|
|
// Info implements vfs.File.
|
|
|
|
func (f *File) Info() (fs.FileInfo, error) {
|
|
|
|
return &fileInfo{name: path.Base(f.filePath), size: f.fileSize}, 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read implements vfs.File.
|
|
|
|
func (f *File) Read(ctx context.Context, p []byte) (n int, err error) {
|
|
|
|
pieceIndex := int(f.offset / int64(f.pieceSize))
|
|
|
|
err = f.client.waitPieceToComplete(ctx, f.hash, pieceIndex)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
descriptor, err := f.descriptor()
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
n, err = descriptor.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) (n int, err error) {
|
|
|
|
pieceIndex := int(off / int64(f.pieceSize))
|
|
|
|
err = f.client.waitPieceToComplete(ctx, f.hash, pieceIndex)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
descriptor, err := f.descriptor()
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return descriptor.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 {
|
2024-08-31 23:00:13 +00:00
|
|
|
return fs.ModeDir
|
2024-08-22 22:16:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *File) descriptor() (*os.File, error) {
|
|
|
|
if f.osfile != nil {
|
|
|
|
return f.osfile, nil
|
|
|
|
}
|
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
osfile, err := os.Open(path.Join(f.torrentDir, f.filePath))
|
2024-08-22 22:16:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
f.osfile = osfile
|
|
|
|
|
|
|
|
return f.osfile, nil
|
|
|
|
}
|
|
|
|
|
2024-08-31 23:00:13 +00:00
|
|
|
// Close implements vfs.File.
|
|
|
|
func (f *File) Close(ctx context.Context) error {
|
|
|
|
if f.osfile != nil {
|
|
|
|
err := f.osfile.Close()
|
|
|
|
f.osfile = nil
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-08-22 22:16:16 +00:00
|
|
|
type fileInfo struct {
|
|
|
|
name string
|
|
|
|
size int64
|
|
|
|
}
|
|
|
|
|
|
|
|
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 time.Time{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mode implements fs.FileInfo.
|
|
|
|
func (f *fileInfo) Mode() fs.FileMode {
|
|
|
|
return vfs.ROMode
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|