From a8272a66b760a93a222bd07b49c5aa71f0179ed6 Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Sat, 22 Feb 2020 12:18:49 +0800 Subject: [PATCH] Modify: move aria2 into internal packages / migration version check --- bootstrap/init.go | 33 ++ go.mod | 1 - main.go | 26 +- models/download.go | 2 +- models/migration.go | 53 +-- pkg/aria2/aria2.go | 2 +- pkg/aria2/caller.go | 2 +- pkg/aria2/monitor.go | 2 +- pkg/aria2/monitor_test.go | 2 +- pkg/aria2/notification.go | 2 +- pkg/aria2/rpc/README.md | 257 +++++++++++++ pkg/aria2/rpc/call.go | 274 ++++++++++++++ pkg/aria2/rpc/call_test.go | 23 ++ pkg/aria2/rpc/client.go | 656 ++++++++++++++++++++++++++++++++++ pkg/aria2/rpc/client_test.go | 125 +++++++ pkg/aria2/rpc/const.go | 39 ++ pkg/aria2/rpc/json2.go | 116 ++++++ pkg/aria2/rpc/notification.go | 44 +++ pkg/aria2/rpc/proc.go | 42 +++ pkg/aria2/rpc/proto.go | 40 +++ pkg/aria2/rpc/resp.go | 102 ++++++ pkg/conf/conf.go | 32 +- pkg/conf/defaults.go | 2 +- pkg/conf/version.go | 13 +- pkg/filesystem/image.go | 8 +- pkg/serializer/aria2.go | 2 +- routers/controllers/site.go | 30 +- 27 files changed, 1849 insertions(+), 81 deletions(-) create mode 100644 bootstrap/init.go create mode 100644 pkg/aria2/rpc/README.md create mode 100644 pkg/aria2/rpc/call.go create mode 100644 pkg/aria2/rpc/call_test.go create mode 100644 pkg/aria2/rpc/client.go create mode 100644 pkg/aria2/rpc/client_test.go create mode 100644 pkg/aria2/rpc/const.go create mode 100644 pkg/aria2/rpc/json2.go create mode 100644 pkg/aria2/rpc/notification.go create mode 100644 pkg/aria2/rpc/proc.go create mode 100644 pkg/aria2/rpc/proto.go create mode 100644 pkg/aria2/rpc/resp.go diff --git a/bootstrap/init.go b/bootstrap/init.go new file mode 100644 index 0000000..3f63789 --- /dev/null +++ b/bootstrap/init.go @@ -0,0 +1,33 @@ +package bootstrap + +import ( + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/aria2" + "github.com/HFO4/cloudreve/pkg/auth" + "github.com/HFO4/cloudreve/pkg/authn" + "github.com/HFO4/cloudreve/pkg/cache" + "github.com/HFO4/cloudreve/pkg/conf" + "github.com/HFO4/cloudreve/pkg/crontab" + "github.com/HFO4/cloudreve/pkg/email" + "github.com/HFO4/cloudreve/pkg/task" + "github.com/gin-gonic/gin" +) + +// Init 初始化启动 +func Init(path string) { + conf.Init(path) + // Debug 关闭时,切换为生产模式 + if !conf.SystemConfig.Debug { + gin.SetMode(gin.ReleaseMode) + } + cache.Init() + if conf.SystemConfig.Mode == "master" { + model.Init() + authn.Init() + task.Init() + aria2.Init() + email.Init() + crontab.Init() + } + auth.Init() +} diff --git a/go.mod b/go.mod index 84ee37d..80bc9aa 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,6 @@ require ( github.com/stretchr/testify v1.4.0 github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20200120023323-87ff3bc489ac github.com/upyun/go-sdk v2.1.0+incompatible - github.com/zyxar/argo v0.0.0-20190709183644-6096bc0e6414 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 golang.org/x/text v0.3.2 gopkg.in/go-playground/validator.v8 v8.18.2 diff --git a/main.go b/main.go index 9e96230..6cc6920 100644 --- a/main.go +++ b/main.go @@ -1,35 +1,13 @@ package main import ( - "github.com/HFO4/cloudreve/models" - "github.com/HFO4/cloudreve/pkg/aria2" - "github.com/HFO4/cloudreve/pkg/auth" - "github.com/HFO4/cloudreve/pkg/authn" - "github.com/HFO4/cloudreve/pkg/cache" + "github.com/HFO4/cloudreve/bootstrap" "github.com/HFO4/cloudreve/pkg/conf" - "github.com/HFO4/cloudreve/pkg/crontab" - "github.com/HFO4/cloudreve/pkg/email" - "github.com/HFO4/cloudreve/pkg/task" "github.com/HFO4/cloudreve/routers" - "github.com/gin-gonic/gin" ) func init() { - conf.Init("conf/conf.ini") - // Debug 关闭时,切换为生产模式 - if !conf.SystemConfig.Debug { - gin.SetMode(gin.ReleaseMode) - } - cache.Init() - if conf.SystemConfig.Mode == "master" { - model.Init() - authn.Init() - task.Init() - aria2.Init() - email.Init() - crontab.Init() - } - auth.Init() + bootstrap.Init("conf/conf.ini") } func main() { diff --git a/models/download.go b/models/download.go index d32064f..f3b3ea2 100644 --- a/models/download.go +++ b/models/download.go @@ -2,9 +2,9 @@ package model import ( "encoding/json" + "github.com/HFO4/cloudreve/pkg/aria2/rpc" "github.com/HFO4/cloudreve/pkg/util" "github.com/jinzhu/gorm" - "github.com/zyxar/argo/rpc" ) // Download 离线下载队列模型 diff --git a/models/migration.go b/models/migration.go index 87fc173..6052e94 100644 --- a/models/migration.go +++ b/models/migration.go @@ -3,27 +3,26 @@ package model import ( "github.com/HFO4/cloudreve/pkg/conf" "github.com/HFO4/cloudreve/pkg/util" - "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" - "github.com/mcuadros/go-version" - "io/ioutil" ) +// 是否需要迁移 +func needMigration() bool { + var setting Setting + DB.Where("name = ?", "database_version").First(&setting) + return setting.Value != conf.RequiredDBVersion +} + //执行数据迁移 func migration() { - // 检查 version.lock 确认是否需要执行迁移 - // Debug 模式及测试模式下一定会执行迁移 - if !conf.SystemConfig.Debug && gin.Mode() != gin.TestMode { - if util.Exists("version.lock") { - versionLock, _ := ioutil.ReadFile("version.lock") - if version.Compare(string(versionLock), conf.BackendVersion, "=") { - util.Log().Info("后端版本匹配,跳过数据库迁移") - return - } - } + // 确认是否需要执行迁移 + if !needMigration() { + util.Log().Info("数据库版本匹配,跳过数据库迁移") + return + } - util.Log().Info("开始进行数据库自动迁移...") + util.Log().Info("开始进行数据库初始化...") // 自动迁移模式 if conf.DatabaseConfig.Type == "mysql" { @@ -44,13 +43,7 @@ func migration() { // 向设置数据表添加初始设置 addDefaultSettings() - // 迁移完毕后写入版本锁 version.lock - err := conf.WriteVersionLock() - if err != nil { - util.Log().Warning("无法写入版本控制锁 version.lock, %s", err) - } - - util.Log().Info("数据库自动迁移结束") + util.Log().Info("数据库初始化结束") } @@ -80,9 +73,8 @@ func addDefaultPolicy() { func addDefaultSettings() { defaultSettings := []Setting{ - {Name: "siteURL", Value: ``, Type: "basic"}, + {Name: "siteURL", Value: `http://localhost`, Type: "basic"}, {Name: "siteName", Value: `Cloudreve`, Type: "basic"}, - {Name: "siteStatus", Value: `open`, Type: "basic"}, {Name: "register_enabled", Value: `1`, Type: "register"}, {Name: "default_group", Value: `2`, Type: "register"}, {Name: "siteKeywords", Value: `网盘,网盘`, Type: "basic"}, @@ -137,7 +129,7 @@ solid #e9e9e9;"bgcolor="#fff">重设{siteTitle}密码
亲爱的{userName}
请点击下方按钮完成密码重设。如果非你本人操作,请忽略此邮件。
重设密码
感谢您选择{siteTitle}。
`, Type: "mail_template"}, {Name: "pack_data", Value: `[]`, Type: "pack"}, - {Name: "database_version", Value: `6`, Type: "version"}, + {Name: "database_version", Value: `3.0.0-beta1`, Type: "version"}, {Name: "alipay_enabled", Value: `0`, Type: "payment"}, {Name: "payjs_enabled", Value: `0`, Type: "payment"}, {Name: "payjs_id", Value: ``, Type: "payment"}, @@ -174,6 +166,19 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti {Name: "cron_notify_user", Value: "@hourly", Type: "cron"}, {Name: "cron_ban_user", Value: "@hourly", Type: "cron"}, {Name: "authn_enabled", Value: "1", Type: "authn"}, + {Name: "captcha_height", Value: "60", Type: "captcha"}, + {Name: "captcha_width", Value: "240", Type: "captcha"}, + {Name: "captcha_mode", Value: "3", Type: "captcha"}, + {Name: "captcha_ComplexOfNoiseText", Value: "0", Type: "captcha"}, + {Name: "captcha_ComplexOfNoiseDot", Value: "0", Type: "captcha"}, + {Name: "captcha_IsShowHollowLine", Value: "0", Type: "captcha"}, + {Name: "captcha_IsShowNoiseDot", Value: "0", Type: "captcha"}, + {Name: "captcha_IsShowNoiseText", Value: "0", Type: "captcha"}, + {Name: "captcha_IsShowSlimeLine", Value: "0", Type: "captcha"}, + {Name: "captcha_IsShowSineLine", Value: "0", Type: "captcha"}, + {Name: "captcha_CaptchaLen", Value: "6", Type: "captcha"}, + {Name: "thumb_width", Value: "400", Type: "thumb"}, + {Name: "thumb_height", Value: "300", Type: "thumb"}, } for _, value := range defaultSettings { diff --git a/pkg/aria2/aria2.go b/pkg/aria2/aria2.go index e5c7595..d492938 100644 --- a/pkg/aria2/aria2.go +++ b/pkg/aria2/aria2.go @@ -3,9 +3,9 @@ package aria2 import ( "encoding/json" model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/aria2/rpc" "github.com/HFO4/cloudreve/pkg/serializer" "github.com/HFO4/cloudreve/pkg/util" - "github.com/zyxar/argo/rpc" "net/url" ) diff --git a/pkg/aria2/caller.go b/pkg/aria2/caller.go index 70a4ad2..bb068dc 100644 --- a/pkg/aria2/caller.go +++ b/pkg/aria2/caller.go @@ -3,8 +3,8 @@ package aria2 import ( "context" model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/aria2/rpc" "github.com/HFO4/cloudreve/pkg/util" - "github.com/zyxar/argo/rpc" "path/filepath" "strconv" "strings" diff --git a/pkg/aria2/monitor.go b/pkg/aria2/monitor.go index 09eb343..f5ce324 100644 --- a/pkg/aria2/monitor.go +++ b/pkg/aria2/monitor.go @@ -5,12 +5,12 @@ import ( "encoding/json" "errors" model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/aria2/rpc" "github.com/HFO4/cloudreve/pkg/filesystem" "github.com/HFO4/cloudreve/pkg/filesystem/driver/local" "github.com/HFO4/cloudreve/pkg/filesystem/fsctx" "github.com/HFO4/cloudreve/pkg/task" "github.com/HFO4/cloudreve/pkg/util" - "github.com/zyxar/argo/rpc" "os" "path/filepath" "strconv" diff --git a/pkg/aria2/monitor_test.go b/pkg/aria2/monitor_test.go index 3596208..e27eae8 100644 --- a/pkg/aria2/monitor_test.go +++ b/pkg/aria2/monitor_test.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/DATA-DOG/go-sqlmock" model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/aria2/rpc" "github.com/HFO4/cloudreve/pkg/cache" "github.com/HFO4/cloudreve/pkg/filesystem" "github.com/HFO4/cloudreve/pkg/task" @@ -11,7 +12,6 @@ import ( "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" testMock "github.com/stretchr/testify/mock" - "github.com/zyxar/argo/rpc" "testing" "time" ) diff --git a/pkg/aria2/notification.go b/pkg/aria2/notification.go index e265a60..9eb3c52 100644 --- a/pkg/aria2/notification.go +++ b/pkg/aria2/notification.go @@ -1,7 +1,7 @@ package aria2 import ( - "github.com/zyxar/argo/rpc" + "github.com/HFO4/cloudreve/pkg/aria2/rpc" "sync" ) diff --git a/pkg/aria2/rpc/README.md b/pkg/aria2/rpc/README.md new file mode 100644 index 0000000..ba5d2fd --- /dev/null +++ b/pkg/aria2/rpc/README.md @@ -0,0 +1,257 @@ +# PACKAGE DOCUMENTATION + +**package rpc** + + import "github.com/matzoe/argo/rpc" + + + +## FUNCTIONS + +``` +func Call(address, method string, params, reply interface{}) error +``` + +## TYPES + +``` +type Client struct { + // contains filtered or unexported fields +} +``` + +``` +func New(uri string) *Client +``` + +``` +func (id *Client) AddMetalink(uri string, options ...interface{}) (gid string, err error) +``` +`aria2.addMetalink(metalink[, options[, position]])` This method adds Metalink download by uploading ".metalink" file. `metalink` is of type base64 which contains Base64-encoded ".metalink" file. `options` is of type struct and its members are a pair of option name and value. See Options below for more details. If `position` is given as an integer starting from 0, the new download is inserted at `position` in the +waiting queue. If `position` is not given or `position` is larger than the size of the queue, it is appended at the end of the queue. This method returns array of GID of registered download. If `--rpc-save-upload-metadata` is true, the uploaded data is saved as a file named hex string of SHA-1 hash of data plus ".metalink" in the directory specified by `--dir` option. The example of filename is 0a3893293e27ac0490424c06de4d09242215f0a6.metalink. If same file already exists, it is overwritten. If the file cannot be saved successfully or `--rpc-save-upload-metadata` is false, the downloads added by this method are not saved by `--save-session`. + +``` +func (id *Client) AddTorrent(filename string, options ...interface{}) (gid string, err error) +``` +`aria2.addTorrent(torrent[, uris[, options[, position]]])` This method adds BitTorrent download by uploading ".torrent" file. If you want to add BitTorrent Magnet URI, use `aria2.addUri()` method instead. torrent is of type base64 which contains Base64-encoded ".torrent" file. `uris` is of type array and its element is URI which is of type string. `uris` is used for Web-seeding. For single file torrents, URI can be a complete URI pointing to the resource or if URI ends with /, name in torrent file is added. For multi-file torrents, name and path in torrent are added to form a URI for each file. options is of type struct and its members are +a pair of option name and value. See Options below for more details. If `position` is given as an integer starting from 0, the new download is inserted at `position` in the waiting queue. If `position` is not given or `position` is larger than the size of the queue, it is appended at the end of the queue. This method returns GID of registered download. If `--rpc-save-upload-metadata` is true, the uploaded data is saved as a file named hex string of SHA-1 hash of data plus ".torrent" in the +directory specified by `--dir` option. The example of filename is 0a3893293e27ac0490424c06de4d09242215f0a6.torrent. If same file already exists, it is overwritten. If the file cannot be saved successfully or `--rpc-save-upload-metadata` is false, the downloads added by this method are not saved by -`-save-session`. + +``` +func (id *Client) AddUri(uri string, options ...interface{}) (gid string, err error) +``` + +`aria2.addUri(uris[, options[, position]])` This method adds new HTTP(S)/FTP/BitTorrent Magnet URI. `uris` is of type array and its element is URI which is of type string. For BitTorrent Magnet URI, `uris` must have only one element and it should be BitTorrent Magnet URI. URIs in uris must point to the same file. If you mix other URIs which point to another file, aria2 does not complain but download may +fail. `options` is of type struct and its members are a pair of option name and value. See Options below for more details. If `position` is given as an integer starting from 0, the new download is inserted at position in the waiting queue. If `position` is not given or `position` is larger than the size of the queue, it is appended at the end of the queue. This method returns GID of registered download. + +``` +func (id *Client) ChangeGlobalOption(options map[string]interface{}) (g string, err error) +``` + +`aria2.changeGlobalOption(options)` This method changes global options dynamically. `options` is of type struct. The following `options` are available: + + download-result + log + log-level + max-concurrent-downloads + max-download-result + max-overall-download-limit + max-overall-upload-limit + save-cookies + save-session + server-stat-of + +In addition to them, options listed in Input File subsection are available, except for following options: `checksum`, `index-out`, `out`, `pause` and `select-file`. Using `log` option, you can dynamically start logging or change log file. To stop logging, give empty string("") as a parameter value. Note that log file is always opened in append mode. This method returns OK for success. + +``` +func (id *Client) ChangeOption(gid string, options map[string]interface{}) (g string, err error) +``` + +`aria2.changeOption(gid, options)` This method changes options of the download denoted by `gid` dynamically. `gid` is of type string. `options` is of type struct. The following `options` are available for active downloads: + + bt-max-peers + bt-request-peer-speed-limit + bt-remove-unselected-file + force-save + max-download-limit + max-upload-limit + +For waiting or paused downloads, in addition to the above options, options listed in Input File subsection are available, except for following options: dry-run, metalink-base-uri, parameterized-uri, pause, piece-length and rpc-save-upload-metadata option. This method returns OK for success. + +``` +func (id *Client) ChangePosition(gid string, pos int, how string) (p int, err error) +``` + +`aria2.changePosition(gid, pos, how)` This method changes the position of the download denoted by `gid`. `pos` is of type integer. `how` is of type string. If `how` is `POS_SET`, it moves the download to a position relative to the beginning of the queue. If `how` is `POS_CUR`, it moves the download to a position relative to the current position. If `how` is `POS_END`, it moves the download to a position relative to the end of the queue. If the destination position is less than 0 or beyond the end +of the queue, it moves the download to the beginning or the end of the queue respectively. The response is of type integer and it is the destination position. + +``` +func (id *Client) ChangeUri(gid string, fileindex int, delUris []string, addUris []string, position ...int) (p []int, err error) +``` + +`aria2.changeUri(gid, fileIndex, delUris, addUris[, position])` This method removes URIs in `delUris` from and appends URIs in `addUris` to download denoted by gid. `delUris` and `addUris` are list of string. A download can contain multiple files and URIs are attached to each file. `fileIndex` is used to select which file to remove/attach given URIs. `fileIndex` is 1-based. `position` is used to specify where URIs are inserted in the existing waiting URI list. `position` is 0-based. When +`position` is omitted, URIs are appended to the back of the list. This method first execute removal and then addition. `position` is the `position` after URIs are removed, not the `position` when this method is called. When removing URI, if same URIs exist in download, only one of them is removed for each URI in delUris. In other words, there are three URIs http://example.org/aria2 and you want remove them all, you +have to specify (at least) 3 http://example.org/aria2 in delUris. This method returns a list which contains 2 integers. The first integer is the number of URIs deleted. The second integer is the number of URIs added. + +``` +func (id *Client) ForcePause(gid string) (g string, err error) +``` + +`aria2.forcePause(pid)` This method pauses the download denoted by `gid`. This method behaves just like aria2.pause() except that this method pauses download without any action which takes time such as contacting BitTorrent tracker. + +``` +func (id *Client) ForcePauseAll() (g string, err error) +``` + +`aria2.forcePauseAll()` This method is equal to calling `aria2.forcePause()` for every active/waiting download. This methods returns OK for success. + +``` +func (id *Client) ForceRemove(gid string) (g string, err error) +``` + +`aria2.forceRemove(gid)` This method removes the download denoted by `gid`. This method behaves just like aria2.remove() except that this method removes download without any action which takes time such as contacting BitTorrent tracker. + +``` +func (id *Client) ForceShutdown() (g string, err error) +``` + +`aria2.forceShutdown()` This method shutdowns aria2. This method behaves like `aria2.shutdown()` except that any actions which takes time such as contacting BitTorrent tracker are skipped. This method returns OK. + +``` +func (id *Client) GetFiles(gid string) (m map[string]interface{}, err error) +``` + +`aria2.getFiles(gid)` This method returns file list of the download denoted by `gid`. `gid` is of type string. + +``` +func (id *Client) GetGlobalOption() (m map[string]interface{}, err error) +``` + +`aria2.getGlobalOption()` This method returns global options. The response is of type struct. Its key is the name of option. The value type is string. Note that this method does not return options which have no default value and have not been set by the command-line options, configuration files or RPC methods. Because global options are used as a template for the options of newly added download, the response contains +keys returned by `aria2.getOption()` method. + +``` +func (id *Client) GetGlobalStat() (m map[string]interface{}, err error) +``` + +`aria2.getGlobalStat()` This method returns global statistics such as overall download and upload speed. + +``` +func (id *Client) GetOption(gid string) (m map[string]interface{}, err error) +``` + +`aria2.getOption(gid)` This method returns options of the download denoted by `gid`. The response is of type struct. Its key is the name of option. The value type is string. Note that this method does not return options which have no default value and have not been set by the command-line options, configuration files or RPC methods. + +``` +func (id *Client) GetPeers(gid string) (m []map[string]interface{}, err error) +``` + +`aria2.getPeers(gid)` This method returns peer list of the download denoted by `gid`. `gid` is of type string. This method is for BitTorrent only. + +``` +func (id *Client) GetServers(gid string) (m []map[string]interface{}, err error) +``` + +`aria2.getServers(gid)` This method returns currently connected HTTP(S)/FTP servers of the download denoted by `gid`. `gid` is of type string. + +``` +func (id *Client) GetSessionInfo() (m map[string]interface{}, err error) +``` + +`aria2.getSessionInfo()` This method returns session information. + +``` +func (id *Client) GetUris(gid string) (m map[string]interface{}, err error) +``` + +`aria2.getUris(gid)` This method returns URIs used in the download denoted by `gid`. `gid` is of type string. + +``` +func (id *Client) GetVersion() (m map[string]interface{}, err error) +``` + +`aria2.getVersion()` This method returns version of the program and the list of enabled features. + +``` +func (id *Client) Multicall(methods []map[string]interface{}) (r []interface{}, err error) +``` + +`system.multicall(methods)` This method encapsulates multiple method calls in a single request. `methods` is of type array and its element is struct. The struct contains two keys: `methodName` and `params`. `methodName` is the method name to call and `params` is array containing parameters to the method. This method returns array of responses. The element of array will either be a one-item array containing the return value of each method call or struct of fault element if an encapsulated method call fails. + +``` +func (id *Client) Pause(gid string) (g string, err error) +``` + +`aria2.pause(gid)` This method pauses the download denoted by `gid`. `gid` is of type string. The status of paused download becomes paused. If the download is active, the download is placed on the first position of waiting queue. As long as the status is paused, the download is not started. To change status to waiting, use `aria2.unpause()` method. This method returns GID of paused download. + +``` +func (id *Client) PauseAll() (g string, err error) +``` + +`aria2.pauseAll()` This method is equal to calling `aria2.pause()` for every active/waiting download. This methods returns OK for success. + +``` +func (id *Client) PurgeDowloadResult() (g string, err error) +``` + +`aria2.purgeDownloadResult()` This method purges completed/error/removed downloads to free memory. This method returns OK. + +``` +func (id *Client) Remove(gid string) (g string, err error) +``` + +`aria2.remove(gid)` This method removes the download denoted by gid. `gid` is of type string. If specified download is in progress, it is stopped at first. The status of removed download becomes removed. This method returns GID of removed download. + +``` +func (id *Client) RemoveDownloadResult(gid string) (g string, err error) +``` + +`aria2.removeDownloadResult(gid)` This method removes completed/error/removed download denoted by `gid` from memory. This method returns OK for success. + +``` +func (id *Client) Shutdown() (g string, err error) +``` + +`aria2.shutdown()` This method shutdowns aria2. This method returns OK. + +``` +func (id *Client) TellActive(keys ...string) (m []map[string]interface{}, err error) +``` + +`aria2.tellActive([keys])` This method returns the list of active downloads. The response is of type array and its element is the same struct returned by `aria2.tellStatus()` method. For `keys` parameter, please refer to `aria2.tellStatus()` method. + +``` +func (id *Client) TellStatus(gid string, keys ...string) (m map[string]interface{}, err error) +``` + +`aria2.tellStatus(gid[, keys])` This method returns download progress of the download denoted by `gid`. `gid` is of type string. `keys` is array of string. If it is specified, the response contains only keys in `keys` array. If `keys` is empty or not specified, the response contains all keys. This is useful when you just want specific keys and avoid unnecessary transfers. For example, `aria2.tellStatus("2089b05ecca3d829", ["gid", "status"])` returns `gid` and `status` key. + +``` +func (id *Client) TellStopped(offset, num int, keys ...string) (m []map[string]interface{}, err error) +``` + +`aria2.tellStopped(offset, num[, keys])` This method returns the list of stopped download. `offset` is of type integer and specifies the `offset` from the oldest download. `num` is of type integer and specifies the number of downloads to be returned. For keys parameter, please refer to `aria2.tellStatus()` method. `offset` and `num` have the same semantics as `aria2.tellWaiting()` method. The response is of type array and its element is the same struct returned by `aria2.tellStatus()` method. + +``` +func (id *Client) TellWaiting(offset, num int, keys ...string) (m []map[string]interface{}, err error) +``` +`aria2.tellWaiting(offset, num[, keys])` This method returns the list of waiting download, including paused downloads. `offset` is of type integer and specifies the `offset` from the download waiting at the front. num is of type integer and specifies the number of downloads to be returned. For keys parameter, please refer to aria2.tellStatus() method. If `offset` is a positive integer, this method returns downloads +in the range of `[offset, offset + num)`. `offset` can be a negative integer. `offset == -1` points last download in the waiting queue and `offset == -2` points the download before the last download, and so on. The downloads in the response are in reversed order. For example, imagine that three downloads "A","B" and "C" are waiting in this order. + + aria2.tellWaiting(0, 1) returns ["A"]. + aria2.tellWaiting(1, 2) returns ["B", "C"]. + aria2.tellWaiting(-1, 2) returns ["C", "B"]. + +The response is of type array and its element is the same struct returned by `aria2.tellStatus()` method. + +``` +func (id *Client) Unpause(gid string) (g string, err error) +``` + +`aria2.unpause(gid)` This method changes the status of the download denoted by `gid` from paused to waiting. This makes the download eligible to restart. `gid` is of type string. This method returns GID of unpaused download. + +``` +func (id *Client) UnpauseAll() (g string, err error) +``` + +`aria2.unpauseAll()` This method is equal to calling `aria2.unpause()` for every active/waiting download. This methods returns OK for success. diff --git a/pkg/aria2/rpc/call.go b/pkg/aria2/rpc/call.go new file mode 100644 index 0000000..11cb137 --- /dev/null +++ b/pkg/aria2/rpc/call.go @@ -0,0 +1,274 @@ +package rpc + +import ( + "context" + "errors" + "log" + "net" + "net/http" + "net/url" + "sync" + "sync/atomic" + "time" + + "github.com/gorilla/websocket" +) + +type caller interface { + // Call sends a request of rpc to aria2 daemon + Call(method string, params, reply interface{}) (err error) + Close() error +} + +type httpCaller struct { + uri string + c *http.Client + cancel context.CancelFunc + wg *sync.WaitGroup + once sync.Once +} + +func newHTTPCaller(ctx context.Context, u *url.URL, timeout time.Duration, notifer Notifier) *httpCaller { + c := &http.Client{ + Transport: &http.Transport{ + MaxIdleConnsPerHost: 1, + MaxConnsPerHost: 1, + // TLSClientConfig: tlsConfig, + Dial: (&net.Dialer{ + Timeout: timeout, + KeepAlive: 60 * time.Second, + }).Dial, + TLSHandshakeTimeout: 3 * time.Second, + ResponseHeaderTimeout: timeout, + }, + } + var wg sync.WaitGroup + ctx, cancel := context.WithCancel(ctx) + h := &httpCaller{uri: u.String(), c: c, cancel: cancel, wg: &wg} + if notifer != nil { + h.setNotifier(ctx, *u, notifer) + } + return h +} + +func (h *httpCaller) Close() (err error) { + h.once.Do(func() { + h.cancel() + h.wg.Wait() + }) + return +} + +func (h *httpCaller) setNotifier(ctx context.Context, u url.URL, notifer Notifier) (err error) { + u.Scheme = "ws" + conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + return + } + h.wg.Add(1) + go func() { + defer h.wg.Done() + defer conn.Close() + select { + case <-ctx.Done(): + conn.SetWriteDeadline(time.Now().Add(time.Second)) + if err := conn.WriteMessage(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil { + log.Printf("sending websocket close message: %v", err) + } + return + } + }() + h.wg.Add(1) + go func() { + defer h.wg.Done() + var request websocketResponse + var err error + for { + select { + case <-ctx.Done(): + return + default: + } + if err = conn.ReadJSON(&request); err != nil { + select { + case <-ctx.Done(): + return + default: + } + log.Printf("conn.ReadJSON|err:%v", err.Error()) + return + } + switch request.Method { + case "aria2.onDownloadStart": + notifer.OnDownloadStart(request.Params) + case "aria2.onDownloadPause": + notifer.OnDownloadPause(request.Params) + case "aria2.onDownloadStop": + notifer.OnDownloadStop(request.Params) + case "aria2.onDownloadComplete": + notifer.OnDownloadComplete(request.Params) + case "aria2.onDownloadError": + notifer.OnDownloadError(request.Params) + case "aria2.onBtDownloadComplete": + notifer.OnBtDownloadComplete(request.Params) + default: + log.Printf("unexpected notification: %s", request.Method) + } + } + }() + return +} + +func (h httpCaller) Call(method string, params, reply interface{}) (err error) { + payload, err := EncodeClientRequest(method, params) + if err != nil { + return + } + r, err := h.c.Post(h.uri, "application/json", payload) + if err != nil { + return + } + err = DecodeClientResponse(r.Body, &reply) + r.Body.Close() + return +} + +type websocketCaller struct { + conn *websocket.Conn + sendChan chan *sendRequest + cancel context.CancelFunc + wg *sync.WaitGroup + once sync.Once + timeout time.Duration +} + +func newWebsocketCaller(ctx context.Context, uri string, timeout time.Duration, notifier Notifier) (*websocketCaller, error) { + var header = http.Header{} + conn, _, err := websocket.DefaultDialer.Dial(uri, header) + if err != nil { + return nil, err + } + + sendChan := make(chan *sendRequest, 16) + var wg sync.WaitGroup + ctx, cancel := context.WithCancel(ctx) + w := &websocketCaller{conn: conn, wg: &wg, cancel: cancel, sendChan: sendChan, timeout: timeout} + processor := NewResponseProcessor() + wg.Add(1) + go func() { // routine:recv + defer wg.Done() + defer cancel() + for { + select { + case <-ctx.Done(): + return + default: + } + var resp websocketResponse + if err := conn.ReadJSON(&resp); err != nil { + select { + case <-ctx.Done(): + return + default: + } + log.Printf("conn.ReadJSON|err:%v", err.Error()) + return + } + if resp.Id == nil { // RPC notifications + if notifier != nil { + switch resp.Method { + case "aria2.onDownloadStart": + notifier.OnDownloadStart(resp.Params) + case "aria2.onDownloadPause": + notifier.OnDownloadPause(resp.Params) + case "aria2.onDownloadStop": + notifier.OnDownloadStop(resp.Params) + case "aria2.onDownloadComplete": + notifier.OnDownloadComplete(resp.Params) + case "aria2.onDownloadError": + notifier.OnDownloadError(resp.Params) + case "aria2.onBtDownloadComplete": + notifier.OnBtDownloadComplete(resp.Params) + default: + log.Printf("unexpected notification: %s", resp.Method) + } + } + continue + } + processor.Process(resp.clientResponse) + } + }() + wg.Add(1) + go func() { // routine:send + defer wg.Done() + defer cancel() + defer w.conn.Close() + + for { + select { + case <-ctx.Done(): + if err := w.conn.WriteMessage(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil { + log.Printf("sending websocket close message: %v", err) + } + return + case req := <-sendChan: + processor.Add(req.request.Id, func(resp clientResponse) error { + err := resp.decode(req.reply) + req.cancel() + return err + }) + w.conn.SetWriteDeadline(time.Now().Add(timeout)) + w.conn.WriteJSON(req.request) + } + } + }() + + return w, nil +} + +func (w *websocketCaller) Close() (err error) { + w.once.Do(func() { + w.cancel() + w.wg.Wait() + }) + return +} + +func (w websocketCaller) Call(method string, params, reply interface{}) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), w.timeout) + defer cancel() + select { + case w.sendChan <- &sendRequest{cancel: cancel, request: &clientRequest{ + Version: "2.0", + Method: method, + Params: params, + Id: reqid(), + }, reply: reply}: + + default: + return errors.New("sending channel blocking") + } + + select { + case <-ctx.Done(): + if err := ctx.Err(); err == context.DeadlineExceeded { + return err + } + } + return +} + +type sendRequest struct { + cancel context.CancelFunc + request *clientRequest + reply interface{} +} + +var reqid = func() func() uint64 { + var id = uint64(time.Now().UnixNano()) + return func() uint64 { + return atomic.AddUint64(&id, 1) + } +}() diff --git a/pkg/aria2/rpc/call_test.go b/pkg/aria2/rpc/call_test.go new file mode 100644 index 0000000..64d2520 --- /dev/null +++ b/pkg/aria2/rpc/call_test.go @@ -0,0 +1,23 @@ +package rpc + +import ( + "context" + "testing" + "time" +) + +func TestWebsocketCaller(t *testing.T) { + time.Sleep(time.Second) + c, err := newWebsocketCaller(context.Background(), "ws://localhost:6800/jsonrpc", time.Second, &DummyNotifier{}) + if err != nil { + t.Fatal(err.Error()) + } + defer c.Close() + + var info VersionInfo + if err := c.Call(aria2GetVersion, []interface{}{}, &info); err != nil { + t.Error(err.Error()) + } else { + println(info.Version) + } +} diff --git a/pkg/aria2/rpc/client.go b/pkg/aria2/rpc/client.go new file mode 100644 index 0000000..adb9e39 --- /dev/null +++ b/pkg/aria2/rpc/client.go @@ -0,0 +1,656 @@ +package rpc + +import ( + "context" + "encoding/base64" + "errors" + "io/ioutil" + "net/url" + "time" +) + +// Option is a container for specifying Call parameters and returning results +type Option map[string]interface{} + +type Client interface { + Protocol + Close() error +} + +type client struct { + caller + url *url.URL + token string +} + +var ( + errInvalidParameter = errors.New("invalid parameter") + errNotImplemented = errors.New("not implemented") + errConnTimeout = errors.New("connect to aria2 daemon timeout") +) + +// New returns an instance of Client +func New(ctx context.Context, uri string, token string, timeout time.Duration, notifier Notifier) (Client, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + var caller caller + switch u.Scheme { + case "http", "https": + caller = newHTTPCaller(ctx, u, timeout, notifier) + case "ws", "wss": + caller, err = newWebsocketCaller(ctx, u.String(), timeout, notifier) + if err != nil { + return nil, err + } + default: + return nil, errInvalidParameter + } + c := &client{caller: caller, url: u, token: token} + return c, nil +} + +// `aria2.addUri([secret, ]uris[, options[, position]])` +// This method adds a new download. uris is an array of HTTP/FTP/SFTP/BitTorrent URIs (strings) pointing to the same resource. +// If you mix URIs pointing to different resources, then the download may fail or be corrupted without aria2 complaining. +// When adding BitTorrent Magnet URIs, uris must have only one element and it should be BitTorrent Magnet URI. +// options is a struct and its members are pairs of option name and value. +// If position is given, it must be an integer starting from 0. +// The new download will be inserted at position in the waiting queue. +// If position is omitted or position is larger than the current size of the queue, the new download is appended to the end of the queue. +// This method returns the GID of the newly registered download. +func (c *client) AddURI(uri string, options ...interface{}) (gid string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, []string{uri}) + if options != nil { + params = append(params, options...) + } + err = c.Call(aria2AddURI, params, &gid) + return +} + +// `aria2.addTorrent([secret, ]torrent[, uris[, options[, position]]])` +// This method adds a BitTorrent download by uploading a ".torrent" file. +// If you want to add a BitTorrent Magnet URI, use the aria2.addUri() method instead. +// torrent must be a base64-encoded string containing the contents of the ".torrent" file. +// uris is an array of URIs (string). uris is used for Web-seeding. +// For single file torrents, the URI can be a complete URI pointing to the resource; if URI ends with /, name in torrent file is added. +// For multi-file torrents, name and path in torrent are added to form a URI for each file. options is a struct and its members are pairs of option name and value. +// If position is given, it must be an integer starting from 0. +// The new download will be inserted at position in the waiting queue. +// If position is omitted or position is larger than the current size of the queue, the new download is appended to the end of the queue. +// This method returns the GID of the newly registered download. +// If --rpc-save-upload-metadata is true, the uploaded data is saved as a file named as the hex string of SHA-1 hash of data plus ".torrent" in the directory specified by --dir option. +// E.g. a file name might be 0a3893293e27ac0490424c06de4d09242215f0a6.torrent. +// If a file with the same name already exists, it is overwritten! +// If the file cannot be saved successfully or --rpc-save-upload-metadata is false, the downloads added by this method are not saved by --save-session. +func (c *client) AddTorrent(filename string, options ...interface{}) (gid string, err error) { + co, err := ioutil.ReadFile(filename) + if err != nil { + return + } + file := base64.StdEncoding.EncodeToString(co) + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, file) + if options != nil { + params = append(params, options...) + } + err = c.Call(aria2AddTorrent, params, &gid) + return +} + +// `aria2.addMetalink([secret, ]metalink[, options[, position]])` +// This method adds a Metalink download by uploading a ".metalink" file. +// metalink is a base64-encoded string which contains the contents of the ".metalink" file. +// options is a struct and its members are pairs of option name and value. +// If position is given, it must be an integer starting from 0. +// The new download will be inserted at position in the waiting queue. +// If position is omitted or position is larger than the current size of the queue, the new download is appended to the end of the queue. +// This method returns an array of GIDs of newly registered downloads. +// If --rpc-save-upload-metadata is true, the uploaded data is saved as a file named hex string of SHA-1 hash of data plus ".metalink" in the directory specified by --dir option. +// E.g. a file name might be 0a3893293e27ac0490424c06de4d09242215f0a6.metalink. +// If a file with the same name already exists, it is overwritten! +// If the file cannot be saved successfully or --rpc-save-upload-metadata is false, the downloads added by this method are not saved by --save-session. +func (c *client) AddMetalink(filename string, options ...interface{}) (gid []string, err error) { + co, err := ioutil.ReadFile(filename) + if err != nil { + return + } + file := base64.StdEncoding.EncodeToString(co) + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, file) + if options != nil { + params = append(params, options...) + } + err = c.Call(aria2AddMetalink, params, &gid) + return +} + +// `aria2.remove([secret, ]gid)` +// This method removes the download denoted by gid (string). +// If the specified download is in progress, it is first stopped. +// The status of the removed download becomes removed. +// This method returns GID of removed download. +func (c *client) Remove(gid string) (g string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2Remove, params, &g) + return +} + +// `aria2.forceRemove([secret, ]gid)` +// This method removes the download denoted by gid. +// This method behaves just like aria2.remove() except that this method removes the download without performing any actions which take time, such as contacting BitTorrent trackers to unregister the download first. +func (c *client) ForceRemove(gid string) (g string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2ForceRemove, params, &g) + return +} + +// `aria2.pause([secret, ]gid)` +// This method pauses the download denoted by gid (string). +// The status of paused download becomes paused. +// If the download was active, the download is placed in the front of waiting queue. +// While the status is paused, the download is not started. +// To change status to waiting, use the aria2.unpause() method. +// This method returns GID of paused download. +func (c *client) Pause(gid string) (g string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2Pause, params, &g) + return +} + +// `aria2.pauseAll([secret])` +// This method is equal to calling aria2.pause() for every active/waiting download. +// This methods returns OK. +func (c *client) PauseAll() (ok string, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2PauseAll, params, &ok) + return +} + +// `aria2.forcePause([secret, ]gid)` +// This method pauses the download denoted by gid. +// This method behaves just like aria2.pause() except that this method pauses downloads without performing any actions which take time, such as contacting BitTorrent trackers to unregister the download first. +func (c *client) ForcePause(gid string) (g string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2ForcePause, params, &g) + return +} + +// `aria2.forcePauseAll([secret])` +// This method is equal to calling aria2.forcePause() for every active/waiting download. +// This methods returns OK. +func (c *client) ForcePauseAll() (ok string, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2ForcePauseAll, params, &ok) + return +} + +// `aria2.unpause([secret, ]gid)` +// This method changes the status of the download denoted by gid (string) from paused to waiting, making the download eligible to be restarted. +// This method returns the GID of the unpaused download. +func (c *client) Unpause(gid string) (g string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2Unpause, params, &g) + return +} + +// `aria2.unpauseAll([secret])` +// This method is equal to calling aria2.unpause() for every active/waiting download. +// This methods returns OK. +func (c *client) UnpauseAll() (ok string, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2UnpauseAll, params, &ok) + return +} + +// `aria2.tellStatus([secret, ]gid[, keys])` +// This method returns the progress of the download denoted by gid (string). +// keys is an array of strings. +// If specified, the response contains only keys in the keys array. +// If keys is empty or omitted, the response contains all keys. +// This is useful when you just want specific keys and avoid unnecessary transfers. +// For example, aria2.tellStatus("2089b05ecca3d829", ["gid", "status"]) returns the gid and status keys only. +// The response is a struct and contains following keys. Values are strings. +// https://aria2.github.io/manual/en/html/aria2c.html#aria2.tellStatus +func (c *client) TellStatus(gid string, keys ...string) (info StatusInfo, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + if keys != nil { + params = append(params, keys) + } + err = c.Call(aria2TellStatus, params, &info) + return +} + +// `aria2.getUris([secret, ]gid)` +// This method returns the URIs used in the download denoted by gid (string). +// The response is an array of structs and it contains following keys. Values are string. +// uri URI +// status 'used' if the URI is in use. 'waiting' if the URI is still waiting in the queue. +func (c *client) GetURIs(gid string) (infos []URIInfo, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2GetURIs, params, &infos) + return +} + +// `aria2.getFiles([secret, ]gid)` +// This method returns the file list of the download denoted by gid (string). +// The response is an array of structs which contain following keys. Values are strings. +// https://aria2.github.io/manual/en/html/aria2c.html#aria2.getFiles +func (c *client) GetFiles(gid string) (infos []FileInfo, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2GetFiles, params, &infos) + return +} + +// `aria2.getPeers([secret, ]gid)` +// This method returns a list peers of the download denoted by gid (string). +// This method is for BitTorrent only. +// The response is an array of structs and contains the following keys. Values are strings. +// https://aria2.github.io/manual/en/html/aria2c.html#aria2.getPeers +func (c *client) GetPeers(gid string) (infos []PeerInfo, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2GetPeers, params, &infos) + return +} + +// `aria2.getServers([secret, ]gid)` +// This method returns currently connected HTTP(S)/FTP/SFTP servers of the download denoted by gid (string). +// The response is an array of structs and contains the following keys. Values are strings. +// https://aria2.github.io/manual/en/html/aria2c.html#aria2.getServers +func (c *client) GetServers(gid string) (infos []ServerInfo, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2GetServers, params, &infos) + return +} + +// `aria2.tellActive([secret][, keys])` +// This method returns a list of active downloads. +// The response is an array of the same structs as returned by the aria2.tellStatus() method. +// For the keys parameter, please refer to the aria2.tellStatus() method. +func (c *client) TellActive(keys ...string) (infos []StatusInfo, err error) { + params := make([]interface{}, 0, 1) + if c.token != "" { + params = append(params, "token:"+c.token) + } + if keys != nil { + params = append(params, keys) + } + err = c.Call(aria2TellActive, params, &infos) + return +} + +// `aria2.tellWaiting([secret, ]offset, num[, keys])` +// This method returns a list of waiting downloads, including paused ones. +// offset is an integer and specifies the offset from the download waiting at the front. +// num is an integer and specifies the max. number of downloads to be returned. +// For the keys parameter, please refer to the aria2.tellStatus() method. +// If offset is a positive integer, this method returns downloads in the range of [offset, offset + num). +// offset can be a negative integer. offset == -1 points last download in the waiting queue and offset == -2 points the download before the last download, and so on. +// Downloads in the response are in reversed order then. +// For example, imagine three downloads "A","B" and "C" are waiting in this order. +// aria2.tellWaiting(0, 1) returns ["A"]. +// aria2.tellWaiting(1, 2) returns ["B", "C"]. +// aria2.tellWaiting(-1, 2) returns ["C", "B"]. +// The response is an array of the same structs as returned by aria2.tellStatus() method. +func (c *client) TellWaiting(offset, num int, keys ...string) (infos []StatusInfo, err error) { + params := make([]interface{}, 0, 3) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, offset) + params = append(params, num) + if keys != nil { + params = append(params, keys) + } + err = c.Call(aria2TellWaiting, params, &infos) + return +} + +// `aria2.tellStopped([secret, ]offset, num[, keys])` +// This method returns a list of stopped downloads. +// offset is an integer and specifies the offset from the least recently stopped download. +// num is an integer and specifies the max. number of downloads to be returned. +// For the keys parameter, please refer to the aria2.tellStatus() method. +// offset and num have the same semantics as described in the aria2.tellWaiting() method. +// The response is an array of the same structs as returned by the aria2.tellStatus() method. +func (c *client) TellStopped(offset, num int, keys ...string) (infos []StatusInfo, err error) { + params := make([]interface{}, 0, 3) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, offset) + params = append(params, num) + if keys != nil { + params = append(params, keys) + } + err = c.Call(aria2TellStopped, params, &infos) + return +} + +// `aria2.changePosition([secret, ]gid, pos, how)` +// This method changes the position of the download denoted by gid in the queue. +// pos is an integer. how is a string. +// If how is POS_SET, it moves the download to a position relative to the beginning of the queue. +// If how is POS_CUR, it moves the download to a position relative to the current position. +// If how is POS_END, it moves the download to a position relative to the end of the queue. +// If the destination position is less than 0 or beyond the end of the queue, it moves the download to the beginning or the end of the queue respectively. +// The response is an integer denoting the resulting position. +// For example, if GID#2089b05ecca3d829 is currently in position 3, aria2.changePosition('2089b05ecca3d829', -1, 'POS_CUR') will change its position to 2. Additionally aria2.changePosition('2089b05ecca3d829', 0, 'POS_SET') will change its position to 0 (the beginning of the queue). +func (c *client) ChangePosition(gid string, pos int, how string) (p int, err error) { + params := make([]interface{}, 0, 3) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + params = append(params, pos) + params = append(params, how) + err = c.Call(aria2ChangePosition, params, &p) + return +} + +// `aria2.changeUri([secret, ]gid, fileIndex, delUris, addUris[, position])` +// This method removes the URIs in delUris from and appends the URIs in addUris to download denoted by gid. +// delUris and addUris are lists of strings. +// A download can contain multiple files and URIs are attached to each file. +// fileIndex is used to select which file to remove/attach given URIs. fileIndex is 1-based. +// position is used to specify where URIs are inserted in the existing waiting URI list. position is 0-based. +// When position is omitted, URIs are appended to the back of the list. +// This method first executes the removal and then the addition. +// position is the position after URIs are removed, not the position when this method is called. +// When removing an URI, if the same URIs exist in download, only one of them is removed for each URI in delUris. +// In other words, if there are three URIs http://example.org/aria2 and you want remove them all, you have to specify (at least) 3 http://example.org/aria2 in delUris. +// This method returns a list which contains two integers. +// The first integer is the number of URIs deleted. +// The second integer is the number of URIs added. +func (c *client) ChangeURI(gid string, fileindex int, delUris []string, addUris []string, position ...int) (p []int, err error) { + params := make([]interface{}, 0, 5) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + params = append(params, fileindex) + params = append(params, delUris) + params = append(params, addUris) + if position != nil { + params = append(params, position[0]) + } + err = c.Call(aria2ChangeURI, params, &p) + return +} + +// `aria2.getOption([secret, ]gid)` +// This method returns options of the download denoted by gid. +// The response is a struct where keys are the names of options. +// The values are strings. +// Note that this method does not return options which have no default value and have not been set on the command-line, in configuration files or RPC methods. +func (c *client) GetOption(gid string) (m Option, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2GetOption, params, &m) + return +} + +// `aria2.changeOption([secret, ]gid, options)` +// This method changes options of the download denoted by gid (string) dynamically. options is a struct. +// The following options are available for active downloads: +// bt-max-peers +// bt-request-peer-speed-limit +// bt-remove-unselected-file +// force-save +// max-download-limit +// max-upload-limit +// For waiting or paused downloads, in addition to the above options, options listed in Input File subsection are available, except for following options: dry-run, metalink-base-uri, parameterized-uri, pause, piece-length and rpc-save-upload-metadata option. +// This method returns OK for success. +func (c *client) ChangeOption(gid string, option Option) (ok string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + if option != nil { + params = append(params, option) + } + err = c.Call(aria2ChangeOption, params, &ok) + return +} + +// `aria2.getGlobalOption([secret])` +// This method returns the global options. +// The response is a struct. +// Its keys are the names of options. +// Values are strings. +// Note that this method does not return options which have no default value and have not been set on the command-line, in configuration files or RPC methods. Because global options are used as a template for the options of newly added downloads, the response contains keys returned by the aria2.getOption() method. +func (c *client) GetGlobalOption() (m Option, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2GetGlobalOption, params, &m) + return +} + +// `aria2.changeGlobalOption([secret, ]options)` +// This method changes global options dynamically. +// options is a struct. +// The following options are available: +// bt-max-open-files +// download-result +// log +// log-level +// max-concurrent-downloads +// max-download-result +// max-overall-download-limit +// max-overall-upload-limit +// save-cookies +// save-session +// server-stat-of +// In addition, options listed in the Input File subsection are available, except for following options: checksum, index-out, out, pause and select-file. +// With the log option, you can dynamically start logging or change log file. +// To stop logging, specify an empty string("") as the parameter value. +// Note that log file is always opened in append mode. +// This method returns OK for success. +func (c *client) ChangeGlobalOption(options Option) (ok string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, options) + err = c.Call(aria2ChangeGlobalOption, params, &ok) + return +} + +// `aria2.getGlobalStat([secret])` +// This method returns global statistics such as the overall download and upload speeds. +// The response is a struct and contains the following keys. Values are strings. +// downloadSpeed Overall download speed (byte/sec). +// uploadSpeed Overall upload speed(byte/sec). +// numActive The number of active downloads. +// numWaiting The number of waiting downloads. +// numStopped The number of stopped downloads in the current session. +// This value is capped by the --max-download-result option. +// numStoppedTotal The number of stopped downloads in the current session and not capped by the --max-download-result option. +func (c *client) GetGlobalStat() (info GlobalStatInfo, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2GetGlobalStat, params, &info) + return +} + +// `aria2.purgeDownloadResult([secret])` +// This method purges completed/error/removed downloads to free memory. +// This method returns OK. +func (c *client) PurgeDownloadResult() (ok string, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2PurgeDownloadResult, params, &ok) + return +} + +// `aria2.removeDownloadResult([secret, ]gid)` +// This method removes a completed/error/removed download denoted by gid from memory. +// This method returns OK for success. +func (c *client) RemoveDownloadResult(gid string) (ok string, err error) { + params := make([]interface{}, 0, 2) + if c.token != "" { + params = append(params, "token:"+c.token) + } + params = append(params, gid) + err = c.Call(aria2RemoveDownloadResult, params, &ok) + return +} + +// `aria2.getVersion([secret])` +// This method returns the version of aria2 and the list of enabled features. +// The response is a struct and contains following keys. +// version Version number of aria2 as a string. +// enabledFeatures List of enabled features. Each feature is given as a string. +func (c *client) GetVersion() (info VersionInfo, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2GetVersion, params, &info) + return +} + +// `aria2.getSessionInfo([secret])` +// This method returns session information. +// The response is a struct and contains following key. +// sessionId Session ID, which is generated each time when aria2 is invoked. +func (c *client) GetSessionInfo() (info SessionInfo, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2GetSessionInfo, params, &info) + return +} + +// `aria2.shutdown([secret])` +// This method shutdowns aria2. +// This method returns OK. +func (c *client) Shutdown() (ok string, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2Shutdown, params, &ok) + return +} + +// `aria2.forceShutdown([secret])` +// This method shuts down aria2(). +// This method behaves like :func:'aria2.shutdown` without performing any actions which take time, such as contacting BitTorrent trackers to unregister downloads first. +// This method returns OK. +func (c *client) ForceShutdown() (ok string, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2ForceShutdown, params, &ok) + return +} + +// `aria2.saveSession([secret])` +// This method saves the current session to a file specified by the --save-session option. +// This method returns OK if it succeeds. +func (c *client) SaveSession() (ok string, err error) { + params := []string{} + if c.token != "" { + params = append(params, "token:"+c.token) + } + err = c.Call(aria2SaveSession, params, &ok) + return +} + +// `system.multicall(methods)` +// This methods encapsulates multiple method calls in a single request. +// methods is an array of structs. +// The structs contain two keys: methodName and params. +// methodName is the method name to call and params is array containing parameters to the method call. +// This method returns an array of responses. +// The elements will be either a one-item array containing the return value of the method call or a struct of fault element if an encapsulated method call fails. +func (c *client) Multicall(methods []Method) (r []interface{}, err error) { + if len(methods) == 0 { + err = errInvalidParameter + return + } + err = c.Call(aria2Multicall, methods, &r) + return +} + +// `system.listMethods()` +// This method returns the all available RPC methods in an array of string. +// Unlike other methods, this method does not require secret token. +// This is safe because this method jsut returns the available method names. +func (c *client) ListMethods() (methods []string, err error) { + err = c.Call(aria2ListMethods, []string{}, &methods) + return +} diff --git a/pkg/aria2/rpc/client_test.go b/pkg/aria2/rpc/client_test.go new file mode 100644 index 0000000..abf68f4 --- /dev/null +++ b/pkg/aria2/rpc/client_test.go @@ -0,0 +1,125 @@ +package rpc + +import ( + "context" + "testing" + "time" +) + +func TestHTTPAll(t *testing.T) { + const targetURL = "https://nodejs.org/dist/index.json" + rpc, err := New(context.Background(), "http://localhost:6800/jsonrpc", "", time.Second, &DummyNotifier{}) + if err != nil { + t.Fatal(err) + } + defer rpc.Close() + g, err := rpc.AddURI(targetURL) + if err != nil { + t.Fatal(err) + } + println(g) + if _, err = rpc.TellActive(); err != nil { + t.Error(err) + } + if _, err = rpc.PauseAll(); err != nil { + t.Error(err) + } + if _, err = rpc.TellStatus(g); err != nil { + t.Error(err) + } + if _, err = rpc.GetURIs(g); err != nil { + t.Error(err) + } + if _, err = rpc.GetFiles(g); err != nil { + t.Error(err) + } + if _, err = rpc.GetPeers(g); err != nil { + t.Error(err) + } + if _, err = rpc.TellActive(); err != nil { + t.Error(err) + } + if _, err = rpc.TellWaiting(0, 1); err != nil { + t.Error(err) + } + if _, err = rpc.TellStopped(0, 1); err != nil { + t.Error(err) + } + if _, err = rpc.GetOption(g); err != nil { + t.Error(err) + } + if _, err = rpc.GetGlobalOption(); err != nil { + t.Error(err) + } + if _, err = rpc.GetGlobalStat(); err != nil { + t.Error(err) + } + if _, err = rpc.GetSessionInfo(); err != nil { + t.Error(err) + } + if _, err = rpc.Remove(g); err != nil { + t.Error(err) + } + if _, err = rpc.TellActive(); err != nil { + t.Error(err) + } +} + +func TestWebsocketAll(t *testing.T) { + const targetURL = "https://nodejs.org/dist/index.json" + rpc, err := New(context.Background(), "ws://localhost:6800/jsonrpc", "", time.Second, &DummyNotifier{}) + if err != nil { + t.Fatal(err) + } + defer rpc.Close() + g, err := rpc.AddURI(targetURL) + if err != nil { + t.Fatal(err) + } + println(g) + if _, err = rpc.TellActive(); err != nil { + t.Error(err) + } + if _, err = rpc.PauseAll(); err != nil { + t.Error(err) + } + if _, err = rpc.TellStatus(g); err != nil { + t.Error(err) + } + if _, err = rpc.GetURIs(g); err != nil { + t.Error(err) + } + if _, err = rpc.GetFiles(g); err != nil { + t.Error(err) + } + if _, err = rpc.GetPeers(g); err != nil { + t.Error(err) + } + if _, err = rpc.TellActive(); err != nil { + t.Error(err) + } + if _, err = rpc.TellWaiting(0, 1); err != nil { + t.Error(err) + } + if _, err = rpc.TellStopped(0, 1); err != nil { + t.Error(err) + } + if _, err = rpc.GetOption(g); err != nil { + t.Error(err) + } + if _, err = rpc.GetGlobalOption(); err != nil { + t.Error(err) + } + if _, err = rpc.GetGlobalStat(); err != nil { + t.Error(err) + } + if _, err = rpc.GetSessionInfo(); err != nil { + t.Error(err) + } + if _, err = rpc.Remove(g); err != nil { + t.Error(err) + } + if _, err = rpc.TellActive(); err != nil { + t.Error(err) + } +} diff --git a/pkg/aria2/rpc/const.go b/pkg/aria2/rpc/const.go new file mode 100644 index 0000000..b5d83dd --- /dev/null +++ b/pkg/aria2/rpc/const.go @@ -0,0 +1,39 @@ +package rpc + +const ( + aria2AddURI = "aria2.addUri" + aria2AddTorrent = "aria2.addTorrent" + aria2AddMetalink = "aria2.addMetalink" + aria2Remove = "aria2.remove" + aria2ForceRemove = "aria2.forceRemove" + aria2Pause = "aria2.pause" + aria2PauseAll = "aria2.pauseAll" + aria2ForcePause = "aria2.forcePause" + aria2ForcePauseAll = "aria2.forcePauseAll" + aria2Unpause = "aria2.unpause" + aria2UnpauseAll = "aria2.unpauseAll" + aria2TellStatus = "aria2.tellStatus" + aria2GetURIs = "aria2.getUris" + aria2GetFiles = "aria2.getFiles" + aria2GetPeers = "aria2.getPeers" + aria2GetServers = "aria2.getServers" + aria2TellActive = "aria2.tellActive" + aria2TellWaiting = "aria2.tellWaiting" + aria2TellStopped = "aria2.tellStopped" + aria2ChangePosition = "aria2.changePosition" + aria2ChangeURI = "aria2.changeUri" + aria2GetOption = "aria2.getOption" + aria2ChangeOption = "aria2.changeOption" + aria2GetGlobalOption = "aria2.getGlobalOption" + aria2ChangeGlobalOption = "aria2.changeGlobalOption" + aria2GetGlobalStat = "aria2.getGlobalStat" + aria2PurgeDownloadResult = "aria2.purgeDownloadResult" + aria2RemoveDownloadResult = "aria2.removeDownloadResult" + aria2GetVersion = "aria2.getVersion" + aria2GetSessionInfo = "aria2.getSessionInfo" + aria2Shutdown = "aria2.shutdown" + aria2ForceShutdown = "aria2.forceShutdown" + aria2SaveSession = "aria2.saveSession" + aria2Multicall = "system.multicall" + aria2ListMethods = "system.listMethods" +) diff --git a/pkg/aria2/rpc/json2.go b/pkg/aria2/rpc/json2.go new file mode 100644 index 0000000..3febf7e --- /dev/null +++ b/pkg/aria2/rpc/json2.go @@ -0,0 +1,116 @@ +package rpc + +// based on "github.com/gorilla/rpc/v2/json2" + +// Copyright 2009 The Go Authors. All rights reserved. +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import ( + "bytes" + "encoding/json" + "errors" + "io" +) + +// ---------------------------------------------------------------------------- +// Request and Response +// ---------------------------------------------------------------------------- + +// clientRequest represents a JSON-RPC request sent by a client. +type clientRequest struct { + // JSON-RPC protocol. + Version string `json:"jsonrpc"` + + // A String containing the name of the method to be invoked. + Method string `json:"method"` + + // Object to pass as request parameter to the method. + Params interface{} `json:"params"` + + // The request id. This can be of any type. It is used to match the + // response with the request that it is replying to. + Id uint64 `json:"id"` +} + +// clientResponse represents a JSON-RPC response returned to a client. +type clientResponse struct { + Version string `json:"jsonrpc"` + Result *json.RawMessage `json:"result"` + Error *json.RawMessage `json:"error"` + Id *uint64 `json:"id"` +} + +// EncodeClientRequest encodes parameters for a JSON-RPC client request. +func EncodeClientRequest(method string, args interface{}) (*bytes.Buffer, error) { + var buf bytes.Buffer + c := &clientRequest{ + Version: "2.0", + Method: method, + Params: args, + Id: reqid(), + } + if err := json.NewEncoder(&buf).Encode(c); err != nil { + return nil, err + } + return &buf, nil +} + +func (c clientResponse) decode(reply interface{}) error { + if c.Error != nil { + jsonErr := &Error{} + if err := json.Unmarshal(*c.Error, jsonErr); err != nil { + return &Error{ + Code: E_SERVER, + Message: string(*c.Error), + } + } + return jsonErr + } + + if c.Result == nil { + return ErrNullResult + } + + return json.Unmarshal(*c.Result, reply) +} + +// DecodeClientResponse decodes the response body of a client request into +// the interface reply. +func DecodeClientResponse(r io.Reader, reply interface{}) error { + var c clientResponse + if err := json.NewDecoder(r).Decode(&c); err != nil { + return err + } + return c.decode(reply) +} + +type ErrorCode int + +const ( + E_PARSE ErrorCode = -32700 + E_INVALID_REQ ErrorCode = -32600 + E_NO_METHOD ErrorCode = -32601 + E_BAD_PARAMS ErrorCode = -32602 + E_INTERNAL ErrorCode = -32603 + E_SERVER ErrorCode = -32000 +) + +var ErrNullResult = errors.New("result is null") + +type Error struct { + // A Number that indicates the error type that occurred. + Code ErrorCode `json:"code"` /* required */ + + // A String providing a short description of the error. + // The message SHOULD be limited to a concise single sentence. + Message string `json:"message"` /* required */ + + // A Primitive or Structured value that contains additional information about the error. + Data interface{} `json:"data"` /* optional */ +} + +func (e *Error) Error() string { + return e.Message +} diff --git a/pkg/aria2/rpc/notification.go b/pkg/aria2/rpc/notification.go new file mode 100644 index 0000000..ebca91e --- /dev/null +++ b/pkg/aria2/rpc/notification.go @@ -0,0 +1,44 @@ +package rpc + +import ( + "log" +) + +type Event struct { + Gid string `json:"gid"` // GID of the download +} + +// The RPC server might send notifications to the client. +// Notifications is unidirectional, therefore the client which receives the notification must not respond to it. +// The method signature of a notification is much like a normal method request but lacks the id key + +type websocketResponse struct { + clientResponse + Method string `json:"method"` + Params []Event `json:"params"` +} + +// Notifier handles rpc notification from aria2 server +type Notifier interface { + // OnDownloadStart will be sent when a download is started. + OnDownloadStart([]Event) + // OnDownloadPause will be sent when a download is paused. + OnDownloadPause([]Event) + // OnDownloadStop will be sent when a download is stopped by the user. + OnDownloadStop([]Event) + // OnDownloadComplete will be sent when a download is complete. For BitTorrent downloads, this notification is sent when the download is complete and seeding is over. + OnDownloadComplete([]Event) + // OnDownloadError will be sent when a download is stopped due to an error. + OnDownloadError([]Event) + // OnBtDownloadComplete will be sent when a torrent download is complete but seeding is still going on. + OnBtDownloadComplete([]Event) +} + +type DummyNotifier struct{} + +func (DummyNotifier) OnDownloadStart(events []Event) { log.Printf("%s started.", events) } +func (DummyNotifier) OnDownloadPause(events []Event) { log.Printf("%s paused.", events) } +func (DummyNotifier) OnDownloadStop(events []Event) { log.Printf("%s stopped.", events) } +func (DummyNotifier) OnDownloadComplete(events []Event) { log.Printf("%s completed.", events) } +func (DummyNotifier) OnDownloadError(events []Event) { log.Printf("%s error.", events) } +func (DummyNotifier) OnBtDownloadComplete(events []Event) { log.Printf("bt %s completed.", events) } diff --git a/pkg/aria2/rpc/proc.go b/pkg/aria2/rpc/proc.go new file mode 100644 index 0000000..0184e6d --- /dev/null +++ b/pkg/aria2/rpc/proc.go @@ -0,0 +1,42 @@ +package rpc + +import "sync" + +type ResponseProcFn func(resp clientResponse) error + +type ResponseProcessor struct { + cbs map[uint64]ResponseProcFn + mu *sync.RWMutex +} + +func NewResponseProcessor() *ResponseProcessor { + return &ResponseProcessor{ + make(map[uint64]ResponseProcFn), + &sync.RWMutex{}, + } +} + +func (r *ResponseProcessor) Add(id uint64, fn ResponseProcFn) { + r.mu.Lock() + r.cbs[id] = fn + r.mu.Unlock() +} + +func (r *ResponseProcessor) remove(id uint64) { + r.mu.Lock() + delete(r.cbs, id) + r.mu.Unlock() +} + +// Process called by recv routine +func (r *ResponseProcessor) Process(resp clientResponse) error { + id := *resp.Id + r.mu.RLock() + fn, ok := r.cbs[id] + r.mu.RUnlock() + if ok && fn != nil { + defer r.remove(id) + return fn(resp) + } + return nil +} diff --git a/pkg/aria2/rpc/proto.go b/pkg/aria2/rpc/proto.go new file mode 100644 index 0000000..178fa6b --- /dev/null +++ b/pkg/aria2/rpc/proto.go @@ -0,0 +1,40 @@ +package rpc + +// Protocol is a set of rpc methods that aria2 daemon supports +type Protocol interface { + AddURI(uri string, options ...interface{}) (gid string, err error) + AddTorrent(filename string, options ...interface{}) (gid string, err error) + AddMetalink(filename string, options ...interface{}) (gid []string, err error) + Remove(gid string) (g string, err error) + ForceRemove(gid string) (g string, err error) + Pause(gid string) (g string, err error) + PauseAll() (ok string, err error) + ForcePause(gid string) (g string, err error) + ForcePauseAll() (ok string, err error) + Unpause(gid string) (g string, err error) + UnpauseAll() (ok string, err error) + TellStatus(gid string, keys ...string) (info StatusInfo, err error) + GetURIs(gid string) (infos []URIInfo, err error) + GetFiles(gid string) (infos []FileInfo, err error) + GetPeers(gid string) (infos []PeerInfo, err error) + GetServers(gid string) (infos []ServerInfo, err error) + TellActive(keys ...string) (infos []StatusInfo, err error) + TellWaiting(offset, num int, keys ...string) (infos []StatusInfo, err error) + TellStopped(offset, num int, keys ...string) (infos []StatusInfo, err error) + ChangePosition(gid string, pos int, how string) (p int, err error) + ChangeURI(gid string, fileindex int, delUris []string, addUris []string, position ...int) (p []int, err error) + GetOption(gid string) (m Option, err error) + ChangeOption(gid string, option Option) (ok string, err error) + GetGlobalOption() (m Option, err error) + ChangeGlobalOption(options Option) (ok string, err error) + GetGlobalStat() (info GlobalStatInfo, err error) + PurgeDownloadResult() (ok string, err error) + RemoveDownloadResult(gid string) (ok string, err error) + GetVersion() (info VersionInfo, err error) + GetSessionInfo() (info SessionInfo, err error) + Shutdown() (ok string, err error) + ForceShutdown() (ok string, err error) + SaveSession() (ok string, err error) + Multicall(methods []Method) (r []interface{}, err error) + ListMethods() (methods []string, err error) +} diff --git a/pkg/aria2/rpc/resp.go b/pkg/aria2/rpc/resp.go new file mode 100644 index 0000000..7f3ba82 --- /dev/null +++ b/pkg/aria2/rpc/resp.go @@ -0,0 +1,102 @@ +//go:generate easyjson -all + +package rpc + +// StatusInfo represents response of aria2.tellStatus +type StatusInfo struct { + Gid string `json:"gid"` // GID of the download. + Status string `json:"status"` // active for currently downloading/seeding downloads. waiting for downloads in the queue; download is not started. paused for paused downloads. error for downloads that were stopped because of error. complete for stopped and completed downloads. removed for the downloads removed by user. + TotalLength string `json:"totalLength"` // Total length of the download in bytes. + CompletedLength string `json:"completedLength"` // Completed length of the download in bytes. + UploadLength string `json:"uploadLength"` // Uploaded length of the download in bytes. + BitField string `json:"bitfield"` // Hexadecimal representation of the download progress. The highest bit corresponds to the piece at index 0. Any set bits indicate loaded pieces, while unset bits indicate not yet loaded and/or missing pieces. Any overflow bits at the end are set to zero. When the download was not started yet, this key will not be included in the response. + DownloadSpeed string `json:"downloadSpeed"` // Download speed of this download measured in bytes/sec. + UploadSpeed string `json:"uploadSpeed"` // Upload speed of this download measured in bytes/sec. + InfoHash string `json:"infoHash"` // InfoHash. BitTorrent only. + NumSeeders string `json:"numSeeders"` // The number of seeders aria2 has connected to. BitTorrent only. + Seeder string `json:"seeder"` // true if the local endpoint is a seeder. Otherwise false. BitTorrent only. + PieceLength string `json:"pieceLength"` // Piece length in bytes. + NumPieces string `json:"numPieces"` // The number of pieces. + Connections string `json:"connections"` // The number of peers/servers aria2 has connected to. + ErrorCode string `json:"errorCode"` // The code of the last error for this item, if any. The value is a string. The error codes are defined in the EXIT STATUS section. This value is only available for stopped/completed downloads. + ErrorMessage string `json:"errorMessage"` // The (hopefully) human readable error message associated to errorCode. + FollowedBy []string `json:"followedBy"` // List of GIDs which are generated as the result of this download. For example, when aria2 downloads a Metalink file, it generates downloads described in the Metalink (see the --follow-metalink option). This value is useful to track auto-generated downloads. If there are no such downloads, this key will not be included in the response. + BelongsTo string `json:"belongsTo"` // GID of a parent download. Some downloads are a part of another download. For example, if a file in a Metalink has BitTorrent resources, the downloads of ".torrent" files are parts of that parent. If this download has no parent, this key will not be included in the response. + Dir string `json:"dir"` // Directory to save files. + Files []FileInfo `json:"files"` // Returns the list of files. The elements of this list are the same structs used in aria2.getFiles() method. + BitTorrent struct { + AnnounceList [][]string `json:"announceList"` // List of lists of announce URIs. If the torrent contains announce and no announce-list, announce is converted to the announce-list format. + Comment string `json:"comment"` // The comment of the torrent. comment.utf-8 is used if available. + CreationDate int64 `json:"creationDate"` // The creation time of the torrent. The value is an integer since the epoch, measured in seconds. + Mode string `json:"mode"` // File mode of the torrent. The value is either single or multi. + Info struct { + Name string `json:"name"` // name in info dictionary. name.utf-8 is used if available. + } `json:"info"` // Struct which contains data from Info dictionary. It contains following keys. + } `json:"bittorrent"` // Struct which contains information retrieved from the .torrent (file). BitTorrent only. It contains following keys. +} + +// URIInfo represents an element of response of aria2.getUris +type URIInfo struct { + URI string `json:"uri"` // URI + Status string `json:"status"` // 'used' if the URI is in use. 'waiting' if the URI is still waiting in the queue. +} + +// FileInfo represents an element of response of aria2.getFiles +type FileInfo struct { + Index string `json:"index"` // Index of the file, starting at 1, in the same order as files appear in the multi-file torrent. + Path string `json:"path"` // File path. + Length string `json:"length"` // File size in bytes. + CompletedLength string `json:"completedLength"` // Completed length of this file in bytes. Please note that it is possible that sum of completedLength is less than the completedLength returned by the aria2.tellStatus() method. This is because completedLength in aria2.getFiles() only includes completed pieces. On the other hand, completedLength in aria2.tellStatus() also includes partially completed pieces. + Selected string `json:"selected"` // true if this file is selected by --select-file option. If --select-file is not specified or this is single-file torrent or not a torrent download at all, this value is always true. Otherwise false. + URIs []URIInfo `json:"uris"` // Returns a list of URIs for this file. The element type is the same struct used in the aria2.getUris() method. +} + +// PeerInfo represents an element of response of aria2.getPeers +type PeerInfo struct { + PeerId string `json:"peerId"` // Percent-encoded peer ID. + IP string `json:"ip"` // IP address of the peer. + Port string `json:"port"` // Port number of the peer. + BitField string `json:"bitfield"` // Hexadecimal representation of the download progress of the peer. The highest bit corresponds to the piece at index 0. Set bits indicate the piece is available and unset bits indicate the piece is missing. Any spare bits at the end are set to zero. + AmChoking string `json:"amChoking"` // true if aria2 is choking the peer. Otherwise false. + PeerChoking string `json:"peerChoking"` // true if the peer is choking aria2. Otherwise false. + DownloadSpeed string `json:"downloadSpeed"` // Download speed (byte/sec) that this client obtains from the peer. + UploadSpeed string `json:"uploadSpeed"` // Upload speed(byte/sec) that this client uploads to the peer. + Seeder string `json:"seeder"` // true if this peer is a seeder. Otherwise false. +} + +// ServerInfo represents an element of response of aria2.getServers +type ServerInfo struct { + Index string `json:"index"` // Index of the file, starting at 1, in the same order as files appear in the multi-file metalink. + Servers []struct { + URI string `json:"uri"` // Original URI. + CurrentURI string `json:"currentUri"` // This is the URI currently used for downloading. If redirection is involved, currentUri and uri may differ. + DownloadSpeed string `json:"downloadSpeed"` // Download speed (byte/sec) + } `json:"servers"` // A list of structs which contain the following keys. +} + +// GlobalStatInfo represents response of aria2.getGlobalStat +type GlobalStatInfo struct { + DownloadSpeed string `json:"downloadSpeed"` // Overall download speed (byte/sec). + UploadSpeed string `json:"uploadSpeed"` // Overall upload speed(byte/sec). + NumActive string `json:"numActive"` // The number of active downloads. + NumWaiting string `json:"numWaiting"` // The number of waiting downloads. + NumStopped string `json:"numStopped"` // The number of stopped downloads in the current session. This value is capped by the --max-download-result option. + NumStoppedTotal string `json:"numStoppedTotal"` // The number of stopped downloads in the current session and not capped by the --max-download-result option. +} + +// VersionInfo represents response of aria2.getVersion +type VersionInfo struct { + Version string `json:"version"` // Version number of aria2 as a string. + Features []string `json:"enabledFeatures"` // List of enabled features. Each feature is given as a string. +} + +// SessionInfo represents response of aria2.getSessionInfo +type SessionInfo struct { + Id string `json:"sessionId"` // Session ID, which is generated each time when aria2 is invoked. +} + +// Method is an element of parameters used in system.multicall +type Method struct { + Name string `json:"methodName"` // Method name to call + Params []interface{} `json:"params"` // Array containing parameters to the method call +} diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go index 5e7282d..a22cba6 100644 --- a/pkg/conf/conf.go +++ b/pkg/conf/conf.go @@ -72,11 +72,39 @@ type cors struct { var cfg *ini.File +const defaultConf = ` +[System] +Mode = master +Listen = :5212 +SessionSecret = {SessionSecret} +HashIDSalt = {HashIDSalt} +` + // Init 初始化配置文件 func Init(path string) { var err error - //TODO 配置文件不存在时创建 - //TODO 配置合法性验证 + + if path == "" || !util.Exists(path) { + // 创建初始配置文件 + confContent := util.Replace(map[string]string{ + "{SessionSecret}": util.RandStringRunes(64), + "{HashIDSalt}": util.RandStringRunes(64), + }, defaultConf) + f, err := util.CreatNestedFile("conf.ini") + if err != nil { + util.Log().Panic("无法创建配置文件, %s", err) + } + + // 写入配置文件 + _, err = f.WriteString(confContent) + if err != nil { + util.Log().Panic("无法写入配置文件, %s", err) + } + + f.Close() + path = "conf.ini" + } + cfg, err = ini.Load(path) if err != nil { util.Log().Panic("无法解析配置文件 '%s': %s", path, err) diff --git a/pkg/conf/defaults.go b/pkg/conf/defaults.go index e4639ed..9f5b651 100644 --- a/pkg/conf/defaults.go +++ b/pkg/conf/defaults.go @@ -18,7 +18,7 @@ var DatabaseConfig = &database{ var SystemConfig = &system{ Debug: false, Mode: "master", - Listen: ":5000", + Listen: ":5212", } // CaptchaConfig 验证码配置 diff --git a/pkg/conf/version.go b/pkg/conf/version.go index 2674845..e75bd3f 100644 --- a/pkg/conf/version.go +++ b/pkg/conf/version.go @@ -1,12 +1,7 @@ package conf -import "io/ioutil" +// BackendVersion 当前后端版本号 +const BackendVersion = string("3.0.0-beta1") -// 当前后端版本号 -const BackendVersion = string("3.0.0-b") - -// WriteVersionLock 将当前版本信息写入 version.lock -func WriteVersionLock() error { - err := ioutil.WriteFile("version.lock", []byte(BackendVersion), 0644) - return err -} +// RequiredDBVersion 与当前版本匹配的数据库版本 +const RequiredDBVersion = string("3.0.0-beta1") diff --git a/pkg/filesystem/image.go b/pkg/filesystem/image.go index d32335b..49cd77d 100644 --- a/pkg/filesystem/image.go +++ b/pkg/filesystem/image.go @@ -9,6 +9,7 @@ import ( "github.com/HFO4/cloudreve/pkg/filesystem/response" "github.com/HFO4/cloudreve/pkg/thumb" "github.com/HFO4/cloudreve/pkg/util" + "strconv" ) /* ================ @@ -92,7 +93,12 @@ func (fs *FileSystem) GenerateThumbnail(ctx context.Context, file *model.File) { } // GenerateThumbnailSize 获取要生成的缩略图的尺寸 -// TODO 优先从数据库中获得 func (fs *FileSystem) GenerateThumbnailSize(w, h int) (uint, uint) { + if conf.SystemConfig.Mode == "master" { + options := model.GetSettingByNames("thumb_width", "thumb_height") + w, _ := strconv.ParseUint(options["thumb_width"], 10, 32) + h, _ := strconv.ParseUint(options["thumb_height"], 10, 32) + return uint(w), uint(h) + } return conf.ThumbConfig.MaxWidth, conf.ThumbConfig.MaxHeight } diff --git a/pkg/serializer/aria2.go b/pkg/serializer/aria2.go index 613f0be..0f0ffa0 100644 --- a/pkg/serializer/aria2.go +++ b/pkg/serializer/aria2.go @@ -2,7 +2,7 @@ package serializer import ( model "github.com/HFO4/cloudreve/models" - "github.com/zyxar/argo/rpc" + "github.com/HFO4/cloudreve/pkg/aria2/rpc" "path" ) diff --git a/routers/controllers/site.go b/routers/controllers/site.go index b7507c6..086b66c 100644 --- a/routers/controllers/site.go +++ b/routers/controllers/site.go @@ -2,7 +2,6 @@ package controllers import ( model "github.com/HFO4/cloudreve/models" - "github.com/HFO4/cloudreve/pkg/conf" "github.com/HFO4/cloudreve/pkg/serializer" "github.com/HFO4/cloudreve/pkg/util" "github.com/gin-gonic/gin" @@ -48,20 +47,27 @@ func Ping(c *gin.Context) { // Captcha 获取验证码 func Captcha(c *gin.Context) { + options := model.GetSettingByNames( + "captcha_IsShowHollowLine", + "captcha_IsShowNoiseDot", + "captcha_IsShowNoiseText", + "captcha_IsShowSlimeLine", + "captcha_IsShowSineLine", + ) // 验证码配置 var configD = base64Captcha.ConfigCharacter{ - Height: conf.CaptchaConfig.Height, - Width: conf.CaptchaConfig.Width, + Height: model.GetIntSetting("captcha_height", 60), + Width: model.GetIntSetting("captcha_width", 240), //const CaptchaModeNumber:数字,CaptchaModeAlphabet:字母,CaptchaModeArithmetic:算术,CaptchaModeNumberAlphabet:数字字母混合. - Mode: conf.CaptchaConfig.Mode, - ComplexOfNoiseText: conf.CaptchaConfig.ComplexOfNoiseText, - ComplexOfNoiseDot: conf.CaptchaConfig.ComplexOfNoiseDot, - IsShowHollowLine: conf.CaptchaConfig.IsShowHollowLine, - IsShowNoiseDot: conf.CaptchaConfig.IsShowNoiseDot, - IsShowNoiseText: conf.CaptchaConfig.IsShowNoiseText, - IsShowSlimeLine: conf.CaptchaConfig.IsShowSlimeLine, - IsShowSineLine: conf.CaptchaConfig.IsShowSineLine, - CaptchaLen: conf.CaptchaConfig.CaptchaLen, + Mode: model.GetIntSetting("captcha_mode", 3), + ComplexOfNoiseText: model.GetIntSetting("captcha_ComplexOfNoiseText", 0), + ComplexOfNoiseDot: model.GetIntSetting("captcha_ComplexOfNoiseDot", 0), + IsShowHollowLine: model.IsTrueVal(options["captcha_IsShowHollowLine"]), + IsShowNoiseDot: model.IsTrueVal(options["captcha_IsShowNoiseDot"]), + IsShowNoiseText: model.IsTrueVal(options["captcha_IsShowNoiseText"]), + IsShowSlimeLine: model.IsTrueVal(options["captcha_IsShowSlimeLine"]), + IsShowSineLine: model.IsTrueVal(options["captcha_IsShowSineLine"]), + CaptchaLen: model.GetIntSetting("captcha_CaptchaLen", 6), } // 生成验证码