]> Sergey Matveev's repositories - btrtrc.git/blobdiff - storage/file.go
cmd/btrtrc client
[btrtrc.git] / storage / file.go
index 66b0debfa47c4a2e0a5da0898fa509e1ac36d9aa..6871cad31a06cbe84754dc78a848379daa3e7024 100644 (file)
@@ -5,93 +5,133 @@ import (
        "io"
        "os"
        "path/filepath"
+       "sync"
+       "time"
 
-       "github.com/anacrolix/missinggo"
-       "github.com/anacrolix/torrent/common"
-       "github.com/anacrolix/torrent/segments"
+       "github.com/anacrolix/missinggo/v2"
 
+       "github.com/anacrolix/torrent/common"
        "github.com/anacrolix/torrent/metainfo"
+       "github.com/anacrolix/torrent/segments"
 )
 
-// File-based storage for torrents, that isn't yet bound to a particular
-// torrent.
-type fileClientImpl struct {
-       baseDir   string
-       pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string
-       pc        PieceCompletion
-}
+const fdCacheAliveTime = 10
 
-// The Default path maker just returns the current path
-func defaultPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
-       return baseDir
+type fdCacheEntry struct {
+       last int64
+       fd   *os.File
+       sync.Mutex
 }
 
-func infoHashPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
-       return filepath.Join(baseDir, infoHash.HexString())
-}
+var (
+       fdRCache        = map[string]*fdCacheEntry{}
+       fdRCacheM       sync.Mutex
+       fdWCache        = map[string]*fdCacheEntry{}
+       fdWCacheM       sync.Mutex
+       fdMkdirAllCache = map[string]struct{}{}
+       fdCacheCleanerM sync.Once
+)
 
-// All Torrent data stored in this baseDir
-func NewFile(baseDir string) ClientImplCloser {
-       return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
+func fdCacheCleaner() {
+       cleaner := func(c map[string]*fdCacheEntry, m *sync.Mutex) {
+               now := time.Now().Unix()
+               m.Lock()
+               for k, v := range c {
+                       if now-v.last > fdCacheAliveTime {
+                               go func() {
+                                       v.Lock()
+                                       v.fd.Close()
+                                       v.Unlock()
+                               }()
+                       }
+                       delete(c, k)
+               }
+               m.Unlock()
+       }
+       for range time.Tick(fdCacheAliveTime * time.Second) {
+               cleaner(fdRCache, &fdRCacheM)
+               cleaner(fdWCache, &fdWCacheM)
+       }
 }
 
-func NewFileWithCompletion(baseDir string, completion PieceCompletion) *fileClientImpl {
-       return newFileWithCustomPathMakerAndCompletion(baseDir, nil, completion)
+// File-based storage for torrents, that isn't yet bound to a particular torrent.
+type fileClientImpl struct {
+       opts NewFileClientOpts
 }
 
-// File storage with data partitioned by infohash.
-func NewFileByInfoHash(baseDir string) ClientImpl {
-       return NewFileWithCustomPathMaker(baseDir, infoHashPathMaker)
+// All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
+func NewFile(baseDir string) ClientImplCloser {
+       return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
 }
 
-// Allows passing a function to determine the path for storing torrent data. The function is
-// responsible for sanitizing the info if it uses some part of it (for example sanitizing
-// info.Name).
-func NewFileWithCustomPathMaker(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string) ClientImpl {
-       return newFileWithCustomPathMakerAndCompletion(baseDir, pathMaker, pieceCompletionForDir(baseDir))
+type NewFileClientOpts struct {
+       // The base directory for all downloads.
+       ClientBaseDir   string
+       FilePathMaker   FilePathMaker
+       TorrentDirMaker TorrentDirFilePathMaker
+       PieceCompletion PieceCompletion
 }
 
-func newFileWithCustomPathMakerAndCompletion(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string, completion PieceCompletion) *fileClientImpl {
-       if pathMaker == nil {
-               pathMaker = defaultPathMaker
+// NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
+func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
+       if opts.TorrentDirMaker == nil {
+               opts.TorrentDirMaker = defaultPathMaker
        }
-       return &fileClientImpl{
-               baseDir:   baseDir,
-               pathMaker: pathMaker,
-               pc:        completion,
+       if opts.FilePathMaker == nil {
+               opts.FilePathMaker = func(opts FilePathMakerOpts) string {
+                       var parts []string
+                       if opts.Info.Name != metainfo.NoName {
+                               parts = append(parts, opts.Info.Name)
+                       }
+                       return filepath.Join(append(parts, opts.File.Path...)...)
+               }
        }
+       if opts.PieceCompletion == nil {
+               opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
+       }
+       fdCacheCleanerM.Do(func() { go fdCacheCleaner() })
+       return fileClientImpl{opts}
 }
 
-func (me *fileClientImpl) Close() error {
-       return me.pc.Close()
+func (me fileClientImpl) Close() error {
+       return me.opts.PieceCompletion.Close()
 }
 
-func (fs *fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (TorrentImpl, error) {
-       dir := fs.pathMaker(fs.baseDir, info, infoHash)
+func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
+       dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
        upvertedFiles := info.UpvertedFiles()
        files := make([]file, 0, len(upvertedFiles))
        for i, fileInfo := range upvertedFiles {
-               s, err := ToSafeFilePath(append([]string{info.Name}, fileInfo.Path...)...)
-               if err != nil {
-                       return nil, fmt.Errorf("file %v has unsafe path %q: %w", i, fileInfo.Path, err)
+               filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
+                       Info: info,
+                       File: &fileInfo,
+               }))
+               if !isSubFilepath(dir, filePath) {
+                       err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
+                       return
                }
                f := file{
-                       path:   filepath.Join(dir, s),
+                       path:   filePath,
                        length: fileInfo.Length,
                }
                if f.length == 0 {
-                       err = CreateNativeZeroLengthFile(f.path)
+                       err = CreateNativeZeroLengthFile(PathShortener(f.path))
                        if err != nil {
-                               return nil, fmt.Errorf("creating zero length file: %w", err)
+                               err = fmt.Errorf("creating zero length file: %w", err)
+                               return
                        }
                }
                files = append(files, f)
        }
-       return &fileTorrentImpl{
+       t := &fileTorrentImpl{
                files,
                segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
                infoHash,
-               fs.pc,
+               fs.opts.PieceCompletion,
+       }
+       return TorrentImpl{
+               Piece: t.Piece,
+               Close: t.Close,
        }, nil
 }
 
@@ -128,7 +168,7 @@ func (fs *fileTorrentImpl) Close() error {
 // writes will ever occur to them (no torrent data is associated with a zero-length file). The
 // caller should make sure the file name provided is safe/sanitized.
 func CreateNativeZeroLengthFile(name string) error {
-       os.MkdirAll(filepath.Dir(name), 0777)
+       os.MkdirAll(filepath.Dir(name), 0o777)
        var f io.Closer
        f, err := os.Create(name)
        if err != nil {
@@ -144,30 +184,32 @@ type fileTorrentImplIO struct {
 
 // Returns EOF on short or missing file.
 func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
-       f, err := os.Open(file.path)
-       if os.IsNotExist(err) {
-               // File missing is treated the same as a short file.
-               err = io.EOF
-               return
-       }
-       if err != nil {
-               return
+       fdRCacheM.Lock()
+       pth := PathShortener(file.path)
+       centry := fdRCache[pth]
+       if centry == nil {
+               var fd *os.File
+               fd, err = os.Open(pth)
+               if os.IsNotExist(err) {
+                       // File missing is treated the same as a short file.
+                       err = io.EOF
+               }
+               if err != nil {
+                       fdRCacheM.Unlock()
+                       return
+               }
+               centry = &fdCacheEntry{fd: fd}
+               fdRCache[pth] = centry
        }
-       defer f.Close()
+       fdRCacheM.Unlock()
        // Limit the read to within the expected bounds of this file.
        if int64(len(b)) > file.length-off {
                b = b[:file.length-off]
        }
-       for off < file.length && len(b) != 0 {
-               n1, err1 := f.ReadAt(b, off)
-               b = b[n1:]
-               n += n1
-               off += int64(n1)
-               if n1 == 0 {
-                       err = err1
-                       break
-               }
-       }
+       centry.Lock()
+       centry.last = time.Now().Unix()
+       n, err = centry.fd.ReadAt(b, off)
+       centry.Unlock()
        return
 }
 
@@ -187,24 +229,33 @@ func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
 }
 
 func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
-       //log.Printf("write at %v: %v bytes", off, len(p))
        fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
-               name := fst.fts.files[i].path
-               os.MkdirAll(filepath.Dir(name), 0777)
-               var f *os.File
-               f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0666)
-               if err != nil {
-                       return false
+               name := PathShortener(fst.fts.files[i].path)
+               _, ok := fdMkdirAllCache[filepath.Dir(name)]
+               if !ok {
+                       os.MkdirAll(filepath.Dir(name), 0o777)
+                       fdMkdirAllCache[filepath.Dir(name)] = struct{}{}
                }
+               fdWCacheM.Lock()
+               centry := fdWCache[name]
+               if centry == nil {
+                       var fd *os.File
+                       fd, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
+                       if err != nil {
+                               fdWCacheM.Unlock()
+                               return false
+                       }
+                       centry = &fdCacheEntry{fd: fd}
+                       fdWCache[name] = centry
+               }
+               fdWCacheM.Unlock()
                var n1 int
-               n1, err = f.WriteAt(p[:e.Length], e.Start)
-               //log.Printf("%v %v wrote %v: %v", i, e, n1, err)
-               closeErr := f.Close()
+               centry.Lock()
+               centry.last = time.Now().Unix()
+               n1, err = centry.fd.WriteAt(p[:e.Length], e.Start)
+               centry.Unlock()
                n += n1
                p = p[n1:]
-               if err == nil {
-                       err = closeErr
-               }
                if err == nil && int64(n1) != e.Length {
                        err = io.ErrShortWrite
                }