Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49604966fd | ||
|
|
bdb8ed177f | ||
|
|
2dc7048cba | ||
|
|
d3750464aa | ||
|
|
5c9bcd3731 | ||
|
|
bdda550b2c | ||
|
|
3308d966f0 | ||
|
|
18041ed9ec | ||
|
|
56b85e545e | ||
|
|
c5af916952 | ||
|
|
a1981328aa | ||
|
|
51244133e0 | ||
|
|
0c17093b1a | ||
|
|
6538b29b62 | ||
|
|
81e90e6b89 | ||
|
|
4f39296664 | ||
|
|
ece5b91dc2 | ||
|
|
d9f04ae928 | ||
|
|
e3ccecbd9c | ||
|
|
fb1c2cb060 |
No files matched your search
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,8 +9,6 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
@@ -114,7 +114,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.21+
|
1. Golang 1.22+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,8 +9,6 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,8 +9,6 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,8 +9,6 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,8 +9,6 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 51 KiB |
@@ -6,7 +6,7 @@ require (
|
|||||||
github.com/anacrolix/missinggo/v2 v2.7.3
|
github.com/anacrolix/missinggo/v2 v2.7.3
|
||||||
github.com/anacrolix/torrent v1.56.1
|
github.com/anacrolix/torrent v1.56.1
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe
|
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||||
github.com/go-git/go-git/v5 v5.8.1
|
github.com/go-git/go-git/v5 v5.8.1
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
@@ -66,7 +66,7 @@ require (
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
@@ -114,7 +114,7 @@ require (
|
|||||||
golang.org/x/mod v0.19.0 // indirect
|
golang.org/x/mod v0.19.0 // indirect
|
||||||
golang.org/x/net v0.27.0 // indirect
|
golang.org/x/net v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
golang.org/x/tools v0.23.0 // indirect
|
golang.org/x/tools v0.23.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
|
|||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe h1:jwFJkgsdelB87ohlXaAGSd05Cb5ALDFa9iW9IGRHcRM=
|
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe h1:jwFJkgsdelB87ohlXaAGSd05Cb5ALDFa9iW9IGRHcRM=
|
||||||
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe/go.mod h1:DF+w/nLMIkvRpyhd/0K+Okbh3fVZBtXLwRtS/ccAa5w=
|
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe/go.mod h1:DF+w/nLMIkvRpyhd/0K+Okbh3fVZBtXLwRtS/ccAa5w=
|
||||||
|
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05 h1:oK4+QcKsczZjHYTHD0JAdkvq5w74JEkG95J0XNBx/BI=
|
||||||
|
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co=
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU=
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU=
|
||||||
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
@@ -214,6 +216,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||||
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
|
||||||
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -569,6 +573,8 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
GetConfig func(v any)
|
GetConfig func(v any)
|
||||||
ProxyConfig *base.DownloaderProxyConfig
|
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
|
||||||
FileController
|
FileController
|
||||||
//ContextDialer() (proxy.Dialer, error)
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
}
|
}
|
||||||
@@ -23,6 +25,7 @@ type DefaultFileController struct {
|
|||||||
func NewController() *Controller {
|
func NewController() *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
GetConfig: func(v any) {},
|
GetConfig: func(v any) {},
|
||||||
|
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
|
||||||
FileController: &DefaultFileController{},
|
FileController: &DefaultFileController{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ type FetcherManager interface {
|
|||||||
Filters() []*SchemeFilter
|
Filters() []*SchemeFilter
|
||||||
// Build returns a new fetcher.
|
// Build returns a new fetcher.
|
||||||
Build() Fetcher
|
Build() Fetcher
|
||||||
|
// ParseName name displayed when the task is not yet resolved, parsed from the request URL
|
||||||
|
ParseName(u string) string
|
||||||
|
// AutoRename returns whether the fetcher need renaming the download file when has the same name file.
|
||||||
|
AutoRename() bool
|
||||||
|
|
||||||
// DefaultConfig returns the default configuration of the protocol.
|
// DefaultConfig returns the default configuration of the protocol.
|
||||||
DefaultConfig() any
|
DefaultConfig() any
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/anacrolix/torrent"
|
"github.com/anacrolix/torrent"
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -72,7 +73,7 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||||
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||||
cfg.ListenPort = f.config.ListenPort
|
cfg.ListenPort = f.config.ListenPort
|
||||||
cfg.HTTPProxy = f.ctl.ProxyConfig.ToHandler()
|
cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy)
|
||||||
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||||
ClientBaseDir: cfg.DataDir,
|
ClientBaseDir: cfg.DataDir,
|
||||||
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||||
@@ -97,18 +98,21 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
|
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.meta.Req = req
|
||||||
if err := f.addTorrent(req, false); err != nil {
|
if err := f.addTorrent(req, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.updateRes()
|
f.updateRes()
|
||||||
f.meta.Req = req
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
if f.meta.Res != nil {
|
if f.meta.Res != nil {
|
||||||
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
torrentDirMap[f.meta.Res.Hash] = opts.Path
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -120,7 +124,7 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
||||||
ft.setTorrentDir(f.meta.FolderPath())
|
ft.setTorrentDir(f.meta.Opts.Path)
|
||||||
}
|
}
|
||||||
files := f.torrent.Files()
|
files := f.torrent.Files()
|
||||||
// If the user does not specify the file to download, all files will be downloaded by default
|
// If the user does not specify the file to download, all files will be downloaded by default
|
||||||
@@ -245,16 +249,18 @@ func (f *Fetcher) isDone() bool {
|
|||||||
|
|
||||||
func (f *Fetcher) updateRes() {
|
func (f *Fetcher) updateRes() {
|
||||||
res := &base.Resource{
|
res := &base.Resource{
|
||||||
Name: f.torrent.Name(),
|
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||||
Hash: f.torrent.InfoHash().String(),
|
Hash: f.torrent.InfoHash().String(),
|
||||||
}
|
}
|
||||||
f.torrent.PeerConns()
|
// Directory torrent
|
||||||
|
if f.torrent.Info().Length == 0 {
|
||||||
|
res.Name = f.torrent.Name()
|
||||||
|
}
|
||||||
for i, file := range f.torrent.Files() {
|
for i, file := range f.torrent.Files() {
|
||||||
res.Files[i] = &base.FileInfo{
|
res.Files[i] = &base.FileInfo{
|
||||||
Name: filepath.Base(file.DisplayPath()),
|
Name: filepath.Base(file.DisplayPath()),
|
||||||
Path: util.Dir(file.Path()),
|
Path: util.Dir(file.DisplayPath()),
|
||||||
Size: file.Length(),
|
Size: file.Length(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,7 +295,7 @@ func (f *Fetcher) doUpload(fromUpload bool) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := f.torrent.Stats()
|
stats := f.torrentStats()
|
||||||
f.data.SeedBytes = lastData.SeedBytes + stats.BytesWrittenData.Int64()
|
f.data.SeedBytes = lastData.SeedBytes + stats.BytesWrittenData.Int64()
|
||||||
|
|
||||||
// Check is download complete, if not don't check and stop seeding
|
// Check is download complete, if not don't check and stop seeding
|
||||||
@@ -326,6 +332,17 @@ func (f *Fetcher) doUpload(fromUpload bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get torrent stats maybe panic, see https://github.com/anacrolix/torrent/issues/972
|
||||||
|
func (f *Fetcher) torrentStats() torrent.TorrentStats {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// ignore panic
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return f.torrent.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) UploadedBytes() int64 {
|
func (f *Fetcher) UploadedBytes() int64 {
|
||||||
return f.data.SeedBytes
|
return f.data.SeedBytes
|
||||||
}
|
}
|
||||||
@@ -336,9 +353,6 @@ func (f *Fetcher) WaitUpload() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||||
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = f.initClient(); err != nil {
|
if err = f.initClient(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -458,6 +472,28 @@ func (fm *FetcherManager) Build() fetcher.Fetcher {
|
|||||||
return &Fetcher{}
|
return &Fetcher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) ParseName(u string) string {
|
||||||
|
var name string
|
||||||
|
url, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
params := url.Query()
|
||||||
|
if params.Get("dn") != "" {
|
||||||
|
return params.Get("dn")
|
||||||
|
}
|
||||||
|
if params.Get("xt") != "" {
|
||||||
|
xt := strings.Split(params.Get("xt"), ":")
|
||||||
|
return xt[len(xt)-1]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) AutoRename() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (fm *FetcherManager) DefaultConfig() any {
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
return &config{
|
return &config{
|
||||||
ListenPort: 0,
|
ListenPort: 0,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -33,7 +35,6 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
|
||||||
Size: 1466714112,
|
Size: 1466714112,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -69,33 +70,123 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
t.Run("Resolve Single File", func(t *testing.T) {
|
||||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
err := fetcher.Resolve(&base.Request{
|
||||||
Extra: bt.ReqExtra{
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||||
Trackers: []string{
|
Extra: bt.ReqExtra{
|
||||||
"udp://tracker.birkenwald.de:6969/announce",
|
Trackers: []string{
|
||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
if err != nil {
|
||||||
if err != nil {
|
panic(err)
|
||||||
panic(err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Size: 1466714112,
|
||||||
Size: 1466714112,
|
Range: true,
|
||||||
Range: true,
|
Files: []*base.FileInfo{
|
||||||
Files: []*base.FileInfo{
|
{
|
||||||
{
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Size: 1466714112,
|
||||||
Size: 1466714112,
|
},
|
||||||
},
|
},
|
||||||
},
|
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Resolve Multi Files", func(t *testing.T) {
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "./testdata/test.torrent",
|
||||||
|
Extra: bt.ReqExtra{
|
||||||
|
Trackers: []string{
|
||||||
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &base.Resource{
|
||||||
|
Name: "test",
|
||||||
|
Size: 107484864,
|
||||||
|
Range: true,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "c.txt",
|
||||||
|
Path: "path",
|
||||||
|
Size: 98501754,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "b.txt",
|
||||||
|
Size: 8904996,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "a.txt",
|
||||||
|
Size: 78114,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
u string
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
tests := []struct {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "broken url",
|
||||||
|
args: args{
|
||||||
|
u: "magnet://!@#%github.com",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dn",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed&dn=ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
},
|
||||||
|
want: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no dn",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
|
},
|
||||||
|
want: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non standard magnet",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xxt=abcd",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||||
|
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +212,9 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher
|
|||||||
newController.GetConfig = func(v any) {
|
newController.GetConfig = func(v any) {
|
||||||
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
||||||
}
|
}
|
||||||
newController.ProxyConfig = proxyConfig
|
newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
return proxyConfig.ToHandler()
|
||||||
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
f.meta.Req = req
|
||||||
httpReq, err := f.buildRequest(nil, req)
|
httpReq, err := f.buildRequest(nil, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -99,7 +100,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
||||||
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
|
// response 206 status code, support breakpoint continuation
|
||||||
res.Range = true
|
res.Range = true
|
||||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||||
@@ -111,7 +112,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
res.Size = parse
|
res.Size = parse
|
||||||
}
|
}
|
||||||
} else if base.HttpCodeOK == httpResp.StatusCode {
|
} else if base.HttpCodeOK == httpResp.StatusCode {
|
||||||
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
|
// response 200 status code, not support breakpoint continuation, get file size by Content-Length header
|
||||||
|
// if not found, maybe chunked encoding
|
||||||
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
||||||
if contentLength != "" {
|
if contentLength != "" {
|
||||||
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
||||||
@@ -152,7 +154,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
file.Name = httpReq.URL.Hostname()
|
file.Name = httpReq.URL.Hostname()
|
||||||
}
|
}
|
||||||
res.Files = append(res.Files, file)
|
res.Files = append(res.Files, file)
|
||||||
f.meta.Req = req
|
|
||||||
f.meta.Res = res
|
f.meta.Res = res
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -170,8 +171,12 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
|||||||
opts.Extra = &fhttp.OptsExtra{}
|
opts.Extra = &fhttp.OptsExtra{}
|
||||||
}
|
}
|
||||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||||
if extra.Connections == 0 {
|
if extra.Connections <= 0 {
|
||||||
extra.Connections = f.config.Connections
|
extra.Connections = f.config.Connections
|
||||||
|
// Avoid zero connections configuration
|
||||||
|
if extra.Connections <= 0 {
|
||||||
|
extra.Connections = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -443,7 +448,7 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
|||||||
|
|
||||||
func (f *Fetcher) buildClient() *http.Client {
|
func (f *Fetcher) buildClient() *http.Client {
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
Proxy: f.ctl.ProxyConfig.ToHandler(),
|
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||||
}
|
}
|
||||||
// Cookie handle
|
// Cookie handle
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
@@ -481,6 +486,25 @@ func (fm *FetcherManager) Build() fetcher.Fetcher {
|
|||||||
return &Fetcher{}
|
return &Fetcher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) ParseName(u string) string {
|
||||||
|
var name string
|
||||||
|
url, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Get filePath by URL
|
||||||
|
name = path.Base(url.Path)
|
||||||
|
// If file name is empty, use host name
|
||||||
|
if name == "" || name == "/" || name == "." {
|
||||||
|
name = url.Hostname()
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) AutoRename() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (fm *FetcherManager) DefaultConfig() any {
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
return &config{
|
return &config{
|
||||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"net"
|
"net"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -192,6 +194,54 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
u string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "broken url",
|
||||||
|
args: args{
|
||||||
|
u: "https://!@#%github.com",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file path",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com/index.html",
|
||||||
|
},
|
||||||
|
want: "index.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file path with query and hash",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com/a/b/index.html/#list?name=1",
|
||||||
|
},
|
||||||
|
want: "index.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no file path",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com",
|
||||||
|
},
|
||||||
|
want: "github.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||||
|
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||||
return doDownloadReady(buildFetcher(), listener, connections, t)
|
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||||
}
|
}
|
||||||
@@ -337,10 +387,12 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||||
fetcher := downloadReady(httpListener, 4, t)
|
fetcher := downloadReady(httpListener, 4, t)
|
||||||
ctl := controller.NewController()
|
ctl := controller.NewController()
|
||||||
ctl.ProxyConfig = &base.DownloaderProxyConfig{
|
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
Enable: true,
|
return (&base.DownloaderProxyConfig{
|
||||||
Scheme: "socks5",
|
Enable: true,
|
||||||
Host: proxyListener.Addr().String(),
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
}).ToHandler()
|
||||||
}
|
}
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
@@ -359,11 +411,11 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildFetcher() *Fetcher {
|
func buildFetcher() *Fetcher {
|
||||||
fb := new(FetcherManager)
|
fm := new(FetcherManager)
|
||||||
fetcher := fb.Build()
|
fetcher := fm.Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
newController.GetConfig = func(v any) {
|
newController.GetConfig = func(v any) {
|
||||||
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
json.Unmarshal([]byte(test.ToJson(fm.DefaultConfig())), v)
|
||||||
}
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher.(*Fetcher)
|
return fetcher.(*Fetcher)
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
NotFound = errors.New("not found")
|
|
||||||
BadParams = errors.New("bad params")
|
|
||||||
)
|
|
||||||
@@ -16,6 +16,8 @@ type Request struct {
|
|||||||
Extra any `json:"extra"`
|
Extra any `json:"extra"`
|
||||||
// Labels is used to mark the download task
|
// Labels is used to mark the download task
|
||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
|
// Proxy is special proxy config for request
|
||||||
|
Proxy *RequestProxy `json:"proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Validate() error {
|
func (r *Request) Validate() error {
|
||||||
@@ -25,6 +27,37 @@ func (r *Request) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestProxyMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RequestProxyModeFollow follow setting proxy
|
||||||
|
RequestProxyModeFollow RequestProxyMode = "follow"
|
||||||
|
// RequestProxyModeNone not use proxy
|
||||||
|
RequestProxyModeNone RequestProxyMode = "none"
|
||||||
|
// RequestProxyModeCustom custom proxy
|
||||||
|
RequestProxyModeCustom RequestProxyMode = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestProxy struct {
|
||||||
|
Mode RequestProxyMode `json:"mode"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Usr string `json:"usr"`
|
||||||
|
Pwd string `json:"pwd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestProxy) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||||
|
if p == nil || p.Mode != RequestProxyModeCustom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Scheme == "" || p.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ProxyURL(util.BuildProxyUrl(p.Scheme, p.Host, p.Usr, p.Pwd))
|
||||||
|
}
|
||||||
|
|
||||||
// Resource download resource
|
// Resource download resource
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
// if name is not empty, the resource is a folder and the name is the folder name
|
// if name is not empty, the resource is a folder and the name is the folder name
|
||||||
@@ -94,15 +127,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) Clone() *Options {
|
func (o *Options) Clone() *Options {
|
||||||
if o == nil {
|
return util.DeepClone(o)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &Options{
|
|
||||||
Name: o.Name,
|
|
||||||
Path: o.Path,
|
|
||||||
SelectFiles: o.SelectFiles,
|
|
||||||
Extra: o.Extra,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseReqExtra[E any](req *Request) error {
|
func ParseReqExtra[E any](req *Request) error {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import (
|
|||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/pkgerrors"
|
"github.com/rs/zerolog/pkgerrors"
|
||||||
"github.com/virtuald/go-paniclog"
|
"github.com/virtuald/go-paniclog"
|
||||||
|
"math"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -253,7 +256,20 @@ func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fet
|
|||||||
ctl.GetConfig = func(v any) {
|
ctl.GetConfig = func(v any) {
|
||||||
d.getProtocolConfig(fm.Name(), v)
|
d.getProtocolConfig(fm.Name(), v)
|
||||||
}
|
}
|
||||||
ctl.ProxyConfig = d.cfg.Proxy
|
// Get proxy config, task request proxy config has higher priority, then use global proxy config
|
||||||
|
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
if requestProxy == nil {
|
||||||
|
return d.cfg.Proxy.ToHandler()
|
||||||
|
}
|
||||||
|
switch requestProxy.Mode {
|
||||||
|
case base.RequestProxyModeNone:
|
||||||
|
return nil
|
||||||
|
case base.RequestProxyModeCustom:
|
||||||
|
return requestProxy.ToHandler()
|
||||||
|
default:
|
||||||
|
return d.cfg.Proxy.ToHandler()
|
||||||
|
}
|
||||||
|
}
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,56 +372,28 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
|
|||||||
return d.doCreate(fetcher, opts)
|
return d.doCreate(fetcher, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Pause(id string) (err error) {
|
func (d *Downloader) Pause(filter *TaskFilter) (err error) {
|
||||||
task := d.GetTask(id)
|
if filter == nil || filter.IsEmpty() {
|
||||||
if task == nil {
|
return d.pauseAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.NotStatuses = []base.Status{base.DownloadStatusPause, base.DownloadStatusError, base.DownloadStatusDone}
|
||||||
|
pauseTasks := d.GetTasksByFilter(filter)
|
||||||
|
if len(pauseTasks) == 0 {
|
||||||
return ErrTaskNotFound
|
return ErrTaskNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func() {
|
for _, task := range pauseTasks {
|
||||||
task.lock.Lock()
|
|
||||||
defer task.lock.Unlock()
|
|
||||||
|
|
||||||
if task.Status == base.DownloadStatusPause {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = d.doPause(task); err != nil {
|
if err = d.doPause(task); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.notifyRunning()
|
}
|
||||||
}()
|
d.notifyRunning()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Continue(id string) (err error) {
|
func (d *Downloader) pauseAll() (err error) {
|
||||||
task := d.GetTask(id)
|
|
||||||
if task == nil {
|
|
||||||
return ErrTaskNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func() {
|
|
||||||
d.lock.Lock()
|
|
||||||
defer d.lock.Unlock()
|
|
||||||
|
|
||||||
remainRunningCount := d.remainRunningCount()
|
|
||||||
if remainRunningCount == 0 {
|
|
||||||
for _, t := range d.tasks {
|
|
||||||
if t.Status == base.DownloadStatusRunning {
|
|
||||||
if err = d.doPause(t); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Status = base.DownloadStatusWait
|
|
||||||
d.waitTasks = append(d.waitTasks, t)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return d.doStart(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Downloader) PauseAll() (err error) {
|
|
||||||
func() {
|
func() {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
@@ -423,7 +411,65 @@ func (d *Downloader) PauseAll() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) ContinueAll() (err error) {
|
// Continue specific tasks, if continue tasks will exceed maxRunning, it needs pause some running tasks before that
|
||||||
|
func (d *Downloader) Continue(filter *TaskFilter) (err error) {
|
||||||
|
if filter == nil || filter.IsEmpty() {
|
||||||
|
return d.continueAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.NotStatuses = []base.Status{base.DownloadStatusRunning, base.DownloadStatusDone}
|
||||||
|
continueTasks := d.GetTasksByFilter(filter)
|
||||||
|
if len(continueTasks) == 0 {
|
||||||
|
return ErrTaskNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
realContinueTasks := make([]*Task, 0)
|
||||||
|
func() {
|
||||||
|
d.lock.Lock()
|
||||||
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
continueCount := len(continueTasks)
|
||||||
|
remainRunningCount := d.remainRunningCount()
|
||||||
|
needRunningCount := int(math.Min(float64(d.cfg.MaxRunning), float64(continueCount)))
|
||||||
|
needPauseCount := needRunningCount - remainRunningCount
|
||||||
|
if needPauseCount > 0 {
|
||||||
|
pausedCount := 0
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
if task.Status == base.DownloadStatusRunning {
|
||||||
|
if err = d.doPause(task); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.Status = base.DownloadStatusWait
|
||||||
|
d.waitTasks = append(d.waitTasks, task)
|
||||||
|
pausedCount++
|
||||||
|
}
|
||||||
|
if pausedCount == needPauseCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range continueTasks {
|
||||||
|
if len(realContinueTasks) < needRunningCount {
|
||||||
|
realContinueTasks = append(realContinueTasks, task)
|
||||||
|
} else {
|
||||||
|
task.Status = base.DownloadStatusWait
|
||||||
|
d.waitTasks = append(d.waitTasks, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, task := range realContinueTasks {
|
||||||
|
if err = d.doStart(task); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// continueAll continue all tasks but does not affect tasks already running
|
||||||
|
func (d *Downloader) continueAll() (err error) {
|
||||||
continuedTasks := make([]*Task, 0)
|
continuedTasks := make([]*Task, 0)
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
@@ -444,8 +490,7 @@ func (d *Downloader) ContinueAll() (err error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
for _, task := range continuedTasks {
|
for _, task := range continuedTasks {
|
||||||
tt := task
|
if err = d.doStart(task); err != nil {
|
||||||
if err = d.doStart(tt); err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,40 +498,26 @@ func (d *Downloader) ContinueAll() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Delete(id string, force bool) (err error) {
|
func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
|
||||||
task := d.GetTask(id)
|
if filter == nil || filter.IsEmpty() {
|
||||||
if task == nil {
|
return d.continueAll()
|
||||||
return ErrTaskNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func() {
|
continueTasks := d.GetTasksByFilter(filter)
|
||||||
d.lock.Lock()
|
for _, task := range continueTasks {
|
||||||
defer d.lock.Unlock()
|
if err = d.doStart(task); err != nil {
|
||||||
|
return
|
||||||
for i, t := range d.tasks {
|
|
||||||
if t.ID == id {
|
|
||||||
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for i, t := range d.waitTasks {
|
|
||||||
if t.ID == id {
|
|
||||||
d.waitTasks = append(d.waitTasks[:i], d.waitTasks[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = d.doDelete(task, force)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
d.notifyRunning()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err error) {
|
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||||
deleteTasks := d.GetTasksByStatues(statues)
|
if filter == nil || filter.IsEmpty() {
|
||||||
|
return d.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTasks := d.GetTasksByFilter(filter)
|
||||||
if len(deleteTasks) == 0 {
|
if len(deleteTasks) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -528,6 +559,23 @@ func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) deleteAll() (err error) {
|
||||||
|
func() {
|
||||||
|
d.lock.Lock()
|
||||||
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
d.tasks = make([]*Task, 0)
|
||||||
|
d.waitTasks = make([]*Task, 0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
if err = d.doDelete(task, true); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Downloader) Stats(id string) (sr any, err error) {
|
func (d *Downloader) Stats(id string) (sr any, err error) {
|
||||||
task := d.GetTask(id)
|
task := d.GetTask(id)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
@@ -550,9 +598,6 @@ func (d *Downloader) Stats(id string) (sr any, err error) {
|
|||||||
|
|
||||||
func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
||||||
err = func() error {
|
err = func() error {
|
||||||
d.lock.Lock()
|
|
||||||
defer d.lock.Unlock()
|
|
||||||
|
|
||||||
if err := d.storage.Delete(bucketTask, task.ID); err != nil {
|
if err := d.storage.Delete(bucketTask, task.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -580,10 +625,10 @@ func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
|||||||
task = nil
|
task = nil
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("delete task failed, task id: %s", task.ID)
|
d.Logger.Error().Stack().Err(err).Msgf("delete task failed, task id: %s", task.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,12 +636,12 @@ func (d *Downloader) Close() error {
|
|||||||
d.closed.Store(true)
|
d.closed.Store(true)
|
||||||
|
|
||||||
closeArr := []func() error{
|
closeArr := []func() error{
|
||||||
d.PauseAll,
|
d.pauseAll,
|
||||||
d.storage.Close,
|
|
||||||
}
|
}
|
||||||
for _, fm := range d.cfg.FetchManagers {
|
for _, fm := range d.cfg.FetchManagers {
|
||||||
closeArr = append(closeArr, fm.Close)
|
closeArr = append(closeArr, fm.Close)
|
||||||
}
|
}
|
||||||
|
closeArr = append(closeArr, d.storage.Close)
|
||||||
// Make sure all resources are released, if had error, return the last error
|
// Make sure all resources are released, if had error, return the last error
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for i, close := range closeArr {
|
for i, close := range closeArr {
|
||||||
@@ -653,17 +698,51 @@ func (d *Downloader) GetTasks() []*Task {
|
|||||||
return d.tasks
|
return d.tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) GetTasksByStatues(statues []base.Status) []*Task {
|
// GetTasksByFilter get tasks by filter, if filter is nil, return all tasks
|
||||||
if len(statues) == 0 {
|
// return tasks and if match all tasks
|
||||||
|
func (d *Downloader) GetTasksByFilter(filter *TaskFilter) []*Task {
|
||||||
|
if filter == nil || filter.IsEmpty() {
|
||||||
return d.tasks
|
return d.tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
idMatch := func(task *Task) bool {
|
||||||
|
if len(filter.IDs) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, id := range filter.IDs {
|
||||||
|
if task.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
statusMatch := func(task *Task) bool {
|
||||||
|
if len(filter.Statuses) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, status := range filter.Statuses {
|
||||||
|
if task.Status == status {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
notStatusMatch := func(task *Task) bool {
|
||||||
|
if len(filter.NotStatuses) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, status := range filter.NotStatuses {
|
||||||
|
if task.Status == status {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
tasks := make([]*Task, 0)
|
tasks := make([]*Task, 0)
|
||||||
for _, task := range d.tasks {
|
for _, task := range d.tasks {
|
||||||
for _, status := range statues {
|
if idMatch(task) && statusMatch(task) && notStatusMatch(task) {
|
||||||
if task.Status == status {
|
tasks = append(tasks, task)
|
||||||
tasks = append(tasks, task)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tasks
|
return tasks
|
||||||
@@ -732,6 +811,12 @@ func (d *Downloader) watch(task *Task) {
|
|||||||
d.doOnError(task, err)
|
d.doOnError(task, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When delete a not resolved task, need check if the task resource is nil
|
||||||
|
if task.Meta.Res == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
task.Progress.Used = task.timer.Used()
|
task.Progress.Used = task.timer.Used()
|
||||||
if task.Meta.Res.Size == 0 {
|
if task.Meta.Res.Size == 0 {
|
||||||
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
||||||
@@ -748,6 +833,7 @@ func (d *Downloader) watch(task *Task) {
|
|||||||
d.emit(EventKeyDone, task)
|
d.emit(EventKeyDone, task)
|
||||||
d.emit(EventKeyFinally, task, err)
|
d.emit(EventKeyFinally, task, err)
|
||||||
d.notifyRunning()
|
d.notifyRunning()
|
||||||
|
d.triggerOnDone(task)
|
||||||
|
|
||||||
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
|
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
|
||||||
downloadFilePath := task.Meta.SingleFilepath()
|
downloadFilePath := task.Meta.SingleFilepath()
|
||||||
@@ -843,6 +929,20 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
|
|||||||
opts.Path = storeConfig.DownloadDir
|
opts.Path = storeConfig.DownloadDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if enable white download directory, check if the download directory is in the white list
|
||||||
|
if len(d.cfg.DownloadDirWhiteList) > 0 {
|
||||||
|
inWhiteList := false
|
||||||
|
for _, dir := range d.cfg.DownloadDirWhiteList {
|
||||||
|
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
||||||
|
inWhiteList = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !inWhiteList {
|
||||||
|
return "", errors.New("download directory is not in white list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fm, err := d.parseFm(f.Meta().Req.URL)
|
fm, err := d.parseFm(f.Meta().Req.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -932,26 +1032,28 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isCreate {
|
if isCreate {
|
||||||
d.checkDuplicateLock.Lock()
|
if task.fetcherManager.AutoRename() {
|
||||||
defer d.checkDuplicateLock.Unlock()
|
d.checkDuplicateLock.Lock()
|
||||||
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
|
defer d.checkDuplicateLock.Unlock()
|
||||||
// check if the download file is duplicated and rename it automatically.
|
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
|
||||||
if task.Meta.Res.Name != "" {
|
// check if the download file is duplicated and rename it automatically.
|
||||||
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
|
if task.Meta.Res.Name != "" {
|
||||||
fullDirPath := task.Meta.FolderPath()
|
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
|
||||||
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
fullDirPath := task.Meta.FolderPath()
|
||||||
if err != nil {
|
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task.Meta.Opts.Name = newName
|
||||||
|
} else {
|
||||||
|
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
|
||||||
|
fullFilePath := task.Meta.SingleFilepath()
|
||||||
|
newName, err := util.CheckDuplicateAndRename(fullFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task.Meta.Opts.Name = newName
|
||||||
}
|
}
|
||||||
task.Meta.Opts.Name = newName
|
|
||||||
} else {
|
|
||||||
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
|
|
||||||
fullFilePath := task.Meta.SingleFilepath()
|
|
||||||
newName, err := util.CheckDuplicateAndRename(fullFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
task.Meta.Opts.Name = newName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
|
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
|
||||||
|
|||||||
@@ -82,7 +82,45 @@ func TestDownloader_Create(t *testing.T) {
|
|||||||
want := test.FileMd5(test.BuildFile)
|
want := test.FileMd5(test.BuildFile)
|
||||||
got := test.FileMd5(test.DownloadFile)
|
got := test.FileMd5(test.DownloadFile)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("Download() got = %v, want %v", got, want)
|
t.Errorf("Downloader_Create() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_CreateNotInWhite(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(&DownloaderConfig{
|
||||||
|
DownloadDirWhiteList: []string{"./downloads"},
|
||||||
|
})
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer downloader.Clear()
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
rr, err := downloader.Resolve(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
_, err = downloader.Create(rr.ID, &base.Options{
|
||||||
|
Path: test.Dir,
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !strings.Contains(err.Error(), "white") {
|
||||||
|
t.Errorf("TestDownloader_CreateNotInWhite() got = %v, want %v", err.Error(), "not in white list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +133,7 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
downloader.DeleteByStatues(nil, true)
|
downloader.Delete(nil, true)
|
||||||
downloader.Clear()
|
downloader.Clear()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -159,21 +197,21 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
|
|
||||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||||
// No proxy
|
// No proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
}, nil)
|
||||||
// Disable proxy
|
// Disable proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Enable = false
|
proxyCfg.Enable = false
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Enable system proxy but not set proxy environment variable
|
// Enable system proxy but not set proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Enable proxy but error proxy environment variable
|
// Enable proxy but error proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
||||||
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
@@ -184,33 +222,50 @@ func TestDownloader_CreateWithProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Enable system proxy and set proxy environment variable
|
// Enable system proxy and set proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
||||||
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Invalid proxy scheme
|
// Invalid proxy scheme
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Scheme = ""
|
proxyCfg.Scheme = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Invalid proxy host
|
// Invalid proxy host
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Host = ""
|
proxyCfg.Host = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Use proxy without auth
|
// Use proxy without auth
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Use proxy with auth
|
// Use proxy with auth
|
||||||
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
|
// Request proxy mode follow
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
reqProxy.Mode = base.RequestProxyModeFollow
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
// Request proxy mode none
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
reqProxy.Mode = base.RequestProxyModeNone
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
// Request proxy mode custom
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
||||||
usr, pwd := "", ""
|
usr, pwd := "", ""
|
||||||
if auth {
|
if auth {
|
||||||
usr, pwd = "admin", "123"
|
usr, pwd = "admin", "123"
|
||||||
@@ -223,17 +278,29 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer downloader.Clear()
|
defer downloader.Clear()
|
||||||
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&base.DownloaderProxyConfig{
|
globalProxyCfg := &base.DownloaderProxyConfig{
|
||||||
Enable: true,
|
Enable: true,
|
||||||
Scheme: "socks5",
|
Scheme: "socks5",
|
||||||
Host: proxyListener.Addr().String(),
|
Host: proxyListener.Addr().String(),
|
||||||
Usr: usr,
|
Usr: usr,
|
||||||
Pwd: pwd,
|
Pwd: pwd,
|
||||||
})
|
}
|
||||||
|
if buildProxyConfig != nil {
|
||||||
|
globalProxyCfg = buildProxyConfig(globalProxyCfg)
|
||||||
|
}
|
||||||
|
downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg
|
||||||
|
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: test.ExternalDownloadUrl,
|
URL: test.ExternalDownloadUrl,
|
||||||
}
|
}
|
||||||
|
if buildReqProxy != nil {
|
||||||
|
req.Proxy = buildReqProxy(&base.RequestProxy{
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
Usr: usr,
|
||||||
|
Pwd: pwd,
|
||||||
|
})
|
||||||
|
}
|
||||||
rr, err := downloader.Resolve(req)
|
rr, err := downloader.Resolve(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errHandler == nil {
|
if errHandler == nil {
|
||||||
@@ -333,7 +400,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 1001)
|
time.Sleep(time.Millisecond * 1001)
|
||||||
err = downloader.Pause(id)
|
err = downloader.Pause(&TaskFilter{IDs: []string{id}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -357,7 +424,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
err = downloader.Continue(id)
|
err = downloader.Continue(&TaskFilter{IDs: []string{id}})
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -418,3 +485,151 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
|||||||
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
|
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(nil)
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
downloader.Delete(nil, true)
|
||||||
|
downloader.Clear()
|
||||||
|
}()
|
||||||
|
|
||||||
|
reqs := make([]*base.Request, 0)
|
||||||
|
fileNames := make([]string, 0)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
reqs = append(reqs, req)
|
||||||
|
if i == 0 {
|
||||||
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
|
} else {
|
||||||
|
arr := strings.Split(test.DownloadName, ".")
|
||||||
|
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(reqs))
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
||||||
|
Path: test.Dir,
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter nil", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(nil)
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter nil task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter empty", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter empty task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: taskIds,
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
})
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{"xxx"},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
Statuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
Statuses: []base.Status{base.DownloadStatusError},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match notStatus", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
NotStatuses: []base.Status{base.DownloadStatusRunning, base.DownloadStatusPause},
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter match notStatus task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match notStatus", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
NotStatuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter not match notStatus task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match ids and status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
Statuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Errorf("GetTasksByFilter match ids and status task got = %v, want %v", len(tasks), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match ids and status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
Statuses: []base.Status{base.DownloadStatusError},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter not match ids and status task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -106,7 +106,7 @@ func (e *Engine) await(value any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) Close() {
|
func (e *Engine) Close() {
|
||||||
e.loop.Stop()
|
e.loop.StopNoWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const (
|
|||||||
EventOnResolve ActivationEvent = "onResolve"
|
EventOnResolve ActivationEvent = "onResolve"
|
||||||
EventOnStart ActivationEvent = "onStart"
|
EventOnStart ActivationEvent = "onStart"
|
||||||
EventOnError ActivationEvent = "onError"
|
EventOnError ActivationEvent = "onError"
|
||||||
//EventOnDone ActivationEvent = "onDone"
|
EventOnDone ActivationEvent = "onDone"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
||||||
@@ -304,6 +304,17 @@ func (d *Downloader) triggerOnError(task *Task, err error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) triggerOnDone(task *Task) {
|
||||||
|
doTrigger(d,
|
||||||
|
EventOnDone,
|
||||||
|
task.Meta.Req,
|
||||||
|
&OnErrorContext{
|
||||||
|
Task: NewExtensionTask(d, task),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
||||||
// init extension global object
|
// init extension global object
|
||||||
gopeed := &Instance{
|
gopeed := &Instance{
|
||||||
@@ -611,9 +622,9 @@ func (h InstanceEvents) OnError(fn goja.Callable) {
|
|||||||
h.register(EventOnError, fn)
|
h.register(EventOnError, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (h InstanceEvents) OnDone(fn goja.Callable) {
|
func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||||
// h.register(HookEventOnDone, fn)
|
h.register(EventOnDone, fn)
|
||||||
//}
|
}
|
||||||
|
|
||||||
type ExtensionInfo struct {
|
type ExtensionInfo struct {
|
||||||
Identity string `json:"identity"`
|
Identity string `json:"identity"`
|
||||||
@@ -687,6 +698,10 @@ type OnErrorContext struct {
|
|||||||
Error error `json:"error"`
|
Error error `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnDoneContext struct {
|
||||||
|
Task *Task `json:"task"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExtensionTask struct {
|
type ExtensionTask struct {
|
||||||
*Task
|
*Task
|
||||||
|
|
||||||
@@ -694,18 +709,26 @@ type ExtensionTask struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
||||||
|
newTask := task.clone()
|
||||||
|
// Assign the pointer of the properties that the extension supports modification
|
||||||
|
newTask.Meta = task.Meta
|
||||||
|
newTask.Status = task.Status
|
||||||
return &ExtensionTask{
|
return &ExtensionTask{
|
||||||
Task: task.clone(),
|
Task: newTask,
|
||||||
download: download,
|
download: download,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ExtensionTask) Continue() error {
|
func (t *ExtensionTask) Continue() error {
|
||||||
return t.download.Continue(t.ID)
|
return t.download.Continue(&TaskFilter{
|
||||||
|
IDs: []string{t.ID},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ExtensionTask) Pause() error {
|
func (t *ExtensionTask) Pause() error {
|
||||||
return t.download.Pause(t.ID)
|
return t.download.Pause(&TaskFilter{
|
||||||
|
IDs: []string{t.ID},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSettings(settings []*Setting) map[string]any {
|
func parseSettings(settings []*Setting) map[string]any {
|
||||||
|
|||||||
@@ -247,8 +247,45 @@ func TestDownloader_Extension_OnError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// extension on error modify url and continue download
|
// extension on error modify url and continue download
|
||||||
task := downloader.GetTask(id)
|
task := downloader.GetTask(id)
|
||||||
if task.Meta.Req.URL != "https://github.com" {
|
if task.Status != base.DownloadStatusDone {
|
||||||
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_Extension_OnDone(t *testing.T) {
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_done", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyFinally {
|
||||||
|
errCh <- event.Err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
id, err := downloader.CreateDirect(&base.Request{
|
||||||
|
URL: "https://github.com",
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-errCh:
|
||||||
|
break
|
||||||
|
case <-time.After(time.Second * 10):
|
||||||
|
err = errors.New("timeout")
|
||||||
|
}
|
||||||
|
// wait for script execution
|
||||||
|
time.Sleep(time.Millisecond * 3000)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("extension on done download error: " + err.Error())
|
||||||
|
}
|
||||||
|
// extension on error modify url and continue download
|
||||||
|
task := downloader.GetTask(id)
|
||||||
|
if task.Meta.Req.Labels["modified"] != "true" {
|
||||||
|
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
|
||||||
}
|
}
|
||||||
if task.Status != base.DownloadStatusDone {
|
if task.Status != base.DownloadStatusDone {
|
||||||
t.Fatalf("except status is done, actual: %s", task.Status)
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package download
|
package download
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
||||||
@@ -49,22 +50,54 @@ func NewTask() *Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns the display name of the task.
|
||||||
|
func (t *Task) Name() string {
|
||||||
|
// Custom name first
|
||||||
|
if t.Meta.Opts.Name != "" {
|
||||||
|
return t.Meta.Opts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task is not resolved, parse the name from the URL
|
||||||
|
if t.Meta.Res == nil {
|
||||||
|
fallbackName := "unknown"
|
||||||
|
if t.fetcherManager == nil {
|
||||||
|
return fallbackName
|
||||||
|
}
|
||||||
|
parseName := t.fetcherManager.ParseName(t.Meta.Req.URL)
|
||||||
|
if parseName == "" {
|
||||||
|
return fallbackName
|
||||||
|
}
|
||||||
|
return parseName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task is a folder
|
||||||
|
if t.Meta.Res.Name != "" {
|
||||||
|
return t.Meta.Res.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the name of the first file
|
||||||
|
return t.Meta.Res.Files[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) MarshalJSON() ([]byte, error) {
|
||||||
|
type rawTaskType Task
|
||||||
|
jsonTask := struct {
|
||||||
|
rawTaskType
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{
|
||||||
|
rawTaskType(*t),
|
||||||
|
t.Name(),
|
||||||
|
}
|
||||||
|
return json.Marshal(jsonTask)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Task) updateStatus(status base.Status) {
|
func (t *Task) updateStatus(status base.Status) {
|
||||||
t.UpdatedAt = time.Now()
|
t.UpdatedAt = time.Now()
|
||||||
t.Status = status
|
t.Status = status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) clone() *Task {
|
func (t *Task) clone() *Task {
|
||||||
return &Task{
|
return util.DeepClone(t)
|
||||||
ID: t.ID,
|
|
||||||
Protocol: t.Protocol,
|
|
||||||
Meta: t.Meta,
|
|
||||||
Status: t.Status,
|
|
||||||
Uploading: t.Uploading,
|
|
||||||
Progress: t.Progress,
|
|
||||||
CreatedAt: t.CreatedAt,
|
|
||||||
UpdatedAt: t.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
||||||
@@ -79,13 +112,24 @@ func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) i
|
|||||||
return int64(float64(total) / float64(len(speedArr)) / usedTime)
|
return int64(float64(total) / float64(len(speedArr)) / usedTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskFilter struct {
|
||||||
|
IDs []string
|
||||||
|
Statuses []base.Status
|
||||||
|
NotStatuses []base.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TaskFilter) IsEmpty() bool {
|
||||||
|
return len(f.IDs) == 0 && len(f.Statuses) == 0 && len(f.NotStatuses) == 0
|
||||||
|
}
|
||||||
|
|
||||||
type DownloaderConfig struct {
|
type DownloaderConfig struct {
|
||||||
Controller *controller.Controller
|
Controller *controller.Controller
|
||||||
FetchManagers []fetcher.FetcherManager
|
FetchManagers []fetcher.FetcherManager
|
||||||
|
|
||||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
||||||
Storage Storage
|
Storage Storage
|
||||||
StorageDir string
|
StorageDir string
|
||||||
|
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
gopeed.events.onDone(async function (ctx) {
|
||||||
|
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||||
|
ctx.task.meta.req.labels['modified'] = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "on-done",
|
||||||
|
"title": "gopeed extension on done event test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onDone",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://github.com/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -74,13 +74,27 @@ func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
if taskId == "" {
|
WriteJson(w, errResult)
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Pause(taskId); err != nil {
|
|
||||||
|
if err := Downloader.Pause(filter); err != nil {
|
||||||
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJson(w, model.NewNilResult())
|
||||||
|
}
|
||||||
|
|
||||||
|
func PauseTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filter, errResult := parseFilter(r)
|
||||||
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Downloader.Pause(filter); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -88,29 +102,27 @@ func PauseTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
if taskId == "" {
|
WriteJson(w, errResult)
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Continue(taskId); err != nil {
|
|
||||||
|
if err := Downloader.Continue(filter); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
WriteJson(w, model.NewNilResult())
|
WriteJson(w, model.NewNilResult())
|
||||||
}
|
}
|
||||||
|
|
||||||
func PauseAllTask(w http.ResponseWriter, r *http.Request) {
|
func ContinueTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := Downloader.PauseAll(); err != nil {
|
filter, errResult := parseFilter(r)
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
WriteJson(w, model.NewNilResult())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
|
if err := Downloader.Continue(filter); err != nil {
|
||||||
if err := Downloader.ContinueAll(); err != nil {
|
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -118,14 +130,14 @@ func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
force := r.FormValue("force")
|
WriteJson(w, errResult)
|
||||||
if taskId == "" {
|
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Delete(taskId, force == "true"); err != nil {
|
force := r.FormValue("force")
|
||||||
|
|
||||||
|
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -133,10 +145,14 @@ func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
filter, errResult := parseFilter(r)
|
||||||
status := r.Form["status"]
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
force := r.FormValue("force")
|
force := r.FormValue("force")
|
||||||
if err := Downloader.DeleteByStatues(convertStatues(status), force == "true"); err != nil {
|
|
||||||
|
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,9 +175,13 @@ func GetTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
filter, errResult := parseFilter(r)
|
||||||
status := r.Form["status"]
|
if errResult != nil {
|
||||||
tasks := Downloader.GetTasksByStatues(convertStatues(status))
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := Downloader.GetTasksByFilter(filter)
|
||||||
WriteJson(w, model.NewOkResult(tasks))
|
WriteJson(w, model.NewOkResult(tasks))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +344,32 @@ func GetStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
WriteJson(w, model.NewOkResult(statsResult))
|
WriteJson(w, model.NewOkResult(statsResult))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseIdFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
taskId := vars["id"]
|
||||||
|
if taskId == "" {
|
||||||
|
return nil, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &download.TaskFilter{
|
||||||
|
IDs: []string{taskId},
|
||||||
|
}
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return nil, model.NewErrorResult(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &download.TaskFilter{
|
||||||
|
IDs: r.Form["id"],
|
||||||
|
Statuses: convertStatues(r.Form["status"]),
|
||||||
|
NotStatuses: convertStatues(r.Form["notStatus"]),
|
||||||
|
}
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
func convertStatues(statues []string) []base.Status {
|
func convertStatues(statues []string) []base.Status {
|
||||||
result := make([]base.Status, 0)
|
result := make([]base.Status, 0)
|
||||||
for _, status := range statues {
|
for _, status := range statues {
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StartConfig struct {
|
type StartConfig struct {
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
RefreshInterval int `json:"refreshInterval"`
|
RefreshInterval int `json:"refreshInterval"`
|
||||||
Storage Storage `json:"storage"`
|
Storage Storage `json:"storage"`
|
||||||
StorageDir string `json:"storageDir"`
|
StorageDir string `json:"storageDir"`
|
||||||
ApiToken string `json:"apiToken"`
|
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
ApiToken string `json:"apiToken"`
|
||||||
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
||||||
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
|
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
||||||
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseTasks)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueTasks)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask)
|
|
||||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
|
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
|
||||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
|
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
|
||||||
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
||||||
|
|||||||
@@ -582,8 +582,8 @@ func doTest(handler func()) {
|
|||||||
Downloader.Clear()
|
Downloader.Clear()
|
||||||
}()
|
}()
|
||||||
defer func() {
|
defer func() {
|
||||||
Downloader.PauseAll()
|
Downloader.Pause(nil)
|
||||||
Downloader.DeleteByStatues(nil, true)
|
Downloader.Delete(nil, true)
|
||||||
os.RemoveAll(cfg.StorageDir)
|
os.RemoveAll(cfg.StorageDir)
|
||||||
}()
|
}()
|
||||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||||
|
|||||||
@@ -12,3 +12,17 @@ func MapToStruct(s any, v any) error {
|
|||||||
}
|
}
|
||||||
return json.Unmarshal(b, v)
|
return json.Unmarshal(b, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeepClone[T any](v *T) *T {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var t T
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
json.Unmarshal(b, &t)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeepClone(t *testing.T) {
|
||||||
|
type user struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
|
||||||
|
v int
|
||||||
|
}
|
||||||
|
|
||||||
|
type args[T any] struct {
|
||||||
|
v *T
|
||||||
|
}
|
||||||
|
type testCase[T any] struct {
|
||||||
|
name string
|
||||||
|
args args[T]
|
||||||
|
want *T
|
||||||
|
}
|
||||||
|
tests := []testCase[user]{
|
||||||
|
{
|
||||||
|
name: "case 1",
|
||||||
|
args: args[user]{
|
||||||
|
v: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case 2",
|
||||||
|
args: args[user]{
|
||||||
|
v: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
v: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := DeepClone(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("DeepClone() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ android {
|
|||||||
applicationId "com.gopeed"
|
applicationId "com.gopeed"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||||
minSdkVersion 32
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -2,9 +2,15 @@
|
|||||||
package="com.gopeed">
|
package="com.gopeed">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="Gopeed"
|
android:label="Gopeed"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 63 KiB |
@@ -79,6 +79,7 @@ class ExtraConfig {
|
|||||||
String themeMode;
|
String themeMode;
|
||||||
String locale;
|
String locale;
|
||||||
bool lastDeleteTaskKeep;
|
bool lastDeleteTaskKeep;
|
||||||
|
bool defaultDirectDownload;
|
||||||
|
|
||||||
ExtraConfigBt bt = ExtraConfigBt();
|
ExtraConfigBt bt = ExtraConfigBt();
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ class ExtraConfig {
|
|||||||
this.themeMode = '',
|
this.themeMode = '',
|
||||||
this.locale = '',
|
this.locale = '',
|
||||||
this.lastDeleteTaskKeep = false,
|
this.lastDeleteTaskKeep = false,
|
||||||
|
this.defaultDirectDownload = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ part of 'downloader_config.dart';
|
|||||||
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||||
DownloaderConfig(
|
DownloaderConfig(
|
||||||
downloadDir: json['downloadDir'] as String? ?? '',
|
downloadDir: json['downloadDir'] as String? ?? '',
|
||||||
maxRunning: json['maxRunning'] as int? ?? 0,
|
maxRunning: (json['maxRunning'] as num?)?.toInt() ?? 0,
|
||||||
)
|
)
|
||||||
..protocolConfig = ProtocolConfig.fromJson(
|
..protocolConfig = ProtocolConfig.fromJson(
|
||||||
json['protocolConfig'] as Map<String, dynamic>?)
|
json['protocolConfig'] as Map<String, dynamic>?)
|
||||||
@@ -38,7 +38,7 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
|
|||||||
|
|
||||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
||||||
userAgent: json['userAgent'] as String? ?? '',
|
userAgent: json['userAgent'] as String? ?? '',
|
||||||
connections: json['connections'] as int? ?? 0,
|
connections: (json['connections'] as num?)?.toInt() ?? 0,
|
||||||
useServerCtime: json['useServerCtime'] as bool? ?? false,
|
useServerCtime: json['useServerCtime'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -50,14 +50,14 @@ Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig(
|
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig(
|
||||||
listenPort: json['listenPort'] as int? ?? 0,
|
listenPort: (json['listenPort'] as num?)?.toInt() ?? 0,
|
||||||
trackers: (json['trackers'] as List<dynamic>?)
|
trackers: (json['trackers'] as List<dynamic>?)
|
||||||
?.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
seedKeep: json['seedKeep'] as bool? ?? false,
|
seedKeep: json['seedKeep'] as bool? ?? false,
|
||||||
seedRatio: (json['seedRatio'] as num?)?.toDouble() ?? 0,
|
seedRatio: (json['seedRatio'] as num?)?.toDouble() ?? 0,
|
||||||
seedTime: json['seedTime'] as int? ?? 0,
|
seedTime: (json['seedTime'] as num?)?.toInt() ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
|
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
|
||||||
@@ -72,6 +72,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
|||||||
themeMode: json['themeMode'] as String? ?? '',
|
themeMode: json['themeMode'] as String? ?? '',
|
||||||
locale: json['locale'] as String? ?? '',
|
locale: json['locale'] as String? ?? '',
|
||||||
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
||||||
|
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
||||||
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||||
|
|
||||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||||
@@ -79,6 +80,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
|||||||
'themeMode': instance.themeMode,
|
'themeMode': instance.themeMode,
|
||||||
'locale': instance.locale,
|
'locale': instance.locale,
|
||||||
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
||||||
|
'defaultDirectDownload': instance.defaultDirectDownload,
|
||||||
'bt': instance.bt.toJson(),
|
'bt': instance.bt.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ class Request {
|
|||||||
String url;
|
String url;
|
||||||
Object? extra;
|
Object? extra;
|
||||||
Map<String, String>? labels = {};
|
Map<String, String>? labels = {};
|
||||||
|
RequestProxy? proxy;
|
||||||
|
|
||||||
Request({
|
Request({
|
||||||
required this.url,
|
required this.url,
|
||||||
this.extra,
|
this.extra,
|
||||||
this.labels,
|
this.labels,
|
||||||
|
this.proxy,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Request.fromJson(Map<String, dynamic> json) =>
|
factory Request.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -45,3 +47,24 @@ class ReqExtraBt {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
|
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RequestProxyMode {
|
||||||
|
follow,
|
||||||
|
none,
|
||||||
|
custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class RequestProxy {
|
||||||
|
RequestProxyMode mode = RequestProxyMode.follow;
|
||||||
|
String scheme = 'http';
|
||||||
|
String host = '';
|
||||||
|
String usr = '';
|
||||||
|
String pwd = '';
|
||||||
|
|
||||||
|
RequestProxy();
|
||||||
|
|
||||||
|
factory RequestProxy.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$RequestProxyFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$RequestProxyToJson(this);
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
|||||||
labels: (json['labels'] as Map<String, dynamic>?)?.map(
|
labels: (json['labels'] as Map<String, dynamic>?)?.map(
|
||||||
(k, e) => MapEntry(k, e as String),
|
(k, e) => MapEntry(k, e as String),
|
||||||
),
|
),
|
||||||
|
proxy: json['proxy'] == null
|
||||||
|
? null
|
||||||
|
: RequestProxy.fromJson(json['proxy'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RequestToJson(Request instance) {
|
Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||||
@@ -27,6 +30,7 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
|||||||
|
|
||||||
writeNotNull('extra', instance.extra);
|
writeNotNull('extra', instance.extra);
|
||||||
writeNotNull('labels', instance.labels);
|
writeNotNull('labels', instance.labels);
|
||||||
|
writeNotNull('proxy', instance.proxy?.toJson());
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,3 +54,25 @@ Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
|
|||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'trackers': instance.trackers,
|
'trackers': instance.trackers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy()
|
||||||
|
..mode = $enumDecode(_$RequestProxyModeEnumMap, json['mode'])
|
||||||
|
..scheme = json['scheme'] as String
|
||||||
|
..host = json['host'] as String
|
||||||
|
..usr = json['usr'] as String
|
||||||
|
..pwd = json['pwd'] as String;
|
||||||
|
|
||||||
|
Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'mode': _$RequestProxyModeEnumMap[instance.mode]!,
|
||||||
|
'scheme': instance.scheme,
|
||||||
|
'host': instance.host,
|
||||||
|
'usr': instance.usr,
|
||||||
|
'pwd': instance.pwd,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$RequestProxyModeEnumMap = {
|
||||||
|
RequestProxyMode.follow: 'follow',
|
||||||
|
RequestProxyMode.none: 'none',
|
||||||
|
RequestProxyMode.custom: 'custom',
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ part of 'resource.dart';
|
|||||||
|
|
||||||
Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
|
Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
|
||||||
name: json['name'] as String? ?? "",
|
name: json['name'] as String? ?? "",
|
||||||
size: json['size'] as int? ?? 0,
|
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||||
range: json['range'] as bool? ?? false,
|
range: json['range'] as bool? ?? false,
|
||||||
files: (json['files'] as List<dynamic>)
|
files: (json['files'] as List<dynamic>)
|
||||||
.map((e) => FileInfo.fromJson(e as Map<String, dynamic>))
|
.map((e) => FileInfo.fromJson(e as Map<String, dynamic>))
|
||||||
@@ -27,7 +27,7 @@ Map<String, dynamic> _$ResourceToJson(Resource instance) => <String, dynamic>{
|
|||||||
FileInfo _$FileInfoFromJson(Map<String, dynamic> json) => FileInfo(
|
FileInfo _$FileInfoFromJson(Map<String, dynamic> json) => FileInfo(
|
||||||
path: json['path'] as String? ?? "",
|
path: json['path'] as String? ?? "",
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
size: json['size'] as int? ?? 0,
|
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||||
req: json['req'] == null
|
req: json['req'] == null
|
||||||
? null
|
? null
|
||||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ enum Protocol { http, bt }
|
|||||||
@JsonSerializable(explicitToJson: true)
|
@JsonSerializable(explicitToJson: true)
|
||||||
class Task {
|
class Task {
|
||||||
String id;
|
String id;
|
||||||
|
String name;
|
||||||
Protocol? protocol;
|
Protocol? protocol;
|
||||||
Meta meta;
|
Meta meta;
|
||||||
Status status;
|
Status status;
|
||||||
@@ -21,6 +22,7 @@ class Task {
|
|||||||
|
|
||||||
Task({
|
Task({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.name,
|
||||||
required this.meta,
|
required this.meta,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.uploading,
|
required this.uploading,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ part of 'task.dart';
|
|||||||
|
|
||||||
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||||
status: $enumDecode(_$StatusEnumMap, json['status']),
|
status: $enumDecode(_$StatusEnumMap, json['status']),
|
||||||
uploading: json['uploading'] as bool,
|
uploading: json['uploading'] as bool,
|
||||||
@@ -19,6 +20,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
|||||||
Map<String, dynamic> _$TaskToJson(Task instance) {
|
Map<String, dynamic> _$TaskToJson(Task instance) {
|
||||||
final val = <String, dynamic>{
|
final val = <String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
void writeNotNull(String key, dynamic value) {
|
void writeNotNull(String key, dynamic value) {
|
||||||
@@ -52,11 +54,11 @@ const _$ProtocolEnumMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
|
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
|
||||||
used: json['used'] as int,
|
used: (json['used'] as num).toInt(),
|
||||||
speed: json['speed'] as int,
|
speed: (json['speed'] as num).toInt(),
|
||||||
downloaded: json['downloaded'] as int,
|
downloaded: (json['downloaded'] as num).toInt(),
|
||||||
uploadSpeed: json['uploadSpeed'] as int,
|
uploadSpeed: (json['uploadSpeed'] as num).toInt(),
|
||||||
uploaded: json['uploaded'] as int,
|
uploaded: (json['uploaded'] as num).toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ProgressToJson(Progress instance) => <String, dynamic>{
|
Map<String, dynamic> _$ProgressToJson(Progress instance) => <String, dynamic>{
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:gopeed/api/model/request.dart';
|
||||||
|
|
||||||
|
import '../../app/controllers/app_controller.dart';
|
||||||
|
|
||||||
class CreateController extends GetxController
|
class CreateController extends GetxController
|
||||||
with GetSingleTickerProviderStateMixin {
|
with GetSingleTickerProviderStateMixin {
|
||||||
@@ -13,6 +16,7 @@ class CreateController extends GetxController
|
|||||||
final isConfirming = false.obs;
|
final isConfirming = false.obs;
|
||||||
final showAdvanced = false.obs;
|
final showAdvanced = false.obs;
|
||||||
final directDownload = false.obs;
|
final directDownload = false.obs;
|
||||||
|
final proxyConfig = Rx<RequestProxy?>(null);
|
||||||
late TabController advancedTabController;
|
late TabController advancedTabController;
|
||||||
final oldUrl = "".obs;
|
final oldUrl = "".obs;
|
||||||
final fileDataUri = "".obs;
|
final fileDataUri = "".obs;
|
||||||
@@ -21,6 +25,11 @@ class CreateController extends GetxController
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
advancedTabController = TabController(length: 2, vsync: this);
|
advancedTabController = TabController(length: 2, vsync: this);
|
||||||
|
directDownload.value = Get.find<AppController>()
|
||||||
|
.downloaderConfig
|
||||||
|
.value
|
||||||
|
.extra
|
||||||
|
.defaultDirectDownload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class CreateView extends GetView<CreateController> {
|
|||||||
final _connectionsController = TextEditingController();
|
final _connectionsController = TextEditingController();
|
||||||
final _pathController = TextEditingController();
|
final _pathController = TextEditingController();
|
||||||
final _confirmController = RoundedLoadingButtonController();
|
final _confirmController = RoundedLoadingButtonController();
|
||||||
|
final _proxyIpController = TextEditingController();
|
||||||
|
final _proxyPortController = TextEditingController();
|
||||||
|
final _proxyUsrController = TextEditingController();
|
||||||
|
final _proxyPwdController = TextEditingController();
|
||||||
final _httpUaController = TextEditingController();
|
final _httpUaController = TextEditingController();
|
||||||
final _httpCookieController = TextEditingController();
|
final _httpCookieController = TextEditingController();
|
||||||
final _httpRefererController = TextEditingController();
|
final _httpRefererController = TextEditingController();
|
||||||
@@ -206,7 +210,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.background,
|
.surface,
|
||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(
|
BorderRadius.circular(
|
||||||
10.0),
|
10.0),
|
||||||
@@ -261,7 +265,164 @@ class CreateView extends GetView<CreateController> {
|
|||||||
() => Visibility(
|
() => Visibility(
|
||||||
visible: controller.showAdvanced.value,
|
visible: controller.showAdvanced.value,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(-40, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.wifi_2_bar,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 15,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 150,
|
||||||
|
child: DropdownButton<
|
||||||
|
RequestProxyMode>(
|
||||||
|
hint: Text('proxy'.tr),
|
||||||
|
isExpanded: true,
|
||||||
|
value: controller
|
||||||
|
.proxyConfig.value?.mode,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value != null) {
|
||||||
|
controller.proxyConfig
|
||||||
|
.value = RequestProxy()
|
||||||
|
..mode = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem<
|
||||||
|
RequestProxyMode>(
|
||||||
|
value:
|
||||||
|
RequestProxyMode.follow,
|
||||||
|
child: Text(
|
||||||
|
'followSettings'.tr),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<
|
||||||
|
RequestProxyMode>(
|
||||||
|
value:
|
||||||
|
RequestProxyMode.none,
|
||||||
|
child: Text('noProxy'.tr),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<
|
||||||
|
RequestProxyMode>(
|
||||||
|
value:
|
||||||
|
RequestProxyMode.custom,
|
||||||
|
child:
|
||||||
|
Text('customProxy'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...(controller.proxyConfig.value?.mode ==
|
||||||
|
RequestProxyMode.custom
|
||||||
|
? [
|
||||||
|
SizedBox(
|
||||||
|
width: 150,
|
||||||
|
child: DropdownButtonFormField<
|
||||||
|
String>(
|
||||||
|
value: controller
|
||||||
|
.proxyConfig.value?.scheme,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value != null) {}
|
||||||
|
},
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: 'http',
|
||||||
|
child: Text('HTTP'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: 'https',
|
||||||
|
child: Text('HTTPS'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: 'socks5',
|
||||||
|
child: Text('SOCKS5'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(children: [
|
||||||
|
Flexible(
|
||||||
|
child: TextFormField(
|
||||||
|
controller:
|
||||||
|
_proxyIpController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'server'.tr,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.all(
|
||||||
|
0.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 10)),
|
||||||
|
Flexible(
|
||||||
|
child: TextFormField(
|
||||||
|
controller:
|
||||||
|
_proxyPortController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'port'.tr,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.all(
|
||||||
|
0.0),
|
||||||
|
),
|
||||||
|
keyboardType:
|
||||||
|
TextInputType.number,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter
|
||||||
|
.digitsOnly,
|
||||||
|
NumericalRangeFormatter(
|
||||||
|
min: 0, max: 65535),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Row(children: [
|
||||||
|
Flexible(
|
||||||
|
child: TextFormField(
|
||||||
|
controller:
|
||||||
|
_proxyUsrController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'username'.tr,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.all(
|
||||||
|
0.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 10)),
|
||||||
|
Flexible(
|
||||||
|
child: TextFormField(
|
||||||
|
controller:
|
||||||
|
_proxyPwdController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'password'.tr,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.all(
|
||||||
|
0.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
: const []),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
TabBar(
|
TabBar(
|
||||||
controller: controller.advancedTabController,
|
controller: controller.advancedTabController,
|
||||||
tabs: const [
|
tabs: const [
|
||||||
@@ -416,18 +577,21 @@ class CreateView extends GetView<CreateController> {
|
|||||||
if (isDirect) {
|
if (isDirect) {
|
||||||
await Future.wait(urls.map((url) {
|
await Future.wait(urls.map((url) {
|
||||||
return createTask(CreateTask(
|
return createTask(CreateTask(
|
||||||
req: Request(url: url, extra: parseReqExtra(url)),
|
req: Request(
|
||||||
|
url: url, extra: parseReqExtra(url), proxy: parseProxy()),
|
||||||
opt: Options(
|
opt: Options(
|
||||||
name: isMultiLine ? "" : _renameController.text,
|
name: isMultiLine ? "" : _renameController.text,
|
||||||
path: _pathController.text,
|
path: _pathController.text,
|
||||||
selectFiles: [],
|
selectFiles: [],
|
||||||
extra: parseReqOptsExtra())));
|
extra: parseReqOptsExtra(),
|
||||||
|
)));
|
||||||
}));
|
}));
|
||||||
Get.rootDelegate.offNamed(Routes.TASK);
|
Get.rootDelegate.offNamed(Routes.TASK);
|
||||||
} else {
|
} else {
|
||||||
final rr = await resolve(Request(
|
final rr = await resolve(Request(
|
||||||
url: submitUrl,
|
url: submitUrl,
|
||||||
extra: parseReqExtra(_urlController.text),
|
extra: parseReqExtra(_urlController.text),
|
||||||
|
proxy: parseProxy(),
|
||||||
));
|
));
|
||||||
await _showResolveDialog(rr);
|
await _showResolveDialog(rr);
|
||||||
}
|
}
|
||||||
@@ -440,6 +604,18 @@ class CreateView extends GetView<CreateController> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RequestProxy? parseProxy() {
|
||||||
|
if (controller.proxyConfig.value?.mode == RequestProxyMode.custom) {
|
||||||
|
return RequestProxy()
|
||||||
|
..mode = RequestProxyMode.custom
|
||||||
|
..scheme = _proxyIpController.text
|
||||||
|
..host = "${_proxyIpController.text}:${_proxyPortController.text}"
|
||||||
|
..usr = _proxyUsrController.text
|
||||||
|
..pwd = _proxyPwdController.text;
|
||||||
|
}
|
||||||
|
return controller.proxyConfig.value;
|
||||||
|
}
|
||||||
|
|
||||||
Object? parseReqExtra(String url) {
|
Object? parseReqExtra(String url) {
|
||||||
Object? reqExtra;
|
Object? reqExtra;
|
||||||
if (controller.showAdvanced.value) {
|
if (controller.showAdvanced.value) {
|
||||||
@@ -543,7 +719,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
controller.selectedIndexes.map((index) {
|
controller.selectedIndexes.map((index) {
|
||||||
final file = rr.res.files[index];
|
final file = rr.res.files[index];
|
||||||
return createTask(CreateTask(
|
return createTask(CreateTask(
|
||||||
req: file.req!,
|
req: file.req!..proxy = parseProxy(),
|
||||||
opt: Options(
|
opt: Options(
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: path.join(_pathController.text,
|
path: path.join(_pathController.text,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:gopeed/app/views/copy_button.dart';
|
import 'package:gopeed/app/views/copy_button.dart';
|
||||||
@@ -110,6 +109,28 @@ class SettingView extends GetView<SettingController> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final buildDefaultDirectDownload =
|
||||||
|
_buildConfigItem('defaultDirectDownload'.tr, () {
|
||||||
|
return appController.downloaderConfig.value.extra.defaultDirectDownload
|
||||||
|
? 'on'.tr
|
||||||
|
: 'off'.tr;
|
||||||
|
}, (Key key) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Switch(
|
||||||
|
value:
|
||||||
|
appController.downloaderConfig.value.extra.defaultDirectDownload,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
appController.downloaderConfig.update((val) {
|
||||||
|
val!.extra.defaultDirectDownload = value;
|
||||||
|
});
|
||||||
|
await debounceSave();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
buildBrowserExtension() {
|
buildBrowserExtension() {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text('browserExtension'.tr),
|
title: Text('browserExtension'.tr),
|
||||||
@@ -847,7 +868,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
// advanced config log items start
|
// advanced config log items start
|
||||||
buildLogsDir() {
|
buildLogsDir() {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text("日志目录"),
|
title: Text("logDirectory".tr),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -904,6 +925,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
children: _addDivider([
|
children: _addDivider([
|
||||||
buildDownloadDir(),
|
buildDownloadDir(),
|
||||||
buildMaxRunning(),
|
buildMaxRunning(),
|
||||||
|
buildDefaultDirectDownload(),
|
||||||
buildBrowserExtension(),
|
buildBrowserExtension(),
|
||||||
buildAutoStartup(),
|
buildAutoStartup(),
|
||||||
]),
|
]),
|
||||||
@@ -981,7 +1003,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
: null,
|
: null,
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
const Text('开发者'),
|
Text('developer'.tr),
|
||||||
Card(
|
Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: _addDivider([
|
children: _addDivider([
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ class TaskView extends GetView<TaskController> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('taskName'.tr),
|
title: Text('taskName'.tr),
|
||||||
subtitle:
|
subtitle: buildTooltipSubtitle(selectTask.value?.name)),
|
||||||
buildTooltipSubtitle(selectTask.value?.showName)),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('taskUrl'.tr),
|
title: Text('taskUrl'.tr),
|
||||||
subtitle:
|
subtitle:
|
||||||
@@ -121,42 +120,12 @@ class TaskView extends GetView<TaskController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension TaskEnhance on Task {
|
extension TaskEnhance on Task {
|
||||||
String get showName {
|
|
||||||
if (meta.opts.name.isNotEmpty) {
|
|
||||||
return meta.opts.name;
|
|
||||||
}
|
|
||||||
if (meta.res == null) {
|
|
||||||
final u = Uri.parse(meta.req.url);
|
|
||||||
if (u.scheme.startsWith("http")) {
|
|
||||||
return u.path.isNotEmpty
|
|
||||||
? u.path.substring(u.path.lastIndexOf("/") + 1)
|
|
||||||
: u.host;
|
|
||||||
} else {
|
|
||||||
final params = u.queryParameters;
|
|
||||||
if (params.containsKey("dn")) {
|
|
||||||
return params["dn"]!;
|
|
||||||
} else {
|
|
||||||
return params["xt"]!.split(":").last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (meta.res!.name.isNotEmpty) {
|
|
||||||
return meta.res!.name;
|
|
||||||
}
|
|
||||||
return meta.res!.files[0].name;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isFolder {
|
bool get isFolder {
|
||||||
return meta.res?.name.isNotEmpty ?? false;
|
return meta.res?.name.isNotEmpty ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get explorerUrl {
|
String get explorerUrl {
|
||||||
if (isFolder) {
|
return path.join(Util.safeDir(meta.opts.path), Util.safeDir(name));
|
||||||
return path.join(Util.safeDir(meta.opts.path),
|
|
||||||
Util.safeDir(meta.res!.files[0].path), showName);
|
|
||||||
} else {
|
|
||||||
return path.join(Util.safeDir(meta.opts.path), Util.safeDir(showName));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> explorer() async {
|
Future<void> explorer() async {
|
||||||
|
|||||||
@@ -183,10 +183,10 @@ class BuildTaskListView extends GetView {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(task.showName),
|
title: Text(task.name),
|
||||||
leading: isFolderTask()
|
leading: isFolderTask()
|
||||||
? const Icon(FaIcons.folder)
|
? const Icon(FaIcons.folder)
|
||||||
: Icon(FaIcons.allIcons[findIcon(task.showName)])),
|
: Icon(FaIcons.allIcons[findIcon(task.name)])),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
|
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:toggle_switch/toggle_switch.dart';
|
import 'package:toggle_switch/toggle_switch.dart';
|
||||||
|
|
||||||
import '../../util/mac_secure_util.dart';
|
import '../../util/mac_secure_util.dart';
|
||||||
import '../../util/message.dart';
|
import '../../util/message.dart';
|
||||||
import '../../util/util.dart';
|
import '../../util/util.dart';
|
||||||
|
|
||||||
|
final deviceInfo = DeviceInfoPlugin();
|
||||||
|
|
||||||
class DirectorySelector extends StatefulWidget {
|
class DirectorySelector extends StatefulWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final bool showLabel;
|
final bool showLabel;
|
||||||
@@ -68,12 +72,26 @@ class _DirectorySelectorState extends State<DirectorySelector> {
|
|||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Check write permission
|
|
||||||
final downloadDir =
|
final downloadDir =
|
||||||
(await DownloadsPath.downloadsDirectory())?.path;
|
(await DownloadsPath.downloadsDirectory())?.path;
|
||||||
if (downloadDir == null) {
|
if (downloadDir == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check and request external storage permission when sdk version < 30 (android 11)
|
||||||
|
if ((await deviceInfo.androidInfo).version.sdkInt < 30) {
|
||||||
|
var status = await Permission.storage.status;
|
||||||
|
if (!status.isGranted) {
|
||||||
|
status = await Permission.storage.request();
|
||||||
|
if (!status.isGranted) {
|
||||||
|
showErrorMessage('noStoragePermission'.tr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check write permission
|
||||||
final fileRandomeName =
|
final fileRandomeName =
|
||||||
"test_${DateTime.now().millisecondsSinceEpoch}.tmp";
|
"test_${DateTime.now().millisecondsSinceEpoch}.tmp";
|
||||||
final testFile = File('$downloadDir/Gopeed/$fileRandomeName');
|
final testFile = File('$downloadDir/Gopeed/$fileRandomeName');
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const enUS = {
|
|||||||
'create': 'Create Task',
|
'create': 'Create Task',
|
||||||
'directDownload': 'Direct Download',
|
'directDownload': 'Direct Download',
|
||||||
'advancedOptions': 'Advanced Options',
|
'advancedOptions': 'Advanced Options',
|
||||||
|
'followSettings': 'Follow Settings',
|
||||||
'downloadLink': 'Download Link',
|
'downloadLink': 'Download Link',
|
||||||
'downloadLinkValid': 'Please enter the download link',
|
'downloadLinkValid': 'Please enter the download link',
|
||||||
'downloadLinkHit':
|
'downloadLinkHit':
|
||||||
@@ -35,6 +36,7 @@ const enUS = {
|
|||||||
'connections': 'Connections',
|
'connections': 'Connections',
|
||||||
'useServerCtime': 'Use server time for file creation',
|
'useServerCtime': 'Use server time for file creation',
|
||||||
'maxRunning': 'Max Running Tasks',
|
'maxRunning': 'Max Running Tasks',
|
||||||
|
'defaultDirectDownload': 'Check direct download by default',
|
||||||
'items': '@count items',
|
'items': '@count items',
|
||||||
'subscribeTracker': 'Subscribe Tracker',
|
'subscribeTracker': 'Subscribe Tracker',
|
||||||
'subscribeFail':
|
'subscribeFail':
|
||||||
@@ -60,6 +62,8 @@ const enUS = {
|
|||||||
'set': 'SET',
|
'set': 'SET',
|
||||||
'portInUse': 'Port [@port] is in use, please change the port',
|
'portInUse': 'Port [@port] is in use, please change the port',
|
||||||
'effectAfterRestart': 'Effect after restart',
|
'effectAfterRestart': 'Effect after restart',
|
||||||
|
'developer': 'Developer',
|
||||||
|
'logDirectory': 'Log Directory',
|
||||||
'show': 'Show',
|
'show': 'Show',
|
||||||
'startAll': 'Start All',
|
'startAll': 'Start All',
|
||||||
'pauseAll': 'Pause All',
|
'pauseAll': 'Pause All',
|
||||||
@@ -96,7 +100,7 @@ const enUS = {
|
|||||||
'browserExtension': 'Browser Extension',
|
'browserExtension': 'Browser Extension',
|
||||||
'launchAtStartup': 'Launch at Startup',
|
'launchAtStartup': 'Launch at Startup',
|
||||||
'seedConfig': 'Seed Config',
|
'seedConfig': 'Seed Config',
|
||||||
'seedKeep': 'Keep seeding',
|
'seedKeep': 'Keep seeding until manually stopped',
|
||||||
'seedRatio': 'Seed ratio',
|
'seedRatio': 'Seed ratio',
|
||||||
'seedTime': 'Seed time (minutes)',
|
'seedTime': 'Seed time (minutes)',
|
||||||
'taskDetail': 'Task Detail',
|
'taskDetail': 'Task Detail',
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
const esES = {
|
||||||
|
'es_ES': {
|
||||||
|
'label': 'Español',
|
||||||
|
'error': 'Error',
|
||||||
|
'tip': 'Consejo',
|
||||||
|
'confirm': 'Confirmar',
|
||||||
|
'cancel': 'Cancelar',
|
||||||
|
'on': 'Activado',
|
||||||
|
'off': 'Desactivado',
|
||||||
|
'selectAll': 'Seleccionar Todo',
|
||||||
|
'task': 'Tareas',
|
||||||
|
'downloading': 'descargando',
|
||||||
|
'downloaded': 'descargado',
|
||||||
|
'setting': 'Ajustes',
|
||||||
|
'donate': 'Donar',
|
||||||
|
'exit': 'Salir',
|
||||||
|
'create': 'Crear Tarea',
|
||||||
|
'directDownload': 'Descarga Directa',
|
||||||
|
'advancedOptions': 'Opciones Avanzadas',
|
||||||
|
'downloadLink': 'Enlace de Descarga',
|
||||||
|
'downloadLinkValid': 'Por favor, introduce el enlace de descarga',
|
||||||
|
'downloadLinkHit':
|
||||||
|
'Por favor, introduce el enlace de descarga, se admite HTTP/HTTPS/MAGNET@append',
|
||||||
|
'downloadLinkHitDesktop': ', o arrastra el archivo torrent aquí directamente',
|
||||||
|
'download': 'Descargar',
|
||||||
|
'noFileSelected': 'Por favor, selecciona al menos un archivo para continuar.',
|
||||||
|
'noStoragePermission': 'Se requiere permiso de almacenamiento',
|
||||||
|
'selectFile': 'Seleccionar Archivo',
|
||||||
|
'rename': 'Renombrar',
|
||||||
|
'basic': 'Básico',
|
||||||
|
'advanced': 'Avanzado',
|
||||||
|
'general': 'General',
|
||||||
|
'downloadDir': 'Directorio de Descarga',
|
||||||
|
'downloadDirValid': 'Por favor, selecciona el directorio de descarga',
|
||||||
|
'connections': 'Conexiones',
|
||||||
|
'useServerCtime': 'Usar tiempo del servidor para la creación de archivos',
|
||||||
|
'maxRunning': 'Máximo de Tareas en Ejecución',
|
||||||
|
'items': '@count elementos',
|
||||||
|
'subscribeTracker': 'Suscribirse al Tracker',
|
||||||
|
'subscribeFail':
|
||||||
|
'Suscripción fallida, por favor verifica la red o intenta más tarde',
|
||||||
|
'update': 'Actualizar',
|
||||||
|
'updateDaily': 'Actualizar diariamente',
|
||||||
|
'lastUpdate': 'Última actualización: @time',
|
||||||
|
'addTracker': 'Añadir Tracker',
|
||||||
|
'addTrackerHit': 'Por favor, introduce la URL del servidor tracker, una por línea',
|
||||||
|
'ui': 'Interfaz',
|
||||||
|
'theme': 'Tema',
|
||||||
|
'themeSystem': 'Sistema',
|
||||||
|
'themeLight': 'Claro',
|
||||||
|
'themeDark': 'Oscuro',
|
||||||
|
'locale': 'Idioma',
|
||||||
|
'about': 'Acerca de',
|
||||||
|
'homepage': 'Página de inicio',
|
||||||
|
'version': 'Versión',
|
||||||
|
'protocol': 'Protocolo',
|
||||||
|
'port': 'Puerto',
|
||||||
|
'apiToken': 'Token API',
|
||||||
|
'notSet': 'No establecido',
|
||||||
|
'set': 'Establecido',
|
||||||
|
'portInUse': 'El puerto [@port] está en uso, por favor cambia el puerto',
|
||||||
|
'effectAfterRestart': 'Efectivo después de reiniciar',
|
||||||
|
'show': 'Mostrar',
|
||||||
|
'startAll': 'Iniciar Todo',
|
||||||
|
'pauseAll': 'Pausar Todo',
|
||||||
|
'deleteTask': 'Eliminar Tarea',
|
||||||
|
'deleteTaskTip': 'Mantener archivos descargados',
|
||||||
|
'delete': 'Eliminar',
|
||||||
|
'newVersionTitle': 'Nueva versión @version disponible',
|
||||||
|
'newVersionUpdate': 'Actualizar Ahora',
|
||||||
|
'newVersionLater': 'Más tarde',
|
||||||
|
'extensions': 'Extensiones',
|
||||||
|
'extensionInstallUrl': 'URL de instalación',
|
||||||
|
'extensionInstallSuccess': 'Instalado con éxito',
|
||||||
|
'extensionUpdateSuccess': 'Actualizado con éxito',
|
||||||
|
'extensionDelete': 'Eliminar Extensión',
|
||||||
|
'extensionAlreadyLatest': 'Ya es la versión más reciente',
|
||||||
|
'extensionFind': 'Buscar Extensiones',
|
||||||
|
'extensionDevelop': 'Desarrollar Extensiones',
|
||||||
|
'history': 'Historial',
|
||||||
|
'clearHistory': 'Borrar Historial',
|
||||||
|
'noHistoryFound': 'No se encontró historial',
|
||||||
|
'serviceTitle': 'Servicio de Descarga',
|
||||||
|
'serviceText': 'En ejecución',
|
||||||
|
'network': 'Red',
|
||||||
|
'proxy': 'Proxy',
|
||||||
|
'noProxy': 'Sin Proxy',
|
||||||
|
'systemProxy': 'Proxy del Sistema',
|
||||||
|
'customProxy': 'Proxy Personalizado',
|
||||||
|
'server': 'Servidor',
|
||||||
|
'username': 'Nombre de usuario',
|
||||||
|
'password': 'Contraseña',
|
||||||
|
'thanks': 'Agradecimientos',
|
||||||
|
'thanksDesc':
|
||||||
|
'¡Gracias a todos los contribuyentes que han ayudado a construir y desarrollar la comunidad Gopeed!',
|
||||||
|
'browserExtension': 'Extensión del Navegador',
|
||||||
|
'launchAtStartup': 'Iniciar al Arranque',
|
||||||
|
'seedConfig': 'Configuración de Semilla',
|
||||||
|
'seedKeep': 'Mantener sembrado',
|
||||||
|
'seedRatio': 'Ratio de semilla',
|
||||||
|
'seedTime': 'Tiempo de semilla (minutos)',
|
||||||
|
'taskDetail': 'Detalles de la Tarea',
|
||||||
|
'taskName': 'Nombre de la Tarea',
|
||||||
|
'taskUrl': 'URL de la Tarea',
|
||||||
|
'downloadPath': 'Ruta de Descarga',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@ const zhCN = {
|
|||||||
'create': '创建任务',
|
'create': '创建任务',
|
||||||
'directDownload': '直接下载',
|
'directDownload': '直接下载',
|
||||||
'advancedOptions': '高级选项',
|
'advancedOptions': '高级选项',
|
||||||
|
'followSettings': '跟随设置',
|
||||||
'downloadLink': '下载链接',
|
'downloadLink': '下载链接',
|
||||||
'downloadLinkValid': '请输入下载链接',
|
'downloadLinkValid': '请输入下载链接',
|
||||||
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
|
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
|
||||||
@@ -34,6 +35,7 @@ const zhCN = {
|
|||||||
'connections': '连接数',
|
'connections': '连接数',
|
||||||
'useServerCtime': '创建文件使用服务器时间',
|
'useServerCtime': '创建文件使用服务器时间',
|
||||||
'maxRunning': '最大下载数',
|
'maxRunning': '最大下载数',
|
||||||
|
'defaultDirectDownload': '默认勾选直接下载',
|
||||||
'items': '@count 项',
|
'items': '@count 项',
|
||||||
'subscribeTracker': '订阅 Tracker',
|
'subscribeTracker': '订阅 Tracker',
|
||||||
'subscribeFail': '订阅失败,请检查网络或稍后重试',
|
'subscribeFail': '订阅失败,请检查网络或稍后重试',
|
||||||
@@ -58,6 +60,8 @@ const zhCN = {
|
|||||||
'set': '已设置',
|
'set': '已设置',
|
||||||
'portInUse': '端口[@port]已被占用,请更换端口',
|
'portInUse': '端口[@port]已被占用,请更换端口',
|
||||||
'effectAfterRestart': '此配置项将在重启应用后生效',
|
'effectAfterRestart': '此配置项将在重启应用后生效',
|
||||||
|
'developer': '开发者',
|
||||||
|
'logDirectory': '日志目录',
|
||||||
'show': '显示',
|
'show': '显示',
|
||||||
'startAll': '全部开始',
|
'startAll': '全部开始',
|
||||||
'pauseAll': '全部暂停',
|
'pauseAll': '全部暂停',
|
||||||
@@ -93,7 +97,7 @@ const zhCN = {
|
|||||||
'browserExtension': '浏览器扩展',
|
'browserExtension': '浏览器扩展',
|
||||||
'launchAtStartup': '开机自动运行',
|
'launchAtStartup': '开机自动运行',
|
||||||
'seedConfig': '做种设置',
|
'seedConfig': '做种设置',
|
||||||
'seedKeep': '持续做种',
|
'seedKeep': '持续做种直到手动停止',
|
||||||
'seedRatio': '做种分享率',
|
'seedRatio': '做种分享率',
|
||||||
'seedTime': '做种时间(分钟)',
|
'seedTime': '做种时间(分钟)',
|
||||||
'taskDetail': '任务详情',
|
'taskDetail': '任务详情',
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const zhTW = {
|
|||||||
'create': '建立任務',
|
'create': '建立任務',
|
||||||
'directDownload': '直接下載',
|
'directDownload': '直接下載',
|
||||||
'advancedOptions': '進階選項',
|
'advancedOptions': '進階選項',
|
||||||
|
'followSettings': '跟隨設定',
|
||||||
'downloadLink': '下載連結',
|
'downloadLink': '下載連結',
|
||||||
'downloadLinkValid': '請輸入下載連結',
|
'downloadLinkValid': '請輸入下載連結',
|
||||||
'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append',
|
'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append',
|
||||||
@@ -34,6 +35,7 @@ const zhTW = {
|
|||||||
'connections': '連接數',
|
'connections': '連接數',
|
||||||
'useServerCtime': '使用伺服器時間作為檔案建立時間',
|
'useServerCtime': '使用伺服器時間作為檔案建立時間',
|
||||||
'maxRunning': '最大執行任務數',
|
'maxRunning': '最大執行任務數',
|
||||||
|
'defaultDirectDownload': '預設勾選直接下載',
|
||||||
'items': '@count 個項目',
|
'items': '@count 個項目',
|
||||||
'subscribeTracker': '訂閱 Tracker',
|
'subscribeTracker': '訂閱 Tracker',
|
||||||
'subscribeFail': '訂閱失敗,請檢查網路或稍後再試',
|
'subscribeFail': '訂閱失敗,請檢查網路或稍後再試',
|
||||||
@@ -58,6 +60,8 @@ const zhTW = {
|
|||||||
'set': '已設定',
|
'set': '已設定',
|
||||||
'portInUse': '連接埠 [@port] 已被使用,請更改連接埠',
|
'portInUse': '連接埠 [@port] 已被使用,請更改連接埠',
|
||||||
'effectAfterRestart': '重新啟動後生效',
|
'effectAfterRestart': '重新啟動後生效',
|
||||||
|
'developer': '開發者',
|
||||||
|
'logDirectory': '日誌目錄',
|
||||||
'show': '顯示',
|
'show': '顯示',
|
||||||
'startAll': '全部開始',
|
'startAll': '全部開始',
|
||||||
'pauseAll': '全部暫停',
|
'pauseAll': '全部暫停',
|
||||||
@@ -93,15 +97,9 @@ const zhTW = {
|
|||||||
'browserExtension': '瀏覽器擴充功能',
|
'browserExtension': '瀏覽器擴充功能',
|
||||||
'launchAtStartup': '開機自動執行',
|
'launchAtStartup': '開機自動執行',
|
||||||
'seedConfig': '做種設定',
|
'seedConfig': '做種設定',
|
||||||
'seedKeep': '持續做種',
|
'seedKeep': '持續做種直到手動停止',
|
||||||
'seedRatio': '做種分享率',
|
'seedRatio': '做種分享率',
|
||||||
'seedTime': '做種時間(分鐘)',
|
'seedTime': '做種時間(分鐘)',
|
||||||
/*
|
|
||||||
'taskDetail': '任务详情',
|
|
||||||
'taskName': '任务名称',
|
|
||||||
'taskUrl': '任务链接',
|
|
||||||
'downloadPath': '下载路径',
|
|
||||||
*/
|
|
||||||
'taskDetail': '任務詳情',
|
'taskDetail': '任務詳情',
|
||||||
'taskName': '任務名稱',
|
'taskName': '任務名稱',
|
||||||
'taskUrl': '任務連結',
|
'taskUrl': '任務連結',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'langs/tr_tr.dart';
|
|||||||
import 'langs/vi_vn.dart';
|
import 'langs/vi_vn.dart';
|
||||||
import 'langs/zh_cn.dart';
|
import 'langs/zh_cn.dart';
|
||||||
import 'langs/zh_tw.dart';
|
import 'langs/zh_tw.dart';
|
||||||
|
import 'langs/es_es.dart';
|
||||||
|
|
||||||
final messages = _Messages();
|
final messages = _Messages();
|
||||||
|
|
||||||
@@ -33,5 +34,6 @@ class _Messages extends Translations {
|
|||||||
...itIT,
|
...itIT,
|
||||||
...idID,
|
...idID,
|
||||||
...frFR,
|
...frFR,
|
||||||
|
...esES,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
platform :osx, '10.11'
|
platform :osx, '10.14'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|||||||
@@ -1,58 +1,84 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- desktop_drop (0.0.1):
|
- app_links (1.0.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- file_selector_macos (0.0.1):
|
- desktop_drop (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- macos_secure_bookmarks (0.0.3):
|
- macos_secure_bookmarks (0.0.3):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- package_info_plus (0.0.1):
|
- package_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- path_provider_macos (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- shared_preferences_macos (0.0.1):
|
- screen_retriever (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
|
- shared_preferences_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- tray_manager (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- url_launcher_macos (0.0.1):
|
- url_launcher_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- window_manager (0.2.0):
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
||||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
||||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- macos_secure_bookmarks (from `Flutter/ephemeral/.symlinks/plugins/macos_secure_bookmarks/macos`)
|
- macos_secure_bookmarks (from `Flutter/ephemeral/.symlinks/plugins/macos_secure_bookmarks/macos`)
|
||||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`)
|
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
|
||||||
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
|
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
app_links:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
||||||
desktop_drop:
|
desktop_drop:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||||
file_selector_macos:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
macos_secure_bookmarks:
|
macos_secure_bookmarks:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/macos_secure_bookmarks/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/macos_secure_bookmarks/macos
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
path_provider_macos:
|
path_provider_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||||
shared_preferences_macos:
|
screen_retriever:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
|
||||||
|
share_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||||
|
shared_preferences_foundation:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
|
tray_manager:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
|
window_manager:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
|
||||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||||
file_selector_macos: f1b08a781e66103e3ba279fd5d4024a2478b3af6
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
|
|
||||||
macos_secure_bookmarks: cff041c4b377fa00941bb901aaec4d27fac5edf2
|
macos_secure_bookmarks: cff041c4b377fa00941bb901aaec4d27fac5edf2
|
||||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||||
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||||
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
|
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
|
||||||
|
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||||
|
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||||
|
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
||||||
|
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||||
|
|
||||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.15.2
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 51;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXAggregateTarget section */
|
/* Begin PBXAggregateTarget section */
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 0920;
|
LastSwiftUpdateCheck = 0920;
|
||||||
LastUpgradeCheck = 1300;
|
LastUpgradeCheck = 1430;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
33CC10EC2044A3C60003C045 = {
|
33CC10EC2044A3C60003C045 = {
|
||||||
@@ -259,6 +259,7 @@
|
|||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
@@ -407,7 +408,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
@@ -490,7 +491,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -537,7 +538,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1300"
|
LastUpgradeVersion = "1430"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 592 B |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -241,6 +241,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.4"
|
version: "0.4.4"
|
||||||
|
device_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: device_info_plus
|
||||||
|
sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.2"
|
||||||
|
device_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_platform_interface
|
||||||
|
sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -709,6 +725,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.3.1"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.12"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.5"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.2"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.5.8+1
|
version: 1.6.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@@ -67,6 +67,8 @@ dependencies:
|
|||||||
launch_at_startup: ^0.2.2
|
launch_at_startup: ^0.2.2
|
||||||
args: ^2.5.0
|
args: ^2.5.0
|
||||||
toggle_switch: ^2.3.0
|
toggle_switch: ^2.3.0
|
||||||
|
permission_handler: ^11.3.1
|
||||||
|
device_info_plus: ^9.1.2
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||