]> Sergey Matveev's repositories - btrtrc.git/blob - storage/file.go
Per-file locks
[btrtrc.git] / storage / file.go
1 package storage
2
3 import (
4         "fmt"
5         "io"
6         "os"
7         "path/filepath"
8         "sync"
9         "time"
10
11         "github.com/anacrolix/missinggo/v2"
12
13         "github.com/anacrolix/torrent/common"
14         "github.com/anacrolix/torrent/metainfo"
15         "github.com/anacrolix/torrent/segments"
16 )
17
18 type fdCacheEntry struct {
19         fd *os.File
20         sync.Mutex
21 }
22
23 var (
24         fdRCache        = map[string]*fdCacheEntry{}
25         fdRCacheM       sync.Mutex
26         fdWCache        = map[string]*fdCacheEntry{}
27         fdWCacheM       sync.Mutex
28         fdMkdirAllCache = map[string]struct{}{}
29         fdCacheCleanerM sync.Once
30 )
31
32 func fdCacheCleaner() {
33         cleaner := func(c *map[string]*fdCacheEntry, m *sync.Mutex) {
34                 m.Lock()
35                 prev := fdRCache
36                 *c = make(map[string]*fdCacheEntry)
37                 m.Unlock()
38                 for _, v := range prev {
39                         v.Lock()
40                         v.fd.Close()
41                         v.Unlock()
42                 }
43         }
44         for range time.Tick(10 * time.Second) {
45                 cleaner(&fdRCache, &fdRCacheM)
46                 cleaner(&fdWCache, &fdWCacheM)
47         }
48 }
49
50 // File-based storage for torrents, that isn't yet bound to a particular torrent.
51 type fileClientImpl struct {
52         opts NewFileClientOpts
53 }
54
55 // All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
56 func NewFile(baseDir string) ClientImplCloser {
57         return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
58 }
59
60 type NewFileClientOpts struct {
61         // The base directory for all downloads.
62         ClientBaseDir   string
63         FilePathMaker   FilePathMaker
64         TorrentDirMaker TorrentDirFilePathMaker
65         PieceCompletion PieceCompletion
66 }
67
68 // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
69 func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
70         if opts.TorrentDirMaker == nil {
71                 opts.TorrentDirMaker = defaultPathMaker
72         }
73         if opts.FilePathMaker == nil {
74                 opts.FilePathMaker = func(opts FilePathMakerOpts) string {
75                         var parts []string
76                         if opts.Info.Name != metainfo.NoName {
77                                 parts = append(parts, opts.Info.Name)
78                         }
79                         return filepath.Join(append(parts, opts.File.Path...)...)
80                 }
81         }
82         if opts.PieceCompletion == nil {
83                 opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
84         }
85         fdCacheCleanerM.Do(func() { go fdCacheCleaner() })
86         return fileClientImpl{opts}
87 }
88
89 func (me fileClientImpl) Close() error {
90         return me.opts.PieceCompletion.Close()
91 }
92
93 func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
94         dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
95         upvertedFiles := info.UpvertedFiles()
96         files := make([]file, 0, len(upvertedFiles))
97         for i, fileInfo := range upvertedFiles {
98                 filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
99                         Info: info,
100                         File: &fileInfo,
101                 }))
102                 if !isSubFilepath(dir, filePath) {
103                         err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
104                         return
105                 }
106                 f := file{
107                         path:   filePath,
108                         length: fileInfo.Length,
109                 }
110                 if f.length == 0 {
111                         err = CreateNativeZeroLengthFile(f.path)
112                         if err != nil {
113                                 err = fmt.Errorf("creating zero length file: %w", err)
114                                 return
115                         }
116                 }
117                 files = append(files, f)
118         }
119         t := &fileTorrentImpl{
120                 files,
121                 segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
122                 infoHash,
123                 fs.opts.PieceCompletion,
124         }
125         return TorrentImpl{
126                 Piece: t.Piece,
127                 Close: t.Close,
128         }, nil
129 }
130
131 type file struct {
132         // The safe, OS-local file path.
133         path   string
134         length int64
135 }
136
137 type fileTorrentImpl struct {
138         files          []file
139         segmentLocater segments.Index
140         infoHash       metainfo.Hash
141         completion     PieceCompletion
142 }
143
144 func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
145         // Create a view onto the file-based torrent storage.
146         _io := fileTorrentImplIO{fts}
147         // Return the appropriate segments of this.
148         return &filePieceImpl{
149                 fts,
150                 p,
151                 missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
152                 io.NewSectionReader(_io, p.Offset(), p.Length()),
153         }
154 }
155
156 func (fs *fileTorrentImpl) Close() error {
157         return nil
158 }
159
160 // A helper to create zero-length files which won't appear for file-orientated storage since no
161 // writes will ever occur to them (no torrent data is associated with a zero-length file). The
162 // caller should make sure the file name provided is safe/sanitized.
163 func CreateNativeZeroLengthFile(name string) error {
164         os.MkdirAll(filepath.Dir(name), 0o777)
165         var f io.Closer
166         f, err := os.Create(name)
167         if err != nil {
168                 return err
169         }
170         return f.Close()
171 }
172
173 // Exposes file-based storage of a torrent, as one big ReadWriterAt.
174 type fileTorrentImplIO struct {
175         fts *fileTorrentImpl
176 }
177
178 // Returns EOF on short or missing file.
179 func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
180         fdRCacheM.Lock()
181         centry := fdRCache[file.path]
182         if centry == nil {
183                 var fd *os.File
184                 fd, err = os.Open(file.path)
185                 if os.IsNotExist(err) {
186                         // File missing is treated the same as a short file.
187                         err = io.EOF
188                 }
189                 if err != nil {
190                         fdRCacheM.Unlock()
191                         return
192                 }
193                 centry = &fdCacheEntry{fd: fd}
194                 fdRCache[file.path] = centry
195         }
196         fdRCacheM.Unlock()
197         // Limit the read to within the expected bounds of this file.
198         if int64(len(b)) > file.length-off {
199                 b = b[:file.length-off]
200         }
201         centry.Lock()
202         n, err = centry.fd.ReadAt(b, off)
203         centry.Unlock()
204         return
205 }
206
207 // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
208 func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
209         fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
210                 n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
211                 n += n1
212                 b = b[n1:]
213                 err = err1
214                 return err == nil // && int64(n1) == e.Length
215         })
216         if len(b) != 0 && err == nil {
217                 err = io.EOF
218         }
219         return
220 }
221
222 func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
223         fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
224                 name := fst.fts.files[i].path
225                 _, ok := fdMkdirAllCache[filepath.Dir(name)]
226                 if !ok {
227                         os.MkdirAll(filepath.Dir(name), 0o777)
228                         fdMkdirAllCache[filepath.Dir(name)] = struct{}{}
229                 }
230                 fdWCacheM.Lock()
231                 centry := fdWCache[name]
232                 if centry == nil {
233                         var fd *os.File
234                         fd, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
235                         if err != nil {
236                                 fdWCacheM.Unlock()
237                                 return false
238                         }
239                         centry = &fdCacheEntry{fd: fd}
240                         fdWCache[name] = centry
241                 }
242                 fdWCacheM.Unlock()
243                 var n1 int
244                 centry.Lock()
245                 n1, err = centry.fd.WriteAt(p[:e.Length], e.Start)
246                 centry.Unlock()
247                 n += n1
248                 p = p[n1:]
249                 if err == nil && int64(n1) != e.Length {
250                         err = io.ErrShortWrite
251                 }
252                 return err == nil
253         })
254         return
255 }