github.com/anacrolix/torrent@v1.61.0/storage/file-client.go (about)

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log/slog"
     7  	"path/filepath"
     8  
     9  	g "github.com/anacrolix/generics"
    10  	"github.com/anacrolix/log"
    11  
    12  	"github.com/anacrolix/torrent/metainfo"
    13  )
    14  
    15  // File-based storage for torrents, that isn't yet bound to a particular torrent.
    16  type fileClientImpl struct {
    17  	opts NewFileClientOpts
    18  }
    19  
    20  // All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
    21  func NewFile(baseDir string) ClientImplCloser {
    22  	return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
    23  }
    24  
    25  type NewFileClientOpts struct {
    26  	// The base directory for all downloads.
    27  	ClientBaseDir   string
    28  	FilePathMaker   FilePathMaker
    29  	TorrentDirMaker TorrentDirFilePathMaker
    30  	// If part files are enabled, this will default to inferring completion from file names at
    31  	// startup, and keep the rest in memory.
    32  	PieceCompletion PieceCompletion
    33  	UsePartFiles    g.Option[bool]
    34  	Logger          *slog.Logger
    35  }
    36  
    37  // The specific part-files option or the default.
    38  func (me NewFileClientOpts) partFiles() bool {
    39  	return me.UsePartFiles.UnwrapOr(true)
    40  }
    41  
    42  // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
    43  func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
    44  	if opts.TorrentDirMaker == nil {
    45  		opts.TorrentDirMaker = defaultPathMaker
    46  	}
    47  	if opts.FilePathMaker == nil {
    48  		opts.FilePathMaker = func(opts FilePathMakerOpts) string {
    49  			var parts []string
    50  			if opts.Info.BestName() != metainfo.NoName {
    51  				parts = append(parts, opts.Info.BestName())
    52  			}
    53  			return filepath.Join(append(parts, opts.File.BestPath()...)...)
    54  		}
    55  	}
    56  	if opts.PieceCompletion == nil {
    57  		if opts.partFiles() {
    58  			opts.PieceCompletion = NewMapPieceCompletion()
    59  		} else {
    60  			opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
    61  		}
    62  	}
    63  	if opts.Logger == nil {
    64  		opts.Logger = slog.Default()
    65  	}
    66  	return &fileClientImpl{opts}
    67  }
    68  
    69  func (me *fileClientImpl) Close() error {
    70  	return me.opts.PieceCompletion.Close()
    71  }
    72  
    73  var defaultFileIo = func() fileIo {
    74  	return classicFileIo{}
    75  }
    76  
    77  func (fs *fileClientImpl) OpenTorrent(
    78  	ctx context.Context,
    79  	info *metainfo.Info,
    80  	infoHash metainfo.Hash,
    81  ) (_ TorrentImpl, err error) {
    82  	dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
    83  	logger := log.ContextLogger(ctx).Slogger()
    84  	logger.DebugContext(ctx, "opened file torrent storage", slog.String("dir", dir))
    85  	metainfoFileInfos := info.UpvertedFiles()
    86  	files := make([]fileExtra, len(metainfoFileInfos))
    87  	for i, fileInfo := range metainfoFileInfos {
    88  		filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
    89  			Info: info,
    90  			File: &fileInfo,
    91  		}))
    92  		if !isSubFilepath(dir, filePath) {
    93  			err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
    94  			return
    95  		}
    96  		files[i].safeOsPath = filePath
    97  		if metainfoFileInfos[i].Length == 0 {
    98  			err = CreateNativeZeroLengthFile(filePath)
    99  			if err != nil {
   100  				err = fmt.Errorf("creating zero length file: %w", err)
   101  				return
   102  			}
   103  		}
   104  	}
   105  	t := &fileTorrentImpl{
   106  		info,
   107  		files,
   108  		metainfoFileInfos,
   109  		info.FileSegmentsIndex(),
   110  		infoHash,
   111  		defaultFileIo(),
   112  		fs,
   113  	}
   114  	if t.partFiles() {
   115  		err = t.setCompletionFromPartFiles()
   116  		if err != nil {
   117  			err = fmt.Errorf("setting completion from part files: %w", err)
   118  			return
   119  		}
   120  	}
   121  	return TorrentImpl{
   122  		Piece: t.Piece,
   123  		Close: t.Close,
   124  	}, nil
   125  }