]> Sergey Matveev's repositories - btrtrc.git/blobdiff - storage/file.go
cmd/btrtrc client
[btrtrc.git] / storage / file.go
index bbfe6d66122a4eb2e8e42e88ccb15d222ff60dbd..6871cad31a06cbe84754dc78a848379daa3e7024 100644 (file)
@@ -5,14 +5,55 @@ import (
        "io"
        "os"
        "path/filepath"
+       "sync"
+       "time"
 
        "github.com/anacrolix/missinggo/v2"
+
        "github.com/anacrolix/torrent/common"
+       "github.com/anacrolix/torrent/metainfo"
        "github.com/anacrolix/torrent/segments"
+)
 
-       "github.com/anacrolix/torrent/metainfo"
+const fdCacheAliveTime = 10
+
+type fdCacheEntry struct {
+       last int64
+       fd   *os.File
+       sync.Mutex
+}
+
+var (
+       fdRCache        = map[string]*fdCacheEntry{}
+       fdRCacheM       sync.Mutex
+       fdWCache        = map[string]*fdCacheEntry{}
+       fdWCacheM       sync.Mutex
+       fdMkdirAllCache = map[string]struct{}{}
+       fdCacheCleanerM sync.Once
 )
 
+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)
+       }
+}
+
 // File-based storage for torrents, that isn't yet bound to a particular torrent.
 type fileClientImpl struct {
        opts NewFileClientOpts
@@ -48,6 +89,7 @@ func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
        if opts.PieceCompletion == nil {
                opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
        }
+       fdCacheCleanerM.Do(func() { go fdCacheCleaner() })
        return fileClientImpl{opts}
 }
 
@@ -73,7 +115,7 @@ func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash
                        length: fileInfo.Length,
                }
                if f.length == 0 {
-                       err = CreateNativeZeroLengthFile(f.path)
+                       err = CreateNativeZeroLengthFile(PathShortener(f.path))
                        if err != nil {
                                err = fmt.Errorf("creating zero length file: %w", err)
                                return
@@ -126,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 {
@@ -142,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
 }
 
@@ -185,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
                }