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