]> Sergey Matveev's repositories - btrtrc.git/blob - storage/file.go
a4c2901577b5a3baed02c4760862f13381657d41
[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 func newFileWithCustomPathMakerAndCompletion(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string, completion PieceCompletion) *fileClientImpl {
55         if pathMaker == nil {
56                 pathMaker = defaultPathMaker
57         }
58         return &fileClientImpl{
59                 baseDir:   baseDir,
60                 pathMaker: pathMaker,
61                 pc:        completion,
62         }
63 }
64
65 func (me *fileClientImpl) Close() error {
66         return me.pc.Close()
67 }
68
69 func (fs *fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (TorrentImpl, error) {
70         dir := fs.pathMaker(fs.baseDir, info, infoHash)
71         upvertedFiles := info.UpvertedFiles()
72         files := make([]file, 0, len(upvertedFiles))
73         for i, fileInfo := range upvertedFiles {
74                 s, err := ToSafeFilePath(append([]string{info.Name}, fileInfo.Path...)...)
75                 if err != nil {
76                         return nil, fmt.Errorf("file %v has unsafe path %q: %w", i, fileInfo.Path, err)
77                 }
78                 f := file{
79                         path:   filepath.Join(dir, s),
80                         length: fileInfo.Length,
81                 }
82                 if f.length == 0 {
83                         err = CreateNativeZeroLengthFile(f.path)
84                         if err != nil {
85                                 return nil, fmt.Errorf("creating zero length file: %w", err)
86                         }
87                 }
88                 files = append(files, f)
89         }
90         return &fileTorrentImpl{
91                 files,
92                 segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
93                 infoHash,
94                 fs.pc,
95         }, nil
96 }
97
98 type file struct {
99         // The safe, OS-local file path.
100         path   string
101         length int64
102 }
103
104 type fileTorrentImpl struct {
105         files          []file
106         segmentLocater segments.Index
107         infoHash       metainfo.Hash
108         completion     PieceCompletion
109 }
110
111 func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
112         // Create a view onto the file-based torrent storage.
113         _io := fileTorrentImplIO{fts}
114         // Return the appropriate segments of this.
115         return &filePieceImpl{
116                 fts,
117                 p,
118                 missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
119                 io.NewSectionReader(_io, p.Offset(), p.Length()),
120         }
121 }
122
123 func (fs *fileTorrentImpl) Close() error {
124         return nil
125 }
126
127 // A helper to create zero-length files which won't appear for file-orientated storage since no
128 // writes will ever occur to them (no torrent data is associated with a zero-length file). The
129 // caller should make sure the file name provided is safe/sanitized.
130 func CreateNativeZeroLengthFile(name string) error {
131         os.MkdirAll(filepath.Dir(name), 0777)
132         var f io.Closer
133         f, err := os.Create(name)
134         if err != nil {
135                 return err
136         }
137         return f.Close()
138 }
139
140 // Exposes file-based storage of a torrent, as one big ReadWriterAt.
141 type fileTorrentImplIO struct {
142         fts *fileTorrentImpl
143 }
144
145 // Returns EOF on short or missing file.
146 func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
147         f, err := os.Open(file.path)
148         if os.IsNotExist(err) {
149                 // File missing is treated the same as a short file.
150                 err = io.EOF
151                 return
152         }
153         if err != nil {
154                 return
155         }
156         defer f.Close()
157         // Limit the read to within the expected bounds of this file.
158         if int64(len(b)) > file.length-off {
159                 b = b[:file.length-off]
160         }
161         for off < file.length && len(b) != 0 {
162                 n1, err1 := f.ReadAt(b, off)
163                 b = b[n1:]
164                 n += n1
165                 off += int64(n1)
166                 if n1 == 0 {
167                         err = err1
168                         break
169                 }
170         }
171         return
172 }
173
174 // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
175 func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
176         fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
177                 n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
178                 n += n1
179                 b = b[n1:]
180                 err = err1
181                 return err == nil // && int64(n1) == e.Length
182         })
183         if len(b) != 0 && err == nil {
184                 err = io.EOF
185         }
186         return
187 }
188
189 func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
190         //log.Printf("write at %v: %v bytes", off, len(p))
191         fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
192                 name := fst.fts.files[i].path
193                 os.MkdirAll(filepath.Dir(name), 0777)
194                 var f *os.File
195                 f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0666)
196                 if err != nil {
197                         return false
198                 }
199                 var n1 int
200                 n1, err = f.WriteAt(p[:e.Length], e.Start)
201                 //log.Printf("%v %v wrote %v: %v", i, e, n1, err)
202                 closeErr := f.Close()
203                 n += n1
204                 p = p[n1:]
205                 if err == nil {
206                         err = closeErr
207                 }
208                 if err == nil && int64(n1) != e.Length {
209                         err = io.ErrShortWrite
210                 }
211                 return err == nil
212         })
213         return
214 }