11 "github.com/anacrolix/missinggo/v2"
13 "github.com/anacrolix/torrent/common"
14 "github.com/anacrolix/torrent/metainfo"
15 "github.com/anacrolix/torrent/segments"
18 type fdCacheEntry struct {
24 fdRCache = map[string]*fdCacheEntry{}
26 fdWCache = map[string]*fdCacheEntry{}
28 fdMkdirAllCache = map[string]struct{}{}
29 fdCacheCleanerM sync.Once
32 func fdCacheCleaner() {
33 cleaner := func(c *map[string]*fdCacheEntry, m *sync.Mutex) {
36 *c = make(map[string]*fdCacheEntry)
38 for _, v := range prev {
44 for range time.Tick(10 * time.Second) {
45 cleaner(&fdRCache, &fdRCacheM)
46 cleaner(&fdWCache, &fdWCacheM)
50 // File-based storage for torrents, that isn't yet bound to a particular torrent.
51 type fileClientImpl struct {
52 opts NewFileClientOpts
55 // All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
56 func NewFile(baseDir string) ClientImplCloser {
57 return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
60 type NewFileClientOpts struct {
61 // The base directory for all downloads.
63 FilePathMaker FilePathMaker
64 TorrentDirMaker TorrentDirFilePathMaker
65 PieceCompletion PieceCompletion
68 // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
69 func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
70 if opts.TorrentDirMaker == nil {
71 opts.TorrentDirMaker = defaultPathMaker
73 if opts.FilePathMaker == nil {
74 opts.FilePathMaker = func(opts FilePathMakerOpts) string {
76 if opts.Info.Name != metainfo.NoName {
77 parts = append(parts, opts.Info.Name)
79 return filepath.Join(append(parts, opts.File.Path...)...)
82 if opts.PieceCompletion == nil {
83 opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
85 fdCacheCleanerM.Do(func() { go fdCacheCleaner() })
86 return fileClientImpl{opts}
89 func (me fileClientImpl) Close() error {
90 return me.opts.PieceCompletion.Close()
93 func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
94 dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
95 upvertedFiles := info.UpvertedFiles()
96 files := make([]file, 0, len(upvertedFiles))
97 for i, fileInfo := range upvertedFiles {
98 filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
102 if !isSubFilepath(dir, filePath) {
103 err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
108 length: fileInfo.Length,
111 err = CreateNativeZeroLengthFile(f.path)
113 err = fmt.Errorf("creating zero length file: %w", err)
117 files = append(files, f)
119 t := &fileTorrentImpl{
121 segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
123 fs.opts.PieceCompletion,
132 // The safe, OS-local file path.
137 type fileTorrentImpl struct {
139 segmentLocater segments.Index
140 infoHash metainfo.Hash
141 completion PieceCompletion
144 func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
145 // Create a view onto the file-based torrent storage.
146 _io := fileTorrentImplIO{fts}
147 // Return the appropriate segments of this.
148 return &filePieceImpl{
151 missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
152 io.NewSectionReader(_io, p.Offset(), p.Length()),
156 func (fs *fileTorrentImpl) Close() error {
160 // A helper to create zero-length files which won't appear for file-orientated storage since no
161 // writes will ever occur to them (no torrent data is associated with a zero-length file). The
162 // caller should make sure the file name provided is safe/sanitized.
163 func CreateNativeZeroLengthFile(name string) error {
164 os.MkdirAll(filepath.Dir(name), 0o777)
166 f, err := os.Create(name)
173 // Exposes file-based storage of a torrent, as one big ReadWriterAt.
174 type fileTorrentImplIO struct {
178 // Returns EOF on short or missing file.
179 func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
181 centry := fdRCache[file.path]
184 fd, err = os.Open(file.path)
185 if os.IsNotExist(err) {
186 // File missing is treated the same as a short file.
193 centry = &fdCacheEntry{fd: fd}
194 fdRCache[file.path] = centry
197 // Limit the read to within the expected bounds of this file.
198 if int64(len(b)) > file.length-off {
199 b = b[:file.length-off]
202 n, err = centry.fd.ReadAt(b, off)
207 // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
208 func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
209 fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
210 n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
214 return err == nil // && int64(n1) == e.Length
216 if len(b) != 0 && err == nil {
222 func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
223 fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
224 name := fst.fts.files[i].path
225 _, ok := fdMkdirAllCache[filepath.Dir(name)]
227 os.MkdirAll(filepath.Dir(name), 0o777)
228 fdMkdirAllCache[filepath.Dir(name)] = struct{}{}
231 centry := fdWCache[name]
234 fd, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
239 centry = &fdCacheEntry{fd: fd}
240 fdWCache[name] = centry
245 n1, err = centry.fd.WriteAt(p[:e.Length], e.Start)
249 if err == nil && int64(n1) != e.Length {
250 err = io.ErrShortWrite