Skip to content

Commit

Permalink
add cli command to download torrent from magnet
Browse files Browse the repository at this point in the history
  • Loading branch information
cenkalti committed Oct 31, 2021
1 parent 7ad0281 commit 63088a2
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 42 deletions.
108 changes: 76 additions & 32 deletions internal/resumer/boltdbresumer/boltdbresumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,43 @@ import (

// Keys for the persisten storage.
var Keys = struct {
InfoHash []byte
Port []byte
Name []byte
Trackers []byte
URLList []byte
FixedPeers []byte
Dest []byte
Info []byte
Bitfield []byte
AddedAt []byte
BytesDownloaded []byte
BytesUploaded []byte
BytesWasted []byte
SeededFor []byte
Started []byte
CompleteCmdRun []byte
InfoHash []byte
Port []byte
Name []byte
Trackers []byte
URLList []byte
FixedPeers []byte
Dest []byte
Info []byte
Bitfield []byte
AddedAt []byte
BytesDownloaded []byte
BytesUploaded []byte
BytesWasted []byte
SeededFor []byte
Started []byte
StopAfterDownload []byte
StopAfterMetadata []byte
CompleteCmdRun []byte
}{
InfoHash: []byte("info_hash"),
Port: []byte("port"),
Name: []byte("name"),
Trackers: []byte("trackers"),
URLList: []byte("url_list"),
FixedPeers: []byte("fixed_peers"),
Dest: []byte("dest"),
Info: []byte("info"),
Bitfield: []byte("bitfield"),
AddedAt: []byte("added_at"),
BytesDownloaded: []byte("bytes_downloaded"),
BytesUploaded: []byte("bytes_uploaded"),
BytesWasted: []byte("bytes_wasted"),
SeededFor: []byte("seeded_for"),
Started: []byte("started"),
CompleteCmdRun: []byte("complete_cmd_run"),
InfoHash: []byte("info_hash"),
Port: []byte("port"),
Name: []byte("name"),
Trackers: []byte("trackers"),
URLList: []byte("url_list"),
FixedPeers: []byte("fixed_peers"),
Dest: []byte("dest"),
Info: []byte("info"),
Bitfield: []byte("bitfield"),
AddedAt: []byte("added_at"),
BytesDownloaded: []byte("bytes_downloaded"),
BytesUploaded: []byte("bytes_uploaded"),
BytesWasted: []byte("bytes_wasted"),
SeededFor: []byte("seeded_for"),
Started: []byte("started"),
StopAfterDownload: []byte("stop_after_download"),
StopAfterMetadata: []byte("stop_after_metadata"),
CompleteCmdRun: []byte("complete_cmd_run"),
}

// Resumer contains methods for saving/loading resume information of a torrent to a BoltDB database.
Expand Down Expand Up @@ -103,6 +107,8 @@ func (r *Resumer) Write(torrentID string, spec *Spec) error {
_ = b.Put(Keys.BytesWasted, []byte(strconv.FormatInt(spec.BytesWasted, 10)))
_ = b.Put(Keys.SeededFor, []byte(spec.SeededFor.String()))
_ = b.Put(Keys.Started, []byte(strconv.FormatBool(spec.Started)))
_ = b.Put(Keys.StopAfterDownload, []byte(strconv.FormatBool(spec.StopAfterDownload)))
_ = b.Put(Keys.StopAfterMetadata, []byte(strconv.FormatBool(spec.StopAfterMetadata)))
_ = b.Put(Keys.CompleteCmdRun, []byte(strconv.FormatBool(spec.CompleteCmdRun)))
return nil
})
Expand Down Expand Up @@ -141,6 +147,28 @@ func (r *Resumer) WriteStarted(torrentID string, value bool) error {
})
}

// WriteStopAfterDownload writes the start status of a torrent.
func (r *Resumer) WriteStopAfterDownload(torrentID string, value bool) error {
return r.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket(r.bucket).Bucket([]byte(torrentID))
if b == nil {
return nil
}
return b.Put(Keys.StopAfterDownload, []byte(strconv.FormatBool(value)))
})
}

// WriteStopAfterMetadata writes the start status of a torrent.
func (r *Resumer) WriteStopAfterMetadata(torrentID string, value bool) error {
return r.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket(r.bucket).Bucket([]byte(torrentID))
if b == nil {
return nil
}
return b.Put(Keys.StopAfterMetadata, []byte(strconv.FormatBool(value)))
})
}

// WriteCompleteCmdRun writes the start status of a torrent.
func (r *Resumer) WriteCompleteCmdRun(torrentID string) error {
return r.db.Update(func(tx *bbolt.Tx) error {
Expand Down Expand Up @@ -289,6 +317,22 @@ func (r *Resumer) Read(torrentID string) (spec *Spec, err error) {
}
}

value = b.Get(Keys.StopAfterDownload)
if value != nil {
spec.StopAfterDownload, err = strconv.ParseBool(string(value))
if err != nil {
return err
}
}

value = b.Get(Keys.StopAfterMetadata)
if value != nil {
spec.StopAfterMetadata, err = strconv.ParseBool(string(value))
if err != nil {
return err
}
}

value = b.Get(Keys.CompleteCmdRun)
if value != nil {
spec.CompleteCmdRun, err = strconv.ParseBool(string(value))
Expand Down
4 changes: 4 additions & 0 deletions internal/resumer/boltdbresumer/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Spec struct {
SeededFor time.Duration
Started bool
StopAfterDownload bool
StopAfterMetadata bool
CompleteCmdRun bool
}

Expand All @@ -38,6 +39,7 @@ type jsonSpec struct {
BytesWasted int64
Started bool
StopAfterDownload bool
StopAfterMetadata bool
CompleteCmdRun bool

// JSON unsafe types
Expand All @@ -61,6 +63,7 @@ func (s Spec) MarshalJSON() ([]byte, error) {
BytesWasted: s.BytesWasted,
Started: s.Started,
StopAfterDownload: s.StopAfterDownload,
StopAfterMetadata: s.StopAfterMetadata,
CompleteCmdRun: s.CompleteCmdRun,

InfoHash: base64.StdEncoding.EncodeToString(s.InfoHash),
Expand Down Expand Up @@ -102,6 +105,7 @@ func (s *Spec) UnmarshalJSON(b []byte) error {
s.BytesWasted = j.BytesWasted
s.Started = j.Started
s.StopAfterDownload = j.StopAfterDownload
s.StopAfterMetadata = j.StopAfterMetadata
s.CompleteCmdRun = j.CompleteCmdRun
return nil
}
1 change: 1 addition & 0 deletions internal/rpctypes/rpctypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ type AddTorrentOptions struct {
ID string
Stopped bool
StopAfterDownload bool
StopAfterMetadata bool
}

// AddTorrentRequest contains request arguments for Session.AddTorrent method.
Expand Down
117 changes: 115 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,32 @@ func main() {
},
Action: handleDownload,
},
{
Name: "magnet-to-torrent",
Usage: "download torrent from magnet link",
Flags: []cli.Flag{
cli.StringFlag{
Name: "config,c",
Usage: "read config from `FILE`",
Value: "~/rain/config.yaml",
},
cli.StringFlag{
Name: "magnet,m",
Usage: "magnet link",
Required: true,
},
cli.StringFlag{
Name: "output,o",
Usage: "output file",
},
cli.DurationFlag{
Name: "timeout,t",
Usage: "command fails if torrent cannot be downloaded after duration",
Value: time.Minute,
},
},
Action: handleMagnetToTorrent,
},
{
Name: "server",
Usage: "run rpc server and torrent client",
Expand Down Expand Up @@ -160,6 +186,14 @@ func main() {
Name: "stopped",
Usage: "do not start torrent automatically",
},
cli.BoolFlag{
Name: "stop-after-download",
Usage: "stop the torrent after download is finished",
},
cli.BoolFlag{
Name: "stop-after-metadata",
Usage: "stop the torrent after metadata download is finished",
},
cli.StringFlag{
Name: "id",
Usage: "if id is not given, a unique id is automatically generated",
Expand Down Expand Up @@ -733,6 +767,83 @@ func handleDownload(c *cli.Context) error {
}
}

func handleMagnetToTorrent(c *cli.Context) error {
arg := c.String("magnet")
output := c.String("output")
timeout := c.Duration("timeout")
cfg, err := prepareConfig(c)
if err != nil {
return err
}
dbFile, err := ioutil.TempFile("", "")
if err != nil {
return err
}
dbFileName := dbFile.Name()
defer os.Remove(dbFileName)
err = dbFile.Close()
if err != nil {
return err
}
cfg.Database = dbFileName
ses, err := torrent.NewSession(cfg)
if err != nil {
return err
}
defer ses.Close()
opt := &torrent.AddTorrentOptions{
StopAfterMetadata: true,
}
t, err := ses.AddURI(arg, opt)
if err != nil {
return err
}
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
timeoutC := time.After(timeout)
metadataC := t.NotifyMetadata()
for {
select {
case s := <-ch:
log.Noticef("received %s, stopping server", s)
err = t.Stop()
if err != nil {
return err
}
case <-time.After(timeout):
stats := t.Stats()
log.Infof("Status: %s, Peers: %d\n", stats.Status.String(), stats.Peers.Total)
case <-metadataC:
name := output
if name == "" {
name = t.Name() + ".torrent"
}
data, err := t.Torrent()
if err != nil {
return err
}
f, err := os.Create(name)
if err != nil {
return err
}
_, err = f.Write(data)
if err != nil {
return err
}
err = f.Close()
if err != nil {
return err
}
fmt.Println(name)
return nil
case <-timeoutC:
return fmt.Errorf("metadata cannot be downloaded in %s, try increasing timeout", timeout.String())
case err = <-t.NotifyStop():
return err
}
}
}

func handleBeforeClient(c *cli.Context) error {
clt = rainrpc.NewClient(c.String("url"))
clt.SetTimeout(c.Duration("timeout"))
Expand Down Expand Up @@ -772,8 +883,10 @@ func handleAdd(c *cli.Context) error {
var marshalErr error
arg := c.String("torrent")
addOpt := &rainrpc.AddTorrentOptions{
Stopped: c.Bool("stopped"),
ID: c.String("id"),
Stopped: c.Bool("stopped"),
StopAfterDownload: c.Bool("stop-after-download"),
StopAfterMetadata: c.Bool("stop-after-metadata"),
ID: c.String("id"),
}
if isURI(arg) {
resp, err := clt.AddURI(arg, addOpt)
Expand Down
3 changes: 3 additions & 0 deletions rainrpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type AddTorrentOptions struct {
ID string
Stopped bool
StopAfterDownload bool
StopAfterMetadata bool
}

// AddTorrent adds a new torrent by reading .torrent file.
Expand All @@ -75,6 +76,7 @@ func (c *Client) AddTorrent(f io.Reader, options *AddTorrentOptions) (*rpctypes.
args.AddTorrentOptions.ID = options.ID
args.AddTorrentOptions.Stopped = options.Stopped
args.AddTorrentOptions.StopAfterDownload = options.StopAfterDownload
args.AddTorrentOptions.StopAfterMetadata = options.StopAfterMetadata
}
var reply rpctypes.AddTorrentResponse
return &reply.Torrent, c.client.Call("Session.AddTorrent", args, &reply)
Expand All @@ -87,6 +89,7 @@ func (c *Client) AddURI(uri string, options *AddTorrentOptions) (*rpctypes.Torre
args.AddTorrentOptions.ID = options.ID
args.AddTorrentOptions.Stopped = options.Stopped
args.AddTorrentOptions.StopAfterDownload = options.StopAfterDownload
args.AddTorrentOptions.StopAfterMetadata = options.StopAfterMetadata
}
var reply rpctypes.AddURIResponse
return &reply.Torrent, c.client.Call("Session.AddURI", args, &reply)
Expand Down
6 changes: 6 additions & 0 deletions torrent/session_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type AddTorrentOptions struct {
Stopped bool
// Stop torrent after all pieces are downloaded.
StopAfterDownload bool
// Stop torrent after metadata is downloaded from magnet links.
StopAfterMetadata bool
}

// AddTorrent adds a new torrent to the session by reading .torrent metainfo from reader.
Expand Down Expand Up @@ -89,6 +91,7 @@ func (s *Session) addTorrentStopped(r io.Reader, opt *AddTorrentOptions) (*Torre
resumer.Stats{},
webseedsource.NewList(mi.URLList),
opt.StopAfterDownload,
opt.StopAfterMetadata,
false, // completeCmdRun
)
if err != nil {
Expand All @@ -109,6 +112,7 @@ func (s *Session) addTorrentStopped(r io.Reader, opt *AddTorrentOptions) (*Torre
Info: mi.Info.Bytes,
AddedAt: t.addedAt,
StopAfterDownload: opt.StopAfterDownload,
StopAfterMetadata: opt.StopAfterMetadata,
}
err = s.resumer.Write(id, rspec)
if err != nil {
Expand Down Expand Up @@ -200,6 +204,7 @@ func (s *Session) addMagnet(link string, opt *AddTorrentOptions) (*Torrent, erro
resumer.Stats{},
nil, // webseedSources
opt.StopAfterDownload,
opt.StopAfterMetadata,
false, // completeCmdRun
)
if err != nil {
Expand All @@ -219,6 +224,7 @@ func (s *Session) addMagnet(link string, opt *AddTorrentOptions) (*Torrent, erro
FixedPeers: ma.Peers,
AddedAt: t.addedAt,
StopAfterDownload: opt.StopAfterDownload,
StopAfterMetadata: opt.StopAfterMetadata,
}
err = s.resumer.Write(id, rspec)
if err != nil {
Expand Down
Loading

0 comments on commit 63088a2

Please sign in to comment.