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