github.com/anacrolix/torrent@v1.61.0/storage/mmap.go (about) 1 //go:build !wasm 2 // +build !wasm 3 4 package storage 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 14 "github.com/anacrolix/missinggo/v2" 15 "github.com/edsrzf/mmap-go" 16 17 "github.com/anacrolix/torrent/metainfo" 18 mmapSpan "github.com/anacrolix/torrent/mmap-span" 19 ) 20 21 type mmapClientImpl struct { 22 baseDir string 23 pc PieceCompletion 24 } 25 26 // TODO: Support all the same native filepath configuration that NewFileOpts provides. 27 func NewMMap(baseDir string) ClientImplCloser { 28 return NewMMapWithCompletion(baseDir, pieceCompletionForDir(baseDir)) 29 } 30 31 func NewMMapWithCompletion(baseDir string, completion PieceCompletion) *mmapClientImpl { 32 return &mmapClientImpl{ 33 baseDir: baseDir, 34 pc: completion, 35 } 36 } 37 38 func (s *mmapClientImpl) OpenTorrent( 39 _ context.Context, 40 info *metainfo.Info, 41 infoHash metainfo.Hash, 42 ) (_ TorrentImpl, err error) { 43 span, err := mMapTorrent(info, s.baseDir) 44 t := &mmapTorrentStorage{ 45 infoHash: infoHash, 46 span: span, 47 pc: s.pc, 48 } 49 return TorrentImpl{Piece: t.Piece, Close: t.Close}, err 50 } 51 52 func (s *mmapClientImpl) Close() error { 53 return s.pc.Close() 54 } 55 56 type mmapTorrentStorage struct { 57 infoHash metainfo.Hash 58 span *mmapSpan.MMapSpan 59 pc PieceCompletionGetSetter 60 } 61 62 func (ts *mmapTorrentStorage) Piece(p metainfo.Piece) PieceImpl { 63 return mmapStoragePiece{ 64 t: ts, 65 p: p, 66 ReaderAt: io.NewSectionReader(ts.span, p.Offset(), p.Length()), 67 WriterAt: missinggo.NewSectionWriter(ts.span, p.Offset(), p.Length()), 68 } 69 } 70 71 func (ts *mmapTorrentStorage) Close() error { 72 return ts.span.Close() 73 } 74 75 type mmapStoragePiece struct { 76 t *mmapTorrentStorage 77 p metainfo.Piece 78 io.ReaderAt 79 io.WriterAt 80 } 81 82 func (me mmapStoragePiece) Flush() error { 83 // TODO: Flush just the regions of the files we care about. At least this is no worse than it 84 // was previously. 85 return me.t.span.Flush() 86 } 87 88 func (me mmapStoragePiece) pieceKey() metainfo.PieceKey { 89 return metainfo.PieceKey{me.t.infoHash, me.p.Index()} 90 } 91 92 func (sp mmapStoragePiece) Completion() Completion { 93 c, err := sp.t.pc.Get(sp.pieceKey()) 94 if err != nil { 95 panic(err) 96 } 97 return c 98 } 99 100 func (sp mmapStoragePiece) MarkComplete() error { 101 err := sp.t.pc.Set(sp.pieceKey(), true) 102 if err == nil { 103 err = sp.Flush() 104 } 105 return err 106 } 107 108 func (sp mmapStoragePiece) MarkNotComplete() error { 109 return sp.t.pc.Set(sp.pieceKey(), false) 110 } 111 112 func mMapTorrent(md *metainfo.Info, location string) (mms *mmapSpan.MMapSpan, err error) { 113 var mMaps []FileMapping 114 defer func() { 115 if err != nil { 116 for _, mm := range mMaps { 117 err = errors.Join(err, mm.Unmap()) 118 } 119 } 120 }() 121 for _, miFile := range md.UpvertedFiles() { 122 var safeName string 123 safeName, err = ToSafeFilePath(append([]string{md.BestName()}, miFile.BestPath()...)...) 124 if err != nil { 125 return 126 } 127 fileName := filepath.Join(location, safeName) 128 var mm FileMapping 129 mm, err = mmapFile(fileName, miFile.Length) 130 if err != nil { 131 err = fmt.Errorf("file %q: %w", miFile.DisplayPath(md), err) 132 return 133 } 134 mMaps = append(mMaps, mm) 135 } 136 return mmapSpan.New(mMaps, md.FileSegmentsIndex()), nil 137 } 138 139 func mmapFile(name string, size int64) (_ FileMapping, err error) { 140 dir := filepath.Dir(name) 141 err = os.MkdirAll(dir, 0o750) 142 if err != nil { 143 err = fmt.Errorf("making directory %q: %s", dir, err) 144 return 145 } 146 var file *os.File 147 file, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR, filePerm) 148 if err != nil { 149 return 150 } 151 defer func() { 152 if err != nil { 153 file.Close() 154 } 155 }() 156 var fi os.FileInfo 157 fi, err = file.Stat() 158 if err != nil { 159 return 160 } 161 if fi.Size() < size { 162 // I think this is necessary on HFS+. Maybe Linux will SIGBUS too if 163 // you overmap a file but I'm not sure. 164 err = file.Truncate(size) 165 if err != nil { 166 return 167 } 168 } 169 return func() (ret mmapWithFile, err error) { 170 ret.f = file 171 if size == 0 { 172 // Can't mmap() regions with length 0. 173 return 174 } 175 intLen := int(size) 176 if int64(intLen) != size { 177 err = errors.New("size too large for system") 178 return 179 } 180 ret.mmap, err = mmap.MapRegion(file, intLen, mmap.RDWR, 0, 0) 181 if err != nil { 182 err = fmt.Errorf("error mapping region: %s", err) 183 return 184 } 185 if int64(len(ret.mmap)) != size { 186 panic(len(ret.mmap)) 187 } 188 return 189 }() 190 } 191 192 // Combines a mmapped region and file into a storage Mmap abstraction, which handles closing the 193 // mmap file handle. 194 func WrapFileMapping(region mmap.MMap, file *os.File) FileMapping { 195 return mmapWithFile{ 196 f: file, 197 mmap: region, 198 } 199 } 200 201 type FileMapping = mmapSpan.Mmap 202 203 // Handles closing the mmap's file handle (needed for Windows). Could be implemented differently by 204 // OS. 205 type mmapWithFile struct { 206 f *os.File 207 mmap mmap.MMap 208 } 209 210 func (m mmapWithFile) Flush() error { 211 return m.mmap.Flush() 212 } 213 214 func (m mmapWithFile) Unmap() (err error) { 215 if m.mmap != nil { 216 err = m.mmap.Unmap() 217 } 218 fileErr := m.f.Close() 219 if err == nil { 220 err = fileErr 221 } 222 return 223 } 224 225 func (m mmapWithFile) Bytes() []byte { 226 if m.mmap == nil { 227 return nil 228 } 229 return m.mmap 230 }