--- /dev/null
+cmd/btrtrc/README
\ No newline at end of file
--- /dev/null
+btrtrc -- better BitTorrent client
+
+This is a fork of https://github.com/anacrolix/torrent BitTorrent
+library with own cmd/btrtrc client implementation. Comparing to
+cmd/torrent it has much less configuration options, mainly hardcoding
+the most of them. But what advantages does it have, not including its native
+capabilities (IPv6, DHT, μTP, PEX, magnet links, HTTP/UDP trackers)?
+
+* Optimized file-based storage:
+ * linearized I/O operations prevent creation of huge quantity of threads
+ * cached file descriptors save a lot of syscalls
+* Shortened long filenames in file-based storage
+* Ability to specify both IPv4 and IPv6 addresses to announce
+* Ability to specify DHT bootstrap nodes
+* Dynamic addition and removing of the torrents
+* Much richer and nicer coloured status output
+* Ability to calculate seed ratio, by remembering outgoing traffic amount
+* Simpler piece completion database per each torrent in separate files
+* Ability to sequentially create/verify torrent's piece completion database
+
+Look at USAGE file further.
+
+btrtrc is free software: see the file LICENSE in the root of the
+repository for copying conditions.
--- /dev/null
+You have to specify correct -bind, -4 and -6 addresses. If -4/-6 is
+empty, then it won't be announced.
+
+Each second the current time, overall downloaded/uploaded traffic
+amount, number of active peers and current download/upload speed in
+KiB/sec will be shown.
+
+fifos subdirectory will be created with following FIFO files:
+
+* fifos/add -- expects newline delimited paths to .torrent files or
+ magnet: links
+* fifos/del -- expects torrent's info hashes to remove torrents from the
+ client
+* fifos/list -- prints coloured output of all registered torrents in the
+ client. It shows:
+ * info hash (that you can use in all other FIFOs)
+ * name (name of the file or root directory as a rule)
+ * total size
+ * completion percentage
+ * seed ratio
+ * download/upload speeds in KiB/sec
+ * number of: total/pending/active/seeder peers
+ * (optionally) estimated completion time
+* fifos/dht -- prints DHT server statistics
+* fifos/files/HASH -- prints torrent's file list with completion ratio
+* fifos/peers/HASH -- prints torrent's connected peers information:
+ * peer's ID
+ * status flags, that are concatenated string of:
+ i -- am interested
+ c -- am chocking
+ -
+ E -- RC4 encryption
+ e -- header encryption
+ 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
+ v1
+ v2
+ * number of completed pieces
+ * download/upload speeds in KiB/sec during current session
+ * amount of downloaded/uploaded traffic during current session
+ * remote address with port
+ * client's name
+* fifos/top-seed -- list torrents sorted by total transfer data amount
+
+For each torrent, corresponding .torrent file will be created.
+Additional symbolic link with torrent's name will lead to HASH.torrent.
+HASH.bf file is piece completion database. HASH.tx contains overall
+outgoing payload traffic amount and it is updated each 10sec.
+
+If you massively add a bunch of unverified torrents, then
+github.com/anacrolix/torrent will deal with them in parallel. That means
+hash verification is done in random order from HDD's point of view, that
+is rather slow. If you are going to seed many torrents, then it is
+highly advisable to pre-verify them in advance, by using -verify option
+with path to all .torrent files.
+
+Example usage for starting torrents seeding:
+
+ $ cat > seedfile <<EOF
+ big_buck_bunny.torrent
+ exodos.torrent
+ [...]
+ EOF
+ $ btrtrc -verify `cat seedfile`
+ $ btrtrc [...]
+ $ cat seedfile > fifos/add
+ $ cat fifos/list
--- /dev/null
+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)
+}
--- /dev/null
+#!/bin/sh -e
+
+[ -n "$1" ]
+for p in 4 6 ; do
+ echo $1 > fifos${p}/del
+ [ -e txs${p}/$1.tx ] && rm -v txs${p}/$1.tx || :
+done
+rm -v $1.bf
--- /dev/null
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/hex"
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/anacrolix/dht/v2"
+ "github.com/anacrolix/torrent"
+ "github.com/anacrolix/torrent/metainfo"
+ "github.com/anacrolix/torrent/storage"
+ "github.com/anacrolix/torrent/types/infohash"
+ "github.com/dustin/go-humanize"
+)
+
+const (
+ MaxListNameWidth = 40
+ PeersDir = "peers"
+ FilesDir = "files"
+)
+
+type TorrentStat struct {
+ stats torrent.ConnStats
+ rxSpeed int64
+ txSpeed int64
+}
+
+var (
+ FIFOsDir = "fifos"
+ TorrentStats = map[metainfo.Hash]TorrentStat{}
+ TorrentStatsM sync.RWMutex
+ Torrents []*torrent.Torrent
+ TorrentsM sync.RWMutex
+)
+
+func recreateFIFO(pth string) {
+ os.Remove(pth)
+ if err := syscall.Mkfifo(pth, 0o666); 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(0o666))
+ if err != nil {
+ log.Println("OpenFile:", pth, err)
+ time.Sleep(time.Second)
+ continue
+ }
+ ts := make([]*torrent.Torrent, 0, len(Torrents))
+ TorrentsM.RLock()
+ for i := range Torrents {
+ t := Torrents[i]
+ if t.Info() == nil {
+ fmt.Fprintf(fd, "%s not ready\n", t.Name())
+ continue
+ }
+ ts = append(ts, t)
+ }
+ TorrentsM.RUnlock()
+ for _, t := range ts {
+ 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()
+ var eta string
+ if done < 100 && prev.rxSpeed > 0 {
+ etaRaw := time.Duration((t.Length() - t.BytesCompleted()) / prev.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, prev.rxSpeed/1024, Reset,
+ Magenta, prev.txSpeed/1024, Reset,
+ stats.TotalPeers,
+ stats.PendingPeers,
+ stats.ActivePeers,
+ stats.ConnectedSeeders,
+ eta,
+ )
+ }
+ fd.Close()
+ time.Sleep(time.Second)
+ }
+}
+
+func mustParseInt(s string) int {
+ i, err := strconv.Atoi(s)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ return i
+}
+
+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(0o666))
+ 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 {
+ cols := strings.Split(pc.CompletedString(), "/")
+ done := (mustParseInt(cols[0]) * 100) / mustParseInt(cols[1])
+ doneColour := Red
+ if done == 100 {
+ doneColour = Green
+ }
+ stats := pc.Peer.Stats()
+ fmt.Fprintf(fd,
+ "%s%s%s %10s %s%3d%%%s %s%d%s/%s%d%s | %s%s%s / %s%s%s | %s%s%s %q\n",
+ Blue, hex.EncodeToString(pc.PeerID[:]), Reset,
+ pc.StatusFlags(),
+ doneColour, done, Reset,
+ Green, int(pc.DownloadRate()/1024), Reset,
+ Magenta, int(pc.UploadRate()/1024), Reset,
+ Green, humanize.IBytes(uint64(stats.BytesReadData.Int64())), Reset,
+ Magenta, humanize.IBytes(uint64(stats.BytesWrittenData.Int64())), Reset,
+ 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(0o666))
+ 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(0o666))
+ 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)
+ }
+}
+
+type topTorrent struct {
+ name string
+ ratio float64
+ tx int64
+ infoHash metainfo.Hash
+}
+
+func fifoTopSeed(c *torrent.Client) {
+ pth := path.Join(FIFOsDir, "top-seed")
+ recreateFIFO(pth)
+ for {
+ fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0o666))
+ if err != nil {
+ log.Println("OpenFile:", pth, err)
+ time.Sleep(time.Second)
+ continue
+ }
+ var ts []*topTorrent
+ for _, t := range c.Torrents() {
+ if t.Info() == nil {
+ continue
+ }
+ stats := t.Stats()
+ top := topTorrent{
+ infoHash: t.InfoHash(),
+ name: t.Name(),
+ tx: stats.BytesWrittenData.Int64() + TxStats[t.InfoHash()],
+ }
+ top.ratio = float64(top.tx) / float64(t.Length())
+ ts = append(ts, &top)
+ }
+ sort.Sort(ByTxTraffic(ts))
+ for _, t := range ts {
+ fmt.Fprintf(fd,
+ "%s%s%s %s%40s%s %s%4.1f%s %s\n",
+ Blue, t.infoHash.HexString(), Reset,
+ Green, shortenName(t.name), Reset,
+ Magenta, t.ratio, Reset,
+ humanize.IBytes(uint64(t.tx)),
+ )
+ }
+ fd.Close()
+ time.Sleep(time.Second)
+ }
+}
+
+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(0o666))
+ 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 saveTorrent(t *torrent.Torrent) error {
+ pth := storage.PathShortener(t.Name()) + TorrentExt
+ if _, err := os.Stat(pth); err == nil {
+ return nil
+ }
+ var b bytes.Buffer
+ metainfo := t.Metainfo()
+ metainfo.Write(&b)
+ return os.WriteFile(pth, b.Bytes(), 0o666)
+}
+
+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:]))
+ }
+ TorrentsM.Lock()
+ for _, _t := range Torrents {
+ if _t.InfoHash().HexString() == t.InfoHash().HexString() {
+ goto OldOne
+ }
+ }
+ Torrents = append(Torrents, t)
+ OldOne:
+ TorrentsM.Unlock()
+ 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)
+ TorrentsM.Lock()
+ for n, t := range Torrents {
+ if t.InfoHash().HexString() == i.HexString() {
+ Torrents = append(Torrents[:n], Torrents[n+1:]...)
+ break
+ }
+ }
+ TorrentsM.Unlock()
+ t, ok := c.Torrent(i)
+ if !ok {
+ log.Println("no such 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)
+ }
+}
+
+func fifosPrepare() {
+ os.MkdirAll(path.Join(FIFOsDir, PeersDir), 0o777)
+ os.MkdirAll(path.Join(FIFOsDir, FilesDir), 0o777)
+}
+
+func fifosCleanup() {
+ os.Remove(path.Join(FIFOsDir, "list"))
+ os.Remove(path.Join(FIFOsDir, "dht"))
+ os.Remove(path.Join(FIFOsDir, "add"))
+ os.Remove(path.Join(FIFOsDir, "del"))
+ os.Remove(path.Join(FIFOsDir, "top-seed"))
+ os.RemoveAll(path.Join(FIFOsDir, PeersDir))
+ os.RemoveAll(path.Join(FIFOsDir, FilesDir))
+}
--- /dev/null
+package main
+
+import (
+ "flag"
+ "log"
+ "net"
+ "os"
+ "os/signal"
+ "strings"
+ "sync"
+ "syscall"
+
+ "github.com/anacrolix/dht/v2"
+ analog "github.com/anacrolix/log"
+ "golang.org/x/time/rate"
+
+ "github.com/anacrolix/torrent"
+ "github.com/anacrolix/torrent/storage"
+)
+
+const (
+ TorrentExt = ".torrent"
+ UserAgent = "btrtrc/0.2.0"
+)
+
+var (
+ Cancel = make(chan struct{})
+ Jobs sync.WaitGroup
+)
+
+func main() {
+ log.SetFlags(log.Ldate | log.Ltime)
+ fifosDir := flag.String("fifos", "fifos", "Path to fifos/")
+ txsDir := flag.String("txs", "txs", "Path to txs/")
+ dhtBoot := flag.String("dht", "dht.cypherpunks.su: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")
+ rxRate := flag.Int("rx-rate", 0, "Download rate, piece bytes/sec")
+ txRate := flag.Int("rt-rate", 0, "Upload rate, bytes/sec")
+ verify := flag.Bool("verify", false, "Force verification of provided torrents")
+ flag.Parse()
+
+ FIFOsDir = *fifosDir
+ TxsDir = *txsDir
+ os.MkdirAll(TxsDir, 0o777)
+ 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.HTTPUserAgent = UserAgent
+ cc.ExtendedHandshakeClientVersion = UserAgent
+ cc.DefaultStorage = storage.NewFileWithCompletion(".", NewBFPieceCompletion())
+ if *verify {
+ doVerify(cc, flag.Args())
+ return
+ }
+ cc.Seed = true
+ if *pub4 == "" {
+ cc.DisableIPv4 = true
+ } else {
+ cc.PublicIp4 = net.ParseIP(*pub4).To4()
+ }
+ if *pub6 == "" {
+ cc.DisableIPv6 = true
+ } else {
+ cc.PublicIp6 = net.ParseIP(*pub6).To16()
+ }
+ if *txRate != 0 {
+ cc.UploadRateLimiter = rate.NewLimiter(rate.Limit(*txRate), 256<<10)
+ }
+ if *rxRate != 0 {
+ cc.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*rxRate), 1<<16)
+ }
+ 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, 1)
+ signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
+ go func() {
+ <-needsShutdown
+ close(Cancel)
+ client.Close()
+ }()
+
+ fifosPrepare()
+ log.Println("started", client.PublicIPs())
+ Jobs.Add(1)
+ go overallStatus(client)
+ go fifoList(client)
+ go fifoTopSeed(client)
+ go fifoDHTList(client)
+ go fifoAdd(client)
+ go fifoDel(client)
+ Jobs.Add(1)
+ go txStatsDumper(client)
+ <-client.Closed()
+ Jobs.Wait()
+ fifosCleanup()
+ log.Println("finished")
+}
--- /dev/null
+package main
+
+import (
+ "os"
+ "sync"
+
+ "github.com/anacrolix/torrent/metainfo"
+ "github.com/anacrolix/torrent/storage"
+)
+
+type BFPieceCompletion struct {
+ sync.Mutex
+}
+
+func NewBFPieceCompletion() *BFPieceCompletion {
+ return &BFPieceCompletion{}
+}
+
+func (self *BFPieceCompletion) Get(pk metainfo.PieceKey) (c storage.Completion, rerr error) {
+ self.Lock()
+ defer self.Unlock()
+ fd, err := os.OpenFile(pk.InfoHash.HexString()+".bf", os.O_RDWR|os.O_CREATE, 0o666)
+ if err != nil {
+ rerr = err
+ return
+ }
+ defer fd.Close()
+ b := []byte{0}
+ _, err = fd.ReadAt(b, int64(pk.Index))
+ if err != nil {
+ return
+ }
+ c.Ok = true
+ switch string(b) {
+ case "0":
+ c.Complete = false
+ case "1":
+ c.Complete = true
+ default:
+ c.Ok = false
+ }
+ return
+}
+
+func (self *BFPieceCompletion) Set(pk metainfo.PieceKey, complete bool) error {
+ if c, err := self.Get(pk); err == nil && c.Ok && c.Complete == complete {
+ return nil
+ }
+ self.Lock()
+ defer self.Unlock()
+ fd, err := os.OpenFile(pk.InfoHash.HexString()+".bf", os.O_RDWR|os.O_CREATE, 0o666)
+ if err != nil {
+ return err
+ }
+ defer fd.Close()
+ b := []byte{'1'}
+ if !complete {
+ b[0] = '0'
+ }
+ if _, err = fd.WriteAt(b, int64(pk.Index)); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (self *BFPieceCompletion) Close() error {
+ return nil
+}
--- /dev/null
+#!/bin/sh -e
+# Feed seed-file if daemon is restarted
+
+[ -n "$1" ]
+[ -n "$2" ]
+fifos=$1
+seed=$2
+while : ; do
+ cur=`stat -f %m $fifos/add || :`
+ if [ "$cur" != "$prev" ] ; then
+ date
+ grep -v "^#" $seed > $fifos/add
+ prev=`stat -f %m $fifos/add || :`
+ fi
+ sleep 60
+done
--- /dev/null
+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 ByTxTraffic []*topTorrent
+
+func (a ByTxTraffic) Len() int {
+ return len(a)
+}
+
+func (a ByTxTraffic) Swap(i, j int) {
+ a[i], a[j] = a[j], a[i]
+}
+
+func (a ByTxTraffic) Less(i, j int) bool {
+ if a[i].tx == a[j].tx {
+ return a[i].infoHash.HexString() < a[j].infoHash.HexString()
+ }
+ return a[i].tx < a[j].tx
+}
+
+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[:])
+}
--- /dev/null
+package main
+
+import (
+ "log"
+ "time"
+
+ "github.com/anacrolix/torrent"
+ "github.com/dustin/go-humanize"
+)
+
+func overallStatus(c *torrent.Client) {
+ tick := time.Tick(time.Second)
+ var prev torrent.ConnStats
+ for {
+ select {
+ case <-Cancel:
+ Jobs.Done()
+ return
+ case <-tick:
+ }
+ stats := c.ConnStats()
+ var peers int
+ for _, t := range c.Torrents() {
+ if t.Info() == nil {
+ continue
+ }
+ tStats := t.Stats()
+ cur := tStats.Copy()
+ TorrentStatsM.Lock()
+ prev := TorrentStats[t.InfoHash()].stats
+ TorrentStats[t.InfoHash()] = TorrentStat{
+ stats: cur,
+ rxSpeed: cur.BytesReadData.Int64() - prev.BytesReadData.Int64(),
+ txSpeed: cur.BytesWrittenData.Int64() - prev.BytesWrittenData.Int64(),
+ }
+ TorrentStatsM.Unlock()
+ peers += tStats.ActivePeers
+ }
+ log.Printf(
+ "%s / %s | %d | %s%d%s / %s%d%s",
+ humanize.IBytes(uint64(stats.BytesRead.Int64())),
+ humanize.IBytes(uint64(stats.BytesWritten.Int64())),
+ peers,
+ Green, (stats.BytesRead.Int64()-prev.BytesRead.Int64())/1024, Reset,
+ Magenta, (stats.BytesWritten.Int64()-prev.BytesWritten.Int64())/1024, Reset,
+ )
+ prev = stats
+ }
+}
--- /dev/null
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/anacrolix/torrent"
+ "github.com/anacrolix/torrent/metainfo"
+)
+
+const TxExt = ".tx"
+
+var (
+ TxStats = map[metainfo.Hash]int64{}
+ TxStatsM sync.Mutex
+ TxsDir string
+)
+
+func txStatsLoad(h metainfo.Hash) {
+ pth := path.Join(TxsDir, h.HexString()+TxExt)
+ data, err := os.ReadFile(pth)
+ if err != nil {
+ return
+ }
+ if len(data) == 0 {
+ 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 := path.Join(TxsDir, t.InfoHash().HexString()+TxExt)
+ if err := os.WriteFile(pth, []byte(fmt.Sprintf("%d\n", s)), 0o666); 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) {
+ tick := time.Tick(10 * time.Second)
+ for {
+ txStatsDumpAll(c)
+ select {
+ case <-Cancel:
+ Jobs.Done()
+ return
+ case <-tick:
+ }
+ }
+}
--- /dev/null
+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()
+}
--- /dev/null
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path"
+
+ "github.com/anacrolix/torrent/bencode"
+ "github.com/anacrolix/torrent/metainfo"
+)
+
+func walk(root string) ([]string, error) {
+ d, err := os.Open(root)
+ if err != nil {
+ return nil, err
+ }
+ paths := make([]string, 0)
+ for {
+ entries, err := d.ReadDir(1 << 10)
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ return paths, err
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ add, err := walk(path.Join(root, entry.Name()))
+ if err != nil {
+ return paths, err
+ }
+ paths = append(paths, add...)
+ } else {
+ paths = append(paths, path.Join(root, entry.Name()))
+ }
+ }
+ }
+ return paths, nil
+}
+
+func main() {
+ showExcess := flag.Bool("excess", false, "Show excess files")
+ flag.Parse()
+ data, err := os.ReadFile(flag.Arg(0))
+ if err != nil {
+ log.Fatalln(err)
+ }
+ var minfo metainfo.MetaInfo
+ err = bencode.Unmarshal(data, &minfo)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ var info metainfo.Info
+ err = bencode.Unmarshal(minfo.InfoBytes, &info)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ seen := make(map[string]struct{}, len(info.Files)+1)
+ seen[info.Name] = struct{}{}
+ for _, f := range info.Files {
+ var p string
+ if len(f.PathUtf8) > 0 {
+ p = path.Join(f.PathUtf8...)
+ } else {
+ p = path.Join(f.Path...)
+ }
+ p = path.Join(info.Name, p)
+ seen[p] = struct{}{}
+ if !*showExcess {
+ fmt.Print(p, "\t")
+ s, err := os.Stat(p)
+ if err != nil {
+ fmt.Print("!exists\n")
+ continue
+ }
+ if s.Size() != f.Length {
+ fmt.Print("!size\n")
+ continue
+ }
+ fmt.Print("ok\n")
+ }
+ }
+ if !*showExcess {
+ return
+ }
+ paths, err := walk(info.Name)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ for _, f := range paths {
+ if _, exists := seen[f]; !exists {
+ fmt.Print(f, "\n")
+ }
+ }
+}
return
}
+func (cn *Peer) StatusFlags() string {
+ return cn.statusFlags()
+}
+
func (cn *Peer) downloadRate() float64 {
num := cn._stats.BytesReadUsefulData.Int64()
if num == 0 {
return p.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
}
}
+func (cn *Peer) Stats() *ConnStats {
+ return cn.stats()
+}
+
+func (cn *Peer) CompletedString() string {
+ return cn.completedString()
+}
+
func (cn *Peer) readBytes(n int64) {
cn.allStats(add(n, func(cs *ConnStats) *Count { return &cs.BytesRead }))
}
Length: fs.p.Length(),
}, func(i int, extent segments.Extent) bool {
file := fs.files[i]
- s, err := os.Stat(file.path)
+ s, err := os.Stat(PathShortener(file.path))
if err != nil || s.Size() < extent.Start+extent.Length {
verified = false
return false
"log/slog"
"os"
"path/filepath"
+ "sync"
+ "time"
"github.com/anacrolix/log"
"github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent/segments"
)
+const fdCacheAliveTime = 10
+
+type fdCacheEntry struct {
+ last int64
+ fd *os.File
+ sync.Mutex
+}
+
+var (
+ fdRCache = map[string]*fdCacheEntry{}
+ fdRCacheM sync.Mutex
+ fdWCache = map[string]*fdCacheEntry{}
+ fdWCacheM sync.Mutex
+ fdMkdirAllCache = map[string]struct{}{}
+ fdCacheCleanerM sync.Once
+)
+
+func fdCacheCleaner() {
+ cleaner := func(c map[string]*fdCacheEntry, m *sync.Mutex) {
+ now := time.Now().Unix()
+ m.Lock()
+ for k, v := range c {
+ if now-v.last > fdCacheAliveTime {
+ go func() {
+ v.Lock()
+ v.fd.Close()
+ v.Unlock()
+ }()
+ }
+ delete(c, k)
+ }
+ m.Unlock()
+ }
+ for range time.Tick(fdCacheAliveTime * time.Second) {
+ cleaner(fdRCache, &fdRCacheM)
+ cleaner(fdWCache, &fdWCacheM)
+ }
+}
+
// File-based storage for torrents, that isn't yet bound to a particular torrent.
type fileClientImpl struct {
opts NewFileClientOpts
if opts.PieceCompletion == nil {
opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
}
+ fdCacheCleanerM.Do(func() { go fdCacheCleaner() })
return fileClientImpl{opts}
}
length: fileInfo.Length,
}
if f.length == 0 {
- err = CreateNativeZeroLengthFile(f.path)
+ err = CreateNativeZeroLengthFile(PathShortener(f.path))
if err != nil {
err = fmt.Errorf("creating zero length file: %w", err)
return
// 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
+ fdRCacheM.Lock()
+ pth := PathShortener(file.path)
+ centry := fdRCache[pth]
+ if centry == nil {
+ var fd *os.File
+ fd, err = os.Open(pth)
+ if os.IsNotExist(err) {
+ // File missing is treated the same as a short file.
+ err = io.EOF
+ }
+ if err != nil {
+ fdRCacheM.Unlock()
+ return
+ }
+ centry = &fdCacheEntry{fd: fd}
+ fdRCache[pth] = centry
}
- defer f.Close()
+ fdRCacheM.Unlock()
// 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
- }
- }
+ centry.Lock()
+ centry.last = time.Now().Unix()
+ n, err = centry.fd.ReadAt(b, off)
+ centry.Unlock()
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
+ name := PathShortener(fst.fts.files[i].path)
+ _, ok := fdMkdirAllCache[filepath.Dir(name)]
+ if !ok {
+ os.MkdirAll(filepath.Dir(name), 0o777)
+ fdMkdirAllCache[filepath.Dir(name)] = struct{}{}
}
+ fdWCacheM.Lock()
+ centry := fdWCache[name]
+ if centry == nil {
+ var fd *os.File
+ fd, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
+ if err != nil {
+ fdWCacheM.Unlock()
+ return false
+ }
+ centry = &fdCacheEntry{fd: fd}
+ fdWCache[name] = centry
+ }
+ fdWCacheM.Unlock()
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()
+ centry.Lock()
+ centry.last = time.Now().Unix()
+ n1, err = centry.fd.WriteAt(p[:e.Length], e.Start)
+ centry.Unlock()
n += n1
p = p[n1:]
- if err == nil {
- err = closeErr
- }
if err == nil && int64(n1) != e.Length {
err = io.ErrShortWrite
}
--- /dev/null
+package storage
+
+import (
+ "crypto/sha1"
+ "encoding/hex"
+ "path"
+ "strings"
+ "unicode/utf8"
+)
+
+const MaxFilenameLen = 200
+
+func PathShortener(pth string) string {
+ parts := strings.Split(pth, "/")
+ for i, part := range parts {
+ if len(part) <= MaxFilenameLen {
+ continue
+ }
+ n := 0
+ var short []rune
+ for (n < len(part)) && (len(string(short)) <= MaxFilenameLen) {
+ r, w := utf8.DecodeRuneInString(part[n:])
+ n += w
+ short = append(short, r)
+ }
+ h := sha1.Sum([]byte(part))
+ parts[i] = string(short[:len(short)-1]) + "-" + hex.EncodeToString(h[:])
+ }
+ return path.Join(parts...)
+}