diff --git a/internal/rpctypes/rpctypes.go b/internal/rpctypes/rpctypes.go index 83cfee52..159edeff 100644 --- a/internal/rpctypes/rpctypes.go +++ b/internal/rpctypes/rpctypes.go @@ -50,6 +50,18 @@ type Tracker struct { NextAnnounce Time } +// File inside a Torrent. +type File struct { + Path string + Length int64 +} + +// FileStats contains statistics about a File in a Torrent. +type FileStats struct { + File File + BytesCompleted int64 +} + // SessionStats contains statistics about a Session. type SessionStats struct { Uptime int @@ -273,6 +285,26 @@ type GetTorrentWebseedsResponse struct { Webseeds []Webseed } +// GetTorrentFilesRequest contains request arguments for Session.GetTorrentFiles method. +type GetTorrentFilesRequest struct { + ID string +} + +// GetTorrentFilesResponse contains response arguments for Session.GetTorrentFiles method. +type GetTorrentFilesResponse struct { + Files []File +} + +// GetTorrentFileStatsRequest contains request arguments for Session.GetTorrentFileStats method. +type GetTorrentFileStatsRequest struct { + ID string +} + +// GetTorrentFileStatsResponse contains response arguments for Session.GetTorrentFileStats method. +type GetTorrentFileStatsResponse struct { + FileStats []FileStats +} + // StartTorrentRequest contains request arguments for Session.StartTorrent method. type StartTorrentRequest struct { ID string diff --git a/main.go b/main.go index 6977a750..85830728 100644 --- a/main.go +++ b/main.go @@ -270,6 +270,30 @@ func main() { }, }, }, + { + Name: "files", + Usage: "get file list of torrent", + Category: "Getters", + Action: handleFiles, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Required: true, + }, + }, + }, + { + Name: "file-stats", + Usage: "get stats of files in torrent", + Category: "Getters", + Action: handleFileStats, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "id", + Required: true, + }, + }, + }, { Name: "peers", Usage: "get peers of torrent", @@ -1007,6 +1031,34 @@ func handleWebseeds(c *cli.Context) error { return nil } +func handleFiles(c *cli.Context) error { + resp, err := clt.GetTorrentFiles(c.String("id")) + if err != nil { + return err + } + b, err := prettyjson.Marshal(resp) + if err != nil { + return err + } + _, _ = os.Stdout.Write(b) + _, _ = os.Stdout.WriteString("\n") + return nil +} + +func handleFileStats(c *cli.Context) error { + resp, err := clt.GetTorrentFileStats(c.String("id")) + if err != nil { + return err + } + b, err := prettyjson.Marshal(resp) + if err != nil { + return err + } + _, _ = os.Stdout.Write(b) + _, _ = os.Stdout.WriteString("\n") + return nil +} + func handlePeers(c *cli.Context) error { resp, err := clt.GetTorrentPeers(c.String("id")) if err != nil { diff --git a/rainrpc/client.go b/rainrpc/client.go index a9a342b7..4dbeb2d1 100644 --- a/rainrpc/client.go +++ b/rainrpc/client.go @@ -163,6 +163,18 @@ func (c *Client) GetTorrentWebseeds(id string) ([]rpctypes.Webseed, error) { return reply.Webseeds, c.client.Call("Session.GetTorrentWebseeds", args, &reply) } +func (c *Client) GetTorrentFiles(id string) ([]rpctypes.File, error) { + args := rpctypes.GetTorrentFilesRequest{ID: id} + var reply rpctypes.GetTorrentFilesResponse + return reply.Files, c.client.Call("Session.GetTorrentFiles", args, &reply) +} + +func (c *Client) GetTorrentFileStats(id string) ([]rpctypes.FileStats, error) { + args := rpctypes.GetTorrentFileStatsRequest{ID: id} + var reply rpctypes.GetTorrentFileStatsResponse + return reply.FileStats, c.client.Call("Session.GetTorrentFileStats", args, &reply) +} + // StartTorrent starts the torrent. func (c *Client) StartTorrent(id string) error { args := rpctypes.StartTorrentRequest{ID: id} diff --git a/torrent/session_rpc_handler.go b/torrent/session_rpc_handler.go index ea49a914..0d1f6ba5 100644 --- a/torrent/session_rpc_handler.go +++ b/torrent/session_rpc_handler.go @@ -366,6 +366,47 @@ func (h *rpcHandler) GetTorrentWebseeds(args *rpctypes.GetTorrentWebseedsRequest return nil } +func (h *rpcHandler) GetTorrentFiles(args *rpctypes.GetTorrentFilesRequest, reply *rpctypes.GetTorrentFilesResponse) error { + t := h.session.GetTorrent(args.ID) + if t == nil { + return errTorrentNotFound + } + files, err := t.Files() + if err != nil { + return err + } + reply.Files = make([]rpctypes.File, len(files)) + for i, f := range files { + reply.Files[i] = rpctypes.File{ + Path: f.Path(), + Length: f.Length(), + } + } + return nil +} + +func (h *rpcHandler) GetTorrentFileStats(args *rpctypes.GetTorrentFileStatsRequest, reply *rpctypes.GetTorrentFileStatsResponse) error { + t := h.session.GetTorrent(args.ID) + if t == nil { + return errTorrentNotFound + } + stats, err := t.FileStats() + if err != nil { + return err + } + reply.FileStats = make([]rpctypes.FileStats, len(stats)) + for i, s := range stats { + reply.FileStats[i] = rpctypes.FileStats{ + File: rpctypes.File{ + Path: s.File.Path(), + Length: s.File.Length(), + }, + BytesCompleted: s.BytesCompleted, + } + } + return nil +} + func (h *rpcHandler) StartTorrent(args *rpctypes.StartTorrentRequest, reply *rpctypes.StartTorrentResponse) error { t := h.session.GetTorrent(args.ID) if t == nil { diff --git a/torrent/session_torrent.go b/torrent/session_torrent.go index d58067f5..6a344914 100644 --- a/torrent/session_torrent.go +++ b/torrent/session_torrent.go @@ -48,18 +48,16 @@ func (t *Torrent) RootDirectory() string { return t.torrent.RootDirectory() } -// The files in the torrent. -// The paths of the files are relative to the root directory. -func (t *Torrent) FilePaths() ([]string, error) { - return t.torrent.FilePaths() -} - -// The files in the torrent with completion info. An error is returned -// when metainfo isn't ready. +// Files in the torrent. An error is returned when metainfo isn't ready. func (t *Torrent) Files() ([]File, error) { return t.torrent.Files() } +// FileStats returns statistics about each file in the torrent. An error is returned when torrent is not running. +func (t *Torrent) FileStats() ([]FileStats, error) { + return t.torrent.FileStats() +} + // InfoHash returns the hash of the info dictionary of torrent file. // Two different torrents may have the same info hash. func (t *Torrent) InfoHash() InfoHash { diff --git a/torrent/torrent.go b/torrent/torrent.go index c873b027..d74f3f74 100644 --- a/torrent/torrent.go +++ b/torrent/torrent.go @@ -418,25 +418,33 @@ func (t *torrent) RootDirectory() string { return t.storage.RootDir() } -func (t *torrent) FilePaths() ([]string, error) { +func (t *torrent) Files() ([]File, error) { if t.info == nil { return nil, errors.New("torrent metadata not ready") } - - var filePaths []string + files := make([]File, 0, len(t.info.Files)) for _, f := range t.info.Files { if !f.Padding { - filePaths = append(filePaths, f.Path) + files = append(files, File{ + path: f.Path, + length: f.Length, + }) } } - return filePaths, nil + return files, nil } -func (t *torrent) Files() ([]File, error) { - if t.info == nil || len(t.pieces) == 0 { +func (t *torrent) FileStats() ([]FileStats, error) { + if len(t.pieces) == 0 { return nil, errors.New("torrent not running so file stats unavailable") } + files, err := t.Files() + if err != nil { + return nil, err + } + + // calculate bytes completed for each file fileComp := make(map[string]int64) for _, p := range t.pieces { if p.Done { @@ -448,39 +456,34 @@ func (t *torrent) Files() ([]File, error) { } } - var files []File - for _, f := range t.info.Files { - if !f.Padding { - files = append(files, - File{ - path: f.Path, - stats: FileStats{ - BytesTotal: f.Length, - BytesCompleted: fileComp[f.Path], - }, - }) - } + stats := make([]FileStats, 0, len(files)) + for _, f := range files { + stats = append(stats, + FileStats{ + File: f, + BytesCompleted: fileComp[f.path], + }) } - return files, nil + return stats, nil } type FileStats struct { - BytesTotal int64 + File BytesCompleted int64 } type File struct { - path string - stats FileStats + path string + length int64 } func (f File) Path() string { return f.path } -func (f File) Stats() FileStats { - return f.stats +func (f File) Length() int64 { + return f.length } func (t *torrent) announceDHT() {