package qbittorrent import ( "context" "fmt" "io" "io/fs" "os" "path" "strings" "time" "git.kmsign.ru/royalcat/tstor/pkg/qbittorrent" "git.kmsign.ru/royalcat/tstor/src/vfs" ) type FS struct { client *cacheClient name string hash string dataDir string content map[string]*qbittorrent.TorrentContent files map[string]fs.FileInfo vfs.FilesystemPrototype } var _ vfs.Filesystem = (*FS)(nil) 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{ client: client, name: name, hash: hash, dataDir: dataDir, content: content, files: files, 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 } cnt, ok := f.content[name] if ok { return openFile(ctx, f.client, f.dataDir, f.hash, cnt) } for p := range f.content { if strings.HasPrefix(p, name) { return vfs.NewDirFile(name), nil } } return nil, vfs.ErrNotExist } // ReadDir implements vfs.Filesystem. func (fs *FS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) { return vfs.ListDirFromInfo(fs.files, name) } // Stat implements vfs.Filesystem. 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 { return vfs.ErrNotImplemented } 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{ client: client, hash: hash, torrentDir: torrentDir, filePath: content.Name, contentIndex: content.Index, pieceSize: props.PieceSize, fileSize: content.Size, offset: 0, }, nil } type File struct { client *cacheClient hash string 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 { return fs.ModeDir } func (f *File) descriptor() (*os.File, error) { if f.osfile != nil { return f.osfile, nil } osfile, err := os.Open(path.Join(f.torrentDir, f.filePath)) if err != nil { return nil, err } f.osfile = osfile return f.osfile, nil } // 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 }