]> Sergey Matveev's repositories - btrtrc.git/commitdiff
Initial draft client
authorSergey Matveev <stargrave@stargrave.org>
Sat, 26 Nov 2022 10:08:02 +0000 (13:08 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Fri, 13 Jan 2023 08:32:42 +0000 (11:32 +0300)
13 files changed:
cmd/btrtrc/README [new file with mode: 0644]
cmd/btrtrc/add.go [new file with mode: 0644]
cmd/btrtrc/colour.go [new file with mode: 0644]
cmd/btrtrc/list.go [new file with mode: 0644]
cmd/btrtrc/main.go [new file with mode: 0644]
cmd/btrtrc/pc.go [new file with mode: 0644]
cmd/btrtrc/sort.go [new file with mode: 0644]
cmd/btrtrc/status.go [new file with mode: 0644]
cmd/btrtrc/txstats.go [new file with mode: 0644]
cmd/btrtrc/verify.go [new file with mode: 0644]
go.mod
go.sum
peerconn.go

diff --git a/cmd/btrtrc/README b/cmd/btrtrc/README
new file mode 100644 (file)
index 0000000..a10226b
--- /dev/null
@@ -0,0 +1,18 @@
+Peer's status string:
+
+* i -- am interested
+* c -- am chocking
+* -
+* E -- RC4 encryption
+* e -- header encryption
+* discovery source:
+  * Tr -- tracker
+  * I  -- incoming
+  * Hg -- DHT get_peers
+  * Ha -- DHT announce_peer
+  * X  -- PEX
+  * M  -- direct (through magnet:)
+* U -- UTP
+* -
+* i -- he interested
+* c -- he chocking
diff --git a/cmd/btrtrc/add.go b/cmd/btrtrc/add.go
new file mode 100644 (file)
index 0000000..812176a
--- /dev/null
@@ -0,0 +1,130 @@
+package main
+
+import (
+       "bufio"
+       "encoding/hex"
+       "log"
+       "os"
+       "path"
+       "strings"
+       "time"
+
+       "github.com/anacrolix/torrent"
+       "github.com/anacrolix/torrent/metainfo"
+       "github.com/anacrolix/torrent/types/infohash"
+)
+
+type stringAddr string
+
+func (stringAddr) Network() string   { return "" }
+func (me stringAddr) String() string { return string(me) }
+
+func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
+       for _, ta := range addrs {
+               ret = append(ret, torrent.PeerInfo{Addr: stringAddr(ta)})
+       }
+       return
+}
+
+func readLinesFromFIFO(pth string) []string {
+       fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
+       if err != nil {
+               log.Println("OpenFile:", pth, err)
+               time.Sleep(time.Second)
+               return nil
+       }
+       var lines []string
+       scanner := bufio.NewScanner(fd)
+       for scanner.Scan() {
+               t := scanner.Text()
+               if len(t) > 0 {
+                       lines = append(lines, t)
+               }
+       }
+       fd.Close()
+       return lines
+}
+
+func fifoAdd(c *torrent.Client) {
+       pth := path.Join(FIFOsDir, "add")
+       recreateFIFO(pth)
+       for {
+               for _, what := range readLinesFromFIFO(pth) {
+                       cols := strings.Fields(what)
+                       what = cols[0]
+                       var t *torrent.Torrent
+                       var err error
+                       if strings.HasPrefix(what, "magnet:") {
+                               t, err = c.AddMagnet(what)
+                               if err != nil {
+                                       log.Println("AddMagnet:", what, err)
+                                       continue
+                               }
+                       } else {
+                               metaInfo, err := metainfo.LoadFromFile(what)
+                               if err != nil {
+                                       log.Println("LoadFromFile:", what, err)
+                                       continue
+                               }
+                               t, err = c.AddTorrent(metaInfo)
+                               if err != nil {
+                                       log.Println("AddTorrent:", what, err)
+                                       continue
+                               }
+                       }
+                       if len(cols) > 1 {
+                               t.AddPeers(resolveTestPeers(cols[1:]))
+                       }
+                       go fifoPeerList(t)
+                       go fifoFileList(t)
+                       log.Println("added:", t.InfoHash().HexString(), t.Name())
+                       go func() {
+                               <-t.GotInfo()
+                               if err = saveTorrent(t); err != nil {
+                                       log.Println("saveTorrent:", err)
+                               }
+                               txStatsLoad(t.InfoHash())
+                               t.DownloadAll()
+                       }()
+               }
+               time.Sleep(time.Second)
+       }
+}
+
+func fifoDel(c *torrent.Client) {
+       pth := path.Join(FIFOsDir, "del")
+       recreateFIFO(pth)
+       for {
+               for _, what := range readLinesFromFIFO(pth) {
+                       raw, err := hex.DecodeString(what)
+                       if err != nil {
+                               log.Println(err)
+                               continue
+                       }
+                       if len(raw) != infohash.Size {
+                               log.Println("bad length")
+                               continue
+                       }
+                       var i infohash.T
+                       copy(i[:], raw)
+                       t, ok := c.Torrent(i)
+                       if !ok {
+                               log.Println("no suck torrent", what)
+                               continue
+                       }
+                       txStatsDump(t)
+                       txStatsDel(t.InfoHash())
+                       t.Drop()
+                       for _, where := range []string{"files", "peers"} {
+                               pth := path.Join(where, t.InfoHash().HexString())
+                               os.Remove(pth)
+                               fd, err := os.Open(pth)
+                               if err == nil {
+                                       fd.Close()
+                               }
+                       }
+                       log.Println("deleted:", what, t.Name())
+               }
+               time.Sleep(time.Second)
+       }
+}
diff --git a/cmd/btrtrc/colour.go b/cmd/btrtrc/colour.go
new file mode 100644 (file)
index 0000000..eb664e1
--- /dev/null
@@ -0,0 +1,25 @@
+package main
+
+import (
+       "bytes"
+
+       "golang.org/x/term"
+)
+
+var (
+       Blue    string
+       Green   string
+       Magenta string
+       Red     string
+       Reset   string
+)
+
+func init() {
+       var b bytes.Buffer
+       t := term.NewTerminal(&b, "")
+       Blue = string(t.Escape.Blue)
+       Green = string(t.Escape.Green)
+       Magenta = string(t.Escape.Magenta)
+       Red = string(t.Escape.Red)
+       Reset = string(t.Escape.Reset)
+}
diff --git a/cmd/btrtrc/list.go b/cmd/btrtrc/list.go
new file mode 100644 (file)
index 0000000..f97f236
--- /dev/null
@@ -0,0 +1,208 @@
+package main
+
+import (
+       "encoding/hex"
+       "fmt"
+       "log"
+       "os"
+       "path"
+       "sort"
+       "strings"
+       "sync"
+       "syscall"
+       "time"
+
+       "github.com/anacrolix/dht/v2"
+       "github.com/anacrolix/torrent"
+       "github.com/anacrolix/torrent/metainfo"
+       "github.com/dustin/go-humanize"
+)
+
+const (
+       MaxListNameWidth = 40
+       FIFOsDir         = "fifos"
+       PeersDir         = "peers"
+       FilesDir         = "files"
+)
+
+var (
+       TorrentStats  = map[metainfo.Hash]torrent.TorrentStats{}
+       TorrentStatsM sync.RWMutex
+)
+
+func recreateFIFO(pth string) {
+       os.Remove(pth)
+       if err := syscall.Mkfifo(pth, 0666); err != nil {
+               log.Fatalln(err)
+       }
+}
+
+func shortenName(name string) string {
+       s := []rune(name)
+       if len(s) > MaxListNameWidth {
+               s = s[:MaxListNameWidth]
+       }
+       return string(s)
+}
+
+func fifoList(c *torrent.Client) {
+       pth := path.Join(FIFOsDir, "list")
+       recreateFIFO(pth)
+       for {
+               fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
+               if err != nil {
+                       log.Println("OpenFile:", pth, err)
+                       time.Sleep(time.Second)
+                       continue
+               }
+               ts := c.Torrents()
+               sort.Sort(ByInfoHash(ts))
+               for _, t := range ts {
+                       if t.Info() == nil {
+                               fmt.Fprintf(fd, "%s not ready\n", t.Name())
+                               continue
+                       }
+                       stats := t.Stats()
+                       done := t.BytesCompleted() * 100 / t.Length()
+                       percColour := Red
+                       if done == 100 {
+                               percColour = Green
+                       }
+                       tx := stats.BytesWrittenData.Int64()
+                       tx += TxStats[t.InfoHash()]
+                       ratio := float64(tx) / float64(t.Length())
+                       TorrentStatsM.RLock()
+                       prev := TorrentStats[t.InfoHash()]
+                       TorrentStatsM.RUnlock()
+                       rxSpeed := stats.BytesReadData.Int64() - prev.BytesReadData.Int64()
+                       txSpeed := stats.BytesWrittenData.Int64() - prev.BytesWrittenData.Int64()
+                       var eta string
+                       if done < 100 && rxSpeed > 0 {
+                               etaRaw := time.Duration((t.Length() - t.BytesCompleted()) / rxSpeed)
+                               etaRaw *= time.Second
+                               eta = etaRaw.String()
+                       }
+                       fmt.Fprintf(fd,
+                               "%s%s%s %s%40s%s %8s %s%3d%%%s %4.1f %s%d%s/%s%d%s %d/%d/%d/%d %s\n",
+                               Blue, t.InfoHash().HexString(), Reset,
+                               Green, shortenName(t.Name()), Reset,
+                               humanize.IBytes(uint64(t.Length())),
+                               percColour, done, Reset,
+                               ratio,
+                               Green, rxSpeed/1024, Reset,
+                               Magenta, txSpeed/1024, Reset,
+                               stats.TotalPeers,
+                               stats.PendingPeers,
+                               stats.ActivePeers,
+                               stats.ConnectedSeeders,
+                               eta,
+                       )
+               }
+               fd.Close()
+               time.Sleep(time.Second)
+       }
+}
+
+func fifoPeerList(t *torrent.Torrent) {
+       pth := path.Join(FIFOsDir, PeersDir, t.InfoHash().HexString())
+       recreateFIFO(pth)
+       for {
+               fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
+               if err != nil {
+                       if os.IsNotExist(err) {
+                               break
+                       }
+                       log.Println("OpenFile:", pth, err)
+                       time.Sleep(time.Second)
+                       continue
+               }
+               pcs := t.PeerConns()
+               sort.Sort(ByPeerID(pcs))
+               for _, pc := range pcs {
+                       completed := pc.CompletedString()
+                       completedColour := Red
+                       cols := strings.Split(completed, "/")
+                       if cols[0] == cols[1] {
+                               completedColour = Green
+                       }
+                       stats := pc.Peer.Stats()
+                       fmt.Fprintf(fd,
+                               "%s%s%s %10s %s%11s%s %s%d%s/%s%d%s %s / %s | %s%s%s %q\n",
+                               Blue, hex.EncodeToString(pc.PeerID[:]), Reset,
+                               pc.StatusFlags(),
+                               completedColour, pc.CompletedString(), Reset,
+                               Green, int(pc.DownloadRate()/1024), Reset,
+                               Magenta, int(pc.UploadRate()/1024), Reset,
+                               humanize.IBytes(uint64(stats.BytesReadData.Int64())),
+                               humanize.IBytes(uint64(stats.BytesWrittenData.Int64())),
+                               Green, pc.RemoteAddr, Reset,
+                               pc.PeerClientName,
+                       )
+               }
+               fd.Close()
+               time.Sleep(time.Second)
+       }
+}
+
+func fifoFileList(t *torrent.Torrent) {
+       pth := path.Join(FIFOsDir, FilesDir, t.InfoHash().HexString())
+       recreateFIFO(pth)
+       for {
+               fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
+               if err != nil {
+                       if os.IsNotExist(err) {
+                               break
+                       }
+                       log.Println("OpenFile:", pth, err)
+                       time.Sleep(time.Second)
+                       continue
+               }
+               for n, f := range t.Files() {
+                       var done int64
+                       if f.Length() > 0 {
+                               done = (f.BytesCompleted() * 100) / f.Length()
+                       }
+                       percColour := Green
+                       if done < 100 {
+                               percColour = Red
+                       }
+                       fmt.Fprintf(fd,
+                               "%5d %8s %3d%% | %s%s%s\n",
+                               n, humanize.IBytes(uint64(f.Length())), done,
+                               percColour, f.Path(), Reset,
+                       )
+               }
+               fd.Close()
+               time.Sleep(time.Second)
+       }
+}
+
+func fifoDHTList(c *torrent.Client) {
+       pth := path.Join(FIFOsDir, "dht")
+       recreateFIFO(pth)
+       for {
+               fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
+               if err != nil {
+                       if os.IsNotExist(err) {
+                               break
+                       }
+                       log.Println("OpenFile:", pth, err)
+                       time.Sleep(time.Second)
+                       continue
+               }
+               for _, s := range c.DhtServers() {
+                       stats := s.Stats().(dht.ServerStats)
+                       fmt.Fprintf(
+                               fd, "%s%s%s all:%d good:%d await:%d succ:%d bad:%d\n",
+                               Green, s.Addr().String(), Reset,
+                               stats.Nodes,
+                               stats.GoodNodes,
+                               stats.OutstandingTransactions,
+                               stats.SuccessfulOutboundAnnouncePeerQueries,
+                               stats.BadNodes,
+                       )
+               }
+               fd.Close()
+               time.Sleep(time.Second)
+       }
+}
diff --git a/cmd/btrtrc/main.go b/cmd/btrtrc/main.go
new file mode 100644 (file)
index 0000000..cf932b6
--- /dev/null
@@ -0,0 +1,88 @@
+package main
+
+import (
+       "bytes"
+       "flag"
+       "log"
+       "net"
+       "os"
+       "os/signal"
+       "path"
+       "strings"
+       "syscall"
+
+       "github.com/anacrolix/dht/v2"
+       analog "github.com/anacrolix/log"
+
+       "github.com/anacrolix/torrent"
+       "github.com/anacrolix/torrent/storage"
+)
+
+const TorrentExt = ".torrent"
+
+func saveTorrent(t *torrent.Torrent) error {
+       pth := t.InfoHash().HexString() + TorrentExt
+       if _, err := os.Stat(pth); err == nil {
+               return nil
+       }
+       var b bytes.Buffer
+       t.Metainfo().Write(&b)
+       if err := os.WriteFile(pth, b.Bytes(), 0666); err != nil {
+               return err
+       }
+       return os.Symlink(pth, t.Name()+TorrentExt)
+}
+
+func main() {
+       log.SetFlags(log.Ldate | log.Ltime)
+       dhtBoot := flag.String("dht", "dht.cypherpunks.ru:8991", "Comma-separated list of DHT bootstrap nodes")
+       addr := flag.String("bind", "[::]:6881", "Address to bind to")
+       pub4 := flag.String("4", "", "External IPv4 address")
+       pub6 := flag.String("6", "", "External IPv6 address")
+       debug := flag.Bool("debug", false, "Enable debug messages")
+       noDHT := flag.Bool("nodht", false, "Disable DHT")
+       verify := flag.Bool("verify", false, "Force verification of provided torrents")
+       flag.Parse()
+
+       dht.DefaultGlobalBootstrapHostPorts = strings.Split(*dhtBoot, ",")
+       cc := torrent.NewDefaultClientConfig()
+       cc.Debug = *debug
+       cc.DisableAcceptRateLimiting = true
+       cc.NoDefaultPortForwarding = true
+       cc.DisableWebtorrent = true
+       cc.Logger = analog.Default.WithNames("main", "client")
+       cc.DefaultStorage = storage.NewFileWithCompletion(".", NewBoltPieceCompletion())
+       if *verify {
+               doVerify(cc, flag.Args())
+               return
+       }
+       cc.Seed = true
+       cc.PublicIp4 = net.ParseIP(*pub4).To4()
+       cc.PublicIp6 = net.ParseIP(*pub6).To16()
+       cc.NoDHT = *noDHT
+       cc.SetListenAddr(*addr)
+       client, err := torrent.NewClient(cc)
+       if err != nil {
+               log.Fatalln("torrent.NewClient:", err)
+       }
+       defer client.Close()
+
+       needsShutdown := make(chan os.Signal)
+       signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
+       go func() {
+               <-needsShutdown
+               txStatsDumpAll(client)
+               client.Close()
+       }()
+
+       os.MkdirAll(path.Join(FIFOsDir, PeersDir), 0777)
+       os.MkdirAll(path.Join(FIFOsDir, FilesDir), 0777)
+       log.Println("started", client.PublicIPs())
+       go overallStatus(client)
+       go fifoList(client)
+       go fifoDHTList(client)
+       go fifoAdd(client)
+       go fifoDel(client)
+       go txStatsDumper(client)
+       <-client.Closed()
+}
diff --git a/cmd/btrtrc/pc.go b/cmd/btrtrc/pc.go
new file mode 100644 (file)
index 0000000..12911fa
--- /dev/null
@@ -0,0 +1,111 @@
+package main
+
+// based on storage/bolt-piece-completion.go
+
+import (
+       "encoding/binary"
+       "sync"
+       "time"
+
+       "go.etcd.io/bbolt"
+
+       "github.com/anacrolix/torrent/metainfo"
+       "github.com/anacrolix/torrent/storage"
+)
+
+const (
+       boltDbCompleteValue   = "c"
+       boltDbIncompleteValue = "i"
+)
+
+var completionBucketKey = []byte("completion")
+
+type boltPieceCompletion struct {
+       dbs     map[metainfo.Hash]*bbolt.DB
+       dbsLock sync.Mutex
+}
+
+var _ storage.PieceCompletion = (*boltPieceCompletion)(nil)
+
+func NewBoltPieceCompletion() *boltPieceCompletion {
+       return &boltPieceCompletion{dbs: make(map[metainfo.Hash]*bbolt.DB)}
+}
+
+func (me *boltPieceCompletion) Get(pk metainfo.PieceKey) (cn storage.Completion, err error) {
+       me.dbsLock.Lock()
+       db, ok := me.dbs[pk.InfoHash]
+       if !ok {
+               db, err = bbolt.Open(
+                       pk.InfoHash.HexString()+".bolt",
+                       0o666,
+                       &bbolt.Options{Timeout: time.Second},
+               )
+               if err != nil {
+                       me.dbsLock.Unlock()
+                       return
+               }
+               db.NoSync = true
+               me.dbs[pk.InfoHash] = db
+       }
+       me.dbsLock.Unlock()
+       err = 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
+       }
+       me.dbsLock.Lock()
+       db := me.dbs[pk.InfoHash]
+       me.dbsLock.Unlock()
+       return 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 {
+       me.dbsLock.Lock()
+       for _, db := range me.dbs {
+               db.Close()
+       }
+       me.dbsLock.Unlock()
+       return nil
+}
diff --git a/cmd/btrtrc/sort.go b/cmd/btrtrc/sort.go
new file mode 100644 (file)
index 0000000..cfb6478
--- /dev/null
@@ -0,0 +1,35 @@
+package main
+
+import (
+       "encoding/hex"
+
+       "github.com/anacrolix/torrent"
+)
+
+type ByInfoHash []*torrent.Torrent
+
+func (a ByInfoHash) Len() int {
+       return len(a)
+}
+
+func (a ByInfoHash) Swap(i, j int) {
+       a[i], a[j] = a[j], a[i]
+}
+
+func (a ByInfoHash) Less(i, j int) bool {
+       return a[i].InfoHash().HexString() < a[j].InfoHash().HexString()
+}
+
+type ByPeerID []*torrent.PeerConn
+
+func (a ByPeerID) Len() int {
+       return len(a)
+}
+
+func (a ByPeerID) Swap(i, j int) {
+       a[i], a[j] = a[j], a[i]
+}
+
+func (a ByPeerID) Less(i, j int) bool {
+       return hex.EncodeToString(a[i].PeerID[:]) < hex.EncodeToString(a[j].PeerID[:])
+}
diff --git a/cmd/btrtrc/status.go b/cmd/btrtrc/status.go
new file mode 100644 (file)
index 0000000..2ffc530
--- /dev/null
@@ -0,0 +1,40 @@
+package main
+
+import (
+       "log"
+       "time"
+
+       "github.com/anacrolix/torrent"
+       "github.com/dustin/go-humanize"
+)
+
+func overallStatus(c *torrent.Client) {
+       var prev torrent.ConnStats
+       for range time.Tick(time.Second) {
+               stats := c.ConnStats()
+               var peers int
+               for _, t := range c.Torrents() {
+                       if t.Info() == nil {
+                               continue
+                       }
+                       tStats := t.Stats()
+                       TorrentStatsM.Lock()
+                       TorrentStats[t.InfoHash()] = tStats
+                       TorrentStatsM.Unlock()
+                       peers += tStats.ActivePeers
+               }
+               log.Printf(
+                       "%s / %s | %d | %s%s%s / %s%s%s",
+                       humanize.IBytes(uint64(stats.BytesRead.Int64())),
+                       humanize.IBytes(uint64(stats.BytesWritten.Int64())),
+                       peers,
+                       Green,
+                       humanize.IBytes(uint64(stats.BytesRead.Int64()-prev.BytesRead.Int64())),
+                       Reset,
+                       Magenta,
+                       humanize.IBytes(uint64(stats.BytesWritten.Int64()-prev.BytesWritten.Int64())),
+                       Reset,
+               )
+               prev = stats
+       }
+}
diff --git a/cmd/btrtrc/txstats.go b/cmd/btrtrc/txstats.go
new file mode 100644 (file)
index 0000000..2249e56
--- /dev/null
@@ -0,0 +1,67 @@
+package main
+
+import (
+       "fmt"
+       "log"
+       "os"
+       "strconv"
+       "sync"
+       "time"
+
+       "github.com/anacrolix/torrent"
+       "github.com/anacrolix/torrent/metainfo"
+)
+
+const TxExt = ".tx"
+
+var (
+       TxStats  = map[metainfo.Hash]int64{}
+       TxStatsM sync.Mutex
+)
+
+func txStatsLoad(h metainfo.Hash) {
+       pth := h.HexString() + TxExt
+       data, err := os.ReadFile(pth)
+       if err != nil {
+               return
+       }
+       v, err := strconv.ParseInt(string(data[:len(data)-1]), 10, 64)
+       if err != nil {
+               log.Println("ParseInt:", pth, err)
+               return
+       }
+       TxStatsM.Lock()
+       TxStats[h] = v
+       TxStatsM.Unlock()
+}
+
+func txStatsDel(h metainfo.Hash) {
+       TxStatsM.Lock()
+       delete(TxStats, h)
+       TxStatsM.Unlock()
+}
+
+func txStatsDump(t *torrent.Torrent) {
+       stats := t.Stats()
+       TxStatsM.Lock()
+       s := stats.BytesWrittenData.Int64() + TxStats[t.InfoHash()]
+       pth := t.InfoHash().HexString() + TxExt
+       if err := os.WriteFile(pth, []byte(fmt.Sprintf("%d\n", s)), 0666); err != nil {
+               log.Println("WriteFile:", pth, err)
+       }
+       TxStatsM.Unlock()
+}
+
+func txStatsDumpAll(c *torrent.Client) {
+       for _, t := range c.Torrents() {
+               if t.Info() != nil {
+                       txStatsDump(t)
+               }
+       }
+}
+
+func txStatsDumper(c *torrent.Client) {
+       for range time.Tick(10 * time.Second) {
+               txStatsDumpAll(c)
+       }
+}
diff --git a/cmd/btrtrc/verify.go b/cmd/btrtrc/verify.go
new file mode 100644 (file)
index 0000000..b2a8e6f
--- /dev/null
@@ -0,0 +1,54 @@
+package main
+
+import (
+       "fmt"
+       "log"
+
+       "github.com/anacrolix/torrent"
+       "github.com/anacrolix/torrent/metainfo"
+)
+
+func doVerify(cc *torrent.ClientConfig, pths []string) {
+       cc.DisableTrackers = true
+       cc.NoDHT = true
+       cc.NoUpload = true
+       cc.DisableUTP = true
+       cc.DisableTCP = true
+       cc.DisableIPv6 = true
+       cc.DisableIPv4 = true
+       cc.AcceptPeerConnections = false
+       cc.DisableWebseeds = true
+       client, err := torrent.NewClient(cc)
+       if err != nil {
+               log.Fatalln("torrent.NewClient:", err)
+       }
+       for _, pth := range pths {
+               metaInfo, err := metainfo.LoadFromFile(pth)
+               if err != nil {
+                       log.Fatalln("LoadFromFile:", err)
+               }
+               t, err := client.AddTorrent(metaInfo)
+               if err != nil {
+                       log.Fatalln("AddTorrent:", err)
+               }
+               <-t.GotInfo()
+               if err = saveTorrent(t); err != nil {
+                       log.Println("saveTorrent:", err)
+               }
+               go func() {
+                       sub := t.SubscribePieceStateChanges()
+                       defer sub.Close()
+                       var last int
+                       for piece := range sub.Values {
+                               if piece.Hashing && piece.Index > last {
+                                       fmt.Printf("\r%s: %d / %d", pth, piece.Index, t.NumPieces())
+                                       last = piece.Index
+                               }
+                       }
+               }()
+               t.VerifyData()
+               fmt.Printf("\n")
+       }
+       client.Close()
+       <-client.Closed()
+}
diff --git a/go.mod b/go.mod
index 4b16520d5a3e162ca6a21dfb7bba216ef3913fcb..d7de21618b506126925ab67eff2bdae173ff54b7 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -44,6 +44,7 @@ require (
        go.opentelemetry.io/otel v1.8.0
        go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.8.0
        go.opentelemetry.io/otel/sdk v1.8.0
+       golang.org/x/term v0.2.0
        golang.org/x/time v0.0.0-20220609170525-579cf78fd858
 )
 
@@ -80,7 +81,7 @@ require (
        golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
        golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
        golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
-       golang.org/x/sys v0.0.0-20220702020025-31831981b65f // indirect
+       golang.org/x/sys v0.2.0 // indirect
        golang.org/x/text v0.3.7 // indirect
        golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
        google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect
diff --git a/go.sum b/go.sum
index 4734a5f2c12a0edbe32b897629e35ca40d24a0d3..2f825cdb4d55cc69675511bb46b73eb55b03295b 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -601,10 +601,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY=
-golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
index 3c515aaf989d130e88670a89215abb14b1364e53..322e7625ba37b59884dbc7051a23b897893dc1e4 100644 (file)
@@ -283,6 +283,10 @@ func (cn *Peer) completedString() string {
        return fmt.Sprintf("%d/%d", have, cn.bestPeerNumPieces())
 }
 
+func (cn *Peer) CompletedString() string {
+       return cn.completedString()
+}
+
 func (cn *PeerConn) onGotInfo(info *metainfo.Info) {
        cn.setNumPieces(info.NumPieces())
 }
@@ -348,6 +352,10 @@ func (cn *Peer) statusFlags() (ret string) {
        return
 }
 
+func (cn *Peer) StatusFlags() string {
+       return cn.statusFlags()
+}
+
 func (cn *Peer) downloadRate() float64 {
        num := cn._stats.BytesReadUsefulData.Int64()
        if num == 0 {
@@ -363,6 +371,17 @@ func (cn *Peer) DownloadRate() float64 {
        return cn.downloadRate()
 }
 
+func (cn *Peer) UploadRate() float64 {
+       cn.locker().RLock()
+       defer cn.locker().RUnlock()
+       num := cn._stats.BytesWrittenData.Int64()
+       if num == 0 {
+               return 0
+       }
+       return float64(num) / time.Now().Sub(cn.completedHandshake).Seconds()
+}
+
+
 func (cn *Peer) iterContiguousPieceRequests(f func(piece pieceIndex, count int)) {
        var last Option[pieceIndex]
        var count int
@@ -1836,6 +1855,10 @@ func (cn *Peer) stats() *ConnStats {
        return &cn._stats
 }
 
+func (cn *Peer) Stats() *ConnStats {
+       return cn.stats()
+}
+
 func (p *Peer) TryAsPeerConn() (*PeerConn, bool) {
        pc, ok := p.peerImpl.(*PeerConn)
        return pc, ok