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 const fdCacheAliveTime = 10
20 type fdCacheEntry struct {
27 fdRCache = map[string]*fdCacheEntry{}
29 fdWCache = map[string]*fdCacheEntry{}
31 fdMkdirAllCache = map[string]struct{}{}
32 fdCacheCleanerM sync.Once
35 func fdCacheCleaner() {
36 cleaner := func(c map[string]*fdCacheEntry, m *sync.Mutex) {
37 now := time.Now().Unix()
40 if now-v.last > fdCacheAliveTime {
51 for range time.Tick(fdCacheAliveTime * time.Second) {
52 cleaner(fdRCache, &fdRCacheM)
53 cleaner(fdWCache, &fdWCacheM)
57 // File-based storage for torrents, that isn't yet bound to a particular torrent.
58 type fileClientImpl struct {
59 opts NewFileClientOpts
62 // All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
63 func NewFile(baseDir string) ClientImplCloser {
64 return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
67 type NewFileClientOpts struct {
68 // The base directory for all downloads.
70 FilePathMaker FilePathMaker
71 TorrentDirMaker TorrentDirFilePathMaker
72 PieceCompletion PieceCompletion
75 // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
76 func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
77 if opts.TorrentDirMaker == nil {
78 opts.TorrentDirMaker = defaultPathMaker
80 if opts.FilePathMaker == nil {
81 opts.FilePathMaker = func(opts FilePathMakerOpts) string {
83 if opts.Info.Name != metainfo.NoName {
84 parts = append(parts, opts.Info.Name)
86 return filepath.Join(append(parts, opts.File.Path...)...)
89 if opts.PieceCompletion == nil {
90 opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
92 fdCacheCleanerM.Do(func() { go fdCacheCleaner() })
93 return fileClientImpl{opts}
96 func (me fileClientImpl) Close() error {
97 return me.opts.PieceCompletion.Close()
100 func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
101 dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
102 upvertedFiles := info.UpvertedFiles()
103 files := make([]file, 0, len(upvertedFiles))
104 for i, fileInfo := range upvertedFiles {
105 filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
109 if !isSubFilepath(dir, filePath) {
110 err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
115 length: fileInfo.Length,
118 err = CreateNativeZeroLengthFile(f.path)
120 err = fmt.Errorf("creating zero length file: %w", err)
124 files = append(files, f)
126 t := &fileTorrentImpl{
128 segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
130 fs.opts.PieceCompletion,
139 // The safe, OS-local file path.
144 type fileTorrentImpl struct {
146 segmentLocater segments.Index
147 infoHash metainfo.Hash
148 completion PieceCompletion
151 func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
152 // Create a view onto the file-based torrent storage.
153 _io := fileTorrentImplIO{fts}
154 // Return the appropriate segments of this.
155 return &filePieceImpl{
158 missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
159 io.NewSectionReader(_io, p.Offset(), p.Length()),
163 func (fs *fileTorrentImpl) Close() error {
167 // A helper to create zero-length files which won't appear for file-orientated storage since no
168 // writes will ever occur to them (no torrent data is associated with a zero-length file). The
169 // caller should make sure the file name provided is safe/sanitized.
170 func CreateNativeZeroLengthFile(name string) error {
171 os.MkdirAll(filepath.Dir(name), 0o777)
173 f, err := os.Create(name)
180 // Exposes file-based storage of a torrent, as one big ReadWriterAt.
181 type fileTorrentImplIO struct {
185 // Returns EOF on short or missing file.
186 func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
188 centry := fdRCache[file.path]
191 fd, err = os.Open(file.path)
192 if os.IsNotExist(err) {
193 // File missing is treated the same as a short file.
200 centry = &fdCacheEntry{fd: fd}
201 fdRCache[file.path] = centry
204 // Limit the read to within the expected bounds of this file.
205 if int64(len(b)) > file.length-off {
206 b = b[:file.length-off]
209 centry.last = time.Now().Unix()
210 n, err = centry.fd.ReadAt(b, off)
215 // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
216 func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
217 fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
218 n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
222 return err == nil // && int64(n1) == e.Length
224 if len(b) != 0 && err == nil {
230 func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
231 fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
232 name := fst.fts.files[i].path
233 _, ok := fdMkdirAllCache[filepath.Dir(name)]
235 os.MkdirAll(filepath.Dir(name), 0o777)
236 fdMkdirAllCache[filepath.Dir(name)] = struct{}{}
239 centry := fdWCache[name]
242 fd, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
247 centry = &fdCacheEntry{fd: fd}
248 fdWCache[name] = centry
253 centry.last = time.Now().Unix()
254 n1, err = centry.fd.WriteAt(p[:e.Length], e.Start)
258 if err == nil && int64(n1) != e.Length {
259 err = io.ErrShortWrite