mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 09:30:27 +00:00
feat: reuse fetcher (#71)
This commit is contained in:
1 parent
17cb59c606
commit
1a3c8c026b
36 files changed
+475
-297
No files matched your search
@@ -0,0 +1,17 @@
|
||||
# Gopeed contributors guide
|
||||
|
||||
Firstly, thank you for your interest in contributing to Gopeed. This guide will help you better participate in the development of Gopeed.
|
||||
|
||||
## Branch description
|
||||
|
||||
This project only has one main branch, namely the `main` branch. If you want to participate in the development of Gopeed, please fork this project first, and then develop in your fork project. After development is completed, submit a PR to this project and merge it into the `main` branch.
|
||||
|
||||
## Local development
|
||||
|
||||
It is recommended to develop and debug through the web. First, start the backend service, the code is located in `cmd/api/main.go`, the default port of the service is `9999`, and then start the front-end project in `debug` mode to run.
|
||||
|
||||
## Translation
|
||||
|
||||
The internationalization files of Gopeed are located in the `ui/flutter/lib/i18n/langs` directory. You only need to add the corresponding language file in this directory.
|
||||
|
||||
> Tip: Please refer to `en_us.dart` for translation.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Gopeed 贡献指南
|
||||
|
||||
首先感谢您对贡献代码感兴趣,这份指南将帮助您更好的参与到 Gopeed 的开发中来。
|
||||
|
||||
## 分支说明
|
||||
|
||||
本项目只有一个主分支,即 `main` 分支,如果您想要参与到 Gopeed 的开发中来,请先 fork 本项目,然后在您的 fork 项目中进行开发,开发完成后再向本项目提交 PR,合并到 `main` 分支。
|
||||
|
||||
## 本地开发
|
||||
|
||||
建议通过 web 端进行开发调试,首先启动后端服务,代码位于 `cmd/api/main.go` ,服务启动默认端口为 `9999`,然后以 `debug` 模式启动前端项目即可运行。
|
||||
|
||||
## 翻译
|
||||
|
||||
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
|
||||
|
||||
> 请注意以 `en_us.dart` 为参照进行翻译。
|
||||
@@ -91,6 +91,10 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
||||
git clone [email protected]:monkeyWie/gopeed.git
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
|
||||
|
||||
### Build
|
||||
|
||||
#### Desktop
|
||||
@@ -144,8 +148,6 @@ flutter build apk
|
||||
|
||||
#### Web
|
||||
|
||||
Web platform communicates directly with the backend http server, no additional environment is required.
|
||||
|
||||
command:
|
||||
|
||||
```bash
|
||||
|
||||
+5
-3
@@ -91,6 +91,10 @@ docker-compose up -d
|
||||
git clone [email protected]:monkeyWie/gopeed.git
|
||||
```
|
||||
|
||||
### 贡献代码
|
||||
|
||||
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
|
||||
|
||||
### 编译
|
||||
|
||||
#### 桌面端
|
||||
@@ -142,9 +146,7 @@ cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
#### Web 端(推荐本地调试使用)
|
||||
|
||||
Web 端直接与后端 http 服务通讯,不需要额外准备环境。
|
||||
#### Web 端
|
||||
|
||||
构建命令:
|
||||
|
||||
|
||||
+2
-2
@@ -56,10 +56,10 @@ var (
|
||||
)
|
||||
|
||||
func printProgress(task *download.Task, title string) {
|
||||
rate := float64(task.Progress.Downloaded) / float64(task.Res.Size)
|
||||
rate := float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
|
||||
completeWidth := int(progressWidth * rate)
|
||||
speed := util.ByteFmt(task.Progress.Speed)
|
||||
totalSize := util.ByteFmt(task.Res.Size)
|
||||
totalSize := util.ByteFmt(task.Meta.Res.Size)
|
||||
sb.WriteString(fmt.Sprintf("\r%s [", title))
|
||||
for i := 0; i < progressWidth; i++ {
|
||||
if i < completeWidth {
|
||||
|
||||
@@ -6,24 +6,34 @@ import (
|
||||
)
|
||||
|
||||
// Fetcher defines the interface for a download protocol.
|
||||
// One fetcher for each download task
|
||||
type Fetcher interface {
|
||||
// Name return the name of the protocol.
|
||||
Name() string
|
||||
|
||||
Setup(ctl *controller.Controller) error
|
||||
// Resolve resource info from request
|
||||
Resolve(req *base.Request) (res *base.Resource, err error)
|
||||
Resolve(req *base.Request) error
|
||||
// Create ready to download, but not started
|
||||
Create(res *base.Resource, opts *base.Options) (err error)
|
||||
Start() (err error)
|
||||
Pause() (err error)
|
||||
Continue() (err error)
|
||||
Close() (err error)
|
||||
Create(opts *base.Options) error
|
||||
Start() error
|
||||
Pause() error
|
||||
Continue() error
|
||||
Close() error
|
||||
|
||||
// Meta returns the meta information of the download.
|
||||
Meta() *FetcherMeta
|
||||
// Progress returns the progress of the download.
|
||||
Progress() Progress
|
||||
// Wait for the download to complete, this method will block until the download is done.
|
||||
Wait() (err error)
|
||||
Wait() error
|
||||
}
|
||||
|
||||
// FetcherMeta defines the meta information of a fetcher.
|
||||
type FetcherMeta struct {
|
||||
Req *base.Request `json:"req"`
|
||||
Res *base.Resource `json:"res"`
|
||||
Opts *base.Options `json:"opts"`
|
||||
}
|
||||
|
||||
// FetcherBuilder defines the interface for a fetcher builder.
|
||||
@@ -36,11 +46,12 @@ type FetcherBuilder interface {
|
||||
// Store fetcher
|
||||
Store(fetcher Fetcher) (any, error)
|
||||
// Restore fetcher
|
||||
Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) Fetcher)
|
||||
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
|
||||
}
|
||||
|
||||
type DefaultFetcher struct {
|
||||
Ctl *controller.Controller
|
||||
Meta *FetcherMeta
|
||||
DoneCh chan error
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,10 @@ import (
|
||||
var client *torrent.Client
|
||||
|
||||
type Fetcher struct {
|
||||
Ctl *controller.Controller
|
||||
ctl *controller.Controller
|
||||
|
||||
torrent *torrent.Torrent
|
||||
res *base.Resource
|
||||
opts *base.Options
|
||||
meta *fetcher.FetcherMeta
|
||||
|
||||
ready atomic.Bool
|
||||
progress fetcher.Progress
|
||||
@@ -34,7 +33,10 @@ func (f *Fetcher) Name() string {
|
||||
}
|
||||
|
||||
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
|
||||
f.Ctl = ctl
|
||||
f.ctl = ctl
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
var once sync.Once
|
||||
once.Do(func() {
|
||||
cfg := torrent.NewDefaultClientConfig()
|
||||
@@ -66,46 +68,12 @@ func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Fetcher) Wait() (err error) {
|
||||
for {
|
||||
if f.ready.Load() {
|
||||
done := true
|
||||
for _, selectIndex := range f.opts.SelectFiles {
|
||||
file := f.torrent.Files()[selectIndex]
|
||||
if file.BytesCompleted() < file.Length() {
|
||||
done = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if done {
|
||||
// remove unselected files
|
||||
for i, file := range f.torrent.Files() {
|
||||
selected := false
|
||||
for _, selectIndex := range f.opts.SelectFiles {
|
||||
if i == selectIndex {
|
||||
selected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !selected {
|
||||
util.SafeRemove(filepath.Join(f.opts.Path, file.Path()))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
|
||||
if err = f.addTorrent(req.URL, true); err != nil {
|
||||
return
|
||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
if err := f.addTorrent(req.URL); err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.torrent.Drop()
|
||||
res = &base.Resource{
|
||||
Req: req,
|
||||
res := &base.Resource{
|
||||
Name: f.torrent.Name(),
|
||||
Range: true,
|
||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||
@@ -119,36 +87,39 @@ func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
|
||||
}
|
||||
res.Size += file.Length()
|
||||
}
|
||||
return
|
||||
f.meta.Req = req
|
||||
f.meta.Res = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) (err error) {
|
||||
f.res = res
|
||||
f.opts = opts
|
||||
if len(f.opts.SelectFiles) == 0 {
|
||||
f.opts.SelectFiles = make([]int, 0)
|
||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||
f.meta.Opts = opts
|
||||
if len(opts.SelectFiles) == 0 {
|
||||
opts.SelectFiles = make([]int, 0)
|
||||
for i := range f.torrent.Files() {
|
||||
f.opts.SelectFiles = append(f.opts.SelectFiles, i)
|
||||
opts.SelectFiles = append(opts.SelectFiles, i)
|
||||
}
|
||||
}
|
||||
if f.opts.Path != "" {
|
||||
f.torrentPaths[res.Hash] = f.opts.Path
|
||||
if opts.Path != "" {
|
||||
f.torrentPaths[f.meta.Res.Hash] = f.meta.Opts.Path
|
||||
}
|
||||
|
||||
f.progress = make(fetcher.Progress, len(f.opts.SelectFiles))
|
||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||
f.ready.Store(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Start() (err error) {
|
||||
if err = f.addTorrent(f.res.Req.URL, false); err != nil {
|
||||
return
|
||||
if f.torrent == nil {
|
||||
if err = f.addTorrent(f.meta.Req.URL); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
files := f.torrent.Files()
|
||||
if len(f.opts.SelectFiles) == len(files) {
|
||||
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||
f.torrent.DownloadAll()
|
||||
} else {
|
||||
for _, selectIndex := range f.opts.SelectFiles {
|
||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||
file := files[selectIndex]
|
||||
file.Download()
|
||||
}
|
||||
@@ -171,19 +142,56 @@ func (f *Fetcher) Close() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Wait() (err error) {
|
||||
for {
|
||||
if f.ready.Load() {
|
||||
done := true
|
||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||
file := f.torrent.Files()[selectIndex]
|
||||
if file.BytesCompleted() < file.Length() {
|
||||
done = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if done {
|
||||
// remove unselected files
|
||||
for i, file := range f.torrent.Files() {
|
||||
selected := false
|
||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||
if i == selectIndex {
|
||||
selected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !selected {
|
||||
util.SafeRemove(filepath.Join(f.meta.Opts.Path, file.Path()))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||
return f.meta
|
||||
}
|
||||
|
||||
func (f *Fetcher) Progress() fetcher.Progress {
|
||||
if !f.ready.Load() {
|
||||
return f.progress
|
||||
}
|
||||
for i := range f.progress {
|
||||
selectIndex := f.opts.SelectFiles[i]
|
||||
selectIndex := f.meta.Opts.SelectFiles[i]
|
||||
file := f.torrent.Files()[selectIndex]
|
||||
f.progress[i] = file.BytesCompleted()
|
||||
}
|
||||
return f.progress
|
||||
}
|
||||
|
||||
func (f *Fetcher) addTorrent(url string, resolve bool) (err error) {
|
||||
func (f *Fetcher) addTorrent(url string) (err error) {
|
||||
schema := util.ParseSchema(url)
|
||||
if schema == "MAGNET" {
|
||||
f.torrent, err = client.AddMagnet(url)
|
||||
@@ -194,7 +202,7 @@ func (f *Fetcher) addTorrent(url string, resolve bool) (err error) {
|
||||
return
|
||||
}
|
||||
var cfg config
|
||||
exist, err := f.Ctl.GetConfig(&cfg)
|
||||
exist, err := f.ctl.GetConfig(&cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -206,9 +214,6 @@ func (f *Fetcher) addTorrent(url string, resolve bool) (err error) {
|
||||
f.torrent.AddTrackers(announceList)
|
||||
}
|
||||
<-f.torrent.GotInfo()
|
||||
if resolve {
|
||||
return
|
||||
}
|
||||
f.ready.Store(true)
|
||||
return
|
||||
}
|
||||
@@ -230,6 +235,6 @@ func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) {
|
||||
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func TestFetcher_Config(t *testing.T) {
|
||||
}
|
||||
|
||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
res, err := fetcher.Resolve(&base.Request{
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -27,7 +27,6 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
}
|
||||
|
||||
want := &base.Resource{
|
||||
Req: res.Req,
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
Range: true,
|
||||
@@ -39,8 +38,8 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
},
|
||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||
}
|
||||
if !reflect.DeepEqual(want, res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", res, want)
|
||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/monkeyWie/gopeed/internal/controller"
|
||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
||||
"github.com/monkeyWie/gopeed/pkg/base"
|
||||
fhttp "github.com/monkeyWie/gopeed/pkg/protocol/http"
|
||||
@@ -16,9 +17,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -51,10 +50,10 @@ func newChunk(begin int64, end int64) *chunk {
|
||||
}
|
||||
|
||||
type Fetcher struct {
|
||||
*fetcher.DefaultFetcher
|
||||
ctl *controller.Controller
|
||||
doneCh chan error
|
||||
|
||||
res *base.Resource
|
||||
opts *base.Options
|
||||
meta *fetcher.FetcherMeta
|
||||
status base.Status
|
||||
chunks []*chunk
|
||||
|
||||
@@ -68,25 +67,34 @@ func (f *Fetcher) Name() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
||||
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
|
||||
f.ctl = ctl
|
||||
f.doneCh = make(chan error, 1)
|
||||
f.pauseCh = make(chan interface{}, 1)
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
httpReq, err := buildRequest(nil, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
client := buildClient()
|
||||
// 只访问一个字节,测试资源是否支持Range请求
|
||||
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
|
||||
httpResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
// 拿到响应头就关闭,不用加defer
|
||||
httpResp.Body.Close()
|
||||
res := &base.Resource{
|
||||
Req: req,
|
||||
Range: false,
|
||||
Files: []*base.FileInfo{},
|
||||
}
|
||||
@@ -98,7 +106,7 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
||||
if contentTotal != "" {
|
||||
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
res.Size = parse
|
||||
}
|
||||
@@ -108,12 +116,12 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
||||
if contentLength != "" {
|
||||
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
res.Size = parse
|
||||
}
|
||||
} else {
|
||||
return nil, NewRequestError(httpResp.StatusCode, httpResp.Status)
|
||||
return NewRequestError(httpResp.StatusCode, httpResp.Status)
|
||||
}
|
||||
file := &base.FileInfo{
|
||||
Size: res.Size,
|
||||
@@ -127,27 +135,28 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
||||
}
|
||||
}
|
||||
// Get file filePath by URL
|
||||
if file.Name == "" && strings.Count(req.URL, "/") > 2 {
|
||||
file.Name = filepath.Base(req.URL)
|
||||
if file.Name == "" {
|
||||
file.Name = path.Base(httpReq.URL.Path)
|
||||
}
|
||||
// unknown file filePath
|
||||
if file.Name == "" {
|
||||
if file.Name == "" || file.Name == "/" || file.Name == "." {
|
||||
file.Name = "unknown"
|
||||
}
|
||||
res.Files = append(res.Files, file)
|
||||
res.Name = file.Name
|
||||
return res, nil
|
||||
f.meta.Req = req
|
||||
f.meta.Res = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
|
||||
f.res = res
|
||||
f.opts = opts
|
||||
func (f *Fetcher) Create(opts *base.Options) error {
|
||||
f.meta.Opts = opts
|
||||
f.status = base.DownloadStatusReady
|
||||
|
||||
if err := base.ParseReqExtra[fhttp.ReqExtra](res.Req); err != nil {
|
||||
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := base.ParseOptsExtra[fhttp.OptsExtra](opts); err != nil {
|
||||
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.Extra == nil {
|
||||
@@ -156,7 +165,7 @@ func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
|
||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||
if extra.Connections == 0 {
|
||||
var cfg config
|
||||
exist, err := f.Ctl.GetConfig(&cfg)
|
||||
exist, err := f.ctl.GetConfig(&cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -172,7 +181,7 @@ func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
|
||||
func (f *Fetcher) Start() (err error) {
|
||||
// 创建文件
|
||||
name := f.filepath()
|
||||
f.file, err = f.Ctl.Touch(name, f.res.Size)
|
||||
f.file, err = f.ctl.Touch(name, f.meta.Res.Size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -215,6 +224,10 @@ func (f *Fetcher) Close() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||
return f.meta
|
||||
}
|
||||
|
||||
func (f *Fetcher) Progress() fetcher.Progress {
|
||||
p := make(fetcher.Progress, 0)
|
||||
if len(f.chunks) > 0 {
|
||||
@@ -227,14 +240,18 @@ func (f *Fetcher) Progress() fetcher.Progress {
|
||||
return p
|
||||
}
|
||||
|
||||
func (f *Fetcher) Wait() (err error) {
|
||||
return <-f.doneCh
|
||||
}
|
||||
|
||||
func (f *Fetcher) filepath() string {
|
||||
return util.Filepath(f.opts.Path, f.res.Files[0].Name, f.opts.Name)
|
||||
return util.Filepath(f.meta.Opts.Path, f.meta.Res.Files[0].Name, f.meta.Opts.Name)
|
||||
}
|
||||
|
||||
func (f *Fetcher) fetch() {
|
||||
f.ctx, f.cancel = context.WithCancel(context.Background())
|
||||
eg, _ := errgroup.WithContext(f.ctx)
|
||||
for i := 0; i < f.opts.Extra.(*fhttp.OptsExtra).Connections; i++ {
|
||||
for i := 0; i < f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections; i++ {
|
||||
i := i
|
||||
eg.Go(func() error {
|
||||
return f.fetchChunk(i)
|
||||
@@ -253,7 +270,7 @@ func (f *Fetcher) fetch() {
|
||||
} else {
|
||||
f.status = base.DownloadStatusDone
|
||||
}
|
||||
f.DoneCh <- err
|
||||
f.doneCh <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -261,7 +278,7 @@ func (f *Fetcher) fetch() {
|
||||
func (f *Fetcher) fetchChunk(index int) (err error) {
|
||||
chunk := f.chunks[index]
|
||||
|
||||
httpReq, err := buildRequest(f.ctx, f.res.Req)
|
||||
httpReq, err := buildRequest(f.ctx, f.meta.Req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -283,7 +300,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
|
||||
resp *http.Response
|
||||
retry bool
|
||||
)
|
||||
if f.res.Range {
|
||||
if f.meta.Res.Range {
|
||||
httpReq.Header.Set(base.HttpHeaderRange,
|
||||
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
||||
} else {
|
||||
@@ -337,7 +354,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
isComplete := f.res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
|
||||
isComplete := f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
|
||||
if f.status == base.DownloadStatusPause && !isComplete {
|
||||
chunk.Status = base.DownloadStatusPause
|
||||
return
|
||||
@@ -348,10 +365,10 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||
if f.res.Range {
|
||||
connections := f.opts.Extra.(*fhttp.OptsExtra).Connections
|
||||
if f.meta.Res.Range {
|
||||
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
||||
// 每个连接平均需要下载的分块大小
|
||||
chunkSize := f.res.Size / int64(connections)
|
||||
chunkSize := f.meta.Res.Size / int64(connections)
|
||||
chunks = make([]*chunk, connections)
|
||||
for i := 0; i < connections; i++ {
|
||||
var (
|
||||
@@ -360,7 +377,7 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||
)
|
||||
if i == connections-1 {
|
||||
// 最后一个分块需要保证把文件下载完
|
||||
end = f.res.Size - 1
|
||||
end = f.meta.Res.Size - 1
|
||||
} else {
|
||||
end = begin + chunkSize - 1
|
||||
}
|
||||
@@ -440,10 +457,7 @@ func (fb *FetcherBuilder) Schemes() []string {
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
||||
return &Fetcher{
|
||||
DefaultFetcher: new(fetcher.DefaultFetcher),
|
||||
pauseCh: make(chan interface{}),
|
||||
}
|
||||
return &Fetcher{}
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
@@ -453,16 +467,15 @@ func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) {
|
||||
return &fetcherData{}, func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher {
|
||||
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||
fd := v.(*fetcherData)
|
||||
fb := &FetcherBuilder{}
|
||||
fetcher := fb.Build().(*Fetcher)
|
||||
fetcher.res = res
|
||||
fetcher.opts = opts
|
||||
fetcher.meta = meta
|
||||
fetcher.status = base.DownloadStatusPause
|
||||
base.ParseReqExtra[fhttp.ReqExtra](res.Req)
|
||||
base.ParseOptsExtra[fhttp.OptsExtra](opts)
|
||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||
if len(fd.Chunks) == 0 {
|
||||
fetcher.chunks = fetcher.splitChunk()
|
||||
} else {
|
||||
|
||||
@@ -43,15 +43,14 @@ func testResolve(startTestServer func() net.Listener, want *base.Resource, t *te
|
||||
listener := startTestServer()
|
||||
defer listener.Close()
|
||||
fetcher := buildFetcher()
|
||||
res, err := fetcher.Resolve(&base.Request{
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Req = nil
|
||||
if !reflect.DeepEqual(want, res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", res, want)
|
||||
if !reflect.DeepEqual(want, fetcher.meta.Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +113,7 @@ func TestFetcher_DownloadResume(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFetcher_Config(t *testing.T) {
|
||||
fetcher, _, _ := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t)
|
||||
fetcher := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -130,13 +129,12 @@ func TestFetcher_Config(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func downloadReady(listener net.Listener, connections int, t *testing.T) (fetcher fetcher.Fetcher, res *base.Resource, opts *base.Options) {
|
||||
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||
}
|
||||
|
||||
func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int, t *testing.T) (fetcher fetcher.Fetcher, res *base.Resource, opts *base.Options) {
|
||||
fetcher = f
|
||||
res, err := fetcher.Resolve(&base.Request{
|
||||
func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||
err := f.Resolve(&base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -148,20 +146,20 @@ func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int,
|
||||
Connections: connections,
|
||||
}
|
||||
}
|
||||
opts = &base.Options{
|
||||
opts := &base.Options{
|
||||
Name: test.DownloadName,
|
||||
Path: test.Dir,
|
||||
Extra: extra,
|
||||
}
|
||||
err = fetcher.Create(res, opts)
|
||||
err = f.Create(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
return f
|
||||
}
|
||||
|
||||
func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
||||
fetcher := downloadReady(listener, connections, t)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -178,8 +176,8 @@ func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
||||
}
|
||||
|
||||
func downloadPost(listener net.Listener, connections int, t *testing.T) {
|
||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
||||
fetcher.(*Fetcher).res.Req.Extra = &http.ReqExtra{
|
||||
fetcher := downloadReady(listener, connections, t)
|
||||
fetcher.Meta().Req.Extra = &http.ReqExtra{
|
||||
Method: "POST",
|
||||
Header: map[string]string{
|
||||
"Authorization": "Bearer 123456",
|
||||
@@ -202,7 +200,7 @@ func downloadPost(listener net.Listener, connections int, t *testing.T) {
|
||||
}
|
||||
|
||||
func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
||||
fetcher := downloadReady(listener, connections, t)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -227,7 +225,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
||||
}
|
||||
|
||||
func downloadError(listener net.Listener, connections int, t *testing.T) {
|
||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
||||
fetcher := downloadReady(listener, connections, t)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -239,7 +237,7 @@ func downloadError(listener net.Listener, connections int, t *testing.T) {
|
||||
}
|
||||
|
||||
func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
fetcher, res, opts := downloadReady(listener, connections, t)
|
||||
fetcher := downloadReady(listener, connections, t)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -255,7 +253,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
fetcher.Pause()
|
||||
|
||||
_, f := fb.Restore()
|
||||
f(res, opts, data)
|
||||
f(fetcher.Meta(), data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ type Request struct {
|
||||
|
||||
// 资源信息
|
||||
type Resource struct {
|
||||
Req *Request `json:"req"`
|
||||
// 资源名称
|
||||
Name string `json:"name"`
|
||||
// 资源总大小
|
||||
|
||||
+58
-34
@@ -2,6 +2,7 @@ package download
|
||||
|
||||
import (
|
||||
"errors"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"github.com/monkeyWie/gopeed/internal/controller"
|
||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
||||
"github.com/monkeyWie/gopeed/pkg/base"
|
||||
@@ -31,6 +32,7 @@ type Progress struct {
|
||||
|
||||
type Downloader struct {
|
||||
fetchBuilders map[string]fetcher.FetcherBuilder
|
||||
fetcherMap map[string]fetcher.Fetcher
|
||||
storage Storage
|
||||
tasks []*Task
|
||||
listener Listener
|
||||
@@ -47,7 +49,9 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
||||
cfg.Init()
|
||||
|
||||
d := &Downloader{
|
||||
fetchBuilders: make(map[string]fetcher.FetcherBuilder),
|
||||
fetchBuilders: make(map[string]fetcher.FetcherBuilder),
|
||||
fetcherMap: make(map[string]fetcher.Fetcher),
|
||||
|
||||
refreshInterval: cfg.RefreshInterval,
|
||||
storage: cfg.Storage,
|
||||
lock: &sync.Mutex{},
|
||||
@@ -128,18 +132,16 @@ func (d *Downloader) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Downloader) buildFetcher(url string) (fetcher.FetcherBuilder, fetcher.Fetcher, error) {
|
||||
func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
|
||||
schema := util.ParseSchema(url)
|
||||
fetchBuilder, ok := d.fetchBuilders[schema]
|
||||
if !ok {
|
||||
fetchBuilder, ok = d.fetchBuilders[util.FileSchema]
|
||||
}
|
||||
if ok {
|
||||
fetcher := fetchBuilder.Build()
|
||||
d.setupFetcher(fetcher)
|
||||
return fetchBuilder, fetcher, nil
|
||||
return fetchBuilder, nil
|
||||
}
|
||||
return nil, nil, errors.New("unsupported protocol")
|
||||
return nil, errors.New("unsupported protocol")
|
||||
}
|
||||
|
||||
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
||||
@@ -150,26 +152,49 @@ func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
||||
fetcher.Setup(ctl)
|
||||
}
|
||||
|
||||
func (d *Downloader) Resolve(req *base.Request) (*base.Resource, error) {
|
||||
_, fetcher, err := d.buildFetcher(req.URL)
|
||||
func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
|
||||
fb, err := d.parseFb(req.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
res, err := fetcher.Resolve(req)
|
||||
fetcher := fb.Build()
|
||||
d.setupFetcher(fetcher)
|
||||
err = fetcher.Resolve(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
return res, nil
|
||||
rrId, err := gonanoid.New()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
d.fetcherMap[rrId] = fetcher
|
||||
rr = &ResolveResult{
|
||||
ID: rrId,
|
||||
Res: fetcher.Meta().Res,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId string, err error) {
|
||||
func (d *Downloader) DirectCreate(req *base.Request, opts *base.Options) (taskId string, err error) {
|
||||
rr, err := d.Resolve(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return d.Create(rr.ID, opts)
|
||||
}
|
||||
|
||||
func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) {
|
||||
if opts == nil {
|
||||
opts = &base.Options{}
|
||||
}
|
||||
fetcher, ok := d.fetcherMap[rrId]
|
||||
if !ok {
|
||||
return "", errors.New("invalid resource id")
|
||||
}
|
||||
delete(d.fetcherMap, rrId)
|
||||
if len(opts.SelectFiles) == 0 {
|
||||
opts.SelectFiles = make([]int, 0)
|
||||
for i := range res.Files {
|
||||
for i := range fetcher.Meta().Res.Files {
|
||||
opts.SelectFiles = append(opts.SelectFiles, i)
|
||||
}
|
||||
}
|
||||
@@ -183,25 +208,24 @@ func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId stri
|
||||
}
|
||||
}
|
||||
|
||||
fetcherBuilder, fetcher, err := d.buildFetcher(res.Req.URL)
|
||||
fb, err := d.parseFb(fetcher.Meta().Req.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = fetcher.Create(res, opts)
|
||||
err = fetcher.Create(opts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
task := NewTask()
|
||||
task.fetcherBuilder = fetcherBuilder
|
||||
task.fetcherBuilder = fb
|
||||
task.fetcher = fetcher
|
||||
task.Res = res
|
||||
task.Opts = opts
|
||||
task.Meta = fetcher.Meta()
|
||||
task.Progress = &Progress{}
|
||||
task.Status = base.DownloadStatusRunning
|
||||
// calculate select files size
|
||||
for _, selectIndex := range opts.SelectFiles {
|
||||
task.Size += res.Files[selectIndex].Size
|
||||
task.Size += fetcher.Meta().Res.Files[selectIndex].Size
|
||||
}
|
||||
initTask(task)
|
||||
task.timer.Start()
|
||||
@@ -303,10 +327,10 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
|
||||
}
|
||||
if force {
|
||||
names := make([]string, 0)
|
||||
for _, file := range task.Res.Files {
|
||||
names = append(names, util.Filepath(file.Path, file.Name, task.Opts.Name))
|
||||
for _, file := range task.Meta.Res.Files {
|
||||
names = append(names, util.Filepath(file.Path, file.Name, task.Meta.Opts.Name))
|
||||
}
|
||||
util.SafeRemoveAll(task.Opts.Path, names)
|
||||
util.SafeRemoveAll(task.Meta.Opts.Path, names)
|
||||
}
|
||||
d.emit(EventKeyDelete, task)
|
||||
task = nil
|
||||
@@ -403,7 +427,7 @@ func (d *Downloader) watch(task *Task) {
|
||||
task.Progress.Used = task.timer.Used()
|
||||
if task.Size == 0 {
|
||||
task.Size = task.fetcher.Progress().TotalDownloaded()
|
||||
task.Res.Size = task.Size
|
||||
task.Meta.Res.Size = task.Size
|
||||
}
|
||||
used := task.Progress.Used / int64(time.Second)
|
||||
if used == 0 {
|
||||
@@ -420,19 +444,19 @@ func (d *Downloader) watch(task *Task) {
|
||||
|
||||
func (d *Downloader) restoreFetcher(task *Task) error {
|
||||
if task.fetcher == nil {
|
||||
fetcherBuilder, _, err := d.buildFetcher(task.Res.Req.URL)
|
||||
fb, err := d.parseFb(task.Meta.Req.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.fetcherBuilder = fetcherBuilder
|
||||
task.fetcherBuilder = fb
|
||||
err = func() error {
|
||||
v, f := fetcherBuilder.Restore()
|
||||
v, f := fb.Restore()
|
||||
if v != nil {
|
||||
err := d.storage.Pop(bucketSave, task.ID, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.fetcher = f(task.Res, task.Opts, v)
|
||||
task.fetcher = f(task.Meta, v)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
@@ -440,10 +464,10 @@ func (d *Downloader) restoreFetcher(task *Task) error {
|
||||
// TODO log
|
||||
}
|
||||
if task.fetcher == nil {
|
||||
task.fetcher = fetcherBuilder.Build()
|
||||
task.fetcher = fb.Build()
|
||||
}
|
||||
d.setupFetcher(task.fetcher)
|
||||
task.fetcher.Create(task.Res, task.Opts)
|
||||
task.fetcher.Create(task.Meta.Opts)
|
||||
go d.watch(task)
|
||||
}
|
||||
return nil
|
||||
@@ -472,7 +496,7 @@ func (b *boot) Extra(extra interface{}) *boot {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *boot) Resolve() (*base.Resource, error) {
|
||||
func (b *boot) Resolve() (*ResolveResult, error) {
|
||||
return defaultDownloader.Resolve(&base.Request{
|
||||
URL: b.url,
|
||||
Extra: b.extra,
|
||||
@@ -485,12 +509,12 @@ func (b *boot) Listener(listener Listener) *boot {
|
||||
}
|
||||
|
||||
func (b *boot) Create(opts *base.Options) (string, error) {
|
||||
res, err := b.Resolve()
|
||||
rr, err := b.Resolve()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defaultDownloader.Listener(b.listener)
|
||||
return defaultDownloader.Create(res, opts)
|
||||
return defaultDownloader.Create(rr.ID, opts)
|
||||
}
|
||||
|
||||
func Boot() *boot {
|
||||
|
||||
@@ -22,13 +22,12 @@ func TestDownloader_Resolve(t *testing.T) {
|
||||
req := &base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
}
|
||||
res, err := downloader.Resolve(req)
|
||||
rr, err := downloader.Resolve(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := &base.Resource{
|
||||
Name: test.BuildName,
|
||||
Req: req,
|
||||
Size: test.BuildSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
@@ -39,8 +38,8 @@ func TestDownloader_Resolve(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(want, res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", res, want)
|
||||
if !reflect.DeepEqual(want, rr.Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +55,7 @@ func TestDownloader_Create(t *testing.T) {
|
||||
req := &base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
}
|
||||
res, err := downloader.Resolve(req)
|
||||
rr, err := downloader.Resolve(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -68,7 +67,7 @@ func TestDownloader_Create(t *testing.T) {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
_, err = downloader.Create(res, &base.Options{
|
||||
_, err = downloader.Create(rr.ID, &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
Extra: http.OptsExtra{
|
||||
@@ -100,12 +99,12 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
||||
req := &base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
}
|
||||
res, err := downloader.Resolve(req)
|
||||
rr, err := downloader.Resolve(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := downloader.Create(res, &base.Options{
|
||||
id, err := downloader.Create(rr.ID, &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
Extra: http.OptsExtra{
|
||||
|
||||
@@ -1,12 +1,59 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"github.com/monkeyWie/gopeed/internal/controller"
|
||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
||||
"github.com/monkeyWie/gopeed/internal/protocol/bt"
|
||||
"github.com/monkeyWie/gopeed/internal/protocol/http"
|
||||
"github.com/monkeyWie/gopeed/pkg/base"
|
||||
"github.com/monkeyWie/gopeed/pkg/util"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ResolveResult struct {
|
||||
ID string `json:"id"`
|
||||
Res *base.Resource `json:"res"`
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||
Status base.Status `json:"status"`
|
||||
Progress *Progress `json:"progress"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
||||
fetcherBuilder fetcher.FetcherBuilder
|
||||
fetcher fetcher.Fetcher
|
||||
timer *util.Timer
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
func NewTask() *Task {
|
||||
id, err := gonanoid.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Task{
|
||||
ID: id,
|
||||
Status: base.DownloadStatusReady,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task) clone() *Task {
|
||||
return &Task{
|
||||
ID: t.ID,
|
||||
Meta: t.Meta,
|
||||
Status: t.Status,
|
||||
Progress: t.Progress,
|
||||
Size: t.Size,
|
||||
CreatedAt: t.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
type DownloaderConfig struct {
|
||||
Controller *controller.Controller
|
||||
FetchBuilders []fetcher.FetcherBuilder
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
||||
"github.com/monkeyWie/gopeed/pkg/base"
|
||||
"github.com/monkeyWie/gopeed/pkg/util"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Res *base.Resource `json:"res"`
|
||||
Opts *base.Options `json:"opts"`
|
||||
Status base.Status `json:"status"`
|
||||
Progress *Progress `json:"progress"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
||||
fetcherBuilder fetcher.FetcherBuilder
|
||||
fetcher fetcher.Fetcher
|
||||
timer *util.Timer
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
func NewTask() *Task {
|
||||
id, err := gonanoid.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Task{
|
||||
ID: id,
|
||||
Status: base.DownloadStatusReady,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task) clone() *Task {
|
||||
return &Task{
|
||||
ID: t.ID,
|
||||
Res: t.Res,
|
||||
Opts: t.Opts,
|
||||
Status: t.Status,
|
||||
Progress: t.Progress,
|
||||
Size: t.Size,
|
||||
CreatedAt: t.CreatedAt,
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -16,19 +16,30 @@ import (
|
||||
func Resolve(w http.ResponseWriter, r *http.Request) {
|
||||
var req base.Request
|
||||
if util.ReadJson(w, r, &req) {
|
||||
resource, err := Downloader.Resolve(&req)
|
||||
rr, err := Downloader.Resolve(&req)
|
||||
if err != nil {
|
||||
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
util.WriteJsonOk(w, model.NewResultWithData(resource))
|
||||
util.WriteJsonOk(w, model.NewResultWithData(rr))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTask
|
||||
if util.ReadJson(w, r, &req) {
|
||||
taskId, err := Downloader.Create(req.Res, req.Opts)
|
||||
var (
|
||||
taskId string
|
||||
err error
|
||||
)
|
||||
if req.Rid != "" {
|
||||
taskId, err = Downloader.Create(req.Rid, req.Opts)
|
||||
} else if req.Req != nil {
|
||||
taskId, err = Downloader.DirectCreate(req.Req, req.Opts)
|
||||
} else {
|
||||
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("param is required: rid or req"))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import "github.com/monkeyWie/gopeed/pkg/base"
|
||||
|
||||
type CreateTask struct {
|
||||
Res *base.Resource `json:"res"`
|
||||
Opts *base.Options `json:"opts"`
|
||||
Rid string `json:"rid"`
|
||||
Req *base.Request `json:"req"`
|
||||
Opts *base.Options `json:"opts"`
|
||||
}
|
||||
@@ -34,7 +34,6 @@ var (
|
||||
}
|
||||
taskRes = &base.Resource{
|
||||
Name: test.BuildName,
|
||||
Req: taskReq,
|
||||
Size: test.BuildSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
@@ -46,7 +45,7 @@ var (
|
||||
},
|
||||
}
|
||||
createReq = &model.CreateTask{
|
||||
Res: taskRes,
|
||||
Req: taskReq,
|
||||
Opts: &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
@@ -59,9 +58,9 @@ var (
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[*base.Resource](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||
if !test.JsonEqual(taskRes, resp) {
|
||||
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp), test.ToJson(taskRes))
|
||||
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||
if !test.JsonEqual(taskRes, resp.Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp.Res), test.ToJson(taskRes))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -148,6 +147,7 @@ func TestPauseAndContinueTask(t *testing.T) {
|
||||
func TestDeleteTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId, nil)
|
||||
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
checkCode(code, http.StatusNotFound)
|
||||
@@ -157,7 +157,7 @@ func TestDeleteTask(t *testing.T) {
|
||||
func TestDeleteTaskForce(t *testing.T) {
|
||||
doTest(func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId+"?force=true", nil)
|
||||
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
checkCode(code, http.StatusNotFound)
|
||||
|
||||
+6
-6
@@ -4,11 +4,11 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
syspath "path"
|
||||
)
|
||||
|
||||
func Dir(path string) string {
|
||||
dir := filepath.Dir(path)
|
||||
dir := syspath.Dir(path)
|
||||
if dir == "." {
|
||||
return ""
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func Filepath(path string, originName string, customName string) string {
|
||||
if customName == "" {
|
||||
customName = originName
|
||||
}
|
||||
return filepath.Join(path, customName)
|
||||
return syspath.Join(path, customName)
|
||||
}
|
||||
|
||||
// SafeRemove remove file safely, ignoring errors if the path does not exist.
|
||||
@@ -33,7 +33,7 @@ func SafeRemove(name string) error {
|
||||
// SafeRemoveAll remove file and parent directories safely
|
||||
func SafeRemoveAll(path string, names []string) error {
|
||||
for _, name := range names {
|
||||
err := SafeRemove(filepath.Join(path, name))
|
||||
err := SafeRemove(syspath.Join(path, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,12 +45,12 @@ func SafeRemoveAll(path string, names []string) error {
|
||||
}
|
||||
|
||||
func safeRemoveParent(path string, subPath string) error {
|
||||
currPath := filepath.Dir(subPath)
|
||||
currPath := syspath.Dir(subPath)
|
||||
if currPath == "." {
|
||||
return nil
|
||||
}
|
||||
// if directory is empty, remove it
|
||||
dir := filepath.Join(path, filepath.Dir(subPath))
|
||||
dir := syspath.Join(path, syspath.Dir(subPath))
|
||||
empty, err := isEmpty(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -87,10 +87,13 @@ func TestFilepath(t *testing.T) {
|
||||
|
||||
func TestSafeRemove(t *testing.T) {
|
||||
name := "test_safe_remove.data"
|
||||
_, err := os.Create(name)
|
||||
file, err := os.Create(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := SafeRemove(name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -185,10 +188,13 @@ func preCreate(t *testing.T, path string, names []string) {
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := os.Create(fullPath)
|
||||
file, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import 'dart:io';
|
||||
import 'package:dio/adapter.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'model/resolve_result.dart';
|
||||
import '../util/util.dart';
|
||||
import 'model/create_task.dart';
|
||||
import 'model/request.dart';
|
||||
import 'model/resource.dart';
|
||||
import 'model/result.dart';
|
||||
import 'model/task.dart';
|
||||
|
||||
@@ -82,10 +82,10 @@ Future<T> _parse<T>(
|
||||
}
|
||||
}
|
||||
|
||||
Future<Resource> resolve(Request request) async {
|
||||
return _parse<Resource>(
|
||||
Future<ResolveResult> resolve(Request request) async {
|
||||
return _parse<ResolveResult>(
|
||||
() => _client.dio.post("/api/v1/resolve", data: request),
|
||||
(data) => Resource.fromJson(data));
|
||||
(data) => ResolveResult.fromJson(data));
|
||||
}
|
||||
|
||||
Future<String> createTask(CreateTask createTask) async {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'options.dart';
|
||||
import 'resource.dart';
|
||||
import 'request.dart';
|
||||
|
||||
part 'create_task.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class CreateTask {
|
||||
Resource res;
|
||||
String? rid;
|
||||
Request? req;
|
||||
Options? opts;
|
||||
|
||||
CreateTask({
|
||||
required this.res,
|
||||
this.rid,
|
||||
this.req,
|
||||
this.opts,
|
||||
});
|
||||
|
||||
|
||||
@@ -7,16 +7,17 @@ part of 'create_task.dart';
|
||||
// **************************************************************************
|
||||
|
||||
CreateTask _$CreateTaskFromJson(Map<String, dynamic> json) => CreateTask(
|
||||
res: Resource.fromJson(json['res'] as Map<String, dynamic>),
|
||||
rid: json['rid'] as String?,
|
||||
req: json['req'] == null
|
||||
? null
|
||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
opts: json['opts'] == null
|
||||
? null
|
||||
: Options.fromJson(json['opts'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateTaskToJson(CreateTask instance) {
|
||||
final val = <String, dynamic>{
|
||||
'res': instance.res.toJson(),
|
||||
};
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
@@ -24,6 +25,8 @@ Map<String, dynamic> _$CreateTaskToJson(CreateTask instance) {
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('rid', instance.rid);
|
||||
writeNotNull('req', instance.req?.toJson());
|
||||
writeNotNull('opts', instance.opts?.toJson());
|
||||
return val;
|
||||
}
|
||||
@@ -22,8 +22,8 @@ class ProtocolConfig {
|
||||
|
||||
ProtocolConfig();
|
||||
|
||||
factory ProtocolConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProtocolConfigFromJson(json);
|
||||
factory ProtocolConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
json == null ? ProtocolConfig() : _$ProtocolConfigFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ProtocolConfigToJson(this);
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ class ExtraConfig {
|
||||
|
||||
ExtraConfig();
|
||||
|
||||
factory ExtraConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$ExtraConfigFromJson(json);
|
||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
json == null ? ExtraConfig() : _$ExtraConfigFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||
DownloaderConfig()
|
||||
..downloadDir = json['downloadDir'] as String
|
||||
..protocolConfig = ProtocolConfig.fromJson(
|
||||
json['protocolConfig'] as Map<String, dynamic>)
|
||||
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>);
|
||||
json['protocolConfig'] as Map<String, dynamic>?)
|
||||
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?);
|
||||
|
||||
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
|
||||
<String, dynamic>{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:gopeed/api/model/options.dart';
|
||||
import 'package:gopeed/api/model/request.dart';
|
||||
import 'package:gopeed/api/model/resource.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'meta.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Meta {
|
||||
Request req;
|
||||
Resource res;
|
||||
Options opts;
|
||||
|
||||
Meta({
|
||||
required this.req,
|
||||
required this.res,
|
||||
required this.opts,
|
||||
});
|
||||
|
||||
factory Meta.fromJson(Map<String, dynamic> json) => _$MetaFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$MetaToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'meta.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Meta _$MetaFromJson(Map<String, dynamic> json) => Meta(
|
||||
req: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
res: Resource.fromJson(json['res'] as Map<String, dynamic>),
|
||||
opts: Options.fromJson(json['opts'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MetaToJson(Meta instance) => <String, dynamic>{
|
||||
'req': instance.req.toJson(),
|
||||
'res': instance.res.toJson(),
|
||||
'opts': instance.opts.toJson(),
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'request.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true, genericArgumentFactories: true)
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Request {
|
||||
String url;
|
||||
Map<String, dynamic>? extra;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:gopeed/api/model/resource.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'resolve_result.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class ResolveResult {
|
||||
String id;
|
||||
Resource res;
|
||||
|
||||
ResolveResult({
|
||||
required this.id,
|
||||
required this.res,
|
||||
});
|
||||
|
||||
factory ResolveResult.fromJson(Map<String, dynamic> json) =>
|
||||
_$ResolveResultFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ResolveResultToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'resolve_result.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ResolveResult _$ResolveResultFromJson(Map<String, dynamic> json) =>
|
||||
ResolveResult(
|
||||
id: json['id'] as String,
|
||||
res: Resource.fromJson(json['res'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ResolveResultToJson(ResolveResult instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'res': instance.res.toJson(),
|
||||
};
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'request.dart';
|
||||
|
||||
part 'resource.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Resource {
|
||||
Request req;
|
||||
String name;
|
||||
int size;
|
||||
bool range;
|
||||
@@ -14,8 +11,7 @@ class Resource {
|
||||
String hash;
|
||||
|
||||
Resource(
|
||||
{required this.req,
|
||||
required this.name,
|
||||
{required this.name,
|
||||
required this.size,
|
||||
required this.range,
|
||||
required this.files,
|
||||
|
||||
@@ -7,7 +7,6 @@ part of 'resource.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
|
||||
req: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
name: json['name'] as String,
|
||||
size: json['size'] as int,
|
||||
range: json['range'] as bool,
|
||||
@@ -18,7 +17,6 @@ Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ResourceToJson(Resource instance) => <String, dynamic>{
|
||||
'req': instance.req.toJson(),
|
||||
'name': instance.name,
|
||||
'size': instance.size,
|
||||
'range': instance.range,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'meta.dart';
|
||||
import 'options.dart';
|
||||
import 'resource.dart';
|
||||
|
||||
@@ -10,8 +11,7 @@ enum Status { ready, running, pause, error, done }
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Task {
|
||||
String id;
|
||||
Resource res;
|
||||
Options opts;
|
||||
Meta meta;
|
||||
Status status;
|
||||
Progress progress;
|
||||
int size;
|
||||
@@ -19,8 +19,7 @@ class Task {
|
||||
|
||||
Task({
|
||||
required this.id,
|
||||
required this.res,
|
||||
required this.opts,
|
||||
required this.meta,
|
||||
required this.status,
|
||||
required this.progress,
|
||||
required this.size,
|
||||
|
||||
@@ -8,8 +8,7 @@ part of 'task.dart';
|
||||
|
||||
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||
id: json['id'] as String,
|
||||
res: Resource.fromJson(json['res'] as Map<String, dynamic>),
|
||||
opts: Options.fromJson(json['opts'] as Map<String, dynamic>),
|
||||
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||
status: $enumDecode(_$StatusEnumMap, json['status']),
|
||||
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
||||
size: json['size'] as int,
|
||||
@@ -18,8 +17,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||
|
||||
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'res': instance.res.toJson(),
|
||||
'opts': instance.opts.toJson(),
|
||||
'meta': instance.meta.toJson(),
|
||||
'status': _$StatusEnumMap[instance.status]!,
|
||||
'progress': instance.progress.toJson(),
|
||||
'size': instance.size,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/resolve_result.dart';
|
||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||
|
||||
import '../../api/api.dart';
|
||||
import '../../api/model/create_task.dart';
|
||||
import '../../api/model/options.dart';
|
||||
import '../../api/model/request.dart';
|
||||
import '../../api/model/resource.dart';
|
||||
import '../../routes/router.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../../widget/directory_selector.dart';
|
||||
@@ -71,10 +71,10 @@ class CreateView extends GetView<CreateController> {
|
||||
if (_resolveFormKey.currentState!.validate()) {
|
||||
_confirmController.start();
|
||||
try {
|
||||
final res = await resolve(Request(
|
||||
final rr = await resolve(Request(
|
||||
url: _urlController.text,
|
||||
));
|
||||
await _showResolveDialog(res);
|
||||
await _showResolveDialog(rr);
|
||||
} catch (e) {
|
||||
Get.snackbar('error'.tr, e.toString());
|
||||
} finally {
|
||||
@@ -101,7 +101,8 @@ class CreateView extends GetView<CreateController> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showResolveDialog(Resource res) async {
|
||||
Future<void> _showResolveDialog(ResolveResult rr) async {
|
||||
final res = rr.res;
|
||||
final appController = Get.find<AppController>();
|
||||
final setting = appController.downloaderConfig.value;
|
||||
|
||||
@@ -180,7 +181,7 @@ class CreateView extends GetView<CreateController> {
|
||||
// }
|
||||
|
||||
await createTask(CreateTask(
|
||||
res: res,
|
||||
rid: rr.id,
|
||||
opts: Options(
|
||||
name: '',
|
||||
path: pathController.text,
|
||||
|
||||
@@ -25,7 +25,7 @@ class Item extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(task.res.name),
|
||||
title: Text(task.meta.res.name),
|
||||
leading: const Icon(Icons.insert_drive_file),
|
||||
),
|
||||
Row(
|
||||
@@ -63,8 +63,8 @@ class Item extends StatelessWidget {
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
final file = File(Util.buildAbsPath(task.opts.path,
|
||||
task.res.files[0].path, task.res.files[0].name));
|
||||
final file = File(Util.buildAbsPath(task.meta.opts.path,
|
||||
task.meta.res.files[0].path, task.meta.res.files[0].name));
|
||||
await launchUrl(file.parent.uri);
|
||||
},
|
||||
));
|
||||
|
||||
Reference in new issue
Block a user