9 "github.com/anacrolix/missinggo/v2"
11 "github.com/anacrolix/torrent/common"
12 "github.com/anacrolix/torrent/metainfo"
13 "github.com/anacrolix/torrent/segments"
16 // File-based storage for torrents, that isn't yet bound to a particular torrent.
17 type fileClientImpl struct {
18 opts NewFileClientOpts
21 // All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
22 func NewFile(baseDir string) ClientImplCloser {
23 return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
26 type NewFileClientOpts struct {
27 // The base directory for all downloads.
29 FilePathMaker FilePathMaker
30 TorrentDirMaker TorrentDirFilePathMaker
31 PieceCompletion PieceCompletion
34 // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
35 func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
36 if opts.TorrentDirMaker == nil {
37 opts.TorrentDirMaker = defaultPathMaker
39 if opts.FilePathMaker == nil {
40 opts.FilePathMaker = func(opts FilePathMakerOpts) string {
42 if opts.Info.Name != metainfo.NoName {
43 parts = append(parts, opts.Info.Name)
45 return filepath.Join(append(parts, opts.File.Path...)...)
48 if opts.PieceCompletion == nil {
49 opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
51 return fileClientImpl{opts}
54 func (me fileClientImpl) Close() error {
55 return me.opts.PieceCompletion.Close()
58 func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
59 dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
60 upvertedFiles := info.UpvertedFiles()
61 files := make([]file, 0, len(upvertedFiles))
62 for i, fileInfo := range upvertedFiles {
63 filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
67 if !isSubFilepath(dir, filePath) {
68 err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
73 length: fileInfo.Length,
76 err = CreateNativeZeroLengthFile(f.path)
78 err = fmt.Errorf("creating zero length file: %w", err)
82 files = append(files, f)
84 t := &fileTorrentImpl{
86 segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
88 fs.opts.PieceCompletion,
97 // The safe, OS-local file path.
102 type fileTorrentImpl struct {
104 segmentLocater segments.Index
105 infoHash metainfo.Hash
106 completion PieceCompletion
109 func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
110 // Create a view onto the file-based torrent storage.
111 _io := fileTorrentImplIO{fts}
112 // Return the appropriate segments of this.
113 return &filePieceImpl{
116 missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
117 io.NewSectionReader(_io, p.Offset(), p.Length()),
121 func (fs *fileTorrentImpl) Close() error {
125 // A helper to create zero-length files which won't appear for file-orientated storage since no
126 // writes will ever occur to them (no torrent data is associated with a zero-length file). The
127 // caller should make sure the file name provided is safe/sanitized.
128 func CreateNativeZeroLengthFile(name string) error {
129 os.MkdirAll(filepath.Dir(name), 0o777)
131 f, err := os.Create(name)
138 // Exposes file-based storage of a torrent, as one big ReadWriterAt.
139 type fileTorrentImplIO struct {
143 // Returns EOF on short or missing file.
144 func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
145 f, err := os.Open(file.path)
146 if os.IsNotExist(err) {
147 // File missing is treated the same as a short file.
155 // Limit the read to within the expected bounds of this file.
156 if int64(len(b)) > file.length-off {
157 b = b[:file.length-off]
159 for off < file.length && len(b) != 0 {
160 n1, err1 := f.ReadAt(b, off)
172 // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
173 func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
174 fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
175 n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
179 return err == nil // && int64(n1) == e.Length
181 if len(b) != 0 && err == nil {
187 func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
188 // log.Printf("write at %v: %v bytes", off, len(p))
189 fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
190 name := fst.fts.files[i].path
191 os.MkdirAll(filepath.Dir(name), 0o777)
193 f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
198 n1, err = f.WriteAt(p[:e.Length], e.Start)
199 // log.Printf("%v %v wrote %v: %v", i, e, n1, err)
200 closeErr := f.Close()
206 if err == nil && int64(n1) != e.Length {
207 err = io.ErrShortWrite