]> Sergey Matveev's repositories - btrtrc.git/blobdiff - storage/file.go
Drop support for go 1.20
[btrtrc.git] / storage / file.go
index 6dc3aeebb781be357d50ab80c99ecbba8173a970..b873964787886d4e62169deecc2141bfc3ef5473 100644 (file)
 package storage
 
 import (
+       "fmt"
        "io"
        "os"
        "path/filepath"
 
-       "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.
+// 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
-}
-
-// The Default path maker just returns the current path
-func defaultPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
-       return baseDir
-}
-
-func infoHashPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
-       return filepath.Join(baseDir, infoHash.HexString())
+       opts NewFileClientOpts
 }
 
-// All Torrent data stored in this baseDir
+// 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))
 }
 
-func NewFileWithCompletion(baseDir string, completion PieceCompletion) *fileClientImpl {
-       return newFileWithCustomPathMakerAndCompletion(baseDir, nil, completion)
+type NewFileClientOpts struct {
+       // The base directory for all downloads.
+       ClientBaseDir   string
+       FilePathMaker   FilePathMaker
+       TorrentDirMaker TorrentDirFilePathMaker
+       PieceCompletion PieceCompletion
 }
 
-// File storage with data partitioned by infohash.
-func NewFileByInfoHash(baseDir string) ClientImpl {
-       return NewFileWithCustomPathMaker(baseDir, infoHashPathMaker)
-}
-
-// Allows passing a function to determine the path for storing torrent data
-func NewFileWithCustomPathMaker(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string) ClientImpl {
-       return newFileWithCustomPathMakerAndCompletion(baseDir, pathMaker, pieceCompletionForDir(baseDir))
-}
-
-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)
        }
+       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)
-       err := CreateNativeZeroLengthFiles(info, dir)
-       if err != nil {
-               return nil, err
-       }
+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()
-       return &fileTorrentImpl{
-               dir,
-               info.Name,
-               upvertedFiles,
+       files := make([]file, 0, len(upvertedFiles))
+       for i, fileInfo := range upvertedFiles {
+               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,
+                       length: fileInfo.Length,
+               }
+               if f.length == 0 {
+                       err = CreateNativeZeroLengthFile(f.path)
+                       if err != nil {
+                               err = fmt.Errorf("creating zero length file: %w", err)
+                               return
+                       }
+               }
+               files = append(files, f)
+       }
+       t := &fileTorrentImpl{
+               files,
                segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
                infoHash,
-               fs.pc,
+               fs.opts.PieceCompletion,
+       }
+       return TorrentImpl{
+               Piece: t.Piece,
+               Close: t.Close,
        }, nil
 }
 
+type file struct {
+       // The safe, OS-local file path.
+       path   string
+       length int64
+}
+
 type fileTorrentImpl struct {
-       dir            string
-       infoName       string
-       upvertedFiles  []metainfo.FileInfo
+       files          []file
        segmentLocater segments.Index
        infoHash       metainfo.Hash
        completion     PieceCompletion
@@ -105,24 +122,17 @@ func (fs *fileTorrentImpl) Close() error {
        return nil
 }
 
-// Creates natives files for any zero-length file entries in the info. This is
-// a helper for file-based storages, which don't address or write to zero-
-// length files because they have no corresponding pieces.
-func CreateNativeZeroLengthFiles(info *metainfo.Info, dir string) (err error) {
-       for _, fi := range info.UpvertedFiles() {
-               if fi.Length != 0 {
-                       continue
-               }
-               name := filepath.Join(append([]string{dir, info.Name}, fi.Path...)...)
-               os.MkdirAll(filepath.Dir(name), 0777)
-               var f io.Closer
-               f, err = os.Create(name)
-               if err != nil {
-                       break
-               }
-               f.Close()
+// A helper to create zero-length files which won't appear for file-orientated storage since no
+// 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), 0o777)
+       var f io.Closer
+       f, err := os.Create(name)
+       if err != nil {
+               return err
        }
-       return
+       return f.Close()
 }
 
 // Exposes file-based storage of a torrent, as one big ReadWriterAt.
@@ -131,8 +141,8 @@ type fileTorrentImplIO struct {
 }
 
 // Returns EOF on short or missing file.
-func (fst *fileTorrentImplIO) readFileAt(fi metainfo.FileInfo, b []byte, off int64) (n int, err error) {
-       f, err := os.Open(fst.fts.fileInfoName(fi))
+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
@@ -143,10 +153,10 @@ func (fst *fileTorrentImplIO) readFileAt(fi metainfo.FileInfo, b []byte, off int
        }
        defer f.Close()
        // Limit the read to within the expected bounds of this file.
-       if int64(len(b)) > fi.Length-off {
-               b = b[:fi.Length-off]
+       if int64(len(b)) > file.length-off {
+               b = b[:file.length-off]
        }
-       for off < fi.Length && len(b) != 0 {
+       for off < file.length && len(b) != 0 {
                n1, err1 := f.ReadAt(b, off)
                b = b[n1:]
                n += n1
@@ -162,7 +172,7 @@ func (fst *fileTorrentImplIO) readFileAt(fi metainfo.FileInfo, b []byte, off int
 // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
 func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
        fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
-               n1, err1 := fst.readFileAt(fst.fts.upvertedFiles[i], b[:e.Length], e.Start)
+               n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
                n += n1
                b = b[n1:]
                err = err1
@@ -175,18 +185,18 @@ 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))
+       // 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.fileInfoName(fst.fts.upvertedFiles[i])
-               os.MkdirAll(filepath.Dir(name), 0777)
+               name := fst.fts.files[i].path
+               os.MkdirAll(filepath.Dir(name), 0o777)
                var f *os.File
-               f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0666)
+               f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
                if err != nil {
                        return false
                }
                var n1 int
                n1, err = f.WriteAt(p[:e.Length], e.Start)
-               //log.Printf("%v %v wrote %v: %v", i, e, n1, err)
+               // log.Printf("%v %v wrote %v: %v", i, e, n1, err)
                closeErr := f.Close()
                n += n1
                p = p[n1:]
@@ -200,7 +210,3 @@ func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
        })
        return
 }
-
-func (fts *fileTorrentImpl) fileInfoName(fi metainfo.FileInfo) string {
-       return filepath.Join(append([]string{fts.dir, fts.infoName}, fi.Path...)...)
-}