]> Sergey Matveev's repositories - btrtrc.git/blob - storage/file.go
Add default param name in TorrentImpl.Piece func
[btrtrc.git] / storage / file.go
1 package storage
2
3 import (
4         "fmt"
5         "io"
6         "os"
7         "path/filepath"
8
9         "github.com/anacrolix/missinggo"
10         "github.com/anacrolix/torrent/common"
11         "github.com/anacrolix/torrent/segments"
12
13         "github.com/anacrolix/torrent/metainfo"
14 )
15
16 // File-based storage for torrents, that isn't yet bound to a particular
17 // torrent.
18 type fileClientImpl struct {
19         baseDir   string
20         pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string
21         pc        PieceCompletion
22 }
23
24 // The Default path maker just returns the current path
25 func defaultPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
26         return baseDir
27 }
28
29 func infoHashPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
30         return filepath.Join(baseDir, infoHash.HexString())
31 }
32
33 // All Torrent data stored in this baseDir
34 func NewFile(baseDir string) ClientImplCloser {
35         return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
36 }
37
38 func NewFileWithCompletion(baseDir string, completion PieceCompletion) *fileClientImpl {
39         return NewFileWithCustomPathMakerAndCompletion(baseDir, nil, completion)
40 }
41
42 // File storage with data partitioned by infohash.
43 func NewFileByInfoHash(baseDir string) ClientImplCloser {
44         return NewFileWithCustomPathMaker(baseDir, infoHashPathMaker)
45 }
46
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
49 // info.Name).
50 func NewFileWithCustomPathMaker(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string) ClientImplCloser {
51         return NewFileWithCustomPathMakerAndCompletion(baseDir, pathMaker, pieceCompletionForDir(baseDir))
52 }
53
54 // Allows passing custom PieceCompletion 
55 func NewFileWithCustomPathMakerAndCompletion(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string, completion PieceCompletion) *fileClientImpl {
56         if pathMaker == nil {
57                 pathMaker = defaultPathMaker
58         }
59         return &fileClientImpl{
60                 baseDir:   baseDir,
61                 pathMaker: pathMaker,
62                 pc:        completion,
63         }
64 }
65
66 func (me *fileClientImpl) Close() error {
67         return me.pc.Close()
68 }
69
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 {
75                 var s string
76                 s, err = ToSafeFilePath(append([]string{info.Name}, fileInfo.Path...)...)
77                 if err != nil {
78                         err = fmt.Errorf("file %v has unsafe path %q: %w", i, fileInfo.Path, err)
79                         return
80                 }
81                 f := file{
82                         path:   filepath.Join(dir, s),
83                         length: fileInfo.Length,
84                 }
85                 if f.length == 0 {
86                         err = CreateNativeZeroLengthFile(f.path)
87                         if err != nil {
88                                 err = fmt.Errorf("creating zero length file: %w", err)
89                                 return
90                         }
91                 }
92                 files = append(files, f)
93         }
94         t := &fileTorrentImpl{
95                 files,
96                 segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
97                 infoHash,
98                 fs.pc,
99         }
100         return TorrentImpl{
101                 Piece: t.Piece,
102                 Close: t.Close,
103         }, nil
104 }
105
106 type file struct {
107         // The safe, OS-local file path.
108         path   string
109         length int64
110 }
111
112 type fileTorrentImpl struct {
113         files          []file
114         segmentLocater segments.Index
115         infoHash       metainfo.Hash
116         completion     PieceCompletion
117 }
118
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{
124                 fts,
125                 p,
126                 missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
127                 io.NewSectionReader(_io, p.Offset(), p.Length()),
128         }
129 }
130
131 func (fs *fileTorrentImpl) Close() error {
132         return nil
133 }
134
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)
140         var f io.Closer
141         f, err := os.Create(name)
142         if err != nil {
143                 return err
144         }
145         return f.Close()
146 }
147
148 // Exposes file-based storage of a torrent, as one big ReadWriterAt.
149 type fileTorrentImplIO struct {
150         fts *fileTorrentImpl
151 }
152
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.
158                 err = io.EOF
159                 return
160         }
161         if err != nil {
162                 return
163         }
164         defer f.Close()
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]
168         }
169         for off < file.length && len(b) != 0 {
170                 n1, err1 := f.ReadAt(b, off)
171                 b = b[n1:]
172                 n += n1
173                 off += int64(n1)
174                 if n1 == 0 {
175                         err = err1
176                         break
177                 }
178         }
179         return
180 }
181
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)
186                 n += n1
187                 b = b[n1:]
188                 err = err1
189                 return err == nil // && int64(n1) == e.Length
190         })
191         if len(b) != 0 && err == nil {
192                 err = io.EOF
193         }
194         return
195 }
196
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)
202                 var f *os.File
203                 f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0666)
204                 if err != nil {
205                         return false
206                 }
207                 var n1 int
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()
211                 n += n1
212                 p = p[n1:]
213                 if err == nil {
214                         err = closeErr
215                 }
216                 if err == nil && int64(n1) != e.Length {
217                         err = io.ErrShortWrite
218                 }
219                 return err == nil
220         })
221         return
222 }