github.com/anacrolix/torrent@v1.61.0/storage/file-client.go (about) 1 package storage 2 3 import ( 4 "context" 5 "fmt" 6 "log/slog" 7 "path/filepath" 8 9 g "github.com/anacrolix/generics" 10 "github.com/anacrolix/log" 11 12 "github.com/anacrolix/torrent/metainfo" 13 ) 14 15 // File-based storage for torrents, that isn't yet bound to a particular torrent. 16 type fileClientImpl struct { 17 opts NewFileClientOpts 18 } 19 20 // All Torrent data stored in this baseDir. The info names of each torrent are used as directories. 21 func NewFile(baseDir string) ClientImplCloser { 22 return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir)) 23 } 24 25 type NewFileClientOpts struct { 26 // The base directory for all downloads. 27 ClientBaseDir string 28 FilePathMaker FilePathMaker 29 TorrentDirMaker TorrentDirFilePathMaker 30 // If part files are enabled, this will default to inferring completion from file names at 31 // startup, and keep the rest in memory. 32 PieceCompletion PieceCompletion 33 UsePartFiles g.Option[bool] 34 Logger *slog.Logger 35 } 36 37 // The specific part-files option or the default. 38 func (me NewFileClientOpts) partFiles() bool { 39 return me.UsePartFiles.UnwrapOr(true) 40 } 41 42 // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem. 43 func NewFileOpts(opts NewFileClientOpts) ClientImplCloser { 44 if opts.TorrentDirMaker == nil { 45 opts.TorrentDirMaker = defaultPathMaker 46 } 47 if opts.FilePathMaker == nil { 48 opts.FilePathMaker = func(opts FilePathMakerOpts) string { 49 var parts []string 50 if opts.Info.BestName() != metainfo.NoName { 51 parts = append(parts, opts.Info.BestName()) 52 } 53 return filepath.Join(append(parts, opts.File.BestPath()...)...) 54 } 55 } 56 if opts.PieceCompletion == nil { 57 if opts.partFiles() { 58 opts.PieceCompletion = NewMapPieceCompletion() 59 } else { 60 opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir) 61 } 62 } 63 if opts.Logger == nil { 64 opts.Logger = slog.Default() 65 } 66 return &fileClientImpl{opts} 67 } 68 69 func (me *fileClientImpl) Close() error { 70 return me.opts.PieceCompletion.Close() 71 } 72 73 var defaultFileIo = func() fileIo { 74 return classicFileIo{} 75 } 76 77 func (fs *fileClientImpl) OpenTorrent( 78 ctx context.Context, 79 info *metainfo.Info, 80 infoHash metainfo.Hash, 81 ) (_ TorrentImpl, err error) { 82 dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash) 83 logger := log.ContextLogger(ctx).Slogger() 84 logger.DebugContext(ctx, "opened file torrent storage", slog.String("dir", dir)) 85 metainfoFileInfos := info.UpvertedFiles() 86 files := make([]fileExtra, len(metainfoFileInfos)) 87 for i, fileInfo := range metainfoFileInfos { 88 filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{ 89 Info: info, 90 File: &fileInfo, 91 })) 92 if !isSubFilepath(dir, filePath) { 93 err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir) 94 return 95 } 96 files[i].safeOsPath = filePath 97 if metainfoFileInfos[i].Length == 0 { 98 err = CreateNativeZeroLengthFile(filePath) 99 if err != nil { 100 err = fmt.Errorf("creating zero length file: %w", err) 101 return 102 } 103 } 104 } 105 t := &fileTorrentImpl{ 106 info, 107 files, 108 metainfoFileInfos, 109 info.FileSegmentsIndex(), 110 infoHash, 111 defaultFileIo(), 112 fs, 113 } 114 if t.partFiles() { 115 err = t.setCompletionFromPartFiles() 116 if err != nil { 117 err = fmt.Errorf("setting completion from part files: %w", err) 118 return 119 } 120 } 121 return TorrentImpl{ 122 Piece: t.Piece, 123 Close: t.Close, 124 }, nil 125 }