package torrent import ( "context" "errors" "io/fs" "log/slog" "os" "path" "path/filepath" "slices" "git.kmsign.ru/royalcat/tstor/pkg/rlog" "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/metainfo" "github.com/anacrolix/torrent/storage" ) // NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem. func NewFileStorage(baseDir string, pc storage.PieceCompletion) *fileStorage { return &fileStorage{ client: storage.NewFileOpts(storage.NewFileClientOpts{ ClientBaseDir: baseDir, PieceCompletion: pc, TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string { return torrentDir(baseDir, infoHash) }, FilePathMaker: func(opts storage.FilePathMakerOpts) string { return filePath(*opts.File) }, }), baseDir: baseDir, pieceCompletion: pc, dupIndex: newDupIndex(), log: rlog.Component("daemon", "torrent"), } } // File-based storage for torrents, that isn't yet bound to a particular torrent. type fileStorage struct { baseDir string client storage.ClientImplCloser pieceCompletion storage.PieceCompletion dupIndex *dupIndex log *rlog.Logger } var _ storage.ClientImplCloser = (*fileStorage)(nil) func (me *fileStorage) Close() error { return errors.Join( me.client.Close(), me.pieceCompletion.Close(), ) } func (fs *fileStorage) fullFilePath(infoHash metainfo.Hash, fileInfo metainfo.FileInfo) string { return filepath.Join( torrentDir(fs.baseDir, infoHash), filePath(fileInfo), ) } func (fs *fileStorage) DeleteFile(file *torrent.File) error { infoHash := file.Torrent().InfoHash() torrentDir := torrentDir(fs.baseDir, infoHash) fileInfo := file.FileInfo() relFilePath := filePath(fileInfo) filePath := path.Join(torrentDir, relFilePath) for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ { pk := metainfo.PieceKey{InfoHash: infoHash, Index: i} err := fs.pieceCompletion.Set(pk, false) if err != nil { return err } } return os.Remove(filePath) } func (fs *fileStorage) CleanupDirs(ctx context.Context, expected []*Controller, dryRun bool) ([]string, error) { log := fs.log.With(slog.Int("expectedTorrents", len(expected)), slog.Bool("dryRun", dryRun)) expectedEntries := []string{} for _, e := range expected { expectedEntries = append(expectedEntries, e.Torrent().InfoHash().HexString()) } entries, err := os.ReadDir(fs.baseDir) if err != nil { return nil, err } toDelete := []string{} for _, v := range entries { if !slices.Contains(expectedEntries, v.Name()) { toDelete = append(toDelete, v.Name()) } } if ctx.Err() != nil { return nil, ctx.Err() } log.Info(ctx, "deleting trash data", slog.Int("dirsCount", len(toDelete))) if !dryRun { for i, name := range toDelete { p := path.Join(fs.baseDir, name) log.Warn(ctx, "deleting trash data", slog.String("path", p)) err := os.RemoveAll(p) if err != nil { return toDelete[:i], err } } } return toDelete, nil } func (s *fileStorage) CleanupFiles(ctx context.Context, expected []*Controller, dryRun bool) ([]string, error) { log := s.log.With(slog.Int("expectedTorrents", len(expected)), slog.Bool("dryRun", dryRun)) expectedEntries := []string{} { for _, e := range expected { files, err := e.Files(ctx) if err != nil { return nil, err } for _, f := range files { expectedEntries = append(expectedEntries, s.fullFilePath(e.Torrent().InfoHash(), f.FileInfo())) } } } entries := []string{} err := filepath.WalkDir(s.baseDir, func(path string, info fs.DirEntry, err error) error { if err != nil { return err } if ctx.Err() != nil { return ctx.Err() } if info.IsDir() { return nil } entries = append(entries, path) return nil }) if err != nil { return nil, err } toDelete := []string{} for _, v := range entries { if !slices.Contains(expectedEntries, v) { toDelete = append(toDelete, v) } } if ctx.Err() != nil { return toDelete, ctx.Err() } log.Info(ctx, "deleting trash data", slog.Int("filesCount", len(toDelete))) if !dryRun { for i, p := range toDelete { s.log.Warn(ctx, "deleting trash data", slog.String("path", p)) err := os.Remove(p) if err != nil { return toDelete[i:], err } } } return toDelete, nil } func (s *fileStorage) iterFiles(ctx context.Context, iter func(ctx context.Context, path string, entry fs.FileInfo) error) error { return filepath.Walk(s.baseDir, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } if ctx.Err() != nil { return ctx.Err() } if info.IsDir() { return nil } return iter(ctx, path, info) }) }