feat: Waku v2 bridge

Issue #12610
This commit is contained in:
Michal Iskierko
2023-11-12 13:29:38 +01:00
parent 56e7bd01ca
commit 6d31343205
6716 changed files with 1982502 additions and 5891 deletions
+97
View File
@@ -0,0 +1,97 @@
//go:build !noboltdb && !wasm
// +build !noboltdb,!wasm
package storage
import (
"encoding/binary"
"os"
"path/filepath"
"time"
"go.etcd.io/bbolt"
"github.com/anacrolix/torrent/metainfo"
)
const (
boltDbCompleteValue = "c"
boltDbIncompleteValue = "i"
)
var completionBucketKey = []byte("completion")
type boltPieceCompletion struct {
db *bbolt.DB
}
var _ PieceCompletion = (*boltPieceCompletion)(nil)
func NewBoltPieceCompletion(dir string) (ret PieceCompletion, err error) {
os.MkdirAll(dir, 0o750)
p := filepath.Join(dir, ".torrent.bolt.db")
db, err := bbolt.Open(p, 0o660, &bbolt.Options{
Timeout: time.Second,
})
if err != nil {
return
}
db.NoSync = true
ret = &boltPieceCompletion{db}
return
}
func (me boltPieceCompletion) Get(pk metainfo.PieceKey) (cn Completion, err error) {
err = me.db.View(func(tx *bbolt.Tx) error {
cb := tx.Bucket(completionBucketKey)
if cb == nil {
return nil
}
ih := cb.Bucket(pk.InfoHash[:])
if ih == nil {
return nil
}
var key [4]byte
binary.BigEndian.PutUint32(key[:], uint32(pk.Index))
cn.Ok = true
switch string(ih.Get(key[:])) {
case boltDbCompleteValue:
cn.Complete = true
case boltDbIncompleteValue:
cn.Complete = false
default:
cn.Ok = false
}
return nil
})
return
}
func (me boltPieceCompletion) Set(pk metainfo.PieceKey, b bool) error {
if c, err := me.Get(pk); err == nil && c.Ok && c.Complete == b {
return nil
}
return me.db.Update(func(tx *bbolt.Tx) error {
c, err := tx.CreateBucketIfNotExists(completionBucketKey)
if err != nil {
return err
}
ih, err := c.CreateBucketIfNotExists(pk.InfoHash[:])
if err != nil {
return err
}
var key [4]byte
binary.BigEndian.PutUint32(key[:], uint32(pk.Index))
return ih.Put(key[:], []byte(func() string {
if b {
return boltDbCompleteValue
} else {
return boltDbIncompleteValue
}
}()))
})
}
func (me *boltPieceCompletion) Close() error {
return me.db.Close()
}
+112
View File
@@ -0,0 +1,112 @@
//go:build !noboltdb && !wasm
// +build !noboltdb,!wasm
package storage
import (
"encoding/binary"
"io"
"go.etcd.io/bbolt"
"github.com/anacrolix/torrent/metainfo"
)
type boltPiece struct {
db *bbolt.DB
p metainfo.Piece
ih metainfo.Hash
key [24]byte
}
var (
_ PieceImpl = (*boltPiece)(nil)
dataBucketKey = []byte("data")
)
func (me *boltPiece) pc() PieceCompletionGetSetter {
return boltPieceCompletion{me.db}
}
func (me *boltPiece) pk() metainfo.PieceKey {
return metainfo.PieceKey{me.ih, me.p.Index()}
}
func (me *boltPiece) Completion() Completion {
c, err := me.pc().Get(me.pk())
switch err {
case bbolt.ErrDatabaseNotOpen:
return Completion{}
case nil:
default:
panic(err)
}
return c
}
func (me *boltPiece) MarkComplete() error {
return me.pc().Set(me.pk(), true)
}
func (me *boltPiece) MarkNotComplete() error {
return me.pc().Set(me.pk(), false)
}
func (me *boltPiece) ReadAt(b []byte, off int64) (n int, err error) {
err = me.db.View(func(tx *bbolt.Tx) error {
db := tx.Bucket(dataBucketKey)
if db == nil {
return io.EOF
}
ci := off / chunkSize
off %= chunkSize
for len(b) != 0 {
ck := me.chunkKey(int(ci))
_b := db.Get(ck[:])
// If the chunk is the wrong size, assume it's missing as we can't rely on the data.
if len(_b) != chunkSize {
return io.EOF
}
n1 := copy(b, _b[off:])
off = 0
ci++
b = b[n1:]
n += n1
}
return nil
})
return
}
func (me *boltPiece) chunkKey(index int) (ret [26]byte) {
copy(ret[:], me.key[:])
binary.BigEndian.PutUint16(ret[24:], uint16(index))
return
}
func (me *boltPiece) WriteAt(b []byte, off int64) (n int, err error) {
err = me.db.Update(func(tx *bbolt.Tx) error {
db, err := tx.CreateBucketIfNotExists(dataBucketKey)
if err != nil {
return err
}
ci := off / chunkSize
off %= chunkSize
for len(b) != 0 {
_b := make([]byte, chunkSize)
ck := me.chunkKey(int(ci))
copy(_b, db.Get(ck[:]))
n1 := copy(_b[off:], b)
db.Put(ck[:], _b)
if n1 > len(b) {
break
}
b = b[n1:]
off = 0
ci++
n += n1
}
return nil
})
return
}
+64
View File
@@ -0,0 +1,64 @@
//go:build !noboltdb && !wasm
// +build !noboltdb,!wasm
package storage
import (
"encoding/binary"
"path/filepath"
"time"
"github.com/anacrolix/missinggo/expect"
"go.etcd.io/bbolt"
"github.com/anacrolix/torrent/metainfo"
)
const (
// Chosen to match the usual chunk size in a torrent client. This way, most chunk writes are to
// exactly one full item in bbolt DB.
chunkSize = 1 << 14
)
type boltClient struct {
db *bbolt.DB
}
type boltTorrent struct {
cl *boltClient
ih metainfo.Hash
}
func NewBoltDB(filePath string) ClientImplCloser {
db, err := bbolt.Open(filepath.Join(filePath, "bolt.db"), 0o600, &bbolt.Options{
Timeout: time.Second,
})
expect.Nil(err)
db.NoSync = true
return &boltClient{db}
}
func (me *boltClient) Close() error {
return me.db.Close()
}
func (me *boltClient) OpenTorrent(_ *metainfo.Info, infoHash metainfo.Hash) (TorrentImpl, error) {
t := &boltTorrent{me, infoHash}
return TorrentImpl{
Piece: t.Piece,
Close: t.Close,
}, nil
}
func (me *boltTorrent) Piece(p metainfo.Piece) PieceImpl {
ret := &boltPiece{
p: p,
db: me.cl.db,
ih: me.ih,
}
copy(ret.key[:], me.ih[:])
binary.BigEndian.PutUint32(ret.key[20:], uint32(p.Index()))
return ret
}
func (boltTorrent) Close() error { return nil }
@@ -0,0 +1,11 @@
// Bolt piece completion is available, and sqlite is not.
//go:build !noboltdb && !wasm && (js || nosqlite)
// +build !noboltdb
// +build !wasm
// +build js nosqlite
package storage
func NewDefaultPieceCompletionForDir(dir string) (PieceCompletion, error) {
return NewBoltPieceCompletion(dir)
}
@@ -0,0 +1,13 @@
// Bolt piece completion is not available, and neither is sqlite.
//go:build wasm || noboltdb
// +build wasm noboltdb
package storage
import (
"errors"
)
func NewDefaultPieceCompletionForDir(dir string) (PieceCompletion, error) {
return nil, errors.New("y ur OS no have features")
}
+2
View File
@@ -0,0 +1,2 @@
// Package storage implements storage backends for package torrent.
package storage
+34
View File
@@ -0,0 +1,34 @@
package storage
import (
"github.com/anacrolix/torrent/metainfo"
)
func NewFileWithCompletion(baseDir string, completion PieceCompletion) ClientImplCloser {
return NewFileWithCustomPathMakerAndCompletion(baseDir, nil, completion)
}
// File storage with data partitioned by infohash.
func NewFileByInfoHash(baseDir string) ClientImplCloser {
return NewFileWithCustomPathMaker(baseDir, infoHashPathMaker)
}
// Deprecated: Allows passing a function to determine the path for storing torrent data. The
// function is responsible for sanitizing the info if it uses some part of it (for example
// sanitizing info.Name).
func NewFileWithCustomPathMaker(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string) ClientImplCloser {
return NewFileWithCustomPathMakerAndCompletion(baseDir, pathMaker, pieceCompletionForDir(baseDir))
}
// Deprecated: Allows passing custom PieceCompletion
func NewFileWithCustomPathMakerAndCompletion(
baseDir string,
pathMaker TorrentDirFilePathMaker,
completion PieceCompletion,
) ClientImplCloser {
return NewFileOpts(NewFileClientOpts{
ClientBaseDir: baseDir,
TorrentDirMaker: pathMaker,
PieceCompletion: completion,
})
}
+34
View File
@@ -0,0 +1,34 @@
package storage
import "github.com/anacrolix/torrent/metainfo"
type requiredLength struct {
fileIndex int
length int64
}
func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
if n == 0 {
return
}
for i, fi := range info.UpvertedFiles() {
if off >= fi.Length {
off -= fi.Length
continue
}
n1 := n
if off+n1 > fi.Length {
n1 = fi.Length - off
}
ret = append(ret, requiredLength{
fileIndex: i,
length: off + n1,
})
n -= n1
if n == 0 {
return
}
off = 0
}
panic("extent exceeds torrent bounds")
}
+38
View File
@@ -0,0 +1,38 @@
package storage
import (
"os"
"path/filepath"
"strings"
"github.com/anacrolix/torrent/metainfo"
)
// Determines the filepath to be used for each file in a torrent.
type FilePathMaker func(opts FilePathMakerOpts) string
// Determines the directory for a given torrent within a storage client.
type TorrentDirFilePathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string
// Info passed to a FilePathMaker.
type FilePathMakerOpts struct {
Info *metainfo.Info
File *metainfo.FileInfo
}
// defaultPathMaker just returns the storage client's base directory.
func defaultPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
return baseDir
}
func infoHashPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
return filepath.Join(baseDir, infoHash.HexString())
}
func isSubFilepath(base, sub string) bool {
rel, err := filepath.Rel(base, sub)
if err != nil {
return false
}
return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
}
+54
View File
@@ -0,0 +1,54 @@
package storage
import (
"io"
"log"
"os"
"github.com/anacrolix/torrent/metainfo"
)
type filePieceImpl struct {
*fileTorrentImpl
p metainfo.Piece
io.WriterAt
io.ReaderAt
}
var _ PieceImpl = (*filePieceImpl)(nil)
func (me *filePieceImpl) pieceKey() metainfo.PieceKey {
return metainfo.PieceKey{me.infoHash, me.p.Index()}
}
func (fs *filePieceImpl) Completion() Completion {
c, err := fs.completion.Get(fs.pieceKey())
if err != nil {
log.Printf("error getting piece completion: %s", err)
c.Ok = false
return c
}
if c.Complete {
// If it's allegedly complete, check that its constituent files have the necessary length.
for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
s, err := os.Stat(fs.files[fi.fileIndex].path)
if err != nil || s.Size() < fi.length {
c.Complete = false
break
}
}
}
if !c.Complete {
// The completion was wrong, fix it.
fs.completion.Set(fs.pieceKey(), false)
}
return c
}
func (fs *filePieceImpl) MarkComplete() error {
return fs.completion.Set(fs.pieceKey(), true)
}
func (fs *filePieceImpl) MarkNotComplete() error {
return fs.completion.Set(fs.pieceKey(), false)
}
+212
View File
@@ -0,0 +1,212 @@
package storage
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent/common"
"github.com/anacrolix/torrent/segments"
"github.com/anacrolix/torrent/metainfo"
)
// File-based storage for torrents, that isn't yet bound to a particular torrent.
type fileClientImpl struct {
opts NewFileClientOpts
}
// All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
func NewFile(baseDir string) ClientImplCloser {
return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
}
type NewFileClientOpts struct {
// The base directory for all downloads.
ClientBaseDir string
FilePathMaker FilePathMaker
TorrentDirMaker TorrentDirFilePathMaker
PieceCompletion PieceCompletion
}
// NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
if opts.TorrentDirMaker == nil {
opts.TorrentDirMaker = defaultPathMaker
}
if opts.FilePathMaker == nil {
opts.FilePathMaker = func(opts FilePathMakerOpts) string {
var parts []string
if opts.Info.Name != metainfo.NoName {
parts = append(parts, opts.Info.Name)
}
return filepath.Join(append(parts, opts.File.Path...)...)
}
}
if opts.PieceCompletion == nil {
opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
}
return fileClientImpl{opts}
}
func (me fileClientImpl) Close() error {
return me.opts.PieceCompletion.Close()
}
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
upvertedFiles := info.UpvertedFiles()
files := make([]file, 0, len(upvertedFiles))
for i, fileInfo := range upvertedFiles {
filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
Info: info,
File: &fileInfo,
}))
if !isSubFilepath(dir, filePath) {
err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
return
}
f := file{
path: filePath,
length: fileInfo.Length,
}
if f.length == 0 {
err = CreateNativeZeroLengthFile(f.path)
if err != nil {
err = fmt.Errorf("creating zero length file: %w", err)
return
}
}
files = append(files, f)
}
t := &fileTorrentImpl{
files,
segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
infoHash,
fs.opts.PieceCompletion,
}
return TorrentImpl{
Piece: t.Piece,
Close: t.Close,
}, nil
}
type file struct {
// The safe, OS-local file path.
path string
length int64
}
type fileTorrentImpl struct {
files []file
segmentLocater segments.Index
infoHash metainfo.Hash
completion PieceCompletion
}
func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
// Create a view onto the file-based torrent storage.
_io := fileTorrentImplIO{fts}
// Return the appropriate segments of this.
return &filePieceImpl{
fts,
p,
missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
io.NewSectionReader(_io, p.Offset(), p.Length()),
}
}
func (fs *fileTorrentImpl) Close() error {
return nil
}
// A helper to create zero-length files which won't appear for file-orientated storage since no
// writes will ever occur to them (no torrent data is associated with a zero-length file). The
// caller should make sure the file name provided is safe/sanitized.
func CreateNativeZeroLengthFile(name string) error {
os.MkdirAll(filepath.Dir(name), 0o777)
var f io.Closer
f, err := os.Create(name)
if err != nil {
return err
}
return f.Close()
}
// Exposes file-based storage of a torrent, as one big ReadWriterAt.
type fileTorrentImplIO struct {
fts *fileTorrentImpl
}
// Returns EOF on short or missing file.
func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
f, err := os.Open(file.path)
if os.IsNotExist(err) {
// File missing is treated the same as a short file.
err = io.EOF
return
}
if err != nil {
return
}
defer f.Close()
// Limit the read to within the expected bounds of this file.
if int64(len(b)) > file.length-off {
b = b[:file.length-off]
}
for off < file.length && len(b) != 0 {
n1, err1 := f.ReadAt(b, off)
b = b[n1:]
n += n1
off += int64(n1)
if n1 == 0 {
err = err1
break
}
}
return
}
// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
n += n1
b = b[n1:]
err = err1
return err == nil // && int64(n1) == e.Length
})
if len(b) != 0 && err == nil {
err = io.EOF
}
return
}
func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
// log.Printf("write at %v: %v bytes", off, len(p))
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
name := fst.fts.files[i].path
os.MkdirAll(filepath.Dir(name), 0o777)
var f *os.File
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return false
}
var n1 int
n1, err = f.WriteAt(p[:e.Length], e.Start)
// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
closeErr := f.Close()
n += n1
p = p[n1:]
if err == nil {
err = closeErr
}
if err == nil && int64(n1) != e.Length {
err = io.ErrShortWrite
}
return err == nil
})
return
}
+57
View File
@@ -0,0 +1,57 @@
package storage
import (
"io"
"github.com/anacrolix/torrent/metainfo"
)
type ClientImplCloser interface {
ClientImpl
Close() error
}
// Represents data storage for an unspecified torrent.
type ClientImpl interface {
OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (TorrentImpl, error)
}
type TorrentCapacity *func() (cap int64, capped bool)
// Data storage bound to a torrent.
type TorrentImpl struct {
Piece func(p metainfo.Piece) PieceImpl
Close func() error
// Storages that share the same space, will provide equal pointers. The function is called once
// to determine the storage for torrents sharing the same function pointer, and mutated in
// place.
Capacity TorrentCapacity
}
// Interacts with torrent piece data. Optional interfaces to implement include:
// io.WriterTo, such as when a piece supports a more efficient way to write out incomplete chunks.
// SelfHashing, such as when a piece supports a more efficient way to hash its contents.
type PieceImpl interface {
// These interfaces are not as strict as normally required. They can
// assume that the parameters are appropriate for the dimensions of the
// piece.
io.ReaderAt
io.WriterAt
// Called when the client believes the piece data will pass a hash check.
// The storage can move or mark the piece data as read-only as it sees
// fit.
MarkComplete() error
MarkNotComplete() error
// Returns true if the piece is complete.
Completion() Completion
}
type Completion struct {
Complete bool
Ok bool
}
// Allows a storage backend to override hashing (i.e. if it can do it more efficiently than the torrent client can)
type SelfHashing interface {
SelfHash() (metainfo.Hash, error)
}
+34
View File
@@ -0,0 +1,34 @@
package storage
import (
"sync"
"github.com/anacrolix/torrent/metainfo"
)
type mapPieceCompletion struct {
// TODO: Generics
m sync.Map
}
var _ PieceCompletion = (*mapPieceCompletion)(nil)
func NewMapPieceCompletion() PieceCompletion {
return &mapPieceCompletion{}
}
func (*mapPieceCompletion) Close() error { return nil }
func (me *mapPieceCompletion) Get(pk metainfo.PieceKey) (c Completion, err error) {
v, ok := me.m.Load(pk)
if ok {
c.Complete = v.(bool)
}
c.Ok = ok
return
}
func (me *mapPieceCompletion) Set(pk metainfo.PieceKey, b bool) error {
me.m.Store(pk, b)
return nil
}
+177
View File
@@ -0,0 +1,177 @@
//go:build !wasm
// +build !wasm
package storage
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/anacrolix/missinggo/v2"
"github.com/edsrzf/mmap-go"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/mmap_span"
)
type mmapClientImpl struct {
baseDir string
pc PieceCompletion
}
// TODO: Support all the same native filepath configuration that NewFileOpts provides.
func NewMMap(baseDir string) ClientImplCloser {
return NewMMapWithCompletion(baseDir, pieceCompletionForDir(baseDir))
}
func NewMMapWithCompletion(baseDir string, completion PieceCompletion) *mmapClientImpl {
return &mmapClientImpl{
baseDir: baseDir,
pc: completion,
}
}
func (s *mmapClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
span, err := mMapTorrent(info, s.baseDir)
t := &mmapTorrentStorage{
infoHash: infoHash,
span: span,
pc: s.pc,
}
return TorrentImpl{Piece: t.Piece, Close: t.Close}, err
}
func (s *mmapClientImpl) Close() error {
return s.pc.Close()
}
type mmapTorrentStorage struct {
infoHash metainfo.Hash
span *mmap_span.MMapSpan
pc PieceCompletionGetSetter
}
func (ts *mmapTorrentStorage) Piece(p metainfo.Piece) PieceImpl {
return mmapStoragePiece{
pc: ts.pc,
p: p,
ih: ts.infoHash,
ReaderAt: io.NewSectionReader(ts.span, p.Offset(), p.Length()),
WriterAt: missinggo.NewSectionWriter(ts.span, p.Offset(), p.Length()),
}
}
func (ts *mmapTorrentStorage) Close() error {
errs := ts.span.Close()
if len(errs) > 0 {
return errs[0]
}
return nil
}
type mmapStoragePiece struct {
pc PieceCompletionGetSetter
p metainfo.Piece
ih metainfo.Hash
io.ReaderAt
io.WriterAt
}
func (me mmapStoragePiece) pieceKey() metainfo.PieceKey {
return metainfo.PieceKey{me.ih, me.p.Index()}
}
func (sp mmapStoragePiece) Completion() Completion {
c, err := sp.pc.Get(sp.pieceKey())
if err != nil {
panic(err)
}
return c
}
func (sp mmapStoragePiece) MarkComplete() error {
sp.pc.Set(sp.pieceKey(), true)
return nil
}
func (sp mmapStoragePiece) MarkNotComplete() error {
sp.pc.Set(sp.pieceKey(), false)
return nil
}
func mMapTorrent(md *metainfo.Info, location string) (mms *mmap_span.MMapSpan, err error) {
mms = &mmap_span.MMapSpan{}
defer func() {
if err != nil {
mms.Close()
}
}()
for _, miFile := range md.UpvertedFiles() {
var safeName string
safeName, err = ToSafeFilePath(append([]string{md.Name}, miFile.Path...)...)
if err != nil {
return
}
fileName := filepath.Join(location, safeName)
var mm mmap.MMap
mm, err = mmapFile(fileName, miFile.Length)
if err != nil {
err = fmt.Errorf("file %q: %s", miFile.DisplayPath(md), err)
return
}
if mm != nil {
mms.Append(mm)
}
}
mms.InitIndex()
return
}
func mmapFile(name string, size int64) (ret mmap.MMap, err error) {
dir := filepath.Dir(name)
err = os.MkdirAll(dir, 0o750)
if err != nil {
err = fmt.Errorf("making directory %q: %s", dir, err)
return
}
var file *os.File
file, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o666)
if err != nil {
return
}
defer file.Close()
var fi os.FileInfo
fi, err = file.Stat()
if err != nil {
return
}
if fi.Size() < size {
// I think this is necessary on HFS+. Maybe Linux will SIGBUS too if
// you overmap a file but I'm not sure.
err = file.Truncate(size)
if err != nil {
return
}
}
if size == 0 {
// Can't mmap() regions with length 0.
return
}
intLen := int(size)
if int64(intLen) != size {
err = errors.New("size too large for system")
return
}
ret, err = mmap.MapRegion(file, intLen, mmap.RDWR, 0, 0)
if err != nil {
err = fmt.Errorf("error mapping region: %s", err)
return
}
if int64(len(ret)) != size {
panic(len(ret))
}
return
}
+27
View File
@@ -0,0 +1,27 @@
package storage
import (
"github.com/anacrolix/log"
"github.com/anacrolix/torrent/metainfo"
)
type PieceCompletionGetSetter interface {
Get(metainfo.PieceKey) (Completion, error)
Set(_ metainfo.PieceKey, complete bool) error
}
// Implementations track the completion of pieces. It must be concurrent-safe.
type PieceCompletion interface {
PieceCompletionGetSetter
Close() error
}
func pieceCompletionForDir(dir string) (ret PieceCompletion) {
ret, err := NewDefaultPieceCompletionForDir(dir)
if err != nil {
log.Printf("couldn't open piece completion db in %q: %s", dir, err)
ret = NewMapPieceCompletion()
}
return
}
+280
View File
@@ -0,0 +1,280 @@
package storage
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"path"
"sort"
"strconv"
"sync"
"github.com/anacrolix/missinggo/v2/resource"
"github.com/anacrolix/torrent/metainfo"
)
type piecePerResource struct {
rp PieceProvider
opts ResourcePiecesOpts
}
type ResourcePiecesOpts struct {
// After marking a piece complete, don't bother deleting its incomplete blobs.
LeaveIncompleteChunks bool
// Sized puts require being able to stream from a statement executed on another connection.
// Without them, we buffer the entire read and then put that.
NoSizedPuts bool
Capacity *int64
}
func NewResourcePieces(p PieceProvider) ClientImpl {
return NewResourcePiecesOpts(p, ResourcePiecesOpts{})
}
func NewResourcePiecesOpts(p PieceProvider, opts ResourcePiecesOpts) ClientImpl {
return &piecePerResource{
rp: p,
opts: opts,
}
}
type piecePerResourceTorrentImpl struct {
piecePerResource
locks []sync.RWMutex
}
func (piecePerResourceTorrentImpl) Close() error {
return nil
}
func (s piecePerResource) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (TorrentImpl, error) {
t := piecePerResourceTorrentImpl{
s,
make([]sync.RWMutex, info.NumPieces()),
}
return TorrentImpl{Piece: t.Piece, Close: t.Close}, nil
}
func (s piecePerResourceTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
return piecePerResourcePiece{
mp: p,
piecePerResource: s.piecePerResource,
mu: &s.locks[p.Index()],
}
}
type PieceProvider interface {
resource.Provider
}
type ConsecutiveChunkReader interface {
ReadConsecutiveChunks(prefix string) (io.ReadCloser, error)
}
type piecePerResourcePiece struct {
mp metainfo.Piece
piecePerResource
// This protects operations that move complete/incomplete pieces around, which can trigger read
// errors that may cause callers to do more drastic things.
mu *sync.RWMutex
}
var _ io.WriterTo = piecePerResourcePiece{}
func (s piecePerResourcePiece) WriteTo(w io.Writer) (int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if s.mustIsComplete() {
r, err := s.completed().Get()
if err != nil {
return 0, fmt.Errorf("getting complete instance: %w", err)
}
defer r.Close()
return io.Copy(w, r)
}
if ccr, ok := s.rp.(ConsecutiveChunkReader); ok {
return s.writeConsecutiveIncompleteChunks(ccr, w)
}
return io.Copy(w, io.NewSectionReader(s, 0, s.mp.Length()))
}
func (s piecePerResourcePiece) writeConsecutiveIncompleteChunks(ccw ConsecutiveChunkReader, w io.Writer) (int64, error) {
r, err := ccw.ReadConsecutiveChunks(s.incompleteDirPath() + "/")
if err != nil {
return 0, err
}
defer r.Close()
return io.Copy(w, r)
}
// Returns if the piece is complete. Ok should be true, because we are the definitive source of
// truth here.
func (s piecePerResourcePiece) mustIsComplete() bool {
completion := s.Completion()
if !completion.Ok {
panic("must know complete definitively")
}
return completion.Complete
}
func (s piecePerResourcePiece) Completion() Completion {
s.mu.RLock()
defer s.mu.RUnlock()
fi, err := s.completed().Stat()
return Completion{
Complete: err == nil && fi.Size() == s.mp.Length(),
Ok: true,
}
}
type SizedPutter interface {
PutSized(io.Reader, int64) error
}
func (s piecePerResourcePiece) MarkComplete() error {
s.mu.Lock()
defer s.mu.Unlock()
incompleteChunks := s.getChunks()
r, err := func() (io.ReadCloser, error) {
if ccr, ok := s.rp.(ConsecutiveChunkReader); ok {
return ccr.ReadConsecutiveChunks(s.incompleteDirPath() + "/")
}
return ioutil.NopCloser(io.NewSectionReader(incompleteChunks, 0, s.mp.Length())), nil
}()
if err != nil {
return fmt.Errorf("getting incomplete chunks reader: %w", err)
}
defer r.Close()
completedInstance := s.completed()
err = func() error {
if sp, ok := completedInstance.(SizedPutter); ok && !s.opts.NoSizedPuts {
return sp.PutSized(r, s.mp.Length())
} else {
return completedInstance.Put(r)
}
}()
if err == nil && !s.opts.LeaveIncompleteChunks {
// I think we do this synchronously here since we don't want callers to act on the completed
// piece if we're concurrently still deleting chunks. The caller may decide to start
// downloading chunks again and won't expect us to delete them. It seems to be much faster
// to let the resource provider do this if possible.
var wg sync.WaitGroup
for _, c := range incompleteChunks {
wg.Add(1)
go func(c chunk) {
defer wg.Done()
c.instance.Delete()
}(c)
}
wg.Wait()
}
return err
}
func (s piecePerResourcePiece) MarkNotComplete() error {
s.mu.Lock()
defer s.mu.Unlock()
return s.completed().Delete()
}
func (s piecePerResourcePiece) ReadAt(b []byte, off int64) (int, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if s.mustIsComplete() {
return s.completed().ReadAt(b, off)
}
return s.getChunks().ReadAt(b, off)
}
func (s piecePerResourcePiece) WriteAt(b []byte, off int64) (n int, err error) {
s.mu.RLock()
defer s.mu.RUnlock()
i, err := s.rp.NewInstance(path.Join(s.incompleteDirPath(), strconv.FormatInt(off, 10)))
if err != nil {
panic(err)
}
r := bytes.NewReader(b)
if sp, ok := i.(SizedPutter); ok {
err = sp.PutSized(r, r.Size())
} else {
err = i.Put(r)
}
n = len(b) - r.Len()
return
}
type chunk struct {
offset int64
instance resource.Instance
}
type chunks []chunk
func (me chunks) ReadAt(b []byte, off int64) (int, error) {
for {
if len(me) == 0 {
return 0, io.EOF
}
if me[0].offset <= off {
break
}
me = me[1:]
}
n, err := me[0].instance.ReadAt(b, off-me[0].offset)
if n == len(b) {
return n, nil
}
if err == nil || err == io.EOF {
n_, err := me[1:].ReadAt(b[n:], off+int64(n))
return n + n_, err
}
return n, err
}
func (s piecePerResourcePiece) getChunks() (chunks chunks) {
names, err := s.incompleteDir().Readdirnames()
if err != nil {
return
}
for _, n := range names {
offset, err := strconv.ParseInt(n, 10, 64)
if err != nil {
panic(err)
}
i, err := s.rp.NewInstance(path.Join(s.incompleteDirPath(), n))
if err != nil {
panic(err)
}
chunks = append(chunks, chunk{offset, i})
}
sort.Slice(chunks, func(i, j int) bool {
return chunks[i].offset < chunks[j].offset
})
return
}
func (s piecePerResourcePiece) completedInstancePath() string {
return path.Join("completed", s.mp.Hash().HexString())
}
func (s piecePerResourcePiece) completed() resource.Instance {
i, err := s.rp.NewInstance(s.completedInstancePath())
if err != nil {
panic(err)
}
return i
}
func (s piecePerResourcePiece) incompleteDirPath() string {
return path.Join("incompleted", s.mp.Hash().HexString())
}
func (s piecePerResourcePiece) incompleteDir() resource.DirInstance {
i, err := s.rp.NewInstance(s.incompleteDirPath())
if err != nil {
panic(err)
}
return i.(resource.DirInstance)
}
+29
View File
@@ -0,0 +1,29 @@
package storage
import (
"errors"
"path/filepath"
"strings"
)
// Get the first file path component. We can't use filepath.Split because that breaks off the last
// one. We could optimize this to avoid allocating a slice down the track.
func firstComponent(filePath string) string {
return strings.SplitN(filePath, string(filepath.Separator), 2)[0]
}
// Combines file info path components, ensuring the result won't escape into parent directories.
func ToSafeFilePath(fileInfoComponents ...string) (string, error) {
safeComps := make([]string, 0, len(fileInfoComponents))
for _, comp := range fileInfoComponents {
safeComps = append(safeComps, filepath.Clean(comp))
}
safeFilePath := filepath.Join(safeComps...)
fc := firstComponent(safeFilePath)
switch fc {
case "..":
return "", errors.New("escapes root dir")
default:
return safeFilePath, nil
}
}
+83
View File
@@ -0,0 +1,83 @@
// modernc.org/sqlite depends on modernc.org/libc which doesn't work for JS (and probably wasm but I
// think JS is the stronger signal).
//go:build !js && !nosqlite
// +build !js,!nosqlite
package storage
import (
"errors"
"path/filepath"
"sync"
"github.com/anacrolix/torrent/metainfo"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// sqlite is always the default when available.
func NewDefaultPieceCompletionForDir(dir string) (PieceCompletion, error) {
return NewSqlitePieceCompletion(dir)
}
type sqlitePieceCompletion struct {
mu sync.Mutex
closed bool
db *sqlite.Conn
}
var _ PieceCompletion = (*sqlitePieceCompletion)(nil)
func NewSqlitePieceCompletion(dir string) (ret *sqlitePieceCompletion, err error) {
p := filepath.Join(dir, ".torrent.db")
db, err := sqlite.OpenConn(p, 0)
if err != nil {
return
}
err = sqlitex.ExecScript(db, `create table if not exists piece_completion(infohash, "index", complete, unique(infohash, "index"))`)
if err != nil {
db.Close()
return
}
ret = &sqlitePieceCompletion{db: db}
return
}
func (me *sqlitePieceCompletion) Get(pk metainfo.PieceKey) (c Completion, err error) {
me.mu.Lock()
defer me.mu.Unlock()
err = sqlitex.Exec(
me.db, `select complete from piece_completion where infohash=? and "index"=?`,
func(stmt *sqlite.Stmt) error {
c.Complete = stmt.ColumnInt(0) != 0
c.Ok = true
return nil
},
pk.InfoHash.HexString(), pk.Index)
return
}
func (me *sqlitePieceCompletion) Set(pk metainfo.PieceKey, b bool) error {
me.mu.Lock()
defer me.mu.Unlock()
if me.closed {
return errors.New("closed")
}
return sqlitex.Exec(
me.db,
`insert or replace into piece_completion(infohash, "index", complete) values(?, ?, ?)`,
nil,
pk.InfoHash.HexString(), pk.Index, b)
}
func (me *sqlitePieceCompletion) Close() (err error) {
me.mu.Lock()
defer me.mu.Unlock()
if me.closed {
return
}
err = me.db.Close()
me.db = nil
me.closed = true
return
}
+102
View File
@@ -0,0 +1,102 @@
package storage
import (
"io"
"os"
"github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent/metainfo"
)
type Client struct {
ci ClientImpl
}
func NewClient(cl ClientImpl) *Client {
return &Client{cl}
}
func (cl Client) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (*Torrent, error) {
t, err := cl.ci.OpenTorrent(info, infoHash)
if err != nil {
return nil, err
}
return &Torrent{t}, nil
}
type Torrent struct {
TorrentImpl
}
func (t Torrent) Piece(p metainfo.Piece) Piece {
return Piece{t.TorrentImpl.Piece(p), p}
}
type Piece struct {
PieceImpl
mip metainfo.Piece
}
var _ io.WriterTo = Piece{}
// Why do we have this wrapper? Well PieceImpl doesn't implement io.Reader, so we can't let io.Copy
// and friends check for io.WriterTo and fallback for us since they expect an io.Reader.
func (p Piece) WriteTo(w io.Writer) (int64, error) {
if i, ok := p.PieceImpl.(io.WriterTo); ok {
return i.WriteTo(w)
}
n := p.mip.Length()
r := io.NewSectionReader(p, 0, n)
return io.CopyN(w, r, n)
}
func (p Piece) WriteAt(b []byte, off int64) (n int, err error) {
// Callers should not be writing to completed pieces, but it's too
// expensive to be checking this on every single write using uncached
// completions.
// c := p.Completion()
// if c.Ok && c.Complete {
// err = errors.New("piece already completed")
// return
// }
if off+int64(len(b)) > p.mip.Length() {
panic("write overflows piece")
}
b = missinggo.LimitLen(b, p.mip.Length()-off)
return p.PieceImpl.WriteAt(b, off)
}
func (p Piece) ReadAt(b []byte, off int64) (n int, err error) {
if off < 0 {
err = os.ErrInvalid
return
}
if off >= p.mip.Length() {
err = io.EOF
return
}
b = missinggo.LimitLen(b, p.mip.Length()-off)
if len(b) == 0 {
return
}
n, err = p.PieceImpl.ReadAt(b, off)
if n > len(b) {
panic(n)
}
if n == 0 && err == nil {
panic("io.Copy will get stuck")
}
off += int64(n)
// Doing this here may be inaccurate. There's legitimate reasons we may fail to read while the
// data is still there, such as too many open files. There should probably be a specific error
// to return if the data has been lost.
if off < p.mip.Length() {
if err == io.EOF {
p.MarkNotComplete()
}
}
return
}