+97
@@ -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
@@ -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
@@ -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 }
|
||||
Generated
Vendored
+11
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+13
@@ -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
@@ -0,0 +1,2 @@
|
||||
// Package storage implements storage backends for package torrent.
|
||||
package storage
|
||||
+34
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user