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