tstor/daemons/torrent/storage.go
2024-11-24 20:33:44 +03:00

196 lines
4.7 KiB
Go

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)
})
}