mirror of https://github.com/Xhofe/alist
* feat(qbittorrent): authorization and logging in support * feat(qbittorrent/client): support `AddFromLink` * refactor(qbittorrent/client): check authorization when getting a new client * feat(qbittorrent/client): support `GetInfo` * test(qbittorrent/client): update test cases * feat(qbittorrent): init qbittorrent client on bootstrap * feat(qbittorrent): support setting webui url via gin * feat(qbittorrent/client): support deleting * feat(qbittorrent/client): parse `TorrentStatus` enum when unmarshalling json in `GetInfo()` * feat(qbittorrent/client): support getting files by id * feat(qbittorrent): support adding qbittorrent tasks via gin * refactor(qbittorrent/client): return a `Client` interface in `New()` instead of `*client` * refactor: task handle * chore: fix typo * chore: change path --------- Co-authored-by: Andy Hsu <i@nn.ci>pull/3398/head
parent
46b2ed2507
commit
c28168c970
|
@ -29,6 +29,7 @@ the address is defined in config file`,
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Init()
|
||||
bootstrap.InitAria2()
|
||||
bootstrap.InitQbittorrent()
|
||||
bootstrap.LoadStorages()
|
||||
if !flags.Debug && !flags.Dev {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
|
|
@ -154,6 +154,9 @@ func InitialSettings() []model.SettingItem {
|
|||
{Key: conf.GithubClientId, Value: "", Type: conf.TypeString, Group: model.GITHUB, Flag: model.PRIVATE},
|
||||
{Key: conf.GithubClientSecrets, Value: "", Type: conf.TypeString, Group: model.GITHUB, Flag: model.PRIVATE},
|
||||
{Key: conf.GithubLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.GITHUB, Flag: model.PUBLIC},
|
||||
|
||||
// qbittorrent settings
|
||||
{Key: conf.QbittorrentUrl, Value: "http://admin:adminadmin@localhost:8080/", Type: conf.TypeString, Group: model.QBITTORRENT, Flag: model.PRIVATE},
|
||||
}
|
||||
if flags.Dev {
|
||||
initialSettingItems = append(initialSettingItems, []model.SettingItem{
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/qbittorrent"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
)
|
||||
|
||||
func InitQbittorrent() {
|
||||
go func() {
|
||||
err := qbittorrent.InitClient()
|
||||
if err != nil {
|
||||
utils.Log.Infof("qbittorrent not ready.")
|
||||
}
|
||||
}()
|
||||
}
|
|
@ -1,33 +1,33 @@
|
|||
package conf
|
||||
|
||||
const (
|
||||
TypeString = "string"
|
||||
TypeSelect = "select"
|
||||
TypeBool = "bool"
|
||||
TypeText = "text"
|
||||
TypeNumber = "number"
|
||||
TypeString = "string"
|
||||
TypeSelect = "select"
|
||||
TypeBool = "bool"
|
||||
TypeText = "text"
|
||||
TypeNumber = "number"
|
||||
)
|
||||
|
||||
const (
|
||||
// site
|
||||
VERSION = "version"
|
||||
SiteTitle = "site_title"
|
||||
Announcement = "announcement"
|
||||
AllowIndexed = "allow_indexed"
|
||||
// site
|
||||
VERSION = "version"
|
||||
SiteTitle = "site_title"
|
||||
Announcement = "announcement"
|
||||
AllowIndexed = "allow_indexed"
|
||||
|
||||
Logo = "logo"
|
||||
Favicon = "favicon"
|
||||
MainColor = "main_color"
|
||||
Logo = "logo"
|
||||
Favicon = "favicon"
|
||||
MainColor = "main_color"
|
||||
|
||||
// preview
|
||||
TextTypes = "text_types"
|
||||
AudioTypes = "audio_types"
|
||||
VideoTypes = "video_types"
|
||||
ImageTypes = "image_types"
|
||||
ProxyTypes = "proxy_types"
|
||||
ProxyIgnoreHeaders = "proxy_ignore_headers"
|
||||
AudioAutoplay = "audio_autoplay"
|
||||
VideoAutoplay = "video_autoplay"
|
||||
// preview
|
||||
TextTypes = "text_types"
|
||||
AudioTypes = "audio_types"
|
||||
VideoTypes = "video_types"
|
||||
ImageTypes = "image_types"
|
||||
ProxyTypes = "proxy_types"
|
||||
ProxyIgnoreHeaders = "proxy_ignore_headers"
|
||||
AudioAutoplay = "audio_autoplay"
|
||||
VideoAutoplay = "video_autoplay"
|
||||
|
||||
// global
|
||||
HideFiles = "hide_files"
|
||||
|
@ -46,26 +46,29 @@ const (
|
|||
IgnorePaths = "ignore_paths"
|
||||
MaxIndexDepth = "max_index_depth"
|
||||
|
||||
// aria2
|
||||
Aria2Uri = "aria2_uri"
|
||||
Aria2Secret = "aria2_secret"
|
||||
// aria2
|
||||
Aria2Uri = "aria2_uri"
|
||||
Aria2Secret = "aria2_secret"
|
||||
|
||||
// single
|
||||
Token = "token"
|
||||
IndexProgress = "index_progress"
|
||||
// single
|
||||
Token = "token"
|
||||
IndexProgress = "index_progress"
|
||||
|
||||
//Github
|
||||
GithubClientId = "github_client_id"
|
||||
GithubClientSecrets = "github_client_secrets"
|
||||
GithubLoginEnabled = "github_login_enabled"
|
||||
//Github
|
||||
GithubClientId = "github_client_id"
|
||||
GithubClientSecrets = "github_client_secrets"
|
||||
GithubLoginEnabled = "github_login_enabled"
|
||||
|
||||
// qbittorrent
|
||||
QbittorrentUrl = "qbittorrent_url"
|
||||
)
|
||||
|
||||
const (
|
||||
UNKNOWN = iota
|
||||
FOLDER
|
||||
//OFFICE
|
||||
VIDEO
|
||||
AUDIO
|
||||
TEXT
|
||||
IMAGE
|
||||
UNKNOWN = iota
|
||||
FOLDER
|
||||
//OFFICE
|
||||
VIDEO
|
||||
AUDIO
|
||||
TEXT
|
||||
IMAGE
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ const (
|
|||
ARIA2
|
||||
INDEX
|
||||
GITHUB
|
||||
QBITTORRENT
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -20,16 +20,17 @@ type User struct {
|
|||
Role int `json:"role"` // user's role
|
||||
Disabled bool `json:"disabled"`
|
||||
// Determine permissions by bit
|
||||
// 0: can see hidden files
|
||||
// 1: can access without password
|
||||
// 2: can add aria2 tasks
|
||||
// 3: can mkdir and upload
|
||||
// 4: can rename
|
||||
// 5: can move
|
||||
// 6: can copy
|
||||
// 7: can remove
|
||||
// 8: webdav read
|
||||
// 9: webdav write
|
||||
// 0: can see hidden files
|
||||
// 1: can access without password
|
||||
// 2: can add aria2 tasks
|
||||
// 3: can mkdir and upload
|
||||
// 4: can rename
|
||||
// 5: can move
|
||||
// 6: can copy
|
||||
// 7: can remove
|
||||
// 8: webdav read
|
||||
// 9: webdav write
|
||||
// 10: can add qbittorrent tasks
|
||||
Permission int32 `json:"permission"`
|
||||
OtpSecret string `json:"-"`
|
||||
GithubID int `json:"github_id"`
|
||||
|
@ -93,6 +94,10 @@ func (u User) CanWebdavManage() bool {
|
|||
return u.IsAdmin() || (u.Permission>>9)&1 == 1
|
||||
}
|
||||
|
||||
func (u User) CanAddQbittorrentTasks() bool {
|
||||
return u.IsAdmin() || (u.Permission>>10)&1 == 1
|
||||
}
|
||||
|
||||
func (u User) JoinPath(reqPath string) (string, error) {
|
||||
return utils.JoinBasePath(u.BasePath, reqPath)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/task"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func AddURL(ctx context.Context, url string, dstDirPath string) error {
|
||||
// check storage
|
||||
storage, dstDirActualPath, err := op.GetStorageAndActualPath(dstDirPath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed get storage")
|
||||
}
|
||||
// check is it could upload
|
||||
if storage.Config().NoUpload {
|
||||
return errors.WithStack(errs.UploadNotSupported)
|
||||
}
|
||||
// check path is valid
|
||||
obj, err := op.Get(ctx, storage, dstDirActualPath)
|
||||
if err != nil {
|
||||
if !errs.IsObjectNotFound(err) {
|
||||
return errors.WithMessage(err, "failed get object")
|
||||
}
|
||||
} else {
|
||||
if !obj.IsDir() {
|
||||
// can't add to a file
|
||||
return errors.WithStack(errs.NotFolder)
|
||||
}
|
||||
}
|
||||
// call qbittorrent
|
||||
id := uuid.NewString()
|
||||
tempDir := filepath.Join(conf.Conf.TempDir, "qbittorrent", id)
|
||||
err = qbclient.AddFromLink(url, tempDir, id)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to add url %s", url)
|
||||
}
|
||||
DownTaskManager.Submit(task.WithCancelCtx(&task.Task[string]{
|
||||
ID: id,
|
||||
Name: fmt.Sprintf("download %s to [%s](%s)", url, storage.GetStorage().MountPath, dstDirActualPath),
|
||||
Func: func(tsk *task.Task[string]) error {
|
||||
m := &Monitor{
|
||||
tsk: tsk,
|
||||
tempDir: tempDir,
|
||||
dstDirPath: dstDirPath,
|
||||
}
|
||||
return m.Loop()
|
||||
},
|
||||
}))
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,337 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
AddFromLink(link string, savePath string, id string) error
|
||||
GetInfo(id string) (TorrentInfo, error)
|
||||
GetFiles(id string) ([]FileInfo, error)
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type client struct {
|
||||
url *url.URL
|
||||
client http.Client
|
||||
Client
|
||||
}
|
||||
|
||||
func New(webuiUrl string) (Client, error) {
|
||||
u, err := url.Parse(webuiUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c = &client{
|
||||
url: u,
|
||||
client: http.Client{Jar: jar},
|
||||
}
|
||||
|
||||
err = c.checkAuthorization()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *client) checkAuthorization() error {
|
||||
// check authorization
|
||||
if c.authorized() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check authorization after logging in
|
||||
err := c.login()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.authorized() {
|
||||
return nil
|
||||
}
|
||||
return errors.New("unauthorized qbittorrent url")
|
||||
}
|
||||
|
||||
func (c *client) authorized() bool {
|
||||
resp, err := c.post("/api/v2/app/version", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return resp.StatusCode == 200 // the status code will be 403 if not authorized
|
||||
}
|
||||
|
||||
func (c *client) login() error {
|
||||
// prepare HTTP request
|
||||
v := url.Values{}
|
||||
v.Set("username", c.url.User.Username())
|
||||
passwd, _ := c.url.User.Password()
|
||||
v.Set("password", passwd)
|
||||
resp, err := c.post("/api/v2/auth/login", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check result
|
||||
body := make([]byte, 2)
|
||||
_, err = resp.Body.Read(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(body) != "Ok" {
|
||||
return errors.New("failed to login into qBittorrent webui with url: " + c.url.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) post(path string, data url.Values) (*http.Response, error) {
|
||||
u := c.url.JoinPath(path)
|
||||
u.User = nil // remove userinfo for requests
|
||||
|
||||
req, err := http.NewRequest("POST", u.String(), bytes.NewReader([]byte(data.Encode())))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data != nil {
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Cookies() != nil {
|
||||
c.client.Jar.SetCookies(u, resp.Cookies())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *client) AddFromLink(link string, savePath string, id string) error {
|
||||
err := c.checkAuthorization()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(buf)
|
||||
|
||||
addField := func(name string, value string) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = writer.WriteField(name, value)
|
||||
}
|
||||
addField("urls", link)
|
||||
addField("savepath", savePath)
|
||||
addField("tags", "alist-"+id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := c.url.JoinPath("/api/v2/torrents/add")
|
||||
u.User = nil // remove userinfo for requests
|
||||
req, err := http.NewRequest("POST", u.String(), buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check result
|
||||
body := make([]byte, 2)
|
||||
_, err = resp.Body.Read(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 || string(body) != "Ok" {
|
||||
return errors.New("failed to add qBittorrent task: " + link)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TorrentStatus string
|
||||
|
||||
const (
|
||||
ERROR TorrentStatus = "error"
|
||||
MISSINGFILES TorrentStatus = "missingFiles"
|
||||
UPLOADING TorrentStatus = "uploading"
|
||||
PAUSEDUP TorrentStatus = "pausedUP"
|
||||
QUEUEDUP TorrentStatus = "queuedUP"
|
||||
STALLEDUP TorrentStatus = "stalledUP"
|
||||
CHECKINGUP TorrentStatus = "checkingUP"
|
||||
FORCEDUP TorrentStatus = "forcedUP"
|
||||
ALLOCATING TorrentStatus = "allocating"
|
||||
DOWNLOADING TorrentStatus = "downloading"
|
||||
METADL TorrentStatus = "metaDL"
|
||||
PAUSEDDL TorrentStatus = "pausedDL"
|
||||
QUEUEDDL TorrentStatus = "queuedDL"
|
||||
STALLEDDL TorrentStatus = "stalledDL"
|
||||
CHECKINGDL TorrentStatus = "checkingDL"
|
||||
FORCEDDL TorrentStatus = "forcedDL"
|
||||
CHECKINGRESUMEDATA TorrentStatus = "checkingResumeData"
|
||||
MOVING TorrentStatus = "moving"
|
||||
UNKNOWN TorrentStatus = "unknown"
|
||||
)
|
||||
|
||||
// https://github.com/DGuang21/PTGo/blob/main/app/client/client_distributer.go
|
||||
type TorrentInfo struct {
|
||||
AddedOn int `json:"added_on"` // 将 torrent 添加到客户端的时间(Unix Epoch)
|
||||
AmountLeft int64 `json:"amount_left"` // 剩余大小(字节)
|
||||
AutoTmm bool `json:"auto_tmm"` // 此 torrent 是否由 Automatic Torrent Management 管理
|
||||
Availability float64 `json:"availability"` // 当前百分比
|
||||
Category string `json:"category"` //
|
||||
Completed int64 `json:"completed"` // 完成的传输数据量(字节)
|
||||
CompletionOn int `json:"completion_on"` // Torrent 完成的时间(Unix Epoch)
|
||||
ContentPath string `json:"content_path"` // torrent 内容的绝对路径(多文件 torrent 的根路径,单文件 torrent 的绝对文件路径)
|
||||
DlLimit int `json:"dl_limit"` // Torrent 下载速度限制(字节/秒)
|
||||
Dlspeed int `json:"dlspeed"` // Torrent 下载速度(字节/秒)
|
||||
Downloaded int64 `json:"downloaded"` // 已经下载大小
|
||||
DownloadedSession int64 `json:"downloaded_session"` // 此会话下载的数据量
|
||||
Eta int `json:"eta"` //
|
||||
FLPiecePrio bool `json:"f_l_piece_prio"` // 如果第一个最后一块被优先考虑,则为true
|
||||
ForceStart bool `json:"force_start"` // 如果为此 torrent 启用了强制启动,则为true
|
||||
Hash string `json:"hash"` //
|
||||
LastActivity int `json:"last_activity"` // 上次活跃的时间(Unix Epoch)
|
||||
MagnetURI string `json:"magnet_uri"` // 与此 torrent 对应的 Magnet URI
|
||||
MaxRatio int `json:"max_ratio"` // 种子/上传停止种子前的最大共享比率
|
||||
MaxSeedingTime int `json:"max_seeding_time"` // 停止种子种子前的最长种子时间(秒)
|
||||
Name string `json:"name"` //
|
||||
NumComplete int `json:"num_complete"` //
|
||||
NumIncomplete int `json:"num_incomplete"` //
|
||||
NumLeechs int `json:"num_leechs"` // 连接到的 leechers 的数量
|
||||
NumSeeds int `json:"num_seeds"` // 连接到的种子数
|
||||
Priority int `json:"priority"` // 速度优先。如果队列被禁用或 torrent 处于种子模式,则返回 -1
|
||||
Progress float64 `json:"progress"` // 进度
|
||||
Ratio float64 `json:"ratio"` // Torrent 共享比率
|
||||
RatioLimit int `json:"ratio_limit"` //
|
||||
SavePath string `json:"save_path"`
|
||||
SeedingTime int `json:"seeding_time"` // Torrent 完成用时(秒)
|
||||
SeedingTimeLimit int `json:"seeding_time_limit"` // max_seeding_time
|
||||
SeenComplete int `json:"seen_complete"` // 上次 torrent 完成的时间
|
||||
SeqDl bool `json:"seq_dl"` // 如果启用顺序下载,则为true
|
||||
Size int64 `json:"size"` //
|
||||
State TorrentStatus `json:"state"` // 参见https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list
|
||||
SuperSeeding bool `json:"super_seeding"` // 如果启用超级播种,则为true
|
||||
Tags string `json:"tags"` // Torrent 的逗号连接标签列表
|
||||
TimeActive int `json:"time_active"` // 总活动时间(秒)
|
||||
TotalSize int64 `json:"total_size"` // 此 torrent 中所有文件的总大小(字节)(包括未选择的文件)
|
||||
Tracker string `json:"tracker"` // 第一个具有工作状态的tracker。如果没有tracker在工作,则返回空字符串。
|
||||
TrackersCount int `json:"trackers_count"` //
|
||||
UpLimit int `json:"up_limit"` // 上传限制
|
||||
Uploaded int64 `json:"uploaded"` // 累计上传
|
||||
UploadedSession int64 `json:"uploaded_session"` // 当前session累计上传
|
||||
Upspeed int `json:"upspeed"` // 上传速度(字节/秒)
|
||||
}
|
||||
|
||||
func (c *client) GetInfo(id string) (TorrentInfo, error) {
|
||||
var infos []TorrentInfo
|
||||
|
||||
err := c.checkAuthorization()
|
||||
if err != nil {
|
||||
return TorrentInfo{}, err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("tag", "alist-"+id)
|
||||
response, err := c.post("/api/v2/torrents/info", v)
|
||||
if err != nil {
|
||||
return TorrentInfo{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return TorrentInfo{}, err
|
||||
}
|
||||
err = utils.Json.Unmarshal(body, &infos)
|
||||
if err != nil {
|
||||
return TorrentInfo{}, err
|
||||
}
|
||||
if len(infos) != 1 {
|
||||
return TorrentInfo{}, errors.New("there should be exactly one task with tag \"alist-" + id + "\"")
|
||||
}
|
||||
return infos[0], nil
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
Index int `json:"index"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Progress float32 `json:"progress"`
|
||||
Priority int `json:"priority"`
|
||||
IsSeed bool `json:"is_seed"`
|
||||
PieceRange []int `json:"piece_range"`
|
||||
Availability float32 `json:"availability"`
|
||||
}
|
||||
|
||||
func (c *client) GetFiles(id string) ([]FileInfo, error) {
|
||||
var infos []FileInfo
|
||||
|
||||
err := c.checkAuthorization()
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
|
||||
tInfo, err := c.GetInfo(id)
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("hash", tInfo.Hash)
|
||||
response, err := c.post("/api/v2/torrents/files", v)
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
err = utils.Json.Unmarshal(body, &infos)
|
||||
if err != nil {
|
||||
return []FileInfo{}, err
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (c *client) Delete(id string) error {
|
||||
err := c.checkAuthorization()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := c.GetInfo(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := url.Values{}
|
||||
v.Set("hashes", info.Hash)
|
||||
v.Set("deleteFiles", "false")
|
||||
response, err := c.post("/api/v2/torrents/delete", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
return errors.New("failed")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
// test logging in with wrong password
|
||||
u, err := url.Parse("http://admin:admin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var c = &client{
|
||||
url: u,
|
||||
client: http.Client{Jar: jar},
|
||||
}
|
||||
err = c.login()
|
||||
if err == nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// test logging in with correct password
|
||||
u, err = url.Parse("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
c.url = u
|
||||
err = c.login()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// in this test, the `Bypass authentication for clients on localhost` option in qBittorrent webui should be disabled
|
||||
func TestAuthorized(t *testing.T) {
|
||||
// init client
|
||||
u, err := url.Parse("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var c = &client{
|
||||
url: u,
|
||||
client: http.Client{Jar: jar},
|
||||
}
|
||||
|
||||
// test without logging in, which should be unauthorized
|
||||
authorized := c.authorized()
|
||||
if authorized {
|
||||
t.Error("Should not be authorized")
|
||||
}
|
||||
|
||||
// test after logging in
|
||||
err = c.login()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
authorized = c.authorized()
|
||||
if !authorized {
|
||||
t.Error("Should be authorized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
_, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = New("http://admin:wrong_password@127.0.0.1:8080/")
|
||||
if err == nil {
|
||||
t.Error("Should get an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
// init client
|
||||
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// test add
|
||||
err = c.login()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = c.AddFromLink(
|
||||
"https://releases.ubuntu.com/22.04/ubuntu-22.04.1-desktop-amd64.iso.torrent",
|
||||
"D:\\qBittorrentDownload\\alist",
|
||||
"uuid-1",
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = c.AddFromLink(
|
||||
"magnet:?xt=urn:btih:375ae3280cd80a8e9d7212e11dfaf7c45069dd35&dn=archlinux-2023.02.01-x86_64.iso",
|
||||
"D:\\qBittorrentDownload\\alist",
|
||||
"uuid-2",
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInfo(t *testing.T) {
|
||||
// init client
|
||||
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = c.GetInfo("uuid-1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFiles(t *testing.T) {
|
||||
// init client
|
||||
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
files, err := c.GetFiles("uuid-1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(files) != 1 {
|
||||
t.Error("should have exactly one file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
// init client
|
||||
c, err := New("http://admin:adminadmin@127.0.0.1:8080/")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = c.Delete("uuid-2")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/pkg/task"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
tsk *task.Task[string]
|
||||
tempDir string
|
||||
dstDirPath string
|
||||
finish chan struct{}
|
||||
}
|
||||
|
||||
func (m *Monitor) Loop() error {
|
||||
var (
|
||||
err error
|
||||
completed bool
|
||||
)
|
||||
m.finish = make(chan struct{})
|
||||
outer:
|
||||
for {
|
||||
select {
|
||||
case <-m.tsk.Ctx.Done():
|
||||
err = qbclient.Delete(m.tsk.ID)
|
||||
return err
|
||||
case <-time.After(time.Second * 2):
|
||||
completed, err = m.update()
|
||||
if completed {
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.tsk.SetStatus("qbittorrent download completed, transferring")
|
||||
<-m.finish
|
||||
m.tsk.SetStatus("completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Monitor) update() (bool, error) {
|
||||
info, err := qbclient.GetInfo(m.tsk.ID)
|
||||
if err != nil {
|
||||
m.tsk.SetStatus("qbittorrent " + string(info.State))
|
||||
return true, err
|
||||
}
|
||||
|
||||
progress := float64(info.Completed) / float64(info.Size) * 100
|
||||
m.tsk.SetProgress(int(progress))
|
||||
switch info.State {
|
||||
case UPLOADING:
|
||||
case PAUSEDUP:
|
||||
case QUEUEDUP:
|
||||
case STALLEDUP:
|
||||
case FORCEDUP:
|
||||
case CHECKINGUP:
|
||||
err = m.complete()
|
||||
return true, errors.WithMessage(err, "failed to transfer file")
|
||||
case ALLOCATING:
|
||||
case DOWNLOADING:
|
||||
case METADL:
|
||||
case PAUSEDDL:
|
||||
case QUEUEDDL:
|
||||
case STALLEDDL:
|
||||
case CHECKINGDL:
|
||||
case FORCEDDL:
|
||||
case CHECKINGRESUMEDATA:
|
||||
case MOVING:
|
||||
case UNKNOWN: // or maybe should return an error for UNKNOWN?
|
||||
m.tsk.SetStatus("qbittorrent downloading")
|
||||
return false, nil
|
||||
case ERROR:
|
||||
case MISSINGFILES:
|
||||
return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.State)
|
||||
}
|
||||
return true, errors.New("unknown error occurred downloading qbittorrent") // should never happen
|
||||
}
|
||||
|
||||
var TransferTaskManager = task.NewTaskManager(3, func(k *uint64) {
|
||||
atomic.AddUint64(k, 1)
|
||||
})
|
||||
|
||||
func (m *Monitor) complete() error {
|
||||
// check dstDir again
|
||||
storage, dstDirActualPath, err := op.GetStorageAndActualPath(m.dstDirPath)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed get storage")
|
||||
}
|
||||
// get files
|
||||
files, err := qbclient.GetFiles(m.tsk.ID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get files of %s", m.tsk.ID)
|
||||
}
|
||||
log.Debugf("files len: %d", len(files))
|
||||
// upload files
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(files))
|
||||
go func() {
|
||||
wg.Wait()
|
||||
err := os.RemoveAll(m.tempDir)
|
||||
m.finish <- struct{}{}
|
||||
if err != nil {
|
||||
log.Errorf("failed to remove qbittorrent temp dir: %+v", err.Error())
|
||||
}
|
||||
}()
|
||||
for _, file := range files {
|
||||
filePath := filepath.Join(m.tempDir, file.Name)
|
||||
TransferTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
|
||||
Name: fmt.Sprintf("transfer %s to [%s](%s)", filePath, storage.GetStorage().MountPath, dstDirActualPath),
|
||||
Func: func(tsk *task.Task[uint64]) error {
|
||||
defer wg.Done()
|
||||
size := file.Size
|
||||
mimetype := utils.GetMimeType(filePath)
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open file %s", filePath)
|
||||
}
|
||||
stream := &model.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: path.Base(filePath),
|
||||
Size: size,
|
||||
Modified: time.Now(),
|
||||
IsFolder: false,
|
||||
},
|
||||
ReadCloser: f,
|
||||
Mimetype: mimetype,
|
||||
}
|
||||
newDistDir := filepath.Join(dstDirActualPath, file.Name)
|
||||
return op.Put(tsk.Ctx, storage, newDistDir, stream, tsk.SetProgress)
|
||||
},
|
||||
}))
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package qbittorrent
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/setting"
|
||||
"github.com/alist-org/alist/v3/pkg/task"
|
||||
)
|
||||
|
||||
var DownTaskManager = task.NewTaskManager[string](3)
|
||||
var qbclient Client
|
||||
|
||||
func InitClient() error {
|
||||
var err error
|
||||
qbclient = nil
|
||||
|
||||
url := setting.GetStr(conf.QbittorrentUrl)
|
||||
qbclient, err = New(url)
|
||||
return err
|
||||
}
|
||||
|
||||
func IsQbittorrentReady() bool {
|
||||
return qbclient != nil
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package handles
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
"github.com/alist-org/alist/v3/internal/qbittorrent"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SetQbittorrentReq struct {
|
||||
Url string `json:"url" form:"url"`
|
||||
}
|
||||
|
||||
func SetQbittorrent(c *gin.Context) {
|
||||
var req SetQbittorrentReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
items := []model.SettingItem{
|
||||
{Key: conf.QbittorrentUrl, Value: req.Url, Type: conf.TypeString, Group: model.QBITTORRENT, Flag: model.PRIVATE},
|
||||
}
|
||||
if err := op.SaveSettingItems(items); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
if err := qbittorrent.InitClient(); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
common.SuccessResp(c, "ok")
|
||||
}
|
||||
|
||||
type AddQbittorrentReq struct {
|
||||
Urls []string `json:"urls"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func AddQbittorrent(c *gin.Context) {
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if !user.CanAddQbittorrentTasks() {
|
||||
common.ErrorStrResp(c, "permission denied", 403)
|
||||
return
|
||||
}
|
||||
if !qbittorrent.IsQbittorrentReady() {
|
||||
common.ErrorStrResp(c, "qbittorrent not ready", 500)
|
||||
return
|
||||
}
|
||||
var req AddQbittorrentReq
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
reqPath, err := user.JoinPath(req.Path)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 403)
|
||||
return
|
||||
}
|
||||
for _, url := range req.Urls {
|
||||
err := qbittorrent.AddURL(c, url, reqPath)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
common.SuccessResp(c)
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/alist-org/alist/v3/internal/aria2"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/alist-org/alist/v3/internal/qbittorrent"
|
||||
"github.com/alist-org/alist/v3/pkg/task"
|
||||
"github.com/alist-org/alist/v3/server/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -19,9 +20,19 @@ type TaskInfo struct {
|
|||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func getTaskInfoUint(task *task.Task[uint64]) TaskInfo {
|
||||
type K2Str[K comparable] func(k K) string
|
||||
|
||||
func uint64K2Str(k uint64) string {
|
||||
return strconv.FormatUint(k, 10)
|
||||
}
|
||||
|
||||
func strK2Str(str string) string {
|
||||
return str
|
||||
}
|
||||
|
||||
func getTaskInfo[K comparable](task *task.Task[K], k2Str K2Str[K]) TaskInfo {
|
||||
return TaskInfo{
|
||||
ID: strconv.FormatUint(task.ID, 10),
|
||||
ID: k2Str(task.ID),
|
||||
Name: task.Name,
|
||||
State: task.GetState(),
|
||||
Status: task.GetStatus(),
|
||||
|
@ -30,183 +41,68 @@ func getTaskInfoUint(task *task.Task[uint64]) TaskInfo {
|
|||
}
|
||||
}
|
||||
|
||||
func getTaskInfoStr(task *task.Task[string]) TaskInfo {
|
||||
return TaskInfo{
|
||||
ID: task.ID,
|
||||
Name: task.Name,
|
||||
State: task.GetState(),
|
||||
Status: task.GetStatus(),
|
||||
Progress: task.GetProgress(),
|
||||
Error: task.GetErrMsg(),
|
||||
}
|
||||
}
|
||||
|
||||
func getTaskInfosUint(tasks []*task.Task[uint64]) []TaskInfo {
|
||||
func getTaskInfos[K comparable](tasks []*task.Task[K], k2Str K2Str[K]) []TaskInfo {
|
||||
var infos []TaskInfo
|
||||
for _, t := range tasks {
|
||||
infos = append(infos, getTaskInfoUint(t))
|
||||
infos = append(infos, getTaskInfo(t, k2Str))
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
func getTaskInfosStr(tasks []*task.Task[string]) []TaskInfo {
|
||||
var infos []TaskInfo
|
||||
for _, t := range tasks {
|
||||
infos = append(infos, getTaskInfoStr(t))
|
||||
}
|
||||
return infos
|
||||
type Str2K[K comparable] func(str string) (K, error)
|
||||
|
||||
func str2Uint64K(str string) (uint64, error) {
|
||||
return strconv.ParseUint(str, 10, 64)
|
||||
}
|
||||
|
||||
func UndoneDownTask(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfosStr(aria2.DownTaskManager.ListUndone()))
|
||||
func str2StrK(str string) (string, error) {
|
||||
return str, nil
|
||||
}
|
||||
|
||||
func DoneDownTask(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfosStr(aria2.DownTaskManager.ListDone()))
|
||||
}
|
||||
|
||||
func CancelDownTask(c *gin.Context) {
|
||||
tid := c.Query("tid")
|
||||
if err := aria2.DownTaskManager.Cancel(tid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
func taskRoute[K comparable](g *gin.RouterGroup, manager *task.Manager[K], k2Str K2Str[K], str2K Str2K[K]) {
|
||||
g.GET("/undone", func(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfos(manager.ListUndone(), k2Str))
|
||||
})
|
||||
g.GET("/done", func(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfos(manager.ListDone(), k2Str))
|
||||
})
|
||||
g.POST("/cancel", func(c *gin.Context) {
|
||||
tid := c.Query("tid")
|
||||
id, err := str2K(tid)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := manager.Cancel(id); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
})
|
||||
g.POST("/delete", func(c *gin.Context) {
|
||||
tid := c.Query("tid")
|
||||
id, err := str2K(tid)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := manager.Remove(id); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
})
|
||||
g.POST("/clear_done", func(c *gin.Context) {
|
||||
manager.ClearDone()
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteDownTask(c *gin.Context) {
|
||||
tid := c.Query("tid")
|
||||
if err := aria2.DownTaskManager.Remove(tid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func ClearDoneDownTasks(c *gin.Context) {
|
||||
aria2.DownTaskManager.ClearDone()
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
|
||||
func UndoneTransferTask(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfosUint(aria2.TransferTaskManager.ListUndone()))
|
||||
}
|
||||
|
||||
func DoneTransferTask(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfosUint(aria2.TransferTaskManager.ListDone()))
|
||||
}
|
||||
|
||||
func CancelTransferTask(c *gin.Context) {
|
||||
id := c.Query("tid")
|
||||
tid, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := aria2.TransferTaskManager.Cancel(tid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteTransferTask(c *gin.Context) {
|
||||
id := c.Query("tid")
|
||||
tid, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := aria2.TransferTaskManager.Remove(tid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func ClearDoneTransferTasks(c *gin.Context) {
|
||||
aria2.TransferTaskManager.ClearDone()
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
|
||||
func UndoneUploadTask(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfosUint(fs.UploadTaskManager.ListUndone()))
|
||||
}
|
||||
|
||||
func DoneUploadTask(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfosUint(fs.UploadTaskManager.ListDone()))
|
||||
}
|
||||
|
||||
func CancelUploadTask(c *gin.Context) {
|
||||
id := c.Query("tid")
|
||||
tid, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := fs.UploadTaskManager.Cancel(tid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteUploadTask(c *gin.Context) {
|
||||
id := c.Query("tid")
|
||||
tid, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := fs.UploadTaskManager.Remove(tid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func ClearDoneUploadTasks(c *gin.Context) {
|
||||
fs.UploadTaskManager.ClearDone()
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
|
||||
func UndoneCopyTask(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfosUint(fs.CopyTaskManager.ListUndone()))
|
||||
}
|
||||
|
||||
func DoneCopyTask(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfosUint(fs.CopyTaskManager.ListDone()))
|
||||
}
|
||||
|
||||
func CancelCopyTask(c *gin.Context) {
|
||||
id := c.Query("tid")
|
||||
tid, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := fs.CopyTaskManager.Cancel(tid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteCopyTask(c *gin.Context) {
|
||||
id := c.Query("tid")
|
||||
tid, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 400)
|
||||
return
|
||||
}
|
||||
if err := fs.CopyTaskManager.Remove(tid); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
} else {
|
||||
common.SuccessResp(c)
|
||||
}
|
||||
}
|
||||
|
||||
func ClearDoneCopyTasks(c *gin.Context) {
|
||||
fs.CopyTaskManager.ClearDone()
|
||||
common.SuccessResp(c)
|
||||
func SetupTaskRoute(g *gin.RouterGroup) {
|
||||
taskRoute(g.Group("/aria2_down"), aria2.DownTaskManager, strK2Str, str2StrK)
|
||||
taskRoute(g.Group("/aria2_transfer"), aria2.TransferTaskManager, uint64K2Str, str2Uint64K)
|
||||
taskRoute(g.Group("/upload"), fs.UploadTaskManager, uint64K2Str, str2Uint64K)
|
||||
taskRoute(g.Group("/copy"), fs.CopyTaskManager, uint64K2Str, str2Uint64K)
|
||||
taskRoute(g.Group("/qbit_down"), qbittorrent.DownTaskManager, strK2Str, str2StrK)
|
||||
taskRoute(g.Group("/qbit_transfer"), qbittorrent.TransferTaskManager, uint64K2Str, str2Uint64K)
|
||||
}
|
||||
|
|
|
@ -89,28 +89,10 @@ func admin(g *gin.RouterGroup) {
|
|||
setting.POST("/delete", handles.DeleteSetting)
|
||||
setting.POST("/reset_token", handles.ResetToken)
|
||||
setting.POST("/set_aria2", handles.SetAria2)
|
||||
setting.POST("/set_qbittorrent", handles.SetQbittorrent)
|
||||
|
||||
task := g.Group("/task")
|
||||
task.GET("/down/undone", handles.UndoneDownTask)
|
||||
task.GET("/down/done", handles.DoneDownTask)
|
||||
task.POST("/down/cancel", handles.CancelDownTask)
|
||||
task.POST("/down/delete", handles.DeleteDownTask)
|
||||
task.POST("/down/clear_done", handles.ClearDoneDownTasks)
|
||||
task.GET("/transfer/undone", handles.UndoneTransferTask)
|
||||
task.GET("/transfer/done", handles.DoneTransferTask)
|
||||
task.POST("/transfer/cancel", handles.CancelTransferTask)
|
||||
task.POST("/transfer/delete", handles.DeleteTransferTask)
|
||||
task.POST("/transfer/clear_done", handles.ClearDoneTransferTasks)
|
||||
task.GET("/upload/undone", handles.UndoneUploadTask)
|
||||
task.GET("/upload/done", handles.DoneUploadTask)
|
||||
task.POST("/upload/cancel", handles.CancelUploadTask)
|
||||
task.POST("/upload/delete", handles.DeleteUploadTask)
|
||||
task.POST("/upload/clear_done", handles.ClearDoneUploadTasks)
|
||||
task.GET("/copy/undone", handles.UndoneCopyTask)
|
||||
task.GET("/copy/done", handles.DoneCopyTask)
|
||||
task.POST("/copy/cancel", handles.CancelCopyTask)
|
||||
task.POST("/copy/delete", handles.DeleteCopyTask)
|
||||
task.POST("/copy/clear_done", handles.ClearDoneCopyTasks)
|
||||
handles.SetupTaskRoute(task)
|
||||
|
||||
ms := g.Group("/message")
|
||||
ms.POST("/get", message.HttpInstance.GetHandle)
|
||||
|
@ -139,6 +121,7 @@ func _fs(g *gin.RouterGroup) {
|
|||
g.PUT("/form", middlewares.FsUp, handles.FsForm)
|
||||
g.POST("/link", middlewares.AuthAdmin, handles.Link)
|
||||
g.POST("/add_aria2", handles.AddAria2)
|
||||
g.POST("/add_qbit", handles.AddQbittorrent)
|
||||
}
|
||||
|
||||
func Cors(r *gin.Engine) {
|
||||
|
|
Loading…
Reference in New Issue