tstor/src/sources/qbittorrent/fs.go

265 lines
5.2 KiB
Go
Raw Normal View History

package qbittorrent
import (
"context"
2024-08-31 23:00:13 +00:00
"fmt"
"io"
"io/fs"
"os"
"path"
2024-08-31 23:00:13 +00:00
"strings"
"time"
2024-08-31 23:00:13 +00:00
"git.kmsign.ru/royalcat/tstor/pkg/qbittorrent"
"git.kmsign.ru/royalcat/tstor/src/vfs"
)
type FS struct {
2024-08-31 23:00:13 +00:00
client *cacheClient
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
}
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
}
return &FS{
2024-08-31 23:00:13 +00:00
client: client,
name: name,
hash: hash,
dataDir: dataDir,
2024-08-31 23:00:13 +00:00
content: content,
files: files,
FilesystemPrototype: vfs.FilesystemPrototype(name),
}, 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-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-31 23:00:13 +00:00
for p := range f.content {
if strings.HasPrefix(p, name) {
return vfs.NewDirFile(name), nil
}
}
2024-08-31 23:00:13 +00:00
return nil, vfs.ErrNotExist
}
// 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)
}
// 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
}
// 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-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
}
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
}
type File struct {
2024-08-31 23:00:13 +00:00
client *cacheClient
hash string
2024-08-31 23:00:13 +00:00
torrentDir string
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
}
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))
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
}
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
}