]> Sergey Matveev's repositories - btrtrc.git/blob - cmd/torrent/main.go
cmd/torrent: Ensure progress is emitted at consistent intervals
[btrtrc.git] / cmd / torrent / main.go
1 // Downloads torrents from the command-line.
2 package main
3
4 import (
5         "expvar"
6         "fmt"
7         "io"
8         stdLog "log"
9         "net"
10         "net/http"
11         "os"
12         "os/signal"
13         "strings"
14         "syscall"
15         "time"
16
17         "github.com/alexflint/go-arg"
18         "github.com/anacrolix/missinggo"
19         "github.com/anacrolix/torrent/bencode"
20         "github.com/davecgh/go-spew/spew"
21         "github.com/dustin/go-humanize"
22         "golang.org/x/xerrors"
23
24         "github.com/anacrolix/log"
25
26         "github.com/anacrolix/envpprof"
27         "github.com/anacrolix/tagflag"
28         "golang.org/x/time/rate"
29
30         "github.com/anacrolix/torrent"
31         "github.com/anacrolix/torrent/iplist"
32         "github.com/anacrolix/torrent/metainfo"
33         "github.com/anacrolix/torrent/storage"
34 )
35
36 func torrentBar(t *torrent.Torrent, pieceStates bool) {
37         go func() {
38                 start := time.Now()
39                 if t.Info() == nil {
40                         fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name())
41                         <-t.GotInfo()
42                 }
43                 var lastLine string
44                 for range time.Tick(time.Second) {
45                         var completedPieces, partialPieces int
46                         psrs := t.PieceStateRuns()
47                         for _, r := range psrs {
48                                 if r.Complete {
49                                         completedPieces += r.Length
50                                 }
51                                 if r.Partial {
52                                         partialPieces += r.Length
53                                 }
54                         }
55                         line := fmt.Sprintf(
56                                 "%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial)\n",
57                                 time.Since(start),
58                                 t.Name(),
59                                 humanize.Bytes(uint64(t.BytesCompleted())),
60                                 humanize.Bytes(uint64(t.Length())),
61                                 completedPieces,
62                                 t.NumPieces(),
63                                 partialPieces,
64                         )
65                         if line != lastLine {
66                                 lastLine = line
67                                 os.Stdout.WriteString(line)
68                         }
69                         if pieceStates {
70                                 fmt.Println(psrs)
71                         }
72                 }
73         }()
74 }
75
76 type stringAddr string
77
78 func (stringAddr) Network() string   { return "" }
79 func (me stringAddr) String() string { return string(me) }
80
81 func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
82         for _, ta := range flags.TestPeer {
83                 ret = append(ret, torrent.PeerInfo{
84                         Addr: stringAddr(ta),
85                 })
86         }
87         return
88 }
89
90 func addTorrents(client *torrent.Client) error {
91         testPeers := resolveTestPeers(flags.TestPeer)
92         for _, arg := range flags.Torrent {
93                 t, err := func() (*torrent.Torrent, error) {
94                         if strings.HasPrefix(arg, "magnet:") {
95                                 t, err := client.AddMagnet(arg)
96                                 if err != nil {
97                                         return nil, xerrors.Errorf("error adding magnet: %w", err)
98                                 }
99                                 return t, nil
100                         } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
101                                 response, err := http.Get(arg)
102                                 if err != nil {
103                                         return nil, xerrors.Errorf("Error downloading torrent file: %s", err)
104                                 }
105
106                                 metaInfo, err := metainfo.Load(response.Body)
107                                 defer response.Body.Close()
108                                 if err != nil {
109                                         return nil, xerrors.Errorf("error loading torrent file %q: %s\n", arg, err)
110                                 }
111                                 t, err := client.AddTorrent(metaInfo)
112                                 if err != nil {
113                                         return nil, xerrors.Errorf("adding torrent: %w", err)
114                                 }
115                                 return t, nil
116                         } else if strings.HasPrefix(arg, "infohash:") {
117                                 t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:")))
118                                 return t, nil
119                         } else {
120                                 metaInfo, err := metainfo.LoadFromFile(arg)
121                                 if err != nil {
122                                         return nil, xerrors.Errorf("error loading torrent file %q: %s\n", arg, err)
123                                 }
124                                 t, err := client.AddTorrent(metaInfo)
125                                 if err != nil {
126                                         return nil, xerrors.Errorf("adding torrent: %w", err)
127                                 }
128                                 return t, nil
129                         }
130                 }()
131                 if err != nil {
132                         return xerrors.Errorf("adding torrent for %q: %w", arg, err)
133                 }
134                 if flags.Progress {
135                         torrentBar(t, flags.PieceStates)
136                 }
137                 t.AddPeers(testPeers)
138                 go func() {
139                         <-t.GotInfo()
140                         if len(flags.File) == 0 {
141                                 t.DownloadAll()
142                         } else {
143                                 for _, f := range t.Files() {
144                                         for _, fileArg := range flags.File {
145                                                 if f.DisplayPath() == fileArg {
146                                                         f.Download()
147                                                 }
148                                         }
149                                 }
150                         }
151                 }()
152         }
153         return nil
154 }
155
156 var flags struct {
157         Debug bool
158         Stats *bool
159
160         *DownloadCmd      `arg:"subcommand:download"`
161         *ListFilesCmd     `arg:"subcommand:list-files"`
162         *SpewBencodingCmd `arg:"subcommand:spew-bencoding"`
163         *AnnounceCmd      `arg:"subcommand:announce"`
164 }
165
166 type SpewBencodingCmd struct{}
167
168 type DownloadCmd struct {
169         Mmap            bool           `help:"memory-map torrent data"`
170         TestPeer        []string       `help:"addresses of some starting peers"`
171         Seed            bool           `help:"seed after download is complete"`
172         Addr            string         `help:"network listen addr"`
173         UploadRate      *tagflag.Bytes `help:"max piece bytes to send per second"`
174         DownloadRate    *tagflag.Bytes `help:"max bytes per second down from peers"`
175         PackedBlocklist string
176         PublicIP        net.IP
177         Progress        bool `default:"true"`
178         PieceStates     bool
179         Quiet           bool `help:"discard client logging"`
180         Dht             bool `default:"true"`
181
182         TcpPeers        bool `default:"true"`
183         UtpPeers        bool `default:"true"`
184         Webtorrent      bool `default:"true"`
185         DisableWebseeds bool
186
187         Ipv4 bool `default:"true"`
188         Ipv6 bool `default:"true"`
189         Pex  bool `default:"true"`
190
191         File    []string
192         Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"`
193 }
194
195 type ListFilesCmd struct {
196         TorrentPath string `arg:"positional"`
197 }
198
199 func stdoutAndStderrAreSameFile() bool {
200         fi1, _ := os.Stdout.Stat()
201         fi2, _ := os.Stderr.Stat()
202         return os.SameFile(fi1, fi2)
203 }
204
205 func statsEnabled() bool {
206         if flags.Stats == nil {
207                 return flags.Debug
208         }
209         return *flags.Stats
210 }
211
212 func exitSignalHandlers(notify *missinggo.SynchronizedEvent) {
213         c := make(chan os.Signal, 1)
214         signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
215         for {
216                 log.Printf("close signal received: %+v", <-c)
217                 notify.Set()
218         }
219 }
220
221 func main() {
222         if err := mainErr(); err != nil {
223                 log.Printf("error in main: %v", err)
224                 os.Exit(1)
225         }
226 }
227
228 func mainErr() error {
229         stdLog.SetFlags(stdLog.Flags() | stdLog.Lshortfile)
230         p := arg.MustParse(&flags)
231         switch {
232         case flags.AnnounceCmd != nil:
233                 return announceErr()
234         //case :
235         //      return announceErr(flags.Args, parser)
236         case flags.DownloadCmd != nil:
237                 return downloadErr()
238         case flags.ListFilesCmd != nil:
239                 mi, err := metainfo.LoadFromFile(flags.ListFilesCmd.TorrentPath)
240                 if err != nil {
241                         return fmt.Errorf("loading from file %q: %v", flags.ListFilesCmd.TorrentPath, err)
242                 }
243                 info, err := mi.UnmarshalInfo()
244                 if err != nil {
245                         return fmt.Errorf("unmarshalling info from metainfo at %q: %v", flags.ListFilesCmd.TorrentPath, err)
246                 }
247                 for _, f := range info.UpvertedFiles() {
248                         fmt.Println(f.DisplayPath(&info))
249                 }
250                 return nil
251         case flags.SpewBencodingCmd != nil:
252                 d := bencode.NewDecoder(os.Stdin)
253                 for i := 0; ; i++ {
254                         var v interface{}
255                         err := d.Decode(&v)
256                         if err == io.EOF {
257                                 break
258                         }
259                         if err != nil {
260                                 return fmt.Errorf("decoding message index %d: %w", i, err)
261                         }
262                         spew.Dump(v)
263                 }
264                 return nil
265         default:
266                 p.Fail(fmt.Sprintf("unexpected subcommand: %v", p.Subcommand()))
267                 panic("unreachable")
268         }
269 }
270
271 func downloadErr() error {
272         defer envpprof.Stop()
273         clientConfig := torrent.NewDefaultClientConfig()
274         clientConfig.DisableWebseeds = flags.DisableWebseeds
275         clientConfig.DisableTCP = !flags.TcpPeers
276         clientConfig.DisableUTP = !flags.UtpPeers
277         clientConfig.DisableIPv4 = !flags.Ipv4
278         clientConfig.DisableIPv6 = !flags.Ipv6
279         clientConfig.DisableAcceptRateLimiting = true
280         clientConfig.NoDHT = !flags.Dht
281         clientConfig.Debug = flags.Debug
282         clientConfig.Seed = flags.Seed
283         clientConfig.PublicIp4 = flags.PublicIP
284         clientConfig.PublicIp6 = flags.PublicIP
285         clientConfig.DisablePEX = !flags.Pex
286         clientConfig.DisableWebtorrent = !flags.Webtorrent
287         if flags.PackedBlocklist != "" {
288                 blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist)
289                 if err != nil {
290                         return xerrors.Errorf("loading blocklist: %v", err)
291                 }
292                 defer blocklist.Close()
293                 clientConfig.IPBlocklist = blocklist
294         }
295         if flags.Mmap {
296                 clientConfig.DefaultStorage = storage.NewMMap("")
297         }
298         if flags.Addr != "" {
299                 clientConfig.SetListenAddr(flags.Addr)
300         }
301         if flags.UploadRate != nil {
302                 clientConfig.UploadRateLimiter = rate.NewLimiter(rate.Limit(*flags.UploadRate), 256<<10)
303         }
304         if flags.DownloadRate != nil {
305                 clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 1<<20)
306         }
307         if flags.Quiet {
308                 clientConfig.Logger = log.Discard
309         }
310
311         var stop missinggo.SynchronizedEvent
312         defer func() {
313                 stop.Set()
314         }()
315
316         client, err := torrent.NewClient(clientConfig)
317         if err != nil {
318                 return xerrors.Errorf("creating client: %v", err)
319         }
320         defer client.Close()
321         go exitSignalHandlers(&stop)
322         go func() {
323                 <-stop.C()
324                 client.Close()
325         }()
326
327         // Write status on the root path on the default HTTP muxer. This will be bound to localhost
328         // somewhere if GOPPROF is set, thanks to the envpprof import.
329         http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
330                 client.WriteStatus(w)
331         })
332         err = addTorrents(client)
333         if err != nil {
334                 return fmt.Errorf("adding torrents: %w", err)
335         }
336         defer outputStats(client)
337         if client.WaitAll() {
338                 log.Print("downloaded ALL the torrents")
339         } else {
340                 return xerrors.New("y u no complete torrents?!")
341         }
342         if flags.Seed {
343                 outputStats(client)
344                 <-stop.C()
345         }
346         return nil
347 }
348
349 func outputStats(cl *torrent.Client) {
350         if !statsEnabled() {
351                 return
352         }
353         expvar.Do(func(kv expvar.KeyValue) {
354                 fmt.Printf("%s: %s\n", kv.Key, kv.Value)
355         })
356         cl.WriteStatus(os.Stdout)
357 }