From 752bd4a511bdaf0d602c64937b0336e0c05144a7 Mon Sep 17 00:00:00 2001 From: Matt Joiner Date: Tue, 12 Aug 2025 23:40:59 +1000 Subject: [PATCH] Don't assign DisallowData* being assigned in Torrent.MergeSpec This was masking an ancient bug in SetInfoBytes, and AllowDataDownload! --- client.go | 8 +- spec.go | 4 - storage/file-io-classic.go | 28 ++++++ storage/file-io-common.go | 35 +++++++ storage/file-io-mmap.go | 201 +++++++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 7 deletions(-) create mode 100644 storage/file-io-classic.go create mode 100644 storage/file-io-common.go create mode 100644 storage/file-io-mmap.go diff --git a/client.go b/client.go index 76f986aa..f2b4aefd 100644 --- a/client.go +++ b/client.go @@ -1530,6 +1530,9 @@ type AddTorrentOpts struct { // Require pieces to be checked as soon as info is available. This is because we have no way to // schedule an initial check only, and don't want to race against use of Torrent.Complete. IgnoreUnverifiedPieceCompletion bool + // Whether to initially allow data download or upload + DisallowDataUpload bool + DisallowDataDownload bool } // Add or merge a torrent spec. Returns new if the torrent wasn't already in the client. See also @@ -1547,8 +1550,9 @@ func (cl *Client) AddTorrentSpec(spec *TorrentSpec) (t *Torrent, new bool, err e } // The trackers will be merged with the existing ones. If the Info isn't yet known, it will be set. -// spec.DisallowDataDownload/Upload will be read and applied // The display name is replaced if the new spec provides one. Note that any `Storage` is ignored. +// Many fields in the AddTorrentOpts field in TorrentSpec are ignored because the Torrent is already +// added. func (t *Torrent) MergeSpec(spec *TorrentSpec) error { if spec.DisplayName != "" { t.SetDisplayName(spec.DisplayName) @@ -1580,8 +1584,6 @@ func (t *Torrent) MergeSpec(spec *TorrentSpec) error { } t.addTrackers(spec.Trackers) t.maybeNewConns() - t.dataDownloadDisallowed.SetBool(spec.DisallowDataDownload) - t.dataUploadDisallowed = spec.DisallowDataUpload return errors.Join(t.addPieceLayersLocked(spec.PieceLayers)...) } diff --git a/spec.go b/spec.go index e8381943..27274c5e 100644 --- a/spec.go +++ b/spec.go @@ -27,10 +27,6 @@ type TorrentSpec struct { Sources []string // BEP 52 "piece layers" from metainfo PieceLayers map[string]string - - // Whether to allow data download or upload - DisallowDataUpload bool - DisallowDataDownload bool } func TorrentSpecFromMagnetUri(uri string) (spec *TorrentSpec, err error) { diff --git a/storage/file-io-classic.go b/storage/file-io-classic.go new file mode 100644 index 00000000..9a36ae9f --- /dev/null +++ b/storage/file-io-classic.go @@ -0,0 +1,28 @@ +package storage + +import ( + "os" +) + +type classicFileIo struct{} + +func (i classicFileIo) openForSharedRead(name string) (sharedFileIf, error) { + return sharedFiles.Open(name) +} + +type classicFileReader struct { + *os.File +} + +func (c classicFileReader) seekData(offset int64) (ret int64, err error) { + return seekData(c.File, offset) +} + +func (i classicFileIo) openForRead(name string) (fileReader, error) { + f, err := os.Open(name) + return classicFileReader{f}, err +} + +func (classicFileIo) openForWrite(p string, size int64) (f fileWriter, err error) { + return openFileExtra(p, os.O_WRONLY) +} diff --git a/storage/file-io-common.go b/storage/file-io-common.go new file mode 100644 index 00000000..ffc87cc6 --- /dev/null +++ b/storage/file-io-common.go @@ -0,0 +1,35 @@ +package storage + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + + "github.com/anacrolix/missinggo/v2/panicif" +) + +// Opens file for write, creating dirs and fixing permissions as necessary. +func openFileExtra(p string, osRdwr int) (f *os.File, err error) { + panicif.NotZero(osRdwr & ^(os.O_RDONLY | os.O_RDWR | os.O_WRONLY)) + flag := osRdwr | os.O_CREATE + f, err = os.OpenFile(p, flag, filePerm) + if err == nil { + return + } + if errors.Is(err, fs.ErrNotExist) { + err = os.MkdirAll(filepath.Dir(p), dirPerm) + if err != nil { + return + } + } else if errors.Is(err, fs.ErrPermission) { + err = os.Chmod(p, filePerm) + if err != nil { + return + } + } else { + return + } + f, err = os.OpenFile(p, flag, filePerm) + return +} diff --git a/storage/file-io-mmap.go b/storage/file-io-mmap.go new file mode 100644 index 00000000..af070cb3 --- /dev/null +++ b/storage/file-io-mmap.go @@ -0,0 +1,201 @@ +package storage + +import ( + "fmt" + "io" + "io/fs" + "os" + "sync" + "sync/atomic" + + g "github.com/anacrolix/generics" + "github.com/anacrolix/missinggo/v2/panicif" + "github.com/edsrzf/mmap-go" +) + +type mmapFileIo struct { + mu sync.Mutex + paths map[string]*fileMmap +} + +type fileMmap struct { + m mmap.MMap + writable bool + refs atomic.Int32 +} + +func (me *fileMmap) dec() error { + if me.refs.Add(-1) == 0 { + return me.m.Unmap() + } + return nil +} + +func (me *fileMmap) inc() { + panicif.LessThanOrEqual(me.refs.Add(1), 0) +} + +func (m *mmapFileIo) openForSharedRead(name string) (_ sharedFileIf, err error) { + return m.openReadOnly(name) +} + +func (m *mmapFileIo) openForRead(name string) (_ fileReader, err error) { + sh, err := m.openReadOnly(name) + if err != nil { + return + } + return &mmapFileHandle{ + shared: sh, + }, nil +} + +func (m *mmapFileIo) openReadOnly(name string) (_ *mmapSharedFileHandle, err error) { + m.mu.Lock() + defer m.mu.Unlock() + v, ok := m.paths[name] + if ok { + return newMmapFile(v), nil + } + f, err := os.Open(name) + if err != nil { + return + } + defer f.Close() + mm, err := mmap.Map(f, mmap.RDONLY, 0) + if err != nil { + err = fmt.Errorf("mapping file: %w", err) + return + } + v = m.addNewMmap(name, mm, false) + return newMmapFile(v), nil +} + +func (m *mmapFileIo) openForWrite(name string, size int64) (_ fileWriter, err error) { + m.mu.Lock() + defer m.mu.Unlock() + v, ok := m.paths[name] + if ok { + if int64(len(v.m)) == size && v.writable { + v.inc() + return newMmapFile(v), nil + } else { + v.dec() + g.MustDelete(m.paths, name) + } + } + f, err := openFileExtra(name, os.O_RDWR) + if err != nil { + return + } + defer f.Close() + err = f.Truncate(size) + if err != nil { + err = fmt.Errorf("error truncating file: %w", err) + return + } + mm, err := mmap.Map(f, mmap.RDWR, 0) + if err != nil { + return + } + // This can happen due to filesystem changes outside our control. Don't be naive. + if int64(len(mm)) != size { + err = fmt.Errorf("new mmap has wrong size %v, expected %v", len(mm), size) + mm.Unmap() + return + } + return newMmapFile(m.addNewMmap(name, mm, true)), nil +} + +func newMmapFile(f *fileMmap) *mmapSharedFileHandle { + ret := &mmapSharedFileHandle{ + f: f, + } + ret.f.inc() + return ret +} + +func (me *mmapFileIo) addNewMmap(name string, mm mmap.MMap, writable bool) *fileMmap { + v := &fileMmap{ + m: mm, + writable: writable, + } + // One for the store, one for the caller. + v.refs.Store(1) + g.MakeMapIfNil(&me.paths) + g.MapMustAssignNew(me.paths, name, v) + return v +} + +var _ fileIo = (*mmapFileIo)(nil) + +type mmapSharedFileHandle struct { + f *fileMmap + close sync.Once +} + +func (m *mmapSharedFileHandle) WriteAt(p []byte, off int64) (n int, err error) { + //fmt.Println("mmapSharedFileHandle.WriteAt", off, len(p), len(m.f.m)) + n = copy(m.f.m[off:], p) + return +} + +func (m *mmapSharedFileHandle) WriteTo(w io.Writer) (n int64, err error) { + //TODO implement me + panic("implement me") +} + +func (m *mmapSharedFileHandle) ReadAt(p []byte, off int64) (n int, err error) { + n = copy(p, m.f.m[off:]) + if n < len(p) { + if off < 0 { + err = fs.ErrInvalid + return + } + } + if off+int64(n) == int64(len(m.f.m)) { + err = io.EOF + } + return +} + +func (m *mmapSharedFileHandle) Close() (err error) { + m.close.Do(func() { + err = m.f.dec() + }) + return +} + +type mmapFileHandle struct { + shared *mmapSharedFileHandle + pos int64 +} + +func (me *mmapFileHandle) WriteTo(w io.Writer) (n int64, err error) { + n1, err := w.Write(me.shared.f.m[me.pos:]) + n = int64(n1) + me.pos += n + return +} + +func (me *mmapFileHandle) Close() error { + return me.shared.Close() +} + +func (me *mmapFileHandle) Read(p []byte) (n int, err error) { + if me.pos > int64(len(me.shared.f.m)) { + err = io.EOF + return + } + n = copy(p, me.shared.f.m[me.pos:]) + me.pos += int64(n) + if me.pos >= int64(len(me.shared.f.m)) { + err = io.EOF + } + return +} + +func (me *mmapFileHandle) seekData(offset int64) (ret int64, err error) { + me.pos = offset + ret = offset + return +} -- 2.51.0