mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-11 10:54:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39202b44a1 | ||
|
|
457d9e85d6 | ||
|
|
858ad74339 | ||
|
|
755d7ce814 | ||
|
|
854f600d3f | ||
|
|
28da89f08c | ||
|
|
dddefb68a2 |
No files matched your search
@@ -11,6 +11,8 @@ categories:
|
|||||||
- 'fix'
|
- 'fix'
|
||||||
- 'bugfix'
|
- 'bugfix'
|
||||||
- 'bug'
|
- 'bug'
|
||||||
|
- title: '🧪 Tests'
|
||||||
|
label: 'test'
|
||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
label: 'chore'
|
label: 'chore'
|
||||||
- title: '📖 Document'
|
- title: '📖 Document'
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build windows
|
flutter build windows
|
||||||
$mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin"
|
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||||
$system = "C:\Windows\System32"
|
$system = "C:\Windows\System32"
|
||||||
$release = "build\windows\runner\Release\"
|
$release = "build\windows\runner\Release\"
|
||||||
cp $mingw\libstdc++-6.dll $release
|
cp $mingw\libstdc++-6.dll $release
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestError struct {
|
type RequestError struct {
|
||||||
@@ -37,6 +38,8 @@ type chunk struct {
|
|||||||
Begin int64
|
Begin int64
|
||||||
End int64
|
End int64
|
||||||
Downloaded int64
|
Downloaded int64
|
||||||
|
|
||||||
|
retryTimes int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newChunk(begin int64, end int64) *chunk {
|
func newChunk(begin int64, end int64) *chunk {
|
||||||
@@ -255,24 +258,43 @@ func (f *Fetcher) fetch() {
|
|||||||
|
|
||||||
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||||
chunk := f.chunks[index]
|
chunk := f.chunks[index]
|
||||||
|
chunk.retryTimes = 0
|
||||||
|
|
||||||
httpReq, err := buildRequest(ctx, f.meta.Req)
|
httpReq, err := buildRequest(ctx, f.meta.Req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
client = buildClient()
|
client = buildClient()
|
||||||
buf = make([]byte, 8192)
|
buf = make([]byte, 8192)
|
||||||
|
maxRetries = 3
|
||||||
)
|
)
|
||||||
// 重试5次
|
// retry until all remain chunks failed
|
||||||
for i := 0; i < 5; i++ {
|
for true {
|
||||||
// if chunk is completed, return
|
// if chunk is completed, return
|
||||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if chunk.retryTimes >= maxRetries {
|
||||||
|
if !f.meta.Res.Range {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// check if all failed
|
||||||
|
allFailed := true
|
||||||
|
for _, c := range f.chunks {
|
||||||
|
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
|
||||||
|
allFailed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allFailed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
resp *http.Response
|
resp *http.Response
|
||||||
retry bool
|
|
||||||
)
|
)
|
||||||
if f.meta.Res.Range {
|
if f.meta.Res.Range {
|
||||||
httpReq.Header.Set(base.HttpHeaderRange,
|
httpReq.Header.Set(base.HttpHeaderRange,
|
||||||
@@ -292,35 +314,41 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// retry request
|
// retry request after 1 second
|
||||||
|
chunk.retryTimes = chunk.retryTimes + 1
|
||||||
|
time.Sleep(time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 请求成功就重置错误次数,连续失败5次才终止
|
// Http request success, reset retry times
|
||||||
i = 0
|
chunk.retryTimes = 0
|
||||||
retry, err = func() (bool, error) {
|
err = func() error {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := resp.Body.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
chunk.Downloaded += int64(n)
|
chunk.Downloaded += int64(n)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err == io.EOF {
|
||||||
return true, err
|
return nil
|
||||||
}
|
}
|
||||||
break
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
|
||||||
}()
|
}()
|
||||||
if !retry {
|
// if chunk is completed, reset other not complete chunks retry times
|
||||||
// 下载成功,跳出重试
|
if err == nil {
|
||||||
break
|
for _, c := range f.chunks {
|
||||||
|
if c != chunk && c.retryTimes > 0 {
|
||||||
|
c.retryTimes = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func testResolve(startTestServer func() net.Listener, path string, want *base.Re
|
|||||||
func TestFetcher_DownloadNormal(t *testing.T) {
|
func TestFetcher_DownloadNormal(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// 正常下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
downloadNormal(listener, 5, t)
|
downloadNormal(listener, 5, t)
|
||||||
downloadNormal(listener, 8, t)
|
downloadNormal(listener, 8, t)
|
||||||
@@ -86,7 +86,7 @@ func TestFetcher_DownloadNormal(t *testing.T) {
|
|||||||
func TestFetcher_DownloadContinue(t *testing.T) {
|
func TestFetcher_DownloadContinue(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// 暂停继续
|
|
||||||
downloadContinue(listener, 1, t)
|
downloadContinue(listener, 1, t)
|
||||||
downloadContinue(listener, 5, t)
|
downloadContinue(listener, 5, t)
|
||||||
downloadContinue(listener, 8, t)
|
downloadContinue(listener, 8, t)
|
||||||
@@ -96,7 +96,7 @@ func TestFetcher_DownloadContinue(t *testing.T) {
|
|||||||
func TestFetcher_DownloadChunked(t *testing.T) {
|
func TestFetcher_DownloadChunked(t *testing.T) {
|
||||||
listener := test.StartTestCustomServer()
|
listener := test.StartTestCustomServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
downloadNormal(listener, 2, t)
|
downloadNormal(listener, 2, t)
|
||||||
}
|
}
|
||||||
@@ -104,24 +104,33 @@ func TestFetcher_DownloadChunked(t *testing.T) {
|
|||||||
func TestFetcher_DownloadPost(t *testing.T) {
|
func TestFetcher_DownloadPost(t *testing.T) {
|
||||||
listener := test.StartTestPostServer()
|
listener := test.StartTestPostServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// post下载
|
|
||||||
downloadPost(listener, 1, t)
|
downloadPost(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadRetry(t *testing.T) {
|
func TestFetcher_DownloadRetry(t *testing.T) {
|
||||||
listener := test.StartTestRetryServer()
|
listener := test.StartTestRetryServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadError(t *testing.T) {
|
func TestFetcher_DownloadError(t *testing.T) {
|
||||||
listener := test.StartTestErrorServer()
|
listener := test.StartTestErrorServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadError(listener, 1, t)
|
downloadError(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadLimit(t *testing.T) {
|
||||||
|
listener := test.StartTestLimitServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadNormal(listener, 1, t)
|
||||||
|
downloadNormal(listener, 2, t)
|
||||||
|
downloadNormal(listener, 8, t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadResume(t *testing.T) {
|
func TestFetcher_DownloadResume(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,7 +84,7 @@ func StartTestRetryServer() net.Listener {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
counter++
|
counter++
|
||||||
if counter != 1 && counter < 5 {
|
if counter != 1 && counter < 2 {
|
||||||
writer.WriteHeader(500)
|
writer.WriteHeader(500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -134,6 +137,79 @@ func StartTestErrorServer() net.Listener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartTestLimitServer connections limit server
|
||||||
|
func StartTestLimitServer() net.Listener {
|
||||||
|
var connections atomic.Int32
|
||||||
|
|
||||||
|
return startTestServer(func() http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
connections.Add(-1)
|
||||||
|
}()
|
||||||
|
connections.Add(1)
|
||||||
|
if connections.Load() > 4 {
|
||||||
|
writer.WriteHeader(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r := request.Header.Get("Range")
|
||||||
|
if r == "" {
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
writer.WriteHeader(200)
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
} else {
|
||||||
|
// split range
|
||||||
|
s := strings.Split(r, "=")
|
||||||
|
if len(s) != 2 {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = strings.Split(s[1], "-")
|
||||||
|
if len(s) != 2 {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start, err := strconv.ParseInt(s[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseInt(s[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if start < 0 || end < 0 || start > end {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end >= BuildSize {
|
||||||
|
end = BuildSize - 1
|
||||||
|
}
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
|
||||||
|
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
||||||
|
writer.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
writer.WriteHeader(206)
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
file.Seek(start, 0)
|
||||||
|
io.CopyN(writer, file, end-start+1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return mux
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+9
-1
@@ -9,6 +9,15 @@ import (
|
|||||||
type Request struct {
|
type Request struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Extra any `json:"extra"`
|
Extra any `json:"extra"`
|
||||||
|
// Labels is used to mark the download task
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) Validate() error {
|
||||||
|
if r.URL == "" {
|
||||||
|
return fmt.Errorf("invalid request url")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resource download resource
|
// Resource download resource
|
||||||
@@ -36,7 +45,6 @@ func (r *Resource) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resource) CalcSize() {
|
func (r *Resource) CalcSize() {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const (
|
|||||||
bucketConfig = "config"
|
bucketConfig = "config"
|
||||||
// downloader extension bucket
|
// downloader extension bucket
|
||||||
bucketExtension = "extension"
|
bucketExtension = "extension"
|
||||||
|
// downloader extension storage bucket
|
||||||
|
bucketExtensionStorage = "extension_storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -97,7 +99,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
|||||||
|
|
||||||
func (d *Downloader) Setup() error {
|
func (d *Downloader) Setup() error {
|
||||||
// setup storage
|
// setup storage
|
||||||
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig, bucketExtension}); err != nil {
|
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig, bucketExtension, bucketExtensionStorage}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// load config from storage
|
// load config from storage
|
||||||
@@ -427,6 +429,9 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
|
|||||||
Task: task,
|
Task: task,
|
||||||
Err: err,
|
Err: err,
|
||||||
})
|
})
|
||||||
|
if eventKey == EventKeyError {
|
||||||
|
d.emit(EventKeyFinally, task, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,8 +497,8 @@ func (d *Downloader) watch(task *Task) {
|
|||||||
task.Status = base.DownloadStatusDone
|
task.Status = base.DownloadStatusDone
|
||||||
d.storage.Put(bucketTask, task.ID, task.clone())
|
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||||
d.emit(EventKeyDone, task)
|
d.emit(EventKeyDone, task)
|
||||||
|
d.emit(EventKeyFinally, task, err)
|
||||||
}
|
}
|
||||||
d.emit(EventKeyFinally, task, err)
|
|
||||||
if task.Status == EventKeyDone {
|
if task.Status == EventKeyDone {
|
||||||
d.notifyRunning()
|
d.notifyRunning()
|
||||||
}
|
}
|
||||||
@@ -620,6 +625,8 @@ func (d *Downloader) doPauseAll() (err error) {
|
|||||||
func (d *Downloader) start(task *Task) error {
|
func (d *Downloader) start(task *Task) error {
|
||||||
task.lock.Lock()
|
task.lock.Lock()
|
||||||
defer task.lock.Unlock()
|
defer task.lock.Unlock()
|
||||||
|
d.triggerOnStart(task)
|
||||||
|
|
||||||
isCreate := task.Status == base.DownloadStatusReady
|
isCreate := task.Status == base.DownloadStatusReady
|
||||||
task.Status = base.DownloadStatusRunning
|
task.Status = base.DownloadStatusRunning
|
||||||
if task.Meta.Res == nil {
|
if task.Meta.Res == nil {
|
||||||
|
|||||||
+165
-30
@@ -33,6 +33,7 @@ type ActivationEvent string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
EventOnResolve ActivationEvent = "onResolve"
|
EventOnResolve ActivationEvent = "onResolve"
|
||||||
|
EventOnStart ActivationEvent = "onStart"
|
||||||
//EventOnError ActivationEvent = "onError"
|
//EventOnError ActivationEvent = "onError"
|
||||||
//EventOnDone ActivationEvent = "onDone"
|
//EventOnDone ActivationEvent = "onDone"
|
||||||
)
|
)
|
||||||
@@ -47,7 +48,7 @@ func (d *Downloader) InstallExtensionByFolder(path string, devMode bool) (*Exten
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if dev mode, don't copy the extension to the extensions' directory
|
// if dev mode, don't copy to the extensions' directory
|
||||||
if devMode {
|
if devMode {
|
||||||
ext.DevMode = true
|
ext.DevMode = true
|
||||||
ext.DevPath, _ = filepath.Abs(path)
|
ext.DevPath, _ = filepath.Abs(path)
|
||||||
@@ -229,23 +230,71 @@ func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
||||||
|
doTrigger(d,
|
||||||
|
EventOnResolve,
|
||||||
|
req,
|
||||||
|
&OnResolveContext{
|
||||||
|
Req: req,
|
||||||
|
},
|
||||||
|
func(ext *Extension, gopeed *Instance, ctx *OnResolveContext) {
|
||||||
|
// Validate resource structure
|
||||||
|
if ctx.Res != nil && len(ctx.Res.Files) > 0 {
|
||||||
|
if err := ctx.Res.Validate(); err != nil {
|
||||||
|
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Res.CalcSize()
|
||||||
|
}
|
||||||
|
res = ctx.Res
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) triggerOnStart(task *Task) {
|
||||||
|
doTrigger(d,
|
||||||
|
EventOnStart,
|
||||||
|
task.Meta.Req,
|
||||||
|
&OnStartContext{
|
||||||
|
Task: task.clone(),
|
||||||
|
},
|
||||||
|
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
|
||||||
|
// Validate request structure
|
||||||
|
if ctx.Task.Meta.Req != nil {
|
||||||
|
if err := ctx.Task.Meta.Req.Validate(); err != nil {
|
||||||
|
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Modify real task request
|
||||||
|
task.Meta.Req = ctx.Task.Meta.Req
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) {
|
||||||
// init extension global object
|
// init extension global object
|
||||||
gopeed := &Instance{
|
gopeed := &Instance{
|
||||||
Events: make(InstanceEvents),
|
Events: make(InstanceEvents),
|
||||||
}
|
}
|
||||||
ctx := &Context{
|
|
||||||
Req: req,
|
|
||||||
}
|
|
||||||
var err error
|
var err error
|
||||||
for _, ext := range d.extensions {
|
for _, ext := range d.extensions {
|
||||||
if ext.Disabled {
|
if ext.Disabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, script := range ext.Scripts {
|
for _, script := range ext.Scripts {
|
||||||
if script.match(EventOnResolve, req.URL) {
|
if script.match(event, req) {
|
||||||
|
gopeed.Info = NewExtensionInfo(ext)
|
||||||
gopeed.Logger = newInstanceLogger(ext, d.ExtensionLogger)
|
gopeed.Logger = newInstanceLogger(ext, d.ExtensionLogger)
|
||||||
|
gopeed.Settings = parseSettings(ext.Settings)
|
||||||
|
gopeed.Storage = &ContextStorage{
|
||||||
|
storage: d.storage,
|
||||||
|
identity: ext.buildIdentity(),
|
||||||
|
}
|
||||||
scriptFilePath := filepath.Join(d.ExtensionPath(ext), script.Entry)
|
scriptFilePath := filepath.Join(d.ExtensionPath(ext), script.Entry)
|
||||||
if _, err = os.Stat(scriptFilePath); os.IsNotExist(err) {
|
if _, err = os.Stat(scriptFilePath); os.IsNotExist(err) {
|
||||||
|
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] script file not exist", ext.buildIdentity())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
func() {
|
func() {
|
||||||
@@ -256,13 +305,16 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer scriptFile.Close()
|
defer scriptFile.Close()
|
||||||
ctx.Settings = parseSettings(ext.Settings)
|
|
||||||
var scriptBuf []byte
|
var scriptBuf []byte
|
||||||
scriptBuf, err = io.ReadAll(scriptFile)
|
scriptBuf, err = io.ReadAll(scriptFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] read script file failed", ext.buildIdentity())
|
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] read script file failed", ext.buildIdentity())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Init request labels
|
||||||
|
if req.Labels == nil {
|
||||||
|
req.Labels = make(map[string]string)
|
||||||
|
}
|
||||||
engine := engine.NewEngine()
|
engine := engine.NewEngine()
|
||||||
defer engine.Close()
|
defer engine.Close()
|
||||||
err = engine.Runtime.Set("gopeed", gopeed)
|
err = engine.Runtime.Set("gopeed", gopeed)
|
||||||
@@ -275,21 +327,13 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
|||||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] run script failed", ext.buildIdentity())
|
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] run script failed", ext.buildIdentity())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if fn, ok := gopeed.Events[EventOnResolve]; ok {
|
if fn, ok := gopeed.Events[event]; ok {
|
||||||
_, err = engine.CallFunction(fn, ctx)
|
_, err = engine.CallFunction(fn, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", EventOnResolve, ext.buildIdentity())
|
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", ext.buildIdentity(), event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// calculate resource total size
|
handler(ext, gopeed, ctx)
|
||||||
if ctx.Res != nil && len(ctx.Res.Files) > 0 {
|
|
||||||
if err = ctx.Res.Validate(); err != nil {
|
|
||||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Res.CalcSize()
|
|
||||||
}
|
|
||||||
res = ctx.Res
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -392,30 +436,46 @@ type Repository struct {
|
|||||||
type Script struct {
|
type Script struct {
|
||||||
// Event active event name
|
// Event active event name
|
||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
// Matches match request url pattern list
|
// Match rules
|
||||||
Matches []string `json:"matches"`
|
Match *Match `json:"match"`
|
||||||
// Entry js script file path
|
// Entry js script file path
|
||||||
Entry string `json:"entry"`
|
Entry string `json:"entry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Script) match(event ActivationEvent, url string) bool {
|
func (s *Script) match(event ActivationEvent, req *base.Request) bool {
|
||||||
if s.Event == "" {
|
if s.Event == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if s.Event != string(event) {
|
if s.Event != string(event) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(s.Matches) == 0 {
|
if s.Match == nil || (len(s.Match.Urls) == 0 && len(s.Match.Labels) == 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, match := range s.Matches {
|
|
||||||
if util.Match(match, url) {
|
// match url
|
||||||
|
for _, url := range s.Match.Urls {
|
||||||
|
if util.Match(url, req.URL) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match label
|
||||||
|
for _, label := range s.Match.Labels {
|
||||||
|
if _, ok := req.Labels[label]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Match struct {
|
||||||
|
// Urls match expression, refer to https://developer.chrome.com/docs/extensions/mv3/match_patterns/
|
||||||
|
Urls []string `json:"urls"`
|
||||||
|
// Labels match request labels
|
||||||
|
Labels []string `json:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
type SettingType string
|
type SettingType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -442,10 +502,13 @@ type Option struct {
|
|||||||
Value any `json:"value"`
|
Value any `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance inject to js context
|
// Instance inject to js context when extension script is activated
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
Events InstanceEvents `json:"events"`
|
Events InstanceEvents `json:"events"`
|
||||||
Logger *InstanceLogger `json:"logger"`
|
Info *ExtensionInfo `json:"info"`
|
||||||
|
Logger *InstanceLogger `json:"logger"`
|
||||||
|
Settings map[string]any `json:"settings"`
|
||||||
|
Storage *ContextStorage `json:"storage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstanceEvents map[ActivationEvent]goja.Callable
|
type InstanceEvents map[ActivationEvent]goja.Callable
|
||||||
@@ -458,6 +521,10 @@ func (h InstanceEvents) OnResolve(fn goja.Callable) {
|
|||||||
h.register(EventOnResolve, fn)
|
h.register(EventOnResolve, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h InstanceEvents) OnStart(fn goja.Callable) {
|
||||||
|
h.register(EventOnStart, fn)
|
||||||
|
}
|
||||||
|
|
||||||
//func (h InstanceEvents) OnError(fn goja.Callable) {
|
//func (h InstanceEvents) OnError(fn goja.Callable) {
|
||||||
// h.register(HookEventOnError, fn)
|
// h.register(HookEventOnError, fn)
|
||||||
//}
|
//}
|
||||||
@@ -466,6 +533,24 @@ func (h InstanceEvents) OnResolve(fn goja.Callable) {
|
|||||||
// h.register(HookEventOnDone, fn)
|
// h.register(HookEventOnDone, fn)
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
type ExtensionInfo struct {
|
||||||
|
Identity string `json:"identity"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExtensionInfo(ext *Extension) *ExtensionInfo {
|
||||||
|
return &ExtensionInfo{
|
||||||
|
Identity: ext.buildIdentity(),
|
||||||
|
Name: ext.Name,
|
||||||
|
Author: ext.Author,
|
||||||
|
Title: ext.Title,
|
||||||
|
Version: ext.Version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type InstanceLogger struct {
|
type InstanceLogger struct {
|
||||||
identity string
|
identity string
|
||||||
devMode bool
|
devMode bool
|
||||||
@@ -506,10 +591,13 @@ func newInstanceLogger(extension *Extension, logger *logger.Logger) *InstanceLog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Context struct {
|
type OnResolveContext struct {
|
||||||
Req *base.Request `json:"req"`
|
Req *base.Request `json:"req"`
|
||||||
Res *base.Resource `json:"res"`
|
Res *base.Resource `json:"res"`
|
||||||
Settings map[string]any `json:"settings"`
|
}
|
||||||
|
|
||||||
|
type OnStartContext struct {
|
||||||
|
Task *Task `json:"task"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSettings(settings []*Setting) map[string]any {
|
func parseSettings(settings []*Setting) map[string]any {
|
||||||
@@ -547,3 +635,50 @@ func tryParse(val any, settingType SettingType) any {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContextStorage struct {
|
||||||
|
storage Storage
|
||||||
|
identity string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContextStorage) Get(key string) any {
|
||||||
|
raw := s.getRawData()
|
||||||
|
if v, ok := raw[key]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContextStorage) Set(key string, value string) {
|
||||||
|
raw := s.getRawData()
|
||||||
|
raw[key] = value
|
||||||
|
s.storage.Put(bucketExtensionStorage, s.identity, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContextStorage) Remove(key string) {
|
||||||
|
raw := s.getRawData()
|
||||||
|
delete(raw, key)
|
||||||
|
s.storage.Put(bucketExtensionStorage, s.identity, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContextStorage) Keys() []string {
|
||||||
|
raw := s.getRawData()
|
||||||
|
keys := make([]string, 0)
|
||||||
|
for k := range raw {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContextStorage) Clear() {
|
||||||
|
s.storage.Delete(bucketExtensionStorage, s.identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ContextStorage) getRawData() map[string]string {
|
||||||
|
var data map[string]string
|
||||||
|
s.storage.Get(bucketExtensionStorage, s.identity, &data)
|
||||||
|
if data == nil {
|
||||||
|
data = make(map[string]string)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package download
|
package download
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/GopeedLab/gopeed/internal/logger"
|
"github.com/GopeedLab/gopeed/internal/logger"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDownloader_InstallExtensionByFolder(t *testing.T) {
|
func TestDownloader_InstallExtensionByFolder(t *testing.T) {
|
||||||
@@ -132,6 +134,87 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_ExtensionByOnStart(t *testing.T) {
|
||||||
|
downloadAndCheck := func(req *base.Request) {
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_start", 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(req, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-errCh:
|
||||||
|
break
|
||||||
|
case <-time.After(time.Second * 10):
|
||||||
|
err = errors.New("timeout")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
panic("extension on start download error: " + err.Error())
|
||||||
|
}
|
||||||
|
task := downloader.GetTask(id)
|
||||||
|
if task.Meta.Req.URL != "https://github.com" {
|
||||||
|
panic("extension on start modify url error")
|
||||||
|
}
|
||||||
|
if task.Meta.Req.Labels["modified"] != "true" {
|
||||||
|
panic("extension on start modify label error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// url match
|
||||||
|
downloadAndCheck(&base.Request{
|
||||||
|
URL: "https://github.com/gopeed/test/404",
|
||||||
|
})
|
||||||
|
|
||||||
|
// label match
|
||||||
|
downloadAndCheck(&base.Request{
|
||||||
|
URL: "https://xxx.com",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"test": "true",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_Extension_Errors(t *testing.T) {
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/script_error", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rr, err := downloader.Resolve(&base.Request{
|
||||||
|
URL: "https://github.com/test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rr.Res.Files) == 2 {
|
||||||
|
t.Fatal("script error catch failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/function_error", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rr, err := downloader.Resolve(&base.Request{
|
||||||
|
URL: "https://github.com/test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rr.Res.Files) == 2 {
|
||||||
|
t.Fatal("function error catch failed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestDownloader_Extension_Settings(t *testing.T) {
|
func TestDownloader_Extension_Settings(t *testing.T) {
|
||||||
setupDownloader(func(downloader *Downloader) {
|
setupDownloader(func(downloader *Downloader) {
|
||||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_empty", false); err != nil {
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_empty", false); err != nil {
|
||||||
@@ -170,6 +253,23 @@ func TestDownloader_Extension_Settings(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_ExtensionStorage(t *testing.T) {
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/storage", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rr, err := downloader.Resolve(&base.Request{
|
||||||
|
URL: "https://github.com/test",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rr.Res.Files) == 1 {
|
||||||
|
t.Fatal("resolve error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestDownloader_SwitchExtension(t *testing.T) {
|
func TestDownloader_SwitchExtension(t *testing.T) {
|
||||||
setupDownloader(func(downloader *Downloader) {
|
setupDownloader(func(downloader *Downloader) {
|
||||||
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", false)
|
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", false)
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ gopeed.events.onResolve(async function (ctx) {
|
|||||||
size: 1024,
|
size: 1024,
|
||||||
req: {
|
req: {
|
||||||
url: ctx.req.url + "/" + i,
|
url: ctx.req.url + "/" + i,
|
||||||
|
labels:{
|
||||||
|
"from": gopeed.info.name,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
+5
-3
@@ -5,9 +5,11 @@
|
|||||||
"scripts": [
|
"scripts": [
|
||||||
{
|
{
|
||||||
"event": "onResolve",
|
"event": "onResolve",
|
||||||
"matches": [
|
"match": {
|
||||||
"*://github.com/*"
|
"urls": [
|
||||||
],
|
"*://github.com/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"entry": "index.js"
|
"entry": "index.js"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
gopeed.events.onResolve(async function (ctx) {
|
||||||
|
ctx.res = {
|
||||||
|
name: "test",
|
||||||
|
files: Array(2).fill(true).map((_, i) => ({
|
||||||
|
name: `test-${i}.txt`,
|
||||||
|
size: 1024,
|
||||||
|
req: {
|
||||||
|
url: ctx.req.url + "/" + i,
|
||||||
|
extra: {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': ctx.settings.ua,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "extra",
|
||||||
|
"title": "gopeed extension extra test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onResolve",
|
||||||
|
"matches": [
|
||||||
|
"*://github.com/*"
|
||||||
|
],
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"name": "ua",
|
||||||
|
"title": "User-Agent",
|
||||||
|
"type": "string",
|
||||||
|
"value": "gopeed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
gopeed.events.onResolve(async function (ctx) {
|
||||||
|
const aaa = {};
|
||||||
|
gopeed.logger.info(aaa.bbb.ccc);
|
||||||
|
|
||||||
|
ctx.res = {
|
||||||
|
name: "test",
|
||||||
|
files: Array(2).fill(true).map((_, i) => ({
|
||||||
|
name: `test-${i}.txt`,
|
||||||
|
size: 1024,
|
||||||
|
req: {
|
||||||
|
url: ctx.req.url + "/" + i,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "function-error",
|
||||||
|
"title": "gopeed extension function error test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onResolve",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://github.com/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"name": "ua",
|
||||||
|
"title": "User-Agent",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
gopeed.events.onStart(async function (ctx) {
|
||||||
|
ctx.task.meta.req.url = "https://github.com";
|
||||||
|
ctx.task.meta.req.labels['modified'] = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "on-start",
|
||||||
|
"title": "gopeed extension on start event test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onStart",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://github.com/*"
|
||||||
|
],
|
||||||
|
"labels": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const aaa = {};
|
||||||
|
gopeed.logger.info(aaa.bbb.ccc);
|
||||||
|
|
||||||
|
gopeed.events.onResolve(async function (ctx) {
|
||||||
|
ctx.res = {
|
||||||
|
name: "test",
|
||||||
|
files: Array(2).fill(true).map((_, i) => ({
|
||||||
|
name: `test-${i}.txt`,
|
||||||
|
size: 1024,
|
||||||
|
req: {
|
||||||
|
url: ctx.req.url + "/" + i,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "script-error",
|
||||||
|
"title": "gopeed extension script error test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onResolve",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://github.com/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"name": "ua",
|
||||||
|
"title": "User-Agent",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+9
-9
@@ -1,31 +1,31 @@
|
|||||||
gopeed.events.onResolve(async function (ctx) {
|
gopeed.events.onResolve(async function (ctx) {
|
||||||
if (ctx.settings.string != null) {
|
if (gopeed.settings.string != null) {
|
||||||
throw new Error("string is not null");
|
throw new Error("string is not null");
|
||||||
}
|
}
|
||||||
if (ctx.settings.number != null) {
|
if (gopeed.settings.number != null) {
|
||||||
throw new Error("number is not null");
|
throw new Error("number is not null");
|
||||||
}
|
}
|
||||||
if (ctx.settings.boolean != null) {
|
if (gopeed.settings.boolean != null) {
|
||||||
throw new Error("boolean is not null");
|
throw new Error("boolean is not null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.settings.stringDefault !== "default") {
|
if (gopeed.settings.stringDefault !== "default") {
|
||||||
throw new Error("string default value is incorrect");
|
throw new Error("string default value is incorrect");
|
||||||
}
|
}
|
||||||
if (ctx.settings.numberDefault !== 1) {
|
if (gopeed.settings.numberDefault !== 1) {
|
||||||
throw new Error("number default value is incorrect");
|
throw new Error("number default value is incorrect");
|
||||||
}
|
}
|
||||||
if (ctx.settings.booleanDefault !== true) {
|
if (gopeed.settings.booleanDefault !== true) {
|
||||||
throw new Error("boolean default value is incorrect");
|
throw new Error("boolean default value is incorrect");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.settings.stringValued !== "valued") {
|
if (gopeed.settings.stringValued !== "valued") {
|
||||||
throw new Error("string value is incorrect");
|
throw new Error("string value is incorrect");
|
||||||
}
|
}
|
||||||
if (ctx.settings.numberValued !== 1.1) {
|
if (gopeed.settings.numberValued !== 1.1) {
|
||||||
throw new Error("number value is incorrect");
|
throw new Error("number value is incorrect");
|
||||||
}
|
}
|
||||||
if (ctx.settings.booleanValued !== true) {
|
if (gopeed.settings.booleanValued !== true) {
|
||||||
throw new Error("boolean value is incorrect");
|
throw new Error("boolean value is incorrect");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
"scripts": [
|
"scripts": [
|
||||||
{
|
{
|
||||||
"event": "onResolve",
|
"event": "onResolve",
|
||||||
"matches": [
|
"match": {
|
||||||
"*://*/*"
|
"urls": [
|
||||||
],
|
"*://*/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"entry": "index.js"
|
"entry": "index.js"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
gopeed.events.onResolve(async function (ctx) {
|
gopeed.events.onResolve(async function (ctx) {
|
||||||
if (Object.keys(ctx.settings).length > 0){
|
if (Object.keys(gopeed.settings).length > 0){
|
||||||
throw new Error("settings is not empty");
|
throw new Error("settings is not empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
"scripts": [
|
"scripts": [
|
||||||
{
|
{
|
||||||
"event": "onResolve",
|
"event": "onResolve",
|
||||||
"matches": [
|
"match": {
|
||||||
"*://*/*"
|
"urls": [
|
||||||
],
|
"*://*/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"entry": "index.js"
|
"entry": "index.js"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
gopeed.events.onResolve(async function (ctx) {
|
||||||
|
const key = "key"
|
||||||
|
const value1 = "value1", value2 = JSON.stringify({a: 1, b: "2"})
|
||||||
|
|
||||||
|
if (gopeed.storage.get(key) !== null) {
|
||||||
|
throw new Error("storage get null error")
|
||||||
|
}
|
||||||
|
gopeed.storage.remove(key)
|
||||||
|
if(gopeed.storage.keys().length !== 0) {
|
||||||
|
throw new Error("storage keys null error")
|
||||||
|
}
|
||||||
|
|
||||||
|
gopeed.storage.set(key, value1)
|
||||||
|
if (gopeed.storage.get(key) !== value1) {
|
||||||
|
throw new Error("storage put1 error")
|
||||||
|
}
|
||||||
|
|
||||||
|
gopeed.storage.set(key, value2)
|
||||||
|
if (gopeed.storage.get(key) !== value2) {
|
||||||
|
throw new Error("storage put2 error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(gopeed.storage.keys().length !== 1) {
|
||||||
|
throw new Error("storage keys error")
|
||||||
|
}
|
||||||
|
|
||||||
|
gopeed.storage.remove(key)
|
||||||
|
if (gopeed.storage.get(key) !== null) {
|
||||||
|
throw new Error("storage delete error")
|
||||||
|
}
|
||||||
|
|
||||||
|
gopeed.storage.set(key, value1)
|
||||||
|
gopeed.storage.clear()
|
||||||
|
if (gopeed.storage.get(key) !== null) {
|
||||||
|
throw new Error("storage clear error")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.res = {
|
||||||
|
name: "test",
|
||||||
|
files: Array(2).fill(true).map((_, i) => ({
|
||||||
|
name: `test-${i}.txt`,
|
||||||
|
size: 1024,
|
||||||
|
req: {
|
||||||
|
url: ctx.req.url + "/" + i,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "storage",
|
||||||
|
"title": "gopeed extension storage test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onResolve",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://*/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"name": "ua",
|
||||||
|
"title": "User-Agent",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+5
-3
@@ -12,9 +12,11 @@
|
|||||||
"scripts": [
|
"scripts": [
|
||||||
{
|
{
|
||||||
"event": "onResolve",
|
"event": "onResolve",
|
||||||
"matches": [
|
"match": {
|
||||||
"*://github.com/*/releases/*"
|
"urls": [
|
||||||
],
|
"*://*/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"entry": "index.js"
|
"entry": "index.js"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Extension {
|
|||||||
List<Setting>? settings;
|
List<Setting>? settings;
|
||||||
bool disabled;
|
bool disabled;
|
||||||
bool devMode;
|
bool devMode;
|
||||||
|
String devPath;
|
||||||
|
|
||||||
Extension({
|
Extension({
|
||||||
required this.identity,
|
required this.identity,
|
||||||
@@ -29,6 +30,7 @@ class Extension {
|
|||||||
required this.repository,
|
required this.repository,
|
||||||
required this.disabled,
|
required this.disabled,
|
||||||
required this.devMode,
|
required this.devMode,
|
||||||
|
required this.devPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Extension.fromJson(Map<String, dynamic> json) =>
|
factory Extension.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Extension _$ExtensionFromJson(Map<String, dynamic> json) => Extension(
|
|||||||
: Repository.fromJson(json['repository'] as Map<String, dynamic>),
|
: Repository.fromJson(json['repository'] as Map<String, dynamic>),
|
||||||
disabled: json['disabled'] as bool,
|
disabled: json['disabled'] as bool,
|
||||||
devMode: json['devMode'] as bool,
|
devMode: json['devMode'] as bool,
|
||||||
|
devPath: json['devPath'] as String,
|
||||||
)..settings = (json['settings'] as List<dynamic>?)
|
)..settings = (json['settings'] as List<dynamic>?)
|
||||||
?.map((e) => Setting.fromJson(e as Map<String, dynamic>))
|
?.map((e) => Setting.fromJson(e as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -46,6 +47,7 @@ Map<String, dynamic> _$ExtensionToJson(Extension instance) {
|
|||||||
writeNotNull('settings', instance.settings?.map((e) => e.toJson()).toList());
|
writeNotNull('settings', instance.settings?.map((e) => e.toJson()).toList());
|
||||||
val['disabled'] = instance.disabled;
|
val['disabled'] = instance.disabled;
|
||||||
val['devMode'] = instance.devMode;
|
val['devMode'] = instance.devMode;
|
||||||
|
val['devPath'] = instance.devPath;
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ part 'request.g.dart';
|
|||||||
class Request {
|
class Request {
|
||||||
String url;
|
String url;
|
||||||
Object? extra;
|
Object? extra;
|
||||||
|
Map<String, String>? labels = {};
|
||||||
|
|
||||||
Request({
|
Request({
|
||||||
required this.url,
|
required this.url,
|
||||||
this.extra,
|
this.extra,
|
||||||
|
this.labels,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Request.fromJson(Map<String, dynamic> json) =>
|
factory Request.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ part of 'request.dart';
|
|||||||
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
||||||
url: json['url'] as String,
|
url: json['url'] as String,
|
||||||
extra: json['extra'],
|
extra: json['extra'],
|
||||||
|
labels: (json['labels'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, e as String),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RequestToJson(Request instance) {
|
Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||||
@@ -23,6 +26,7 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeNotNull('extra', instance.extra);
|
writeNotNull('extra', instance.extra);
|
||||||
|
writeNotNull('labels', instance.labels);
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
onClick: (menuItem) => {windowManager.destroy()},
|
onClick: (menuItem) => {windowManager.destroy()},
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
await trayManager.setToolTip('Gopeed');
|
||||||
await trayManager.setContextMenu(menu);
|
await trayManager.setContextMenu(menu);
|
||||||
trayManager.addListener(this);
|
trayManager.addListener(this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,14 +110,22 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
height: 48,
|
height: 48,
|
||||||
)
|
)
|
||||||
: Util.isWeb()
|
: Util.isWeb()
|
||||||
? Image.network(join(
|
? Image.network(
|
||||||
'/fs/extensions/${extension.identity}/${extension.icon}'))
|
join(
|
||||||
|
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
)
|
||||||
: Image.file(
|
: Image.file(
|
||||||
File(path.join(
|
extension.devMode
|
||||||
Util.getStorageDir(),
|
? File(path.join(
|
||||||
"extensions",
|
extension.devPath,
|
||||||
extension.identity,
|
extension.icon))
|
||||||
extension.icon)),
|
: File(path.join(
|
||||||
|
Util.getStorageDir(),
|
||||||
|
"extensions",
|
||||||
|
extension.identity,
|
||||||
|
extension.icon)),
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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.4.0+1
|
version: 1.4.2+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|||||||
Reference in new issue
Block a user