9 "github.com/anacrolix/missinggo"
10 "github.com/anacrolix/torrent/common"
11 "github.com/anacrolix/torrent/segments"
13 "github.com/anacrolix/torrent/metainfo"
16 // File-based storage for torrents, that isn't yet bound to a particular
18 type fileClientImpl struct {
20 pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string
24 // The Default path maker just returns the current path
25 func defaultPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
29 func infoHashPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
30 return filepath.Join(baseDir, infoHash.HexString())
33 // All Torrent data stored in this baseDir
34 func NewFile(baseDir string) ClientImplCloser {
35 return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
38 func NewFileWithCompletion(baseDir string, completion PieceCompletion) *fileClientImpl {
39 return NewFileWithCustomPathMakerAndCompletion(baseDir, nil, completion)
42 // File storage with data partitioned by infohash.
43 func NewFileByInfoHash(baseDir string) ClientImplCloser {
44 return NewFileWithCustomPathMaker(baseDir, infoHashPathMaker)
47 // Allows passing a function to determine the path for storing torrent data. The function is
48 // responsible for sanitizing the info if it uses some part of it (for example sanitizing
50 func NewFileWithCustomPathMaker(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string) ClientImplCloser {
51 return NewFileWithCustomPathMakerAndCompletion(baseDir, pathMaker, pieceCompletionForDir(baseDir))
54 // Allows passing custom PieceCompletion
55 func NewFileWithCustomPathMakerAndCompletion(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string, completion PieceCompletion) *fileClientImpl {
57 pathMaker = defaultPathMaker
59 return &fileClientImpl{
66 func (me *fileClientImpl) Close() error {
70 func (fs *fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
71 dir := fs.pathMaker(fs.baseDir, info, infoHash)
72 upvertedFiles := info.UpvertedFiles()
73 files := make([]file, 0, len(upvertedFiles))
74 for i, fileInfo := range upvertedFiles {
76 s, err = ToSafeFilePath(append([]string{info.Name}, fileInfo.Path...)...)
78 err = fmt.Errorf("file %v has unsafe path %q: %w", i, fileInfo.Path, err)
82 path: filepath.Join(dir, s),
83 length: fileInfo.Length,
86 err = CreateNativeZeroLengthFile(f.path)
88 err = fmt.Errorf("creating zero length file: %w", err)
92 files = append(files, f)
94 t := &fileTorrentImpl{
96 segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
107 // The safe, OS-local file path.
112 type fileTorrentImpl struct {
114 segmentLocater segments.Index
115 infoHash metainfo.Hash
116 completion PieceCompletion
119 func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
120 // Create a view onto the file-based torrent storage.
121 _io := fileTorrentImplIO{fts}
122 // Return the appropriate segments of this.
123 return &filePieceImpl{
126 missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
127 io.NewSectionReader(_io, p.Offset(), p.Length()),
131 func (fs *fileTorrentImpl) Close() error {
135 // A helper to create zero-length files which won't appear for file-orientated storage since no
136 // writes will ever occur to them (no torrent data is associated with a zero-length file). The
137 // caller should make sure the file name provided is safe/sanitized.
138 func CreateNativeZeroLengthFile(name string) error {
139 os.MkdirAll(filepath.Dir(name), 0777)
141 f, err := os.Create(name)
148 // Exposes file-based storage of a torrent, as one big ReadWriterAt.
149 type fileTorrentImplIO struct {
153 // Returns EOF on short or missing file.
154 func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
155 f, err := os.Open(file.path)
156 if os.IsNotExist(err) {
157 // File missing is treated the same as a short file.
165 // Limit the read to within the expected bounds of this file.
166 if int64(len(b)) > file.length-off {
167 b = b[:file.length-off]
169 for off < file.length && len(b) != 0 {
170 n1, err1 := f.ReadAt(b, off)
182 // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
183 func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
184 fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
185 n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
189 return err == nil // && int64(n1) == e.Length
191 if len(b) != 0 && err == nil {
197 func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
198 //log.Printf("write at %v: %v bytes", off, len(p))
199 fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
200 name := fst.fts.files[i].path
201 os.MkdirAll(filepath.Dir(name), 0777)
203 f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0666)
208 n1, err = f.WriteAt(p[:e.Length], e.Start)
209 //log.Printf("%v %v wrote %v: %v", i, e, n1, err)
210 closeErr := f.Close()
216 if err == nil && int64(n1) != e.Length {
217 err = io.ErrShortWrite