Compare commits

..
5 Commits
Author SHA1 Message Date
Levi 39202b44a1 fix: http retry strategy (#240) 2023-10-20 12:15:21 +08:00
Levi 457d9e85d6 release: v1.4.2 2023-10-19 18:36:46 +08:00
Levi 858ad74339 feat: extension on start event (#239) 2023-10-19 18:08:27 +08:00
Levi 755d7ce814 fix: add system tray tooltip (#237) 2023-10-19 14:00:15 +08:00
Levi 854f600d3f test: add extension script error case (#238) 2023-10-19 13:59:59 +08:00
33 changed files with 579 additions and 111 deletions

No files matched your search

+2
View File
@@ -11,6 +11,8 @@ categories:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧪 Tests'
label: 'test'
- title: '🧰 Maintenance'
label: 'chore'
- title: '📖 Document'
+1 -1
View File
@@ -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
cd ui/flutter
flutter build windows
$mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin"
$mingw = "C:\Program Files\Git\mingw64\bin"
$system = "C:\Windows\System32"
$release = "build\windows\runner\Release\"
cp $mingw\libstdc++-6.dll $release
+46 -18
View File
@@ -18,6 +18,7 @@ import (
"os"
"path"
"strconv"
"time"
)
type RequestError struct {
@@ -37,6 +38,8 @@ type chunk struct {
Begin int64
End int64
Downloaded int64
retryTimes int
}
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) {
chunk := f.chunks[index]
chunk.retryTimes = 0
httpReq, err := buildRequest(ctx, f.meta.Req)
if err != nil {
return err
}
var (
client = buildClient()
buf = make([]byte, 8192)
client = buildClient()
buf = make([]byte, 8192)
maxRetries = 3
)
// 重试5次
for i := 0; i < 5; i++ {
// retry until all remain chunks failed
for true {
// if chunk is completed, return
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
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 (
resp *http.Response
retry bool
resp *http.Response
)
if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange,
@@ -292,35 +314,41 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return nil
}()
if err != nil {
// retry request
// retry request after 1 second
chunk.retryTimes = chunk.retryTimes + 1
time.Sleep(time.Second)
continue
}
// 请求成功就重置错误次数,连续失败5次才终止
i = 0
retry, err = func() (bool, error) {
// Http request success, reset retry times
chunk.retryTimes = 0
err = func() error {
defer resp.Body.Close()
for {
n, err := resp.Body.Read(buf)
if n > 0 {
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
if err != nil {
return false, err
return err
}
chunk.Downloaded += int64(n)
}
if err != nil {
if err != io.EOF {
return true, err
if err == io.EOF {
return nil
}
break
return err
}
}
return false, nil
}()
if !retry {
// 下载成功,跳出重试
break
// if chunk is completed, reset other not complete chunks retry times
if err == nil {
for _, c := range f.chunks {
if c != chunk && c.retryTimes > 0 {
c.retryTimes = 0
}
}
}
break
}
return
}
+15 -6
View File
@@ -76,7 +76,7 @@ func testResolve(startTestServer func() net.Listener, path string, want *base.Re
func TestFetcher_DownloadNormal(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
// 正常下载
downloadNormal(listener, 1, t)
downloadNormal(listener, 5, t)
downloadNormal(listener, 8, t)
@@ -86,7 +86,7 @@ func TestFetcher_DownloadNormal(t *testing.T) {
func TestFetcher_DownloadContinue(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
// 暂停继续
downloadContinue(listener, 1, t)
downloadContinue(listener, 5, t)
downloadContinue(listener, 8, t)
@@ -96,7 +96,7 @@ func TestFetcher_DownloadContinue(t *testing.T) {
func TestFetcher_DownloadChunked(t *testing.T) {
listener := test.StartTestCustomServer()
defer listener.Close()
// chunked编码下载
downloadNormal(listener, 1, t)
downloadNormal(listener, 2, t)
}
@@ -104,24 +104,33 @@ func TestFetcher_DownloadChunked(t *testing.T) {
func TestFetcher_DownloadPost(t *testing.T) {
listener := test.StartTestPostServer()
defer listener.Close()
// post下载
downloadPost(listener, 1, t)
}
func TestFetcher_DownloadRetry(t *testing.T) {
listener := test.StartTestRetryServer()
defer listener.Close()
// chunked编码下载
downloadNormal(listener, 1, t)
}
func TestFetcher_DownloadError(t *testing.T) {
listener := test.StartTestErrorServer()
defer listener.Close()
// chunked编码下载
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) {
listener := test.StartTestFileServer()
defer listener.Close()
+77 -1
View File
@@ -9,6 +9,9 @@ import (
"net"
"net/http"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
)
@@ -81,7 +84,7 @@ func StartTestRetryServer() net.Listener {
mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
counter++
if counter != 1 && counter < 5 {
if counter != 1 && counter < 2 {
writer.WriteHeader(500)
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 {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
+9 -1
View File
@@ -9,6 +9,15 @@ import (
type Request struct {
URL string `json:"url"`
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
@@ -36,7 +45,6 @@ func (r *Resource) Validate() error {
}
}
return nil
}
func (r *Resource) CalcSize() {
+6 -1
View File
@@ -429,6 +429,9 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
Task: task,
Err: err,
})
if eventKey == EventKeyError {
d.emit(EventKeyFinally, task, err)
}
}
}
@@ -494,8 +497,8 @@ func (d *Downloader) watch(task *Task) {
task.Status = base.DownloadStatusDone
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err)
}
d.emit(EventKeyFinally, task, err)
if task.Status == EventKeyDone {
d.notifyRunning()
}
@@ -622,6 +625,8 @@ func (d *Downloader) doPauseAll() (err error) {
func (d *Downloader) start(task *Task) error {
task.lock.Lock()
defer task.lock.Unlock()
d.triggerOnStart(task)
isCreate := task.Status == base.DownloadStatusReady
task.Status = base.DownloadStatusRunning
if task.Meta.Res == nil {
+118 -35
View File
@@ -33,6 +33,7 @@ type ActivationEvent string
const (
EventOnResolve ActivationEvent = "onResolve"
EventOnStart ActivationEvent = "onStart"
//EventOnError ActivationEvent = "onError"
//EventOnDone ActivationEvent = "onDone"
)
@@ -47,7 +48,7 @@ func (d *Downloader) InstallExtensionByFolder(path string, devMode bool) (*Exten
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 {
ext.DevMode = true
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) {
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
gopeed := &Instance{
Events: make(InstanceEvents),
}
ctx := &Context{
Req: req,
}
var err error
for _, ext := range d.extensions {
if ext.Disabled {
continue
}
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.Settings = parseSettings(ext.Settings)
gopeed.Storage = &ContextStorage{
storage: d.storage,
identity: ext.buildIdentity(),
}
scriptFilePath := filepath.Join(d.ExtensionPath(ext), script.Entry)
if _, err = os.Stat(scriptFilePath); os.IsNotExist(err) {
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] script file not exist", ext.buildIdentity())
continue
}
func() {
@@ -256,17 +305,16 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
return
}
defer scriptFile.Close()
ctx.Settings = parseSettings(ext.Settings)
ctx.Storage = &ContextStorage{
storage: d.storage,
identity: ext.buildIdentity(),
}
var scriptBuf []byte
scriptBuf, err = io.ReadAll(scriptFile)
if err != nil {
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] read script file failed", ext.buildIdentity())
return
}
// Init request labels
if req.Labels == nil {
req.Labels = make(map[string]string)
}
engine := engine.NewEngine()
defer engine.Close()
err = engine.Runtime.Set("gopeed", gopeed)
@@ -279,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())
return
}
if fn, ok := gopeed.Events[EventOnResolve]; ok {
if fn, ok := gopeed.Events[event]; ok {
_, err = engine.CallFunction(fn, ctx)
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
}
// calculate resource total size
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
handler(ext, gopeed, ctx)
}
}()
}
@@ -396,30 +436,46 @@ type Repository struct {
type Script struct {
// Event active event name
Event string `json:"event"`
// Matches match request url pattern list
Matches []string `json:"matches"`
// Match rules
Match *Match `json:"match"`
// Entry js script file path
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 == "" {
return false
}
if s.Event != string(event) {
return false
}
if len(s.Matches) == 0 {
if s.Match == nil || (len(s.Match.Urls) == 0 && len(s.Match.Labels) == 0) {
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 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
const (
@@ -446,10 +502,13 @@ type Option struct {
Value any `json:"value"`
}
// Instance inject to js context
// Instance inject to js context when extension script is activated
type Instance struct {
Events InstanceEvents `json:"events"`
Logger *InstanceLogger `json:"logger"`
Events InstanceEvents `json:"events"`
Info *ExtensionInfo `json:"info"`
Logger *InstanceLogger `json:"logger"`
Settings map[string]any `json:"settings"`
Storage *ContextStorage `json:"storage"`
}
type InstanceEvents map[ActivationEvent]goja.Callable
@@ -462,6 +521,10 @@ func (h InstanceEvents) OnResolve(fn goja.Callable) {
h.register(EventOnResolve, fn)
}
func (h InstanceEvents) OnStart(fn goja.Callable) {
h.register(EventOnStart, fn)
}
//func (h InstanceEvents) OnError(fn goja.Callable) {
// h.register(HookEventOnError, fn)
//}
@@ -470,6 +533,24 @@ func (h InstanceEvents) OnResolve(fn goja.Callable) {
// 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 {
identity string
devMode bool
@@ -510,11 +591,13 @@ func newInstanceLogger(extension *Extension, logger *logger.Logger) *InstanceLog
}
}
type Context struct {
Req *base.Request `json:"req"`
Res *base.Resource `json:"res"`
Settings map[string]any `json:"settings"`
Storage *ContextStorage `json:"storage"`
type OnResolveContext struct {
Req *base.Request `json:"req"`
Res *base.Resource `json:"res"`
}
type OnStartContext struct {
Task *Task `json:"task"`
}
func parseSettings(settings []*Setting) map[string]any {
+83
View File
@@ -1,10 +1,12 @@
package download
import (
"errors"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"os"
"testing"
"time"
)
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) {
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_empty", false); err != nil {
+3
View File
@@ -6,6 +6,9 @@ gopeed.events.onResolve(async function (ctx) {
size: 1024,
req: {
url: ctx.req.url + "/" + i,
labels:{
"from": gopeed.info.name,
}
}
}),
),
+5 -3
View File
@@ -5,9 +5,11 @@
"scripts": [
{
"event": "onResolve",
"matches": [
"*://github.com/*"
],
"match": {
"urls": [
"*://github.com/*"
]
},
"entry": "index.js"
}
],
+18
View File
@@ -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,
},
}
}
}),
),
};
});
+22
View File
@@ -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"
}
]
}
+5
View File
@@ -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';
});
+19
View File
@@ -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"
}
]
}
+16
View File
@@ -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
View File
@@ -1,31 +1,31 @@
gopeed.events.onResolve(async function (ctx) {
if (ctx.settings.string != null) {
if (gopeed.settings.string != 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");
}
if (ctx.settings.boolean != null) {
if (gopeed.settings.boolean != 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");
}
if (ctx.settings.numberDefault !== 1) {
if (gopeed.settings.numberDefault !== 1) {
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");
}
if (ctx.settings.stringValued !== "valued") {
if (gopeed.settings.stringValued !== "valued") {
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");
}
if (ctx.settings.booleanValued !== true) {
if (gopeed.settings.booleanValued !== true) {
throw new Error("boolean value is incorrect");
}
@@ -5,9 +5,11 @@
"scripts": [
{
"event": "onResolve",
"matches": [
"*://*/*"
],
"match": {
"urls": [
"*://*/*"
]
},
"entry": "index.js"
}
],
+1 -1
View File
@@ -1,5 +1,5 @@
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");
}
@@ -5,9 +5,11 @@
"scripts": [
{
"event": "onResolve",
"matches": [
"*://*/*"
],
"match": {
"urls": [
"*://*/*"
]
},
"entry": "index.js"
}
]
+13 -13
View File
@@ -2,36 +2,36 @@ gopeed.events.onResolve(async function (ctx) {
const key = "key"
const value1 = "value1", value2 = JSON.stringify({a: 1, b: "2"})
if (ctx.storage.get(key) !== null) {
if (gopeed.storage.get(key) !== null) {
throw new Error("storage get null error")
}
ctx.storage.remove(key)
if(ctx.storage.keys().length !== 0) {
gopeed.storage.remove(key)
if(gopeed.storage.keys().length !== 0) {
throw new Error("storage keys null error")
}
ctx.storage.set(key, value1)
if (ctx.storage.get(key) !== value1) {
gopeed.storage.set(key, value1)
if (gopeed.storage.get(key) !== value1) {
throw new Error("storage put1 error")
}
ctx.storage.set(key, value2)
if (ctx.storage.get(key) !== value2) {
gopeed.storage.set(key, value2)
if (gopeed.storage.get(key) !== value2) {
throw new Error("storage put2 error")
}
if(ctx.storage.keys().length !== 1) {
if(gopeed.storage.keys().length !== 1) {
throw new Error("storage keys error")
}
ctx.storage.remove(key)
if (ctx.storage.get(key) !== null) {
gopeed.storage.remove(key)
if (gopeed.storage.get(key) !== null) {
throw new Error("storage delete error")
}
ctx.storage.set(key, value1)
ctx.storage.clear()
if (ctx.storage.get(key) !== null) {
gopeed.storage.set(key, value1)
gopeed.storage.clear()
if (gopeed.storage.get(key) !== null) {
throw new Error("storage clear error")
}
+7 -5
View File
@@ -1,13 +1,15 @@
{
"name": "basic",
"title": "gopeed extension basic test",
"name": "storage",
"title": "gopeed extension storage test",
"version": "0.0.1",
"scripts": [
{
"event": "onResolve",
"matches": [
"*://github.com/*"
],
"match": {
"urls": [
"*://*/*"
]
},
"entry": "index.js"
}
],
+5 -3
View File
@@ -12,9 +12,11 @@
"scripts": [
{
"event": "onResolve",
"matches": [
"*://github.com/*/releases/*"
],
"match": {
"urls": [
"*://*/*"
]
},
"entry": "index.js"
}
]
+2
View File
@@ -16,6 +16,7 @@ class Extension {
List<Setting>? settings;
bool disabled;
bool devMode;
String devPath;
Extension({
required this.identity,
@@ -29,6 +30,7 @@ class Extension {
required this.repository,
required this.disabled,
required this.devMode,
required this.devPath,
});
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>),
disabled: json['disabled'] as bool,
devMode: json['devMode'] as bool,
devPath: json['devPath'] as String,
)..settings = (json['settings'] as List<dynamic>?)
?.map((e) => Setting.fromJson(e as Map<String, dynamic>))
.toList();
@@ -46,6 +47,7 @@ Map<String, dynamic> _$ExtensionToJson(Extension instance) {
writeNotNull('settings', instance.settings?.map((e) => e.toJson()).toList());
val['disabled'] = instance.disabled;
val['devMode'] = instance.devMode;
val['devPath'] = instance.devPath;
return val;
}
+2
View File
@@ -6,10 +6,12 @@ part 'request.g.dart';
class Request {
String url;
Object? extra;
Map<String, String>? labels = {};
Request({
required this.url,
this.extra,
this.labels,
});
factory Request.fromJson(Map<String, dynamic> json) =>
+4
View File
@@ -9,6 +9,9 @@ part of 'request.dart';
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
url: json['url'] as String,
extra: json['extra'],
labels: (json['labels'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
);
Map<String, dynamic> _$RequestToJson(Request instance) {
@@ -23,6 +26,7 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
}
writeNotNull('extra', instance.extra);
writeNotNull('labels', instance.labels);
return val;
}
@@ -202,6 +202,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
onClick: (menuItem) => {windowManager.destroy()},
),
]);
await trayManager.setToolTip('Gopeed');
await trayManager.setContextMenu(menu);
trayManager.addListener(this);
}
@@ -110,14 +110,22 @@ class ExtensionView extends GetView<ExtensionController> {
height: 48,
)
: Util.isWeb()
? Image.network(join(
'/fs/extensions/${extension.identity}/${extension.icon}'))
? Image.network(
join(
'/fs/extensions/${extension.identity}/${extension.icon}'),
width: 48,
height: 48,
)
: Image.file(
File(path.join(
Util.getStorageDir(),
"extensions",
extension.identity,
extension.icon)),
extension.devMode
? File(path.join(
extension.devPath,
extension.icon))
: File(path.join(
Util.getStorageDir(),
"extensions",
extension.identity,
extension.icon)),
width: 48,
height: 48,
),
+1 -1
View File
@@ -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.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.4.1+1
version: 1.4.2+1
environment:
sdk: '>=3.0.0 <4.0.0'