diff --git a/.donate/index.md b/.donate/index.md deleted file mode 100644 index 752aa024..00000000 --- a/.donate/index.md +++ /dev/null @@ -1,27 +0,0 @@ -## Donate - -🙏 Thank you for considering a donation to Gopeed project! Your support helps us continue to develop and maintain our software. - -
- - - - - - - - - - - - -
微信支付宝Ko-fiPayPal
wechatpayalipayClick MeClick Me
-
- -### Crypto - -- **BTC**: 35jYkPN8bgkh7AWjt3B1VgMQHqtGYcvF6p -- **ETH:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0 -- **Matic:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0 -- **BNB:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0 -- **USDT(ERC20)**: 0x928a7fd2398c941Ac6713704e606Fa414595e3A0 \ No newline at end of file diff --git a/.donate/qr/alipay.jpg b/.donate/qr/alipay.jpg deleted file mode 100644 index d7d0fd62..00000000 Binary files a/.donate/qr/alipay.jpg and /dev/null differ diff --git a/.donate/qr/wechatpay.jpg b/.donate/qr/wechatpay.jpg deleted file mode 100644 index b8bc54ce..00000000 Binary files a/.donate/qr/wechatpay.jpg and /dev/null differ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 3800c8b7..01e9a05f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -12,4 +12,4 @@ #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] # github: monkeyWie ko_fi: gopeed -custom: https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate +custom: https://docs.gopeed.com/donate diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9543c71..953034d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,8 +4,8 @@ on: workflow_dispatch: env: - GO_VERSION: "1.20" - FLUTTER_VERSION: 3.10.6 + GO_VERSION: "1.19" + FLUTTER_VERSION: "3.10.6" permissions: contents: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0debd6ba..8a6c4d52 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ on: - "Dockerfile" env: - GO_VERSION: "1.20" + GO_VERSION: "1.19" permissions: contents: write diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87eb4b15..ffaec7db 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,8 +27,8 @@ on: - "go.sum" env: - GO_VERSION: "1.20" - FLUTTER_VERSION: 3.10.6 + GO_VERSION: "1.19" + FLUTTER_VERSION: "3.10.6" jobs: # lint: diff --git a/.gitignore b/.gitignore index dff3bdf4..de95dc47 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,8 @@ bin/ .DS_Store -dist/ \ No newline at end of file +node_modules/ +cmd/web/dist/ +.test_storage/ +.test_download/ +/extensions \ No newline at end of file diff --git a/README.md b/README.md index 154f4c6d..21daefb0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) -[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate) +[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) ![](_docs/img/banner.png) @@ -61,7 +61,7 @@ When the docker container is running, you can access the web page through `http: ## Donate -If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you! +If you like this project, please consider [donating](https://docs.gopeed.com/donate) to support the development of this project, thank you! ## Showcase diff --git a/README_ja-JP.md b/README_ja-JP.md index 2f5f4965..42bce9ba 100644 --- a/README_ja-JP.md +++ b/README_ja-JP.md @@ -2,7 +2,7 @@ [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) -[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate) +[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) ![](_docs/img/banner.png) diff --git a/README_zh-CN.md b/README_zh-CN.md index 06bb7d9f..bc1864f5 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -2,7 +2,7 @@ [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) -[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate) +[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) ![](_docs/img/banner.png) @@ -59,9 +59,9 @@ docker-compose up -d 当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。 -## 打赏 +## 赞助 -如果觉得项目对你有帮助,请考虑[打赏](/.donate/index.md#donate)以支持这个项目的发展,非常感谢! +如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢! ## 界面展示 diff --git a/bind/desktop/main.go b/bind/desktop/main.go index 37e3cd31..5b63770f 100644 --- a/bind/desktop/main.go +++ b/bind/desktop/main.go @@ -15,6 +15,7 @@ func Start(cfg *C.char) (int, *C.char) { if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil { return 0, C.CString(err.Error()) } + config.ProductionMode = true realPort, err := rest.Start(&config) if err != nil { return 0, C.CString(err.Error()) diff --git a/bind/mobile/main.go b/bind/mobile/main.go index 451f65a6..0a81ba53 100644 --- a/bind/mobile/main.go +++ b/bind/mobile/main.go @@ -13,6 +13,7 @@ func Start(cfg string) (int, error) { if err := json.Unmarshal([]byte(cfg), &config); err != nil { return 0, err } + config.ProductionMode = true return rest.Start(&config) } diff --git a/cmd/api/main.go b/cmd/api/main.go index 4c87cb9e..fbbb7d17 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,11 +1,11 @@ package main import ( - _ "embed" "github.com/GopeedLab/gopeed/cmd" "github.com/GopeedLab/gopeed/pkg/rest/model" ) +// only for local development func main() { cfg := &model.StartConfig{ Network: "tcp", diff --git a/cmd/web/flags.go b/cmd/web/flags.go index f894eb43..52de292f 100644 --- a/cmd/web/flags.go +++ b/cmd/web/flags.go @@ -28,8 +28,6 @@ func parse() *args { cliArgs.configPath = flag.String("c", "./config.json", "Config file path") flag.Parse() - fmt.Println("parse") - // args priority: config file > cli args cfgArgs := loadConfig(*cliArgs.configPath) if cfgArgs.Address == nil { diff --git a/cmd/web/main.go b/cmd/web/main.go index 3b01870d..7858ac31 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -30,15 +30,16 @@ func main() { } cfg := &model.StartConfig{ - Network: "tcp", - Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port), - Storage: model.StorageBolt, - ApiToken: *args.ApiToken, - WebEnable: true, - WebFS: sub, - WebBasicAuth: webBasicAuth, + Network: "tcp", + Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port), + Storage: model.StorageBolt, + ApiToken: *args.ApiToken, + ProductionMode: true, + WebEnable: true, + WebFS: sub, + WebBasicAuth: webBasicAuth, } - cmd.Start(cfg) + cmd.Start(cfg, true) } func isNotBlank(str *string) bool { diff --git a/go.mod b/go.mod index 4c479b4b..6d38757b 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,13 @@ module github.com/GopeedLab/gopeed -go 1.20 +go 1.19 require ( github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 github.com/anacrolix/torrent v1.52.5 + github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e + github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 + github.com/go-git/go-git/v5 v5.8.1 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/matoous/go-nanoid/v2 v2.0.0 @@ -15,7 +18,11 @@ require ( require ( crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/RoaringBitmap/roaring v1.2.3 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect github.com/alecthomas/atomic v0.1.0-alpha2 // indirect github.com/anacrolix/chansync v0.3.0 // indirect @@ -36,17 +43,29 @@ require ( github.com/benbjohnson/immutable v0.3.0 // indirect github.com/bits-and-blooms/bitset v1.2.2 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect + github.com/cloudflare/circl v1.3.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lispad/go-generics-tools v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/pion/datachannel v1.5.2 // indirect github.com/pion/dtls/v2 v2.2.4 // indirect @@ -66,14 +85,23 @@ require ( github.com/pion/turn/v2 v2.0.8 // indirect github.com/pion/udp v0.1.4 // indirect github.com/pion/webrtc/v3 v3.1.42 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect + github.com/rs/zerolog v1.31.0 // indirect + github.com/sergi/go-diff v1.1.0 // indirect + github.com/skeema/knownhosts v1.2.0 // indirect github.com/tidwall/btree v1.6.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect go.opentelemetry.io/otel v1.8.0 // indirect go.opentelemetry.io/otel/trace v1.8.0 // indirect - golang.org/x/crypto v0.5.0 // indirect + golang.org/x/crypto v0.11.0 // indirect golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect - golang.org/x/net v0.7.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + golang.org/x/tools v0.6.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 6a276bf3..c67af1db 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,16 @@ crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oX crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7oH3UFqMpMmK7kwmwV+22HIs= crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= @@ -14,6 +22,8 @@ github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVO github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0= github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k= github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= @@ -24,6 +34,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U= github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE= @@ -63,6 +74,7 @@ github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg= github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc= github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis= github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= +github.com/anacrolix/publicip v0.2.0/go.mod h1:67G1lVkLo8UjdEcJkwScWVTvlJ35OCDsRJoWXl/wi4g= github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY= github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8= @@ -73,13 +85,16 @@ github.com/anacrolix/sync v0.4.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/tagflag v1.3.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= github.com/anacrolix/torrent v1.52.5 h1:jWowdx+EU6zFVfBwmnL0d3H4J6vTFEGOrHI35YdfIT8= github.com/anacrolix/torrent v1.52.5/go.mod h1:CcM8oPMYye5J42cSqJrmUpqwRFgSsJQ1jCEHwygqnqQ= github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA= github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs= github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4= github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= @@ -95,13 +110,31 @@ github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2w github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog= +github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 h1:ObbB2tzHWWAxzsG5futqeq2Ual2zYlo/+eMkSc5sn8w= +github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562/go.mod h1:X2TOTJ+Uamd454RFp7ig2tmP3hQg0Z2Qk8gbVQmU0mk= github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -110,18 +143,30 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= +github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -131,13 +176,18 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -163,6 +213,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -186,16 +238,23 @@ github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -207,6 +266,13 @@ github.com/lispad/go-generics-tools v1.1.0/go.mod h1:2csd1EJljo/gy5qG4khXol7ivCP github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -215,7 +281,9 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -273,6 +341,8 @@ github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA= github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -297,16 +367,25 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= +github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -335,6 +414,8 @@ github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 h1:x9pIfbdIjn github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459/go.mod h1:nFvuG3SWu3VWqobG3cX8nt57wXU0OOFapeCs/8axIuM= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6 h1:NkHVT1GBVV9hUj1iWglFxAuNlvLHELFn71U2H7aOIog= @@ -354,8 +435,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0= golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= @@ -365,6 +450,8 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -390,9 +477,14 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -403,6 +495,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -416,6 +509,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -424,31 +518,48 @@ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -460,6 +571,8 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -485,14 +598,19 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/controller/controller.go b/internal/controller/controller.go index c33d2c72..f99b8456 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -1,7 +1,6 @@ package controller import ( - "errors" "os" "path/filepath" ) @@ -34,7 +33,7 @@ func (c *DefaultFileController) Touch(name string, size int64) (file *os.File, e return } file, err = os.Create(name) - if err != nil && !errors.Is(err, os.ErrExist) { + if err != nil { return } if size > 0 { diff --git a/internal/fetcher/fetcher.go b/internal/fetcher/fetcher.go index 0624fc73..a31929e1 100644 --- a/internal/fetcher/fetcher.go +++ b/internal/fetcher/fetcher.go @@ -12,14 +12,13 @@ type Fetcher interface { // Name return the name of the protocol. Name() string - Setup(ctl *controller.Controller) error + Setup(ctl *controller.Controller) // Resolve resource info from request Resolve(req *base.Request) error // Create ready to download, but not started Create(opts *base.Options) error Start() error Pause() error - Continue() error Close() error // Meta returns the meta information of the download. @@ -37,12 +36,25 @@ type FetcherMeta struct { Opts *base.Options `json:"opts"` } -func (m *FetcherMeta) Filepath(file *base.FileInfo) string { - finalName := m.Opts.Name - if finalName == "" { - finalName = file.Name +// FolderPath return the folder path of the meta info. +func (m *FetcherMeta) FolderPath() string { + // check if rename folder + folder := m.Res.Name + if m.Opts.Name != "" { + folder = m.Opts.Name } - return path.Join(m.Opts.Path, m.Res.RootDir, finalName) + return path.Join(m.Opts.Path, folder) +} + +// SingleFilepath return the single file path of the meta info. +func (m *FetcherMeta) SingleFilepath() string { + // check if rename file + file := m.Res.Files[0] + fileName := file.Name + if m.Opts.Name != "" { + fileName = m.Opts.Name + } + return path.Join(m.Opts.Path, file.Path, fileName) } // FetcherBuilder defines the interface for a fetcher builder. diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 00000000..a85f3d38 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,52 @@ +package logger + +import ( + "github.com/GopeedLab/gopeed/pkg/util" + "github.com/rs/zerolog" + "io" + "os" + "path/filepath" +) + +type Logger struct { + zerolog.Logger + logFile *os.File +} + +func (l *Logger) CLose() { + l.logFile.Close() +} + +// NewLogger create a new logger +func NewLogger(logFile bool, logPath string) *Logger { + var out io.Writer + if logFile { + // log to file + logDir := filepath.Dir(logPath) + if err := util.CreateDirIfNotExist(logDir); err != nil { + panic(err) + } + var ( + logfile *os.File + err error + ) + logfile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + panic(err) + } + out = logfile + } else { + out = os.Stdout + } + + logger := &Logger{} + if logFile { + logger.logFile = out.(*os.File) + } + logger.Logger = zerolog.New(zerolog.ConsoleWriter{ + NoColor: true, + Out: out, + TimeFormat: "2006-01-02 15:04:05", + }).With().Timestamp().Logger() + return logger +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 00000000..0b71c9e1 --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,35 @@ +package logger + +import ( + "os" + "testing" +) + +func TestNewLogger(t *testing.T) { + logger := NewLogger(false, "") + logger.Info().Msg("test") +} + +func TestNewLoggerFile(t *testing.T) { + logPath := "./testdata/test.log" + logger := NewLogger(true, logPath) + defer func() { + logger.CLose() + os.Remove(logPath) + }() + logger.Info().Msg("test") + + // read log file, if not exist or empty, test fail. + file, err := os.Open(logPath) + if err != nil { + t.Fatal(err) + } + defer file.Close() + stat, err := file.Stat() + if err != nil { + t.Fatal(err) + } + if stat.Size() == 0 { + t.Fatal("log file is empty") + } +} diff --git a/internal/protocol/bt/fetcher.go b/internal/protocol/bt/fetcher.go index bad75c7e..8a804cab 100644 --- a/internal/protocol/bt/fetcher.go +++ b/internal/protocol/bt/fetcher.go @@ -9,7 +9,6 @@ import ( "github.com/GopeedLab/gopeed/pkg/util" "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/metainfo" - "path" "path/filepath" "sync" "sync/atomic" @@ -38,7 +37,7 @@ func (f *Fetcher) Name() string { return "bt" } -func (f *Fetcher) Setup(ctl *controller.Controller) (err error) { +func (f *Fetcher) Setup(ctl *controller.Controller) { f.ctl = ctl if f.meta == nil { f.meta = &fetcher.FetcherMeta{} @@ -77,51 +76,17 @@ func (f *Fetcher) Resolve(req *base.Request) error { if err := f.addTorrent(req); err != nil { return err } - go func() { - // recycle unused torrent resource - time.Sleep(time.Minute * 3) - if !f.create.Load() { - f.torrentReady.Store(false) - f.safeDrop() - } - }() - res := &base.Resource{ - Name: f.torrent.Name(), - Range: true, - RootDir: f.torrent.Name(), - Files: make([]*base.FileInfo, len(f.torrent.Files())), - Hash: f.torrent.InfoHash().String(), - } - for i, file := range f.torrent.Files() { - res.Files[i] = &base.FileInfo{ - Name: filepath.Base(file.DisplayPath()), - Path: util.Dir(file.Path()), - Size: file.Length(), - } - res.Size += file.Length() - } + f.updateRes() f.meta.Req = req - f.meta.Res = res return nil } func (f *Fetcher) Create(opts *base.Options) (err error) { f.create.Store(true) f.meta.Opts = opts - if len(opts.SelectFiles) == 0 { - opts.SelectFiles = make([]int, 0) - for i := range f.torrent.Files() { - opts.SelectFiles = append(opts.SelectFiles, i) - } + if f.meta.Res != nil { + torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath() } - if opts.Path != "" { - torrentDirMap[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir) - if ft, ok := ftMap[f.meta.Res.Hash]; ok { - // reuse resolve fetcher - ft.setTorrentDir(torrentDirMap[f.meta.Res.Hash]) - } - } - f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles)) return nil } @@ -132,6 +97,9 @@ func (f *Fetcher) Start() (err error) { return } } + if ft, ok := ftMap[f.meta.Res.Hash]; ok { + ft.setTorrentDir(f.meta.FolderPath()) + } files := f.torrent.Files() if len(f.meta.Opts.SelectFiles) == len(files) { f.torrent.DownloadAll() @@ -150,10 +118,6 @@ func (f *Fetcher) Pause() (err error) { return } -func (f *Fetcher) Continue() (err error) { - return f.Start() -} - func (f *Fetcher) Close() (err error) { f.safeDrop() return nil @@ -217,6 +181,27 @@ func (f *Fetcher) Progress() fetcher.Progress { return f.progress } +func (f *Fetcher) updateRes() { + res := &base.Resource{ + Name: f.torrent.Name(), + Range: true, + Files: make([]*base.FileInfo, len(f.torrent.Files())), + Hash: f.torrent.InfoHash().String(), + } + for i, file := range f.torrent.Files() { + res.Files[i] = &base.FileInfo{ + Name: filepath.Base(file.DisplayPath()), + Path: util.Dir(file.Path()), + Size: file.Length(), + } + } + res.CalcSize() + f.meta.Res = res + if f.meta.Opts != nil { + f.meta.Opts.InitSelectFiles(len(res.Files)) + } +} + func (f *Fetcher) addTorrent(req *base.Request) (err error) { if err = f.initClient(); err != nil { return diff --git a/internal/protocol/bt/fetcher_test.go b/internal/protocol/bt/fetcher_test.go index 030d78a7..44a83804 100644 --- a/internal/protocol/bt/fetcher_test.go +++ b/internal/protocol/bt/fetcher_test.go @@ -33,10 +33,9 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) { } want := &base.Resource{ - Name: "ubuntu-22.04-live-server-amd64.iso", - Size: 1466714112, - Range: true, - RootDir: "ubuntu-22.04-live-server-amd64.iso", + Name: "ubuntu-22.04-live-server-amd64.iso", + Size: 1466714112, + Range: true, Files: []*base.FileInfo{ { Name: "ubuntu-22.04-live-server-amd64.iso", @@ -69,10 +68,9 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) { } want := &base.Resource{ - Name: "ubuntu-22.04-live-server-amd64.iso", - Size: 1466714112, - Range: true, - RootDir: "ubuntu-22.04-live-server-amd64.iso", + Name: "ubuntu-22.04-live-server-amd64.iso", + Size: 1466714112, + Range: true, Files: []*base.FileInfo{ { Name: "ubuntu-22.04-live-server-amd64.iso", diff --git a/internal/protocol/http/fetcher.go b/internal/protocol/http/fetcher.go index d53d0ae6..acf10403 100644 --- a/internal/protocol/http/fetcher.go +++ b/internal/protocol/http/fetcher.go @@ -3,6 +3,7 @@ package http import ( "bytes" "context" + "errors" "fmt" "github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/fetcher" @@ -33,7 +34,6 @@ func (re *RequestError) Error() string { } type chunk struct { - Status base.Status Begin int64 End int64 Downloaded int64 @@ -41,9 +41,8 @@ type chunk struct { func newChunk(begin int64, end int64) *chunk { return &chunk{ - Status: base.DownloadStatusReady, - Begin: begin, - End: end, + Begin: begin, + End: end, } } @@ -52,23 +51,20 @@ type Fetcher struct { doneCh chan error meta *fetcher.FetcherMeta - status base.Status chunks []*chunk - file *os.File - ctx context.Context - cancel context.CancelFunc - pauseCh chan interface{} + file *os.File + cancel context.CancelFunc + eg *errgroup.Group } func (f *Fetcher) Name() string { return "http" } -func (f *Fetcher) Setup(ctl *controller.Controller) (err error) { +func (f *Fetcher) Setup(ctl *controller.Controller) { f.ctl = ctl f.doneCh = make(chan error, 1) - f.pauseCh = make(chan interface{}, 1) if f.meta == nil { f.meta = &fetcher.FetcherMeta{} } @@ -139,10 +135,9 @@ func (f *Fetcher) Resolve(req *base.Request) error { } // unknown file filePath if file.Name == "" || file.Name == "/" || file.Name == "." { - file.Name = "unknown" + file.Name = httpReq.URL.Hostname() } res.Files = append(res.Files, file) - res.Name = file.Name f.meta.Req = req f.meta.Res = res return nil @@ -150,7 +145,6 @@ func (f *Fetcher) Resolve(req *base.Request) error { func (f *Fetcher) Create(opts *base.Options) error { f.meta.Opts = opts - f.status = base.DownloadStatusReady if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil { return err @@ -178,44 +172,38 @@ func (f *Fetcher) Create(opts *base.Options) error { } func (f *Fetcher) Start() (err error) { - // 创建文件 - name := f.filepath() - f.file, err = f.ctl.Touch(name, f.meta.Res.Size) + name := f.meta.SingleFilepath() + // if file not exist, create it, else open it + _, err = os.Stat(name) + if err != nil { + if os.IsNotExist(err) { + f.file, err = f.ctl.Touch(name, f.meta.Res.Size) + } else { + return + } + } else { + f.file, err = os.OpenFile(name, os.O_RDWR, os.ModeAppend) + } if err != nil { return err } - f.status = base.DownloadStatusRunning - f.chunks = f.splitChunk() + if f.chunks == nil { + f.chunks = f.splitChunk() + } f.fetch() return } func (f *Fetcher) Pause() (err error) { - if base.DownloadStatusRunning != f.status { - return - } - f.status = base.DownloadStatusPause - if f.cancel != nil { f.cancel() - <-f.pauseCh + // wait for pause handle complete + f.eg.Wait() + f.file.Close() } return } -func (f *Fetcher) Continue() (err error) { - if base.DownloadStatusRunning == f.status || base.DownloadStatusDone == f.status { - return - } - f.status = base.DownloadStatusRunning - f.file, err = os.OpenFile(f.filepath(), os.O_RDWR, os.ModeAppend) - if err != nil { - return err - } - f.fetch() - return -} - func (f *Fetcher) Close() (err error) { if err = f.Pause(); err != nil { return @@ -243,41 +231,32 @@ func (f *Fetcher) Wait() (err error) { return <-f.doneCh } -func (f *Fetcher) filepath() string { - return f.meta.Filepath(f.meta.Res.Files[0]) -} - func (f *Fetcher) fetch() { - f.ctx, f.cancel = context.WithCancel(context.Background()) - eg, _ := errgroup.WithContext(f.ctx) + var ctx context.Context + ctx, f.cancel = context.WithCancel(context.Background()) + f.eg, _ = errgroup.WithContext(ctx) for i := 0; i < len(f.chunks); i++ { i := i - eg.Go(func() error { - return f.fetchChunk(i) + f.eg.Go(func() error { + return f.fetchChunk(i, ctx) }) } go func() { - err := eg.Wait() - // 下载停止,关闭文件句柄 - f.file.Close() - if f.status == base.DownloadStatusPause { - f.pauseCh <- nil - } else { - if err != nil { - f.status = base.DownloadStatusError - } else { - f.status = base.DownloadStatusDone - } - f.doneCh <- err + err := f.eg.Wait() + // check if canceled + if errors.Is(err, context.Canceled) { + return } + f.file.Close() + f.doneCh <- err }() } -func (f *Fetcher) fetchChunk(index int) (err error) { +func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) { chunk := f.chunks[index] - httpReq, err := buildRequest(f.ctx, f.meta.Req) + httpReq, err := buildRequest(ctx, f.meta.Req) if err != nil { return err } @@ -287,14 +266,10 @@ func (f *Fetcher) fetchChunk(index int) (err error) { ) // 重试5次 for i := 0; i < 5; i++ { - // 如果下载完成直接返回 - if chunk.Status == base.DownloadStatusDone { + // if chunk is completed, return + if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 { return } - // 如果已暂停直接跳出 - if f.status == base.DownloadStatusPause { - break - } var ( resp *http.Response retry bool @@ -317,7 +292,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) { return nil }() if err != nil { - // 请求失败重试 + // retry request continue } // 请求成功就重置错误次数,连续失败5次才终止 @@ -347,19 +322,6 @@ func (f *Fetcher) fetchChunk(index int) (err error) { break } } - - if err != nil { - chunk.Status = base.DownloadStatusError - return - } - - isComplete := f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 - if f.status == base.DownloadStatusPause && !isComplete { - chunk.Status = base.DownloadStatusPause - return - } - - chunk.Status = base.DownloadStatusDone return } @@ -474,7 +436,6 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v fb := &FetcherBuilder{} fetcher := fb.Build().(*Fetcher) fetcher.meta = meta - fetcher.status = base.DownloadStatusPause base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req) base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts) if len(fd.Chunks) == 0 { diff --git a/internal/protocol/http/fetcher_test.go b/internal/protocol/http/fetcher_test.go index f9290b19..7704f0fb 100644 --- a/internal/protocol/http/fetcher_test.go +++ b/internal/protocol/http/fetcher_test.go @@ -16,7 +16,6 @@ import ( func TestFetcher_Resolve(t *testing.T) { testResolve(test.StartTestFileServer, test.BuildName, &base.Resource{ - Name: test.BuildName, Size: test.BuildSize, Range: true, Files: []*base.FileInfo{ @@ -27,7 +26,6 @@ func TestFetcher_Resolve(t *testing.T) { }, }, t) testResolve(test.StartTestCustomServer, "disposition", &base.Resource{ - Name: test.BuildName, Size: test.BuildSize, Range: false, Files: []*base.FileInfo{ @@ -38,7 +36,6 @@ func TestFetcher_Resolve(t *testing.T) { }, }, t) testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{ - Name: test.BuildName, Size: 0, Range: false, Files: []*base.FileInfo{ @@ -48,6 +45,17 @@ func TestFetcher_Resolve(t *testing.T) { }, }, }, t) + + fetcher := buildFetcher() + err := fetcher.Resolve(&base.Request{ + URL: "http://github.com", + }) + if err != nil { + t.Fatal(err) + } + if fetcher.Meta().Res.Files[0].Name != "github.com" { + t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, "github.com") + } } func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) { @@ -125,7 +133,9 @@ func TestFetcher_DownloadResume(t *testing.T) { } func TestFetcher_Config(t *testing.T) { - fetcher := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t) + listener := test.StartTestFileServer() + defer listener.Close() + fetcher := doDownloadReady(buildConfigFetcher(), listener, 0, t) err := fetcher.Start() if err != nil { t.Fatal(err) @@ -222,7 +232,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) { t.Fatal(err) } time.Sleep(time.Millisecond * 50) - if err := fetcher.Continue(); err != nil { + if err := fetcher.Start(); err != nil { t.Fatal(err) } err = fetcher.Wait() @@ -270,7 +280,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) { t.Fatal(err) } fetcher.Setup(controller.NewController()) - fetcher.Continue() + fetcher.Start() err = fetcher.Wait() if err != nil { diff --git a/internal/test/httptest.go b/internal/test/httptest.go index 7f6aab47..07fb0804 100644 --- a/internal/test/httptest.go +++ b/internal/test/httptest.go @@ -1,6 +1,7 @@ package test import ( + "context" "encoding/json" "fmt" "io" @@ -17,8 +18,10 @@ const ( Dir = "./" BuildFile = Dir + BuildName - DownloadName = "download.data" - DownloadFile = Dir + DownloadName + DownloadName = "download.data" + DownloadRename = "download (1).data" + DownloadFile = Dir + DownloadName + DownloadRenameFile = Dir + DownloadRename ) func StartTestFileServer() net.Listener { @@ -140,6 +143,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener { if err != nil { panic(err) } + defer file.Close() // 随机生成一个文件 l := int64(8192) buf := make([]byte, l) @@ -160,28 +164,29 @@ func startTestServer(serverHandle func() http.Handler) net.Listener { server.Handler = serverHandle() go server.Serve(listener) - return &delFileListener{ - File: file, + return &shutdownListener{ + server: server, Listener: listener, } } -type delFileListener struct { - *os.File +type shutdownListener struct { + server *http.Server net.Listener } -func (c *delFileListener) Close() error { - defer func() { - c.File.Close() - if err := ifExistAndRemove(c.File.Name()); err != nil { - fmt.Println(err) - } - if err := ifExistAndRemove(DownloadFile); err != nil { - fmt.Println(err) - } - }() - return c.Listener.Close() +func (c *shutdownListener) Close() error { + closeErr := c.server.Shutdown(context.Background()) + if err := ifExistAndRemove(BuildFile); err != nil { + fmt.Println(err) + } + if err := ifExistAndRemove(DownloadFile); err != nil { + fmt.Println(err) + } + if err := ifExistAndRemove(DownloadRenameFile); err != nil { + fmt.Println(err) + } + return closeErr } func ifExistAndRemove(name string) error { diff --git a/pkg/base/model.go b/pkg/base/model.go index 7a3f8abe..b27facfd 100644 --- a/pkg/base/model.go +++ b/pkg/base/model.go @@ -1,6 +1,7 @@ package base import ( + "fmt" "github.com/GopeedLab/gopeed/pkg/util" ) @@ -12,20 +13,46 @@ type Request struct { // Resource download resource type Resource struct { + // if name is not empty, the resource is a folder and the name is the folder name Name string `json:"name"` Size int64 `json:"size"` // is support range download - Range bool `json:"range"` - RootDir string `json:"rootDir"` + Range bool `json:"range"` // file list Files []*FileInfo `json:"files"` Hash string `json:"hash"` } +func (r *Resource) Validate() error { + if r.Name == "" { + return fmt.Errorf("invalid resource name") + } + if r.Files == nil || len(r.Files) == 0 { + return fmt.Errorf("invalid resource files") + } + for _, file := range r.Files { + if file.Name == "" { + return fmt.Errorf("invalid resource file name") + } + } + return nil + +} + +func (r *Resource) CalcSize() { + var size int64 + for _, file := range r.Files { + size += file.Size + } + r.Size = size +} + type FileInfo struct { Name string `json:"name"` Path string `json:"path"` Size int64 `json:"size"` + + Req *Request `json:"req"` } // Options for download @@ -40,6 +67,25 @@ type Options struct { Extra any `json:"extra"` } +func (o *Options) InitSelectFiles(fileSize int) { + // if selectFiles is empty, select all files + if len(o.SelectFiles) == 0 { + o.SelectFiles = make([]int, fileSize) + for i := 0; i < fileSize; i++ { + o.SelectFiles[i] = i + } + } +} + +func (o *Options) Clone() *Options { + return &Options{ + Name: o.Name, + Path: o.Path, + SelectFiles: o.SelectFiles, + Extra: o.Extra, + } +} + func ParseReqExtra[E any](req *Request) error { if req.Extra == nil { return nil diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 126be482..740b4d25 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -4,13 +4,13 @@ import ( "errors" "github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/fetcher" + "github.com/GopeedLab/gopeed/internal/logger" "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/util" gonanoid "github.com/matoous/go-nanoid/v2" "github.com/virtuald/go-paniclog" "math" "os" - "path" "path/filepath" "sort" "strings" @@ -26,6 +26,12 @@ const ( bucketSave = "save" // downloader config bucket bucketConfig = "config" + // downloader extension bucket + bucketExtension = "extension" +) + +var ( + ErrTaskNotFound = errors.New("task not found") ) type Listener func(event *Event) @@ -40,16 +46,21 @@ type Progress struct { } type Downloader struct { - cfg *DownloaderConfig - fetchBuilders map[string]fetcher.FetcherBuilder - fetcherMap map[string]fetcher.Fetcher - storage Storage - tasks []*Task - listener Listener + Logger *logger.Logger + ExtensionLogger *logger.Logger + + cfg *DownloaderConfig + fetcherBuilders map[string]fetcher.FetcherBuilder + fetcherCache map[string]fetcher.Fetcher + storage Storage + tasks []*Task + listener Listener lock *sync.Mutex fetcherMapLock *sync.RWMutex closed atomic.Bool + + extensions []*Extension } func NewDownloader(cfg *DownloaderConfig) *Downloader { @@ -61,26 +72,32 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader { d := &Downloader{ cfg: cfg, - fetchBuilders: make(map[string]fetcher.FetcherBuilder), - fetcherMap: make(map[string]fetcher.Fetcher), + fetcherBuilders: make(map[string]fetcher.FetcherBuilder), + fetcherCache: make(map[string]fetcher.Fetcher), storage: cfg.Storage, lock: &sync.Mutex{}, fetcherMapLock: &sync.RWMutex{}, + + extensions: make([]*Extension, 0), } for _, f := range cfg.FetchBuilders { for _, p := range f.Schemes() { - d.fetchBuilders[strings.ToUpper(p)] = f + d.fetcherBuilders[strings.ToUpper(p)] = f } } - logPanic(filepath.Join(cfg.StorageDir, "logs")) + d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log")) + d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log")) + if cfg.ProductionMode { + logPanic(filepath.Join(cfg.StorageDir, "logs")) + } return d } func (d *Downloader) Setup() error { // setup storage - if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig}); err != nil { + if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig, bucketExtension}); err != nil { return err } // load config from storage @@ -100,7 +117,7 @@ func (d *Downloader) Setup() error { d.cfg.DownloaderStoreConfig.Init() // load tasks from storage var tasks []*Task - if err := d.storage.List(bucketTask, &tasks); err != nil { + if err = d.storage.List(bucketTask, &tasks); err != nil { return err } if tasks == nil { @@ -118,16 +135,25 @@ func (d *Downloader) Setup() error { task.Status = base.DownloadStatusPause } } - // sort by create time - sort.Slice(tasks, func(i, j int) bool { - return tasks[i].CreatedAt.Before(tasks[j].CreatedAt) - }) } d.tasks = tasks + // sort by create time + sort.Slice(d.tasks, func(i, j int) bool { + return d.tasks[i].CreatedAt.Before(d.tasks[j].CreatedAt) + }) - // 每个tick统计一次下载速度 + // load extensions from storage + var extensions []*Extension + if err = d.storage.List(bucketExtension, &extensions); err != nil { + return err + } + if extensions == nil { + extensions = make([]*Extension, 0) + } + d.extensions = extensions + + // calculate download speed every tick go func() { - for !d.closed.Load() { if len(d.tasks) > 0 { for _, task := range d.tasks { @@ -151,13 +177,16 @@ func (d *Downloader) Setup() error { // store fetcher progress data, err := task.fetcherBuilder.Store(task.fetcher) if err != nil { - return // TODO log + d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID) + return } if err := d.storage.Put(bucketSave, task.ID, data); err != nil { - return // TODO log + d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID) + return } if err := d.storage.Put(bucketTask, task.ID, task); err != nil { - return // TODO log + d.Logger.Error().Stack().Err(err).Msgf("persist task failed: %s", task.ID) + return } }() } @@ -170,7 +199,7 @@ func (d *Downloader) Setup() error { func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) { schema := util.ParseSchema(url) - fetchBuilder, ok := d.fetchBuilders[schema] + fetchBuilder, ok := d.fetcherBuilders[schema] if ok { return fetchBuilder, nil } @@ -186,22 +215,29 @@ func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) { } func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) { - fb, err := d.parseFb(req.URL) - if err != nil { - return - } - fetcher := fb.Build() - d.setupFetcher(fetcher) - err = fetcher.Resolve(req) - if err != nil { - return - } rrId, err := gonanoid.New() if err != nil { return } + + res := d.triggerOnResolve(req) + if res != nil && len(res.Files) > 0 { + rr = &ResolveResult{ + Res: res, + } + return + } + + fetcher, err := d.buildFetcher(req.URL) + if err != nil { + return + } + err = fetcher.Resolve(req) + if err != nil { + return + } d.fetcherMapLock.Lock() - d.fetcherMap[rrId] = fetcher + d.fetcherCache[rrId] = fetcher d.fetcherMapLock.Unlock() rr = &ResolveResult{ ID: rrId, @@ -211,11 +247,13 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) { } func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId string, err error) { - rr, err := d.Resolve(req) + var fetcher fetcher.Fetcher + fetcher, err = d.buildFetcher(req.URL) if err != nil { return } - return d.Create(rr.ID, opts) + fetcher.Meta().Req = req + return d.doCreate(fetcher, opts) } func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) { @@ -223,95 +261,23 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err opts = &base.Options{} } d.fetcherMapLock.RLock() - fetcher, ok := d.fetcherMap[rrId] + fetcher, ok := d.fetcherCache[rrId] d.fetcherMapLock.RUnlock() if !ok { return "", errors.New("invalid resource id") } defer func() { d.fetcherMapLock.Lock() - delete(d.fetcherMap, rrId) + delete(d.fetcherCache, rrId) d.fetcherMapLock.Unlock() }() - - meta := fetcher.Meta() - meta.Opts = opts - res := meta.Res - if len(opts.SelectFiles) == 0 { - opts.SelectFiles = make([]int, 0) - for i := range res.Files { - opts.SelectFiles = append(opts.SelectFiles, i) - } - } - if opts.Path == "" { - storeConfig, err := d.GetConfig() - if err != nil { - return "", err - } - opts.Path = storeConfig.DownloadDir - } - - fb, err := d.parseFb(fetcher.Meta().Req.URL) - if err != nil { - return - } - - // check if the download file is duplicated and rename it automatically. - files := res.Files - if res.RootDir != "" { - fullDirPath := path.Join(opts.Path, res.RootDir) - newName, err := util.CheckDuplicateAndRename(fullDirPath) - if err != nil { - return "", err - } - res.RootDir = newName - } else if len(files) == 1 { - fullPath := meta.Filepath(files[0]) - newName, err := util.CheckDuplicateAndRename(fullPath) - if err != nil { - return "", err - } - opts.Name = newName - } - - err = fetcher.Create(opts) - if err != nil { - return - } - - task := NewTask() - task.fetcherBuilder = fb - task.fetcher = fetcher - task.Meta = fetcher.Meta() - task.Progress = &Progress{} - task.Status = base.DownloadStatusReady - // calculate select files size - for _, selectIndex := range opts.SelectFiles { - task.Size += res.Files[selectIndex].Size - } - initTask(task) - err = func() error { - d.lock.Lock() - defer d.lock.Unlock() - d.tasks = append(d.tasks, task) - return d.storage.Put(bucketTask, task.ID, task.clone()) - }() - if err != nil { - return - } - taskId = task.ID - d.tryRunning(func(remain int) { - if remain > 0 { - err = d.start(task) - } - }) - return + return d.doCreate(fetcher, opts) } func (d *Downloader) Pause(id string) (err error) { task := d.GetTask(id) if task == nil { - return + return ErrTaskNotFound } if err = d.doPause(task, base.DownloadStatusPause); err != nil { return err @@ -322,7 +288,7 @@ func (d *Downloader) Pause(id string) (err error) { func (d *Downloader) Continue(id string) (err error) { task := d.GetTask(id) if task == nil { - return + return ErrTaskNotFound } d.tryRunning(func(remain int) { if remain == 0 { @@ -347,14 +313,7 @@ func (d *Downloader) Continue(id string) (err error) { func (d *Downloader) PauseAll() (err error) { d.lock.Lock() defer d.lock.Unlock() - for _, task := range d.tasks { - if task.Status == base.DownloadStatusRunning { - if err = d.doPause(task, base.DownloadStatusPause); err != nil { - return - } - } - } - return + return d.doPauseAll() } func (d *Downloader) ContinueAll() (err error) { @@ -384,7 +343,7 @@ func (d *Downloader) Delete(id string, force bool) (err error) { defer d.lock.Unlock() task := d.GetTask(id) if task == nil { - return + return ErrTaskNotFound } task.lock.Lock() defer task.lock.Unlock() @@ -408,16 +367,14 @@ func (d *Downloader) Delete(id string, force bool) (err error) { if err != nil { return } - if force { - if task.Meta.Res.RootDir != "" { - if err = os.RemoveAll(path.Join(task.Meta.Opts.Path, task.Meta.Res.RootDir)); err != nil { + if force && task.Meta.Res != nil { + if task.Meta.Res.Name != "" { + if err = os.RemoveAll(task.Meta.FolderPath()); err != nil { return } } else { - for _, file := range task.Meta.Res.Files { - if err = util.SafeRemove(task.Meta.Filepath(file)); err != nil { - return err - } + if err = util.SafeRemove(task.Meta.SingleFilepath()); err != nil { + return err } } } @@ -435,16 +392,20 @@ func (d *Downloader) Close() error { d.lock.Lock() defer d.lock.Unlock() d.closed.Store(true) - for _, task := range d.tasks { - d.doPause(task, base.DownloadStatusPause) + if err := d.doPauseAll(); err != nil { + return err } return d.storage.Close() } func (d *Downloader) Clear() error { - if err := d.Close(); err != nil { - return err + if !d.closed.Load() { + if err := d.Close(); err != nil { + return err + } } + d.tasks = make([]*Task, 0) + d.extensions = make([]*Extension, 0) if err := d.storage.Clear(); err != nil { return err } @@ -479,7 +440,12 @@ func (d *Downloader) GetTask(id string) *Task { } func (d *Downloader) GetTasks() []*Task { - return d.tasks + // reverse tasks, so that the latest task is in the front + tasks := make([]*Task, len(d.tasks)) + for i := 0; i < len(d.tasks); i++ { + tasks[i] = d.tasks[len(d.tasks)-i-1] + } + return tasks } func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) { @@ -552,21 +518,71 @@ func (d *Downloader) restoreFetcher(task *Task) error { return nil }() if err != nil { - // TODO log + d.Logger.Error().Stack().Err(err).Msgf("deserialize fetcher failed, task id: %s", task.ID) } if task.fetcher == nil { task.fetcher = fb.Build() } d.setupFetcher(task.fetcher) - task.fetcher.Create(task.Meta.Opts) + go d.watch(task) } + task.fetcher.Create(task.Meta.Opts) return nil } -func (d *Downloader) tryRunning(cb func(remain int)) { - d.lock.Lock() - defer d.lock.Unlock() +func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (taskId string, err error) { + if opts == nil { + opts = &base.Options{} + } + meta := fetcher.Meta() + meta.Opts = opts + if opts.Path == "" { + storeConfig, err := d.GetConfig() + if err != nil { + return "", err + } + opts.Path = storeConfig.DownloadDir + } + + fb, err := d.parseFb(fetcher.Meta().Req.URL) + if err != nil { + return + } + + task := NewTask() + task.fetcherBuilder = fb + task.fetcher = fetcher + task.Meta = fetcher.Meta() + task.Progress = &Progress{} + task.Status = base.DownloadStatusReady + initTask(task) + if err = fetcher.Create(opts); err != nil { + return + } + if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { + return + } + taskId = task.ID + + func() { + d.lock.Lock() + defer d.lock.Unlock() + d.tasks = append(d.tasks, task) + }() + go d.watch(task) + go d.tryRunning(func(remain int) { + if remain > 0 { + if err = d.start(task); err != nil { + d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID) + return + } + } + }) + return +} + +func (d *Downloader) tryRunning(cb func(remain int)) { runningCount := 0 for _, task := range d.tasks { if task.Status == base.DownloadStatusRunning { @@ -592,39 +608,75 @@ func (d *Downloader) doNotifyRunning() error { return nil } +func (d *Downloader) doPauseAll() (err error) { + for _, task := range d.tasks { + if err = d.doPause(task, base.DownloadStatusPause); err != nil { + return + } + } + return +} + func (d *Downloader) start(task *Task) error { - var event EventKey - if task.Status == base.DownloadStatusReady { - event = EventKeyStart - } else { - event = EventKeyContinue + task.lock.Lock() + defer task.lock.Unlock() + isCreate := task.Status == base.DownloadStatusReady + task.Status = base.DownloadStatusRunning + if task.Meta.Res == nil { + err := task.fetcher.Resolve(task.Meta.Req) + if err != nil { + task.Status = base.DownloadStatusError + d.emit(EventKeyError, task, err) + return err + } + isCreate = true } - task.Status = base.DownloadStatusRunning + if isCreate { + d.lock.Lock() + defer d.lock.Unlock() + // check if the download file is duplicated and rename it automatically. + if task.Meta.Res.Name != "" { + fullDirPath := task.Meta.FolderPath() + newName, err := util.CheckDuplicateAndRename(fullDirPath) + if err != nil { + return err + } + task.Meta.Opts.Name = newName + } else { + fullFilePath := task.Meta.SingleFilepath() + newName, err := util.CheckDuplicateAndRename(fullFilePath) + if err != nil { + return err + } + task.Meta.Opts.Name = newName + } + } + if err := d.doStart(task); err != nil { + task.Status = base.DownloadStatusError + d.emit(EventKeyError, task, err) + return err + } + return nil +} + +func (d *Downloader) doStart(task *Task) error { task.Progress.Speed = 0 task.timer.Start() if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { return err } - var err error - if event == EventKeyStart { - err = task.fetcher.Start() - } - if event == EventKeyContinue { - err = task.fetcher.Continue() - } - if err != nil { + if err := task.fetcher.Start(); err != nil { return err } - go d.watch(task) - d.emit(event, task) + d.emit(EventKeyStart, task) return nil } func (d *Downloader) doPause(task *Task, status base.Status) (err error) { task.lock.Lock() defer task.lock.Unlock() - if task.Status == base.DownloadStatusRunning { + if task.Status != base.DownloadStatusDone { task.Status = status task.timer.Pause() task.fetcher.Pause() @@ -635,15 +687,26 @@ func (d *Downloader) doPause(task *Task, status base.Status) (err error) { } func (d *Downloader) doContinue(task *Task) (err error) { - task.lock.Lock() - defer task.lock.Unlock() - if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone { - err = d.restoreFetcher(task) - if err != nil { + err = func() error { + task.lock.Lock() + defer task.lock.Unlock() + if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone { + err := d.restoreFetcher(task) + if err != nil { + return err + } + } + return nil + }() + if err != nil { + return + } + go func() { + if err := d.start(task); err != nil { + d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID) return } - err = d.start(task) - } + }() return } @@ -659,6 +722,16 @@ func logPanic(logDir string) { paniclog.RedirectStderr(f) } +func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) { + fb, err := d.parseFb(url) + if err != nil { + return nil, err + } + fetcher := fb.Build() + d.setupFetcher(fetcher) + return fetcher, nil +} + func initTask(task *Task) { task.timer = util.NewTimer(task.Progress.Used) task.lock = &sync.Mutex{} diff --git a/pkg/download/downloader_test.go b/pkg/download/downloader_test.go index 8761b3a1..39a2e50d 100644 --- a/pkg/download/downloader_test.go +++ b/pkg/download/downloader_test.go @@ -27,7 +27,6 @@ func TestDownloader_Resolve(t *testing.T) { t.Fatal(err) } want := &base.Resource{ - Name: test.BuildName, Size: test.BuildSize, Range: true, Files: []*base.FileInfo{ @@ -85,6 +84,51 @@ func TestDownloader_Create(t *testing.T) { } } +func TestDownloader_CreateRename(t *testing.T) { + listener := test.StartTestFileServer() + defer listener.Close() + + downloader := NewDownloader(nil) + if err := downloader.Setup(); err != nil { + t.Fatal(err) + } + defer downloader.Clear() + + req := &base.Request{ + URL: "http://" + listener.Addr().String() + "/" + test.BuildName, + } + var wg sync.WaitGroup + wg.Add(2) + downloader.Listener(func(event *Event) { + if event.Key == EventKeyDone { + wg.Done() + } + }) + for i := 0; i < 2; i++ { + _, err := downloader.CreateDirect(req, &base.Options{ + Path: test.Dir, + Name: test.DownloadName, + Extra: http.OptsExtra{ + Connections: 4, + }, + }) + if err != nil { + t.Fatal(err) + } + } + wg.Wait() + + want := test.FileMd5(test.BuildFile) + got := test.FileMd5(test.DownloadFile) + if want != got { + t.Errorf("Downloader_CreateRename() got = %v, want %v", got, want) + } + got = test.FileMd5(test.DownloadRenameFile) + if want != got { + t.Errorf("Downloader_CreateRename() got = %v, want %v", got, want) + } +} + func TestDownloader_StoreAndRestore(t *testing.T) { listener := test.StartTestSlowFileServer(time.Millisecond * 2000) defer listener.Close() diff --git a/pkg/download/engine/engine.go b/pkg/download/engine/engine.go new file mode 100644 index 00000000..3bd6ae80 --- /dev/null +++ b/pkg/download/engine/engine.go @@ -0,0 +1,125 @@ +package engine + +import ( + _ "embed" + "errors" + "fmt" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/file" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr" + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/eventloop" + "github.com/dop251/goja_nodejs/process" + "github.com/dop251/goja_nodejs/url" +) + +//go:embed polyfill/out/index.js +var polyfillScript string + +type Engine struct { + loop *eventloop.EventLoop + + Runtime *goja.Runtime +} + +// RunString executes the script and returns the go type value +// if script result is promise, it will be resolved +func (e *Engine) RunString(script string) (value any, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + var result goja.Value + e.loop.Run(func(runtime *goja.Runtime) { + result, err = runtime.RunString(script) + }) + if err != nil { + return + } + return resolveResult(result) +} + +// CallFunction calls the function and returns the go type value +// if function result is promise, it will be resolved +func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + var result goja.Value + e.loop.Run(func(runtime *goja.Runtime) { + if args == nil { + result, err = fn(nil) + } else { + var jsArgs []goja.Value + for _, arg := range args { + jsArgs = append(jsArgs, runtime.ToValue(arg)) + } + result, err = fn(nil, jsArgs...) + } + }) + if err != nil { + return nil, err + } + return resolveResult(result) +} + +func (e *Engine) Close() { + e.loop.Stop() +} + +func NewEngine() *Engine { + loop := eventloop.NewEventLoop() + engine := &Engine{ + loop: loop, + } + loop.Run(func(runtime *goja.Runtime) { + engine.Runtime = runtime + runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) + url.Enable(runtime) + process.Enable(runtime) + if err := file.Enable(runtime); err != nil { + return + } + if err := formdata.Enable(runtime); err != nil { + return + } + if err := xhr.Enable(runtime); err != nil { + return + } + if _, err := runtime.RunString(polyfillScript); err != nil { + return + } + // polyfill global + if err := runtime.Set("global", runtime.GlobalObject()); err != nil { + return + } + return + }) + return engine +} + +func Run(script string) (value any, err error) { + engine := NewEngine() + return engine.RunString(script) +} + +// if the value is Promise, it will be resolved and return the result. +func resolveResult(value goja.Value) (any, error) { + export := value.Export() + switch export.(type) { + case *goja.Promise: + p := export.(*goja.Promise) + switch p.State() { + case goja.PromiseStatePending: + return nil, nil + case goja.PromiseStateFulfilled: + return p.Result().Export(), nil + case goja.PromiseStateRejected: + return nil, errors.New(p.Result().String()) + } + } + return export, nil +} diff --git a/pkg/download/engine/engine_test.go b/pkg/download/engine/engine_test.go new file mode 100644 index 00000000..e8c6ffbf --- /dev/null +++ b/pkg/download/engine/engine_test.go @@ -0,0 +1,291 @@ +package engine + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "github.com/GopeedLab/gopeed/internal/test" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/file" + "github.com/dop251/goja" + "io" + "net" + "net/http" + "strconv" + "strings" + "testing" + "time" +) + +func TestPolyfill(t *testing.T) { + doTestPolyfill(t, "XMLHttpRequest") + doTestPolyfill(t, "Blob") + doTestPolyfill(t, "FormData") + doTestPolyfill(t, "fetch") +} + +func TestFetch(t *testing.T) { + server := startServer() + defer server.Close() + engine := NewEngine() + if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", server.Addr().String())); err != nil { + t.Fatal(err) + } + _, err := engine.RunString(` +async function testGet(){ + const resp = await fetch(host+'/get'); + return resp.status; +} + +async function testText(){ + const resp = await fetch(host+'/text',{ + method: 'POST', + body: 'test' + }); + return await resp.text(); +} + +async function testOctetStream(file){ + const resp = await fetch(host+'/octetStream',{ + method: 'POST', + body: file + }); + return await resp.text(); +} + +async function testFormData(file){ + const formData = new FormData(); + formData.append('name', 'test'); + formData.append('f', file); + const resp = await fetch(host+'/formData',{ + method: 'POST', + body: formData + }); + return await resp.json(); +} + +function testProgress(){ + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', host+'/get'); + const xhrUploadPromise = new Promise((resolve, reject) => { + xhr.upload.onprogress = function(e){ + if(e.loaded === e.total){ + resolve(); + } + } + }); + const xhrPromise = new Promise((resolve, reject) => { + xhr.onprogress = function(e){ + if(e.loaded === e.total){ + resolve(); + } + } + }); + Promise.all([xhrUploadPromise, xhrPromise]).then(() => { + resolve(); + }); + xhr.send(); + setTimeout(() => { + reject('timeout'); + }, 1000); + }); +} + +function testAbort(){ + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', host+'/timeout?duration=500'); + xhr.onabort = function() { + resolve(); + }; + xhr.send(); + setTimeout(() => { + xhr.abort(); + }, 200); + setTimeout(() => { + reject('timeout'); + }, 1000); + }); +} + +function testTimeout(){ + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + const t = 500; + xhr.open('GET', host+'/timeout?duration='+t); + xhr.timeout = t - 200; + xhr.onload = function() { + resolve(); + }; + xhr.ontimeout = function() { + reject('timeout'); + }; + xhr.send(); + }); +} +`) + if err != nil { + t.Fatal(err) + } + + result, err := callTestFun(engine, "testGet") + if err != nil { + t.Fatal(err) + } + if result != int64(200) { + t.Fatalf("testGet failed, want %d, got %d", 200, result) + } + + result, err = callTestFun(engine, "testText") + if err != nil { + t.Fatal(err) + } + if result != "test" { + t.Fatalf("testText failed, want %s, got %s", "test", result) + } + + func() { + jsFile, _, md5 := buildFile(t, engine.Runtime) + result, err = callTestFun(engine, "testOctetStream", jsFile) + if err != nil { + t.Fatal(err) + } + if result != md5 { + t.Fatalf("testOctetStream failed, want %s, got %s", md5, result) + } + }() + + func() { + jsFile, goFile, md5 := buildFile(t, engine.Runtime) + result, err = callTestFun(engine, "testFormData", jsFile) + if err != nil { + t.Fatal(err) + } + want := map[string]any{ + "name": "test", + "f": map[string]string{ + "filename": goFile.Name, + "md5": md5, + }, + } + if !test.JsonEqual(result, want) { + t.Fatalf("testFormData failed, want %v, got %v", want, result) + } + }() + + _, err = callTestFun(engine, "testProgress") + if err != nil { + t.Fatal("progress test failed") + } + + _, err = callTestFun(engine, "testAbort") + if err != nil { + t.Fatal("abort test failed") + } + + _, err = callTestFun(engine, "testTimeout") + if err == nil || err.Error() != "timeout" { + t.Fatalf("timeout test failed, want %s, got %s", "timeout", err) + } +} + +func doTestPolyfill(t *testing.T, module string) { + value, err := Run(fmt.Sprintf(` +!!globalThis['%s'] +`, module)) + if err != nil { + t.Fatal(err) + } + if !value.(bool) { + t.Fatalf("module %s not polyfilled", module) + } +} + +func startServer() net.Listener { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) + } + server := &http.Server{} + mux := http.NewServeMux() + mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + mux.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) { + buf, _ := io.ReadAll(r.Body) + w.WriteHeader(http.StatusOK) + w.Write(buf) + }) + mux.HandleFunc("/octetStream", func(w http.ResponseWriter, r *http.Request) { + md5 := calcMd5(r.Body) + w.WriteHeader(http.StatusOK) + w.Write([]byte(md5)) + }) + mux.HandleFunc("/formData", func(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(1024 * 1024 * 30) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + result := make(map[string]any) + for k, v := range r.MultipartForm.Value { + result[k] = v[0] + } + for k, v := range r.MultipartForm.File { + f, _ := v[0].Open() + result[k] = map[string]string{ + "filename": v[0].Filename, + "md5": calcMd5(f), + } + } + w.WriteHeader(http.StatusOK) + buf, _ := json.Marshal(result) + w.Write(buf) + }) + mux.HandleFunc("/timeout", func(w http.ResponseWriter, r *http.Request) { + duration := r.URL.Query().Get("duration") + t, _ := strconv.Atoi(duration) + time.Sleep(time.Duration(t) * time.Millisecond) + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + }) + server.Handler = mux + go server.Serve(listener) + return listener +} + +func buildFile(t *testing.T, runtime *goja.Runtime) (goja.Value, *file.File, string) { + jsFile, err := file.NewJsFile(runtime) + if err != nil { + t.Fatal(err) + } + f := jsFile.Export().(*file.File) + data := "test" + f.Reader = strings.NewReader(data) + f.Name = "test.txt" + f.Size = int64(len(data)) + return jsFile, f, calcMd5(strings.NewReader(data)) +} + +func callTestFun(engine *Engine, fun string, args ...any) (any, error) { + test, ok := goja.AssertFunction(engine.Runtime.Get(fun)) + if !ok { + return nil, errors.New("function not found:" + fun) + } + return engine.CallFunction(test, args...) +} + +func calcMd5(reader io.Reader) string { + // Open a new hash interface to write to + hash := md5.New() + + // Copy the file in the hash interface and check for any error + if _, err := io.Copy(hash, reader); err != nil { + return "" + } + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/pkg/download/engine/inject/file/module.go b/pkg/download/engine/inject/file/module.go new file mode 100644 index 00000000..b3335e3a --- /dev/null +++ b/pkg/download/engine/inject/file/module.go @@ -0,0 +1,32 @@ +package file + +import ( + "errors" + "github.com/dop251/goja" + "io" +) + +type File struct { + io.Reader `json:""` + io.Closer `json:""` + Name string `json:"name"` + Size int64 `json:"size"` +} + +func NewJsFile(runtime *goja.Runtime) (goja.Value, error) { + fileCtor, ok := goja.AssertConstructor(runtime.Get("File")) + if !ok { + return nil, errors.New("file is not defined") + } + return fileCtor(nil) +} + +func Enable(runtime *goja.Runtime) error { + file := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { + instance := &File{} + instanceValue := runtime.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + return instanceValue + }) + return runtime.Set("File", file) +} diff --git a/pkg/download/engine/inject/formdata/module.go b/pkg/download/engine/inject/formdata/module.go new file mode 100644 index 00000000..7c5388e7 --- /dev/null +++ b/pkg/download/engine/inject/formdata/module.go @@ -0,0 +1,68 @@ +package formdata + +import "github.com/dop251/goja" + +type FormData struct { + data map[string]any +} + +func (fd *FormData) Append(name string, value any) { + fd.data[name] = value +} + +func (fd *FormData) Delete(name string) { + delete(fd.data, name) +} + +func (fd *FormData) Entries() []any { + var entries []any + for k, v := range fd.data { + entries = append(entries, []any{k, v}) + } + return entries +} + +func (fd *FormData) Get(name string) any { + return fd.data[name] +} + +func (fd *FormData) GetAll(name string) []any { + return []any{fd.data[name]} +} + +func (fd *FormData) Has(name string) bool { + _, ok := fd.data[name] + return ok +} + +func (fd *FormData) Keys() []string { + var keys []string + for k := range fd.data { + keys = append(keys, k) + } + return keys +} + +func (fd *FormData) Set(name string, value any) { + fd.data[name] = value +} + +func (fd *FormData) Values() []any { + var values []any + for _, v := range fd.data { + values = append(values, v) + } + return values +} + +func Enable(runtime *goja.Runtime) error { + file := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { + instance := &FormData{ + data: make(map[string]any), + } + instanceValue := runtime.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + return instanceValue + }) + return runtime.Set("FormData", file) +} diff --git a/pkg/download/engine/inject/util.go b/pkg/download/engine/inject/util.go new file mode 100644 index 00000000..d00504be --- /dev/null +++ b/pkg/download/engine/inject/util.go @@ -0,0 +1,9 @@ +package inject + +import ( + "github.com/dop251/goja" +) + +func ThrowTypeError(vm *goja.Runtime, msg string) { + panic(vm.NewTypeError(msg)) +} diff --git a/pkg/download/engine/inject/xhr/module.go b/pkg/download/engine/inject/xhr/module.go new file mode 100644 index 00000000..f96f2775 --- /dev/null +++ b/pkg/download/engine/inject/xhr/module.go @@ -0,0 +1,417 @@ +package xhr + +import ( + "bytes" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/file" + "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" + "github.com/dop251/goja" + "io" + "mime/multipart" + "net" + "net/http" + "time" +) + +const ( + eventLoad = "load" + eventReadystatechange = "readystatechange" + eventProgress = "progress" + eventAbort = "abort" + eventError = "error" + eventTimeout = "timeout" +) + +type ProgressEvent struct { + Type string `json:"type"` + LengthComputable bool `json:"lengthComputable"` + Loaded int64 `json:"loaded"` + Total int64 `json:"total"` +} + +type EventProp struct { + eventListeners map[string]func(event *ProgressEvent) + Onload func(event *ProgressEvent) `json:"onload"` + Onprogress func(event *ProgressEvent) `json:"onprogress"` + Onabort func(event *ProgressEvent) `json:"onabort"` + Onerror func(event *ProgressEvent) `json:"onerror"` + Ontimeout func(event *ProgressEvent) `json:"ontimeout"` +} + +func (ep *EventProp) AddEventListener(event string, cb func(event *ProgressEvent)) { + ep.eventListeners[event] = cb +} + +func (ep *EventProp) RemoveEventListener(event string) { + delete(ep.eventListeners, event) +} + +func (ep *EventProp) callOnload() { + event := &ProgressEvent{ + Type: eventLoad, + LengthComputable: false, + } + if ep.Onload != nil { + ep.Onload(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callOnprogress(loaded, total int64) { + event := &ProgressEvent{ + Type: eventProgress, + LengthComputable: true, + Loaded: loaded, + Total: total, + } + if ep.Onprogress != nil { + ep.Onprogress(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callOnabort() { + event := &ProgressEvent{ + Type: eventAbort, + LengthComputable: false, + } + if ep.Onabort != nil { + ep.Onabort(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callOnerror() { + event := &ProgressEvent{ + Type: eventError, + LengthComputable: false, + } + if ep.Onerror != nil { + ep.Onerror(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callOntimeout() { + event := &ProgressEvent{ + Type: eventTimeout, + LengthComputable: false, + } + if ep.Ontimeout != nil { + ep.Ontimeout(event) + } + ep.callEventListener(event) +} + +func (ep *EventProp) callEventListener(event *ProgressEvent) { + if cb, ok := ep.eventListeners[event.Type]; ok { + cb(event) + } +} + +type XMLHttpRequestUpload struct { + *EventProp +} + +type XMLHttpRequest struct { + method string + url string + requestHeaders map[string]string + responseHeaders map[string]string + aborted bool + + Upload *XMLHttpRequestUpload `json:"upload"` + Timeout int `json:"timeout"` + ReadyState int `json:"readyState"` + Status int `json:"status"` + StatusText string `json:"statusText"` + Response string `json:"response"` + ResponseText string `json:"responseText"` + *EventProp + Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"` +} + +func (xhr *XMLHttpRequest) Open(method, url string) { + xhr.method = method + xhr.url = url + xhr.requestHeaders = make(map[string]string) + xhr.responseHeaders = make(map[string]string) + xhr.doReadystatechange(1) +} + +func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) { + xhr.requestHeaders[key] = value +} + +func (xhr *XMLHttpRequest) Send(data goja.Value) { + var req *http.Request + var err error + d := xhr.parseData(data) + var ( + contentType string + contentLength int64 + ) + if d == nil || xhr.method == "GET" || xhr.method == "HEAD" { + req, err = http.NewRequest(xhr.method, xhr.url, nil) + } else { + switch d.(type) { + case string: + req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(d.(string))) + contentType = "text/plain;charset=UTF-8" + contentLength = int64(len(d.(string))) + case *file.File: + req, err = http.NewRequest(xhr.method, xhr.url, d.(*file.File).Reader) + contentType = "application/octet-stream" + contentLength = d.(*file.File).Size + case *formdata.FormData: + pr, pw := io.Pipe() + mw := NewMultipart(pw) + for _, e := range d.(*formdata.FormData).Entries() { + arr := e.([]any) + k := arr[0].(string) + v := arr[1] + switch v.(type) { + case string: + mw.WriteField(k, v.(string)) + case *file.File: + mw.WriteFile(k, v.(*file.File)) + } + } + go func() { + defer pw.Close() + defer mw.Close() + mw.Send() + }() + req, err = http.NewRequest(xhr.method, xhr.url, pr) + contentType = mw.FormDataContentType() + contentLength = mw.Size() + } + } + if err != nil { + xhr.callOnerror() + return + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + if contentLength > 0 { + req.ContentLength = contentLength + } + for k, v := range xhr.requestHeaders { + req.Header.Set(k, v) + } + client := &http.Client{ + Timeout: time.Duration(xhr.Timeout) * time.Millisecond, + } + resp, err := client.Do(req) + if err != nil { + // handle timeout error + if err, ok := err.(net.Error); ok && err.Timeout() { + if xhr.Timeout > 0 { + xhr.Upload.callOntimeout() + xhr.callOntimeout() + } + return + } + xhr.Upload.callOnerror() + xhr.callOnerror() + return + } + defer resp.Body.Close() + xhr.Upload.callOnprogress(contentLength, contentLength) + if !xhr.aborted { + xhr.Upload.callOnload() + } + for k, v := range resp.Header { + xhr.responseHeaders[k] = v[0] + } + xhr.Status = resp.StatusCode + xhr.StatusText = resp.Status + xhr.doReadystatechange(2) + buf, err := io.ReadAll(resp.Body) + if err != nil { + xhr.callOnerror() + return + } + xhr.doReadystatechange(3) + xhr.Response = string(buf) + xhr.ResponseText = xhr.Response + xhr.doReadystatechange(4) + respBodyLen := int64(len(buf)) + xhr.callOnprogress(respBodyLen, respBodyLen) + if !xhr.aborted { + xhr.callOnload() + } + return +} + +func (xhr *XMLHttpRequest) Abort() { + xhr.doReadystatechange(0) + xhr.aborted = true + xhr.Upload.callOnabort() + xhr.callOnabort() +} + +func (xhr *XMLHttpRequest) GetResponseHeader(key string) string { + return xhr.responseHeaders[key] +} + +func (xhr *XMLHttpRequest) GetAllResponseHeaders() string { + var buf bytes.Buffer + for k, v := range xhr.responseHeaders { + buf.WriteString(k) + buf.WriteString(": ") + buf.WriteString(v) + buf.WriteString("\r\n") + } + return buf.String() +} + +func (xhr *XMLHttpRequest) callOnreadystatechange() { + event := &ProgressEvent{ + Type: eventReadystatechange, + LengthComputable: false, + } + if xhr.Onreadystatechange != nil { + xhr.Onreadystatechange(event) + } + xhr.callEventListener(event) +} + +func (xhr *XMLHttpRequest) doReadystatechange(state int) { + if xhr.aborted { + return + } + xhr.ReadyState = state + xhr.callOnreadystatechange() +} + +// parse js data to go struct +func (xhr *XMLHttpRequest) parseData(data goja.Value) any { + // check if data is null or undefined + if data == nil || goja.IsNull(data) || goja.IsUndefined(data) || goja.IsNaN(data) { + return nil + } + // check if data is File + f, ok := data.Export().(*file.File) + if ok { + return f + } + // check if data is FormData + fd, ok := data.Export().(*formdata.FormData) + if ok { + return fd + } + // otherwise, return data as string + return data.String() +} + +func Enable(runtime *goja.Runtime) error { + progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { + if len(call.Arguments) < 1 { + inject.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.") + } + instance := &ProgressEvent{ + Type: call.Argument(0).String(), + } + instanceValue := runtime.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + return instanceValue + }) + xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { + instance := &XMLHttpRequest{ + Upload: &XMLHttpRequestUpload{ + EventProp: &EventProp{ + eventListeners: make(map[string]func(event *ProgressEvent)), + }, + }, + EventProp: &EventProp{ + eventListeners: make(map[string]func(event *ProgressEvent)), + }, + } + instanceValue := runtime.ToValue(instance).(*goja.Object) + instanceValue.SetPrototype(call.This.Prototype()) + return instanceValue + }) + if err := runtime.Set("ProgressEvent", progressEvent); err != nil { + return err + } + if err := runtime.Set("XMLHttpRequest", xhr); err != nil { + return err + } + return nil +} + +// Wrap multipart.Writer and stat content length +type multipartWrapper struct { + statBuffer *bytes.Buffer + statWriter *multipart.Writer + writer *multipart.Writer + fields map[string]any +} + +func NewMultipart(w io.Writer) *multipartWrapper { + var buf bytes.Buffer + return &multipartWrapper{ + statBuffer: &buf, + statWriter: multipart.NewWriter(&buf), + writer: multipart.NewWriter(w), + fields: make(map[string]any), + } +} + +func (w *multipartWrapper) WriteField(fieldname string, value string) error { + w.fields[fieldname] = value + return w.statWriter.WriteField(fieldname, value) +} + +func (w *multipartWrapper) WriteFile(fieldname string, file *file.File) error { + w.fields[fieldname] = file + _, err := w.statWriter.CreateFormFile(fieldname, file.Name) + if err != nil { + return err + } + return nil +} + +func (w *multipartWrapper) Size() int64 { + w.statWriter.Close() + size := int64(w.statBuffer.Len()) + for _, v := range w.fields { + switch v.(type) { + case *file.File: + f := v.(*file.File) + size += f.Size + } + } + return size +} + +func (w *multipartWrapper) Send() error { + for k, v := range w.fields { + switch v.(type) { + case string: + if err := w.writer.WriteField(k, v.(string)); err != nil { + return err + } + case *file.File: + f := v.(*file.File) + fw, err := w.writer.CreateFormFile(k, f.Name) + if err != nil { + return err + } + if _, err = io.Copy(fw, f); err != nil { + return err + } + } + } + return nil +} + +func (w *multipartWrapper) FormDataContentType() string { + return w.writer.FormDataContentType() +} + +func (w *multipartWrapper) Close() error { + return w.writer.Close() +} diff --git a/pkg/download/engine/polyfill/out/index.js b/pkg/download/engine/polyfill/out/index.js new file mode 100644 index 00000000..9e18f3c9 --- /dev/null +++ b/pkg/download/engine/polyfill/out/index.js @@ -0,0 +1 @@ +(()=>{var t={725:function(t,e,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(t){"use strict";var e=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(t,e){return(e=document.createElement("a")).href=t,e},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=e&&e.prototype.append&&e.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(t){}function c(t){return t.map((function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var r=new Uint8Array(t.byteLength);r.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=r.buffer}return e}return t}))}function h(t,r){r=r||{};var o=new e;return c(t).forEach((function(t){o.append(t)})),r.type?o.getBlob(r.type):o.getBlob()}function p(t,e){return new o(c(t),e||{})}i.Blob&&(h.prototype=Blob.prototype,p.prototype=Blob.prototype);var y="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(t){for(var e=0,r=t.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);e=55296&&u<=56319){if(e=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+e/t.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},d="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(t){for(var e=t.length,r=[],o=0;o239?4:u>223?3:u>191?2:1;if(o+l<=e)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=t[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=t[o+1],i=t[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=t[o+1],i=t[o+2],a=t[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",p=0;p>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(e[f],e[l],e[c],e[h])}return r.join("")}var s=Object.create||function(t){function e(){}return e.prototype=t,new e};function u(t){return Object.prototype.toString.call(t).slice(8,-1)}function f(t,e){return"object"==typeof t&&Object.prototype.isPrototypeOf.call(t.prototype,e)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(t){return e=l,r=u(t),-1!==e.indexOf(r)||f(i.ArrayBuffer,t);var e,r}function h(t,r){r=null==r?{}:r;for(var o=0,n=(t=t?t.slice():[]).length;o=e.size&&r.close()}))}})}}catch(t){try{new ReadableStream({}),w=function(t){var e=0;return new ReadableStream({pull:function(r){return t.slice(e,e+524288).arrayBuffer().then((function(o){e+=o.byteLength;var n=new Uint8Array(o);r.enqueue(n),e==t.size&&r.close()}))}})}}catch(t){try{new Response("").body.getReader().read(),w=function(){return new Response(this).body}}catch(t){w=function(){throw new Error("Include https://github.com/MattiasBuelens/web-streams-polyfill")}}}}function v(t){return new Promise((function(e,r){t.onload=t.onerror=function(o){t.onload=t.onerror=null,"load"===o.type?e(t.result||t):r(new Error("Failed to read the blob/file"))}}))}m.arrayBuffer||(m.arrayBuffer=function(){var t=new FileReader;return t.readAsArrayBuffer(this),v(t)}),m.text||(m.text=function(){var t=new FileReader;return t.readAsText(this),v(t)}),m.stream||(m.stream=w)},void 0===(n=o.apply(e,[e]))||(t.exports=n)}},e={};function r(o){var n=e[o];if(void 0!==n)return n.exports;var i=e[o]={exports:{}};return t[o].call(i.exports,i,i.exports,r),i.exports}r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),(()=>{"use strict";var t=r(725);globalThis.Blob=t.Blob,globalThis.crypto={getRandomValues(t){for(let e=0,r=t.length;e(t^this.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)))}};var e="undefined"!=typeof globalThis&&globalThis||"undefined"!=typeof self&&self||void 0!==e&&e,o={searchParams:"URLSearchParams"in e,iterable:"Symbol"in e&&"iterator"in Symbol,blob:"FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in e,arrayBuffer:"ArrayBuffer"in e};if(o.arrayBuffer)var n=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],i=ArrayBuffer.isView||function(t){return t&&n.indexOf(Object.prototype.toString.call(t))>-1};function a(t){if("string"!=typeof t&&(t=String(t)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(t)||""===t)throw new TypeError('Invalid character in header field name: "'+t+'"');return t.toLowerCase()}function s(t){return"string"!=typeof t&&(t=String(t)),t}function u(t){var e={next:function(){var e=t.shift();return{done:void 0===e,value:e}}};return o.iterable&&(e[Symbol.iterator]=function(){return e}),e}function f(t){this.map={},t instanceof f?t.forEach((function(t,e){this.append(e,t)}),this):Array.isArray(t)?t.forEach((function(t){this.append(t[0],t[1])}),this):t&&Object.getOwnPropertyNames(t).forEach((function(e){this.append(e,t[e])}),this)}function l(t){if(t.bodyUsed)return Promise.reject(new TypeError("Already read"));t.bodyUsed=!0}function c(t){return new Promise((function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}}))}function h(t){var e=new FileReader,r=c(e);return e.readAsArrayBuffer(t),r}function p(t){if(t.slice)return t.slice(0);var e=new Uint8Array(t.byteLength);return e.set(new Uint8Array(t)),e.buffer}function y(){return this.bodyUsed=!1,this._initBody=function(t){var e;this.bodyUsed=this.bodyUsed,this._bodyInit=t,t?"string"==typeof t?this._bodyText=t:o.blob&&Blob.prototype.isPrototypeOf(t)?this._bodyBlob=t:o.formData&&FormData.prototype.isPrototypeOf(t)?this._bodyFormData=t:o.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)?this._bodyText=t.toString():o.arrayBuffer&&o.blob&&(e=t)&&DataView.prototype.isPrototypeOf(e)?(this._bodyArrayBuffer=p(t.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):o.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(t)||i(t))?this._bodyArrayBuffer=p(t):this._bodyText=t=Object.prototype.toString.call(t):this._bodyText="",this.headers.get("content-type")||("string"==typeof t?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):o.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},o.blob&&(this.blob=function(){var t=l(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?l(this)||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer)):this.blob().then(h)}),this.text=function(){var t,e,r,o=l(this);if(o)return o;if(this._bodyBlob)return t=this._bodyBlob,r=c(e=new FileReader),e.readAsText(t),r;if(this._bodyArrayBuffer)return Promise.resolve(function(t){for(var e=new Uint8Array(t),r=new Array(e.length),o=0;o-1?o:r),this.mode=e.mode||this.mode||null,this.signal=e.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&n)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(n),!("GET"!==this.method&&"HEAD"!==this.method||"no-store"!==e.cache&&"no-cache"!==e.cache)){var i=/([?&])_=[^&]*/;i.test(this.url)?this.url=this.url.replace(i,"$1_="+(new Date).getTime()):this.url+=(/\?/.test(this.url)?"&":"?")+"_="+(new Date).getTime()}}function w(t){var e=new FormData;return t.trim().split("&").forEach((function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}})),e}function m(t,e){if(!(this instanceof m))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');e||(e={}),this.type="default",this.status=void 0===e.status?200:e.status,this.ok=this.status>=200&&this.status<300,this.statusText=void 0===e.statusText?"":""+e.statusText,this.headers=new f(e.headers),this.url=e.url||"",this._initBody(t)}b.prototype.clone=function(){return new b(this,{body:this._bodyInit})},y.call(b.prototype),y.call(m.prototype),m.prototype.clone=function(){return new m(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new f(this.headers),url:this.url})},m.error=function(){var t=new m(null,{status:0,statusText:""});return t.type="error",t};var v=[301,302,303,307,308];m.redirect=function(t,e){if(-1===v.indexOf(e))throw new RangeError("Invalid status code");return new m(null,{status:e,headers:{location:t}})};var g=e.DOMException;try{new g}catch(t){(g=function(t,e){this.message=t,this.name=e;var r=Error(t);this.stack=r.stack}).prototype=Object.create(Error.prototype),g.prototype.constructor=g}function A(t,r){return new Promise((function(n,i){var a=new b(t,r);if(a.signal&&a.signal.aborted)return i(new g("Aborted","AbortError"));var u=new XMLHttpRequest;function l(){u.abort()}u.onload=function(){var t,e,r={status:u.status,statusText:u.statusText,headers:(t=u.getAllResponseHeaders()||"",e=new f,t.replace(/\r?\n[\t ]+/g," ").split("\r").map((function(t){return 0===t.indexOf("\n")?t.substr(1,t.length):t})).forEach((function(t){var r=t.split(":"),o=r.shift().trim();if(o){var n=r.join(":").trim();e.append(o,n)}})),e)};r.url="responseURL"in u?u.responseURL:r.headers.get("X-Request-URL");var o="response"in u?u.response:u.responseText;setTimeout((function(){n(new m(o,r))}),0)},u.onerror=function(){setTimeout((function(){i(new TypeError("Network request failed"))}),0)},u.ontimeout=function(){setTimeout((function(){i(new TypeError("Network request failed"))}),0)},u.onabort=function(){setTimeout((function(){i(new g("Aborted","AbortError"))}),0)},u.open(a.method,function(t){try{return""===t&&e.location.href?e.location.href:t}catch(e){return t}}(a.url),!0),"include"===a.credentials?u.withCredentials=!0:"omit"===a.credentials&&(u.withCredentials=!1),"responseType"in u&&(o.blob?u.responseType="blob":o.arrayBuffer&&a.headers.get("Content-Type")&&-1!==a.headers.get("Content-Type").indexOf("application/octet-stream")&&(u.responseType="arraybuffer")),!r||"object"!=typeof r.headers||r.headers instanceof f?a.headers.forEach((function(t,e){u.setRequestHeader(e,t)})):Object.getOwnPropertyNames(r.headers).forEach((function(t){u.setRequestHeader(t,s(r.headers[t]))})),a.signal&&(a.signal.addEventListener("abort",l),u.onreadystatechange=function(){4===u.readyState&&a.signal.removeEventListener("abort",l)}),u.send(void 0===a._bodyInit?null:a._bodyInit)}))}A.polyfill=!0,e.fetch||(e.fetch=A,e.Headers=f,e.Request=b,e.Response=m)})()})(); \ No newline at end of file diff --git a/pkg/download/engine/polyfill/package-lock.json b/pkg/download/engine/polyfill/package-lock.json new file mode 100644 index 00000000..7bf817b5 --- /dev/null +++ b/pkg/download/engine/polyfill/package-lock.json @@ -0,0 +1,2236 @@ +{ + "name": "polyfill", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "polyfill", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "blob-polyfill": "^7.0.20220408", + "whatwg-fetch": "^3.6.2" + }, + "devDependencies": { + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.37.0.tgz", + "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.15.11", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz", + "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/info/-/info-2.0.1.tgz", + "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/serve/-/serve-2.0.1.tgz", + "integrity": "sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/blob-polyfill": { + "version": "7.0.20220408", + "resolved": "https://registry.npmmirror.com/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz", + "integrity": "sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ==" + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001478", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz", + "integrity": "sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw==", + "dev": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.359", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz", + "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.16.9", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.16.9.tgz", + "integrity": "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.78.0", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.78.0.tgz", + "integrity": "sha512-gT5DP72KInmE/3azEaQrISjTvLYlSM0j1Ezhht/KLVkrqtv10JoP/RXhwmX/frrutOPuSq3o5Vq0ehR/4Vmd1g==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-5.0.1.tgz", + "integrity": "sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.0.1", + "@webpack-cli/info": "^2.0.1", + "@webpack-cli/serve": "^2.0.1", + "colorette": "^2.0.14", + "commander": "^9.4.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + } + }, + "dependencies": { + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@types/eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.37.0.tgz", + "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmmirror.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/node": { + "version": "18.15.11", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.15.11.tgz", + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz", + "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/info/-/info-2.0.1.tgz", + "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", + "dev": true, + "requires": {} + }, + "@webpack-cli/serve": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@webpack-cli/serve/-/serve-2.0.1.tgz", + "integrity": "sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw==", + "dev": true, + "requires": {} + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "blob-polyfill": { + "version": "7.0.20220408", + "resolved": "https://registry.npmmirror.com/blob-polyfill/-/blob-polyfill-7.0.20220408.tgz", + "integrity": "sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ==" + }, + "browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001478", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz", + "integrity": "sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "electron-to-chromium": { + "version": "1.4.359", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.359.tgz", + "integrity": "sha512-OoVcngKCIuNXtZnsYoqlCvr0Cf3NIPzDIgwUfI9bdTFjXCrr79lI0kwQstLPZ7WhCezLlGksZk/BFAzoXC7GDw==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true + }, + "es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, + "is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "terser": { + "version": "5.16.9", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.16.9.tgz", + "integrity": "sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmmirror.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + } + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "5.78.0", + "resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.78.0.tgz", + "integrity": "sha512-gT5DP72KInmE/3azEaQrISjTvLYlSM0j1Ezhht/KLVkrqtv10JoP/RXhwmX/frrutOPuSq3o5Vq0ehR/4Vmd1g==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + } + }, + "webpack-cli": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/webpack-cli/-/webpack-cli-5.0.1.tgz", + "integrity": "sha512-S3KVAyfwUqr0Mo/ur3NzIp6jnerNpo7GUO6so51mxLi1spqsA17YcMXy0WOIJtBSnj748lthxC6XLbNKh/ZC+A==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.0.1", + "@webpack-cli/info": "^2.0.1", + "@webpack-cli/serve": "^2.0.1", + "colorette": "^2.0.14", + "commander": "^9.4.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmmirror.com/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + } + } +} diff --git a/pkg/download/engine/polyfill/package.json b/pkg/download/engine/polyfill/package.json new file mode 100644 index 00000000..b123c732 --- /dev/null +++ b/pkg/download/engine/polyfill/package.json @@ -0,0 +1,22 @@ +{ + "name": "polyfill", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "webpack --mode production --watch", + "build": "webpack --mode production" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1" + }, + "dependencies": { + "blob-polyfill": "^7.0.20220408", + "whatwg-fetch": "^3.6.2" + } +} diff --git a/pkg/download/engine/polyfill/src/blob/index.js b/pkg/download/engine/polyfill/src/blob/index.js new file mode 100644 index 00000000..1d854e16 --- /dev/null +++ b/pkg/download/engine/polyfill/src/blob/index.js @@ -0,0 +1,3 @@ +import { Blob } from "blob-polyfill"; + +globalThis.Blob = Blob; \ No newline at end of file diff --git a/pkg/download/engine/polyfill/src/crypto/index.js b/pkg/download/engine/polyfill/src/crypto/index.js new file mode 100644 index 00000000..fed0c8f0 --- /dev/null +++ b/pkg/download/engine/polyfill/src/crypto/index.js @@ -0,0 +1,13 @@ +globalThis.crypto = { + getRandomValues(arr) { + for (let i = 0, len = arr.length; i < len; i++) { + arr[i] = Math.floor(Math.random() * 256); + } + return arr; + }, + randomUUID() { + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => { + return (c ^ (this.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) + }) + } +} \ No newline at end of file diff --git a/pkg/download/engine/polyfill/src/fetch/index.js b/pkg/download/engine/polyfill/src/fetch/index.js new file mode 100644 index 00000000..35dae088 --- /dev/null +++ b/pkg/download/engine/polyfill/src/fetch/index.js @@ -0,0 +1 @@ +import 'whatwg-fetch' \ No newline at end of file diff --git a/pkg/download/engine/polyfill/src/index.js b/pkg/download/engine/polyfill/src/index.js new file mode 100644 index 00000000..abf0c1bd --- /dev/null +++ b/pkg/download/engine/polyfill/src/index.js @@ -0,0 +1,3 @@ +import "./blob/index.js" +import "./crypto/index.js" +import "./fetch/index.js" \ No newline at end of file diff --git a/pkg/download/engine/polyfill/webpack.config.js b/pkg/download/engine/polyfill/webpack.config.js new file mode 100644 index 00000000..499e1a22 --- /dev/null +++ b/pkg/download/engine/polyfill/webpack.config.js @@ -0,0 +1,12 @@ +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = fileURLToPath(import.meta.url); + +export default { + entry: "./src/index.js", + output: { + filename: "index.js", + path: path.resolve(__dirname, "../out"), + }, +}; diff --git a/pkg/download/event.go b/pkg/download/event.go index 87a4361d..fc410944 100644 --- a/pkg/download/event.go +++ b/pkg/download/event.go @@ -5,7 +5,6 @@ type EventKey string const ( EventKeyStart = "start" EventKeyPause = "pause" - EventKeyContinue = "continue" EventKeyProgress = "progress" EventKeyError = "error" EventKeyDelete = "delete" diff --git a/pkg/download/extension.go b/pkg/download/extension.go new file mode 100644 index 00000000..4a3eef29 --- /dev/null +++ b/pkg/download/extension.go @@ -0,0 +1,549 @@ +package download + +import ( + "encoding/json" + "fmt" + "github.com/GopeedLab/gopeed/internal/logger" + "github.com/GopeedLab/gopeed/pkg/base" + "github.com/GopeedLab/gopeed/pkg/download/engine" + "github.com/GopeedLab/gopeed/pkg/util" + "github.com/dop251/goja" + "github.com/go-git/go-git/v5" + "io" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +var ( + gitSuffix = ".git" + + tempExtensionsDir = ".extensions" + extensionsDir = "extensions" + extensionIgnoreDirs = []string{gitSuffix, "node_modules"} + + ErrExtensionNoManifest = fmt.Errorf("manifest.json not found") + ErrExtensionNotFound = fmt.Errorf("extension not found") +) + +type ActivationEvent string + +const ( + EventOnResolve ActivationEvent = "onResolve" + //EventOnError ActivationEvent = "onError" + //EventOnDone ActivationEvent = "onDone" +) + +func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) { + return d.fetchExtensionByGit(url, d.InstallExtensionByFolder) +} + +func (d *Downloader) InstallExtensionByFolder(path string, devMode bool) (*Extension, error) { + ext, err := d.parseExtensionByPath(path) + if err != nil { + return nil, err + } + + // if dev mode, don't copy the extension to the extensions' directory + if devMode { + ext.DevMode = true + ext.DevPath, _ = filepath.Abs(path) + } else { + if err = util.CopyDir(path, d.ExtensionPath(ext), extensionIgnoreDirs...); err != nil { + return nil, err + } + } + + // if extension is not installed, add it to the list, otherwise update it + installedExt := d.getExtension(ext.Identity) + if installedExt == nil { + ext.CreatedAt = time.Now() + ext.UpdatedAt = ext.CreatedAt + installedExt = ext + d.extensions = append(d.extensions, installedExt) + } else { + installedExt.update(ext) + } + if err = d.storage.Put(bucketExtension, installedExt.Identity, installedExt); err != nil { + return nil, err + } + return installedExt, nil +} + +// UpgradeCheckExtension Check if there is a new version for the extension. +func (d *Downloader) UpgradeCheckExtension(identity string) (newVersion string, err error) { + ext, err := d.GetExtension(identity) + if err != nil { + return + } + installUrl := ext.buildInstallUrl() + if installUrl == "" { + return + } + _, err = d.fetchExtensionByGit(installUrl, func(tempExtPath string, devMode bool) (*Extension, error) { + tempExt, err := d.parseExtensionByPath(tempExtPath) + if err != nil { + return nil, err + } + if tempExt.Version != ext.Version { + newVersion = tempExt.Version + } + return tempExt, nil + }) + return +} + +func (d *Downloader) UpgradeExtension(identity string) error { + ext, err := d.GetExtension(identity) + if err != nil { + return err + } + installUrl := ext.buildInstallUrl() + if installUrl == "" { + return nil + } + if _, err := d.InstallExtensionByGit(installUrl); err != nil { + return err + } + return nil +} + +func (d *Downloader) UpdateExtensionSettings(identity string, settings map[string]any) error { + ext, err := d.GetExtension(identity) + if err != nil { + return err + } + for _, setting := range ext.Settings { + if value, ok := settings[setting.Name]; ok { + setting.Value = tryParse(value, setting.Type) + } + } + return d.storage.Put(bucketExtension, ext.Identity, ext) +} + +func (d *Downloader) SwitchExtension(identity string, status bool) error { + ext, err := d.GetExtension(identity) + if err != nil { + return err + } + ext.Disabled = !status + return d.storage.Put(bucketExtension, ext.Identity, ext) +} + +func (d *Downloader) DeleteExtension(identity string) error { + ext, err := d.GetExtension(identity) + if err != nil { + return err + } + // remove from disk + if !ext.DevMode { + if err := os.RemoveAll(d.ExtensionPath(ext)); err != nil { + return err + } + } + // remove from extensions + for i, e := range d.extensions { + if e.Identity == identity { + d.extensions = append(d.extensions[:i], d.extensions[i+1:]...) + break + } + } + return d.storage.Delete(bucketExtension, identity) +} + +func (d *Downloader) GetExtensions() []*Extension { + return d.extensions +} + +func (d *Downloader) GetExtension(identity string) (*Extension, error) { + extension := d.getExtension(identity) + if extension == nil { + return nil, ErrExtensionNotFound + } + return extension, nil +} + +func (d *Downloader) getExtension(identity string) *Extension { + for _, ext := range d.extensions { + if ext.Identity == identity { + return ext + } + } + return nil +} + +func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath string, devMode bool) (*Extension, error)) (*Extension, error) { + if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { + url = "https://" + url + } + + // resolve project path + parentPath, projectPath := path.Split(url) + // resolve project name and sub path + pathArr := strings.SplitN(projectPath, "#", 2) + projectPath = strings.TrimSuffix(pathArr[0], gitSuffix) + subPath := "" + if len(pathArr) > 1 { + subPath = pathArr[1] + } + + tempExtDir := filepath.Join(d.cfg.StorageDir, tempExtensionsDir, projectPath) + if err := util.RmAndMkDirAll(tempExtDir); err != nil { + return nil, err + } + // clone project to extension temp dir + gitUrl := parentPath + projectPath + gitSuffix + if _, err := git.PlainClone(tempExtDir, false, &git.CloneOptions{ + URL: gitUrl, + Depth: 1, + }); err != nil { + return nil, err + } + defer os.RemoveAll(tempExtDir) + + return handler(filepath.Join(tempExtDir, subPath), false) +} + +func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) { + // resolve extension manifest + manifestTempPath := filepath.Join(path, "manifest.json") + if _, err := os.Stat(manifestTempPath); os.IsNotExist(err) { + return nil, ErrExtensionNoManifest + } + file, err := os.ReadFile(manifestTempPath) + if err != nil { + return nil, err + } + var ext Extension + if err = json.Unmarshal(file, &ext); err != nil { + return nil, err + } + if err = ext.validate(); err != nil { + return nil, err + } + ext.Identity = ext.buildIdentity() + return &ext, nil +} + +func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) { + // 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) { + gopeed.Logger = newInstanceLogger(ext, d.ExtensionLogger) + scriptFilePath := filepath.Join(d.ExtensionPath(ext), script.Entry) + if _, err = os.Stat(scriptFilePath); os.IsNotExist(err) { + continue + } + func() { + var scriptFile *os.File + scriptFile, err = os.Open(scriptFilePath) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] open script file failed", ext.buildIdentity()) + return + } + defer scriptFile.Close() + ctx.Settings = parseSettings(ext.Settings) + 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 + } + engine := engine.NewEngine() + defer engine.Close() + err = engine.Runtime.Set("gopeed", gopeed) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] engine inject failed", ext.buildIdentity()) + return + } + _, err = engine.RunString(string(scriptBuf)) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] run script failed", ext.buildIdentity()) + return + } + if fn, ok := gopeed.Events[EventOnResolve]; ok { + _, err = engine.CallFunction(fn, ctx) + if err != nil { + gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", EventOnResolve, ext.buildIdentity()) + 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 + } + }() + } + } + } + return +} + +func (d *Downloader) ExtensionPath(ext *Extension) string { + if ext.DevMode { + return ext.DevPath + } + return filepath.Join(d.cfg.StorageDir, extensionsDir, ext.Identity) +} + +type Extension struct { + // Identity is global unique for an extension, it's a combination of author and name + Identity string `json:"identity"` + Name string `json:"name"` + Author string `json:"author"` + Title string `json:"title"` + Description string `json:"description"` + Icon string `json:"icon"` + // Version semantic version string, like: 1.0.0 + Version string `json:"version"` + // Homepage homepage url + Homepage string `json:"homepage"` + // Repository git repository info + Repository *Repository `json:"repository"` + Scripts []*Script `json:"scripts"` + Settings []*Setting `json:"settings"` + // Disabled if true, this extension will be ignored + Disabled bool `json:"disabled"` + + DevMode bool `json:"devMode"` + // DevPath is the local path of extension source code + DevPath string `json:"devPath"` + + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (e *Extension) validate() error { + if e.Name == "" { + return fmt.Errorf("extension name is required") + } + if e.Title == "" { + return fmt.Errorf("extension title is required") + } + if e.Version == "" { + return fmt.Errorf("extension version is required") + } + return nil +} + +func (e *Extension) buildIdentity() string { + if e.Author == "" { + return e.Name + } + return e.Author + "@" + e.Name +} + +func (e *Extension) buildInstallUrl() string { + if e.Repository == nil || e.Repository.Url == "" { + return "" + } + repoUrl := e.Repository.Url + if e.Repository.Directory != "" { + if strings.HasSuffix(repoUrl, "/") { + repoUrl = repoUrl[:len(repoUrl)-1] + } + dir := e.Repository.Directory + if strings.HasPrefix(dir, "/") { + dir = dir[1:] + } + repoUrl = repoUrl + "#" + dir + } + return repoUrl +} + +func (e *Extension) update(newExt *Extension) error { + e.Title = newExt.Title + e.Description = newExt.Description + e.Icon = newExt.Icon + e.Version = newExt.Version + e.Homepage = newExt.Homepage + e.Repository = newExt.Repository + e.Scripts = newExt.Scripts + // don't override settings + //e.Settings = newExt.Settings + e.UpdatedAt = time.Now() + return nil +} + +type Repository struct { + Url string `json:"url"` + Directory string `json:"directory"` +} + +type Script struct { + // Event active event name + Event string `json:"event"` + // Matches match request url pattern list + Matches []string `json:"matches"` + // Entry js script file path + Entry string `json:"entry"` +} + +func (s *Script) match(event ActivationEvent, url string) bool { + if s.Event == "" { + return false + } + if s.Event != string(event) { + return false + } + if len(s.Matches) == 0 { + return false + } + for _, match := range s.Matches { + if util.Match(match, url) { + return true + } + } + return false +} + +type SettingType string + +const ( + SettingTypeString SettingType = "string" + SettingTypeNumber SettingType = "number" + SettingTypeBoolean SettingType = "boolean" +) + +type Setting struct { + Name string `json:"name"` + Title string `json:"title"` + Description string `json:"description"` + Required bool `json:"required"` + // setting type + Type SettingType `json:"type"` + // setting value + Value any `json:"value"` + //Multiple bool `json:"multiple"` + Options []*Option `json:"options"` +} + +type Option struct { + Label string `json:"label"` + Value any `json:"value"` +} + +// Instance inject to js context +type Instance struct { + Events InstanceEvents `json:"events"` + Logger *InstanceLogger `json:"logger"` +} + +type InstanceEvents map[ActivationEvent]goja.Callable + +func (h InstanceEvents) register(name ActivationEvent, fn goja.Callable) { + h[name] = fn +} + +func (h InstanceEvents) OnResolve(fn goja.Callable) { + h.register(EventOnResolve, fn) +} + +//func (h InstanceEvents) OnError(fn goja.Callable) { +// h.register(HookEventOnError, fn) +//} +// +//func (h InstanceEvents) OnDone(fn goja.Callable) { +// h.register(HookEventOnDone, fn) +//} + +type InstanceLogger struct { + identity string + devMode bool + logger *logger.Logger +} + +func (l *InstanceLogger) Debug(msg ...any) { + if l.devMode { + l.logger.Debug().Msg(l.append(msg...)) + } +} + +func (l *InstanceLogger) Info(msg ...any) { + l.logger.Info().Msg(l.append(msg...)) +} + +func (l *InstanceLogger) Warn(msg ...any) { + l.logger.Warn().Msg(l.append(msg...)) +} + +func (l *InstanceLogger) Error(msg ...any) { + l.logger.Error().Msg(l.append(msg...)) +} + +func (l *InstanceLogger) append(msg ...any) string { + strMsg := make([]string, len(msg)) + for i, m := range msg { + strMsg[i] = fmt.Sprint(m) + } + return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " ")) +} + +func newInstanceLogger(extension *Extension, logger *logger.Logger) *InstanceLogger { + return &InstanceLogger{ + identity: extension.buildIdentity(), + devMode: extension.DevMode, + logger: logger, + } +} + +type Context struct { + Req *base.Request `json:"req"` + Res *base.Resource `json:"res"` + Settings map[string]any `json:"settings"` +} + +func parseSettings(settings []*Setting) map[string]any { + m := make(map[string]any) + for _, s := range settings { + var val any + if s.Value != nil { + val = s.Value + } + m[s.Name] = tryParse(val, s.Type) + } + return m +} + +func tryParse(val any, settingType SettingType) any { + if val == nil { + return nil + } + switch settingType { + case SettingTypeString: + return fmt.Sprint(val) + case SettingTypeNumber: + vv, err := strconv.ParseFloat(fmt.Sprint(val), 64) + if err != nil { + return nil + } + return vv + case SettingTypeBoolean: + vv, err := strconv.ParseBool(fmt.Sprint(val)) + if err != nil { + return nil + } + return vv + default: + return nil + } +} diff --git a/pkg/download/extension_test.go b/pkg/download/extension_test.go new file mode 100644 index 00000000..a8cb13d3 --- /dev/null +++ b/pkg/download/extension_test.go @@ -0,0 +1,245 @@ +package download + +import ( + "github.com/GopeedLab/gopeed/internal/logger" + "github.com/GopeedLab/gopeed/pkg/base" + "os" + "testing" +) + +func TestDownloader_InstallExtensionByFolder(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", 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_InstallExtensionByFolderDevMode(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", true); 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_InstallExtensionByGit(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByGit("https://github.com/GopeedLab/gopeed-extension-samples#github-release-sample"); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/GopeedLab/gopeed/releases", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_InstallExtensionByGitSimple(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByGit("github.com/GopeedLab/gopeed-extension-samples#github-release-sample"); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/GopeedLab/gopeed/releases", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_InstallExtensionByGitFull(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByGit("https://github.com/GopeedLab/gopeed-extension-samples.git#github-release-sample"); err != nil { + t.Fatal(err) + } + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/GopeedLab/gopeed/releases", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_UpgradeExtension(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false) + if err != nil { + t.Fatal(err) + } + extensions := downloader.GetExtensions() + if len(extensions) == 0 { + t.Fatal("extension not installed") + } + oldVersion := installedExt.Version + // fetch new version from git + newVersion, err := downloader.UpgradeCheckExtension(installedExt.Identity) + if err != nil { + t.Fatal(err) + } + if newVersion == "" { + t.Fatal("new version not found") + } + // update extension + if err = downloader.UpgradeExtension(installedExt.Identity); err != nil { + t.Fatal(err) + } + upgradeExt := downloader.getExtension(installedExt.Identity) + if upgradeExt.Version == oldVersion { + t.Fatal("extension update fail") + } + + rr, err := downloader.Resolve(&base.Request{ + URL: "https://github.com/GopeedLab/gopeed/releases", + }) + if err != nil { + t.Fatal(err) + } + if len(rr.Res.Files) == 1 { + t.Fatal("resolve error") + } + }) +} + +func TestDownloader_Extension_Settings(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_empty", 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("settings parse error") + } + }) + + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false) + if err != nil { + t.Fatal(err) + } + downloader.UpdateExtensionSettings(installedExt.Identity, map[string]any{ + "stringValued": "valued", + "numberValued": 1.1, + "booleanValued": true, + }) + 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("settings parse error") + } + }) +} + +func TestDownloader_SwitchExtension(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", false) + if err != nil { + t.Fatal(err) + } + if installedExt.Disabled == true { + t.Fatal("extension disabled") + } + if err = downloader.SwitchExtension(installedExt.Identity, false); err != nil { + t.Fatal(err) + } + if installedExt.Disabled == false { + t.Fatal("extension enabled") + } + }) +} + +func TestDownloader_DeleteExtension(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false) + if err != nil { + t.Fatal(err) + } + extensions := downloader.GetExtensions() + if err := downloader.DeleteExtension(installedExt.Identity); err != nil { + t.Fatal(err) + } + extensions = downloader.GetExtensions() + if len(extensions) != 0 { + t.Fatal("extension delete fail") + } + }) +} + +func TestDownloader_Extension_OnResolve(t *testing.T) { + setupDownloader(func(downloader *Downloader) { + installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false) + if err != nil { + t.Fatal(err) + } + if err := downloader.DeleteExtension(installedExt.Identity); err != nil { + t.Fatal(err) + } + extensions := downloader.GetExtensions() + if len(extensions) != 0 { + t.Fatal("extension delete fail") + } + }) +} + +func TestDownloader_Extension_Logger(t *testing.T) { + logger := logger.NewLogger(false, "") + il := newInstanceLogger(&Extension{ + Name: "test", + }, logger) + il.Debug("msg1", "msg2") + il.Info("msg1", "msg2") + il.Warn("msg1", "msg2") + il.Error("msg1", "msg2") +} + +func setupDownloader(fn func(downloader *Downloader)) { + defaultDownloader.Setup() + defaultDownloader.cfg.StorageDir = ".test_storage" + defaultDownloader.cfg.DownloadDir = ".test_download" + defer func() { + defaultDownloader.Clear() + os.RemoveAll(defaultDownloader.cfg.StorageDir) + os.RemoveAll(defaultDownloader.cfg.DownloadDir) + }() + fn(defaultDownloader) +} diff --git a/pkg/download/model.go b/pkg/download/model.go index 582d6075..43d066b2 100644 --- a/pkg/download/model.go +++ b/pkg/download/model.go @@ -75,6 +75,8 @@ type DownloaderConfig struct { Storage Storage StorageDir string + ProductionMode bool + *DownloaderStoreConfig } @@ -99,7 +101,7 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig { // DownloaderStoreConfig is the config that can restore the downloader. type DownloaderStoreConfig struct { - FirstLoad bool `json:"-"` // fromNoStore is the flag that the config is first time init and not from store + FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count @@ -109,7 +111,7 @@ type DownloaderStoreConfig struct { func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig { if cfg.MaxRunning == 0 { - cfg.MaxRunning = 3 + cfg.MaxRunning = 5 } return cfg } diff --git a/pkg/download/storage.go b/pkg/download/storage.go index a8d40926..3daff0f4 100644 --- a/pkg/download/storage.go +++ b/pkg/download/storage.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "sync" ) type Storage interface { @@ -47,10 +48,13 @@ func changeValue(p any, v any) { var memData = make(map[string]map[string]any) type MemStorage struct { + lock *sync.RWMutex } func NewMemStorage() *MemStorage { - return &MemStorage{} + return &MemStorage{ + lock: &sync.RWMutex{}, + } } func (n *MemStorage) Setup(buckets []string) error { @@ -63,11 +67,15 @@ func (n *MemStorage) Setup(buckets []string) error { } func (n *MemStorage) Put(bucket string, key string, v any) error { + n.lock.Lock() + defer n.lock.Unlock() memData[bucket][key] = v return nil } func (n *MemStorage) Get(bucket string, key string, v any) (bool, error) { + n.lock.RLock() + defer n.lock.RUnlock() if dv, ok := memData[bucket][key]; ok { changeValue(v, dv) return true, nil @@ -76,6 +84,8 @@ func (n *MemStorage) Get(bucket string, key string, v any) (bool, error) { } func (n *MemStorage) List(bucket string, v any) error { + n.lock.RLock() + defer n.lock.RUnlock() data := memData[bucket] list := make([]any, 0) for _, v := range data { @@ -86,6 +96,8 @@ func (n *MemStorage) List(bucket string, v any) error { } func (n *MemStorage) Pop(bucket string, key string, v any) error { + n.lock.Lock() + defer n.lock.Unlock() data := memData[bucket] changeValue(v, data[key]) delete(data, key) @@ -93,6 +105,8 @@ func (n *MemStorage) Pop(bucket string, key string, v any) error { } func (n *MemStorage) Delete(bucket string, key string) error { + n.lock.Lock() + defer n.lock.Unlock() delete(memData[bucket], key) return nil } @@ -102,6 +116,8 @@ func (n *MemStorage) Close() error { } func (n *MemStorage) Clear() error { + n.lock.Lock() + defer n.lock.Unlock() memData = make(map[string]map[string]any) return nil } @@ -116,6 +132,9 @@ type BoltStorage struct { } func NewBoltStorage(dir string) *BoltStorage { + if err := os.MkdirAll(dir, 0755); err != nil { + panic(err) + } path := filepath.Join(dir, dbFile) db, err := bbolt.Open(path, 0600, nil) if err != nil { diff --git a/pkg/download/testdata/extensions/basic/index.js b/pkg/download/testdata/extensions/basic/index.js new file mode 100644 index 00000000..79176b6b --- /dev/null +++ b/pkg/download/testdata/extensions/basic/index.js @@ -0,0 +1,13 @@ +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, + } + }), + ), + }; +}); diff --git a/pkg/download/testdata/extensions/basic/manifest.json b/pkg/download/testdata/extensions/basic/manifest.json new file mode 100644 index 00000000..ab8ad2bc --- /dev/null +++ b/pkg/download/testdata/extensions/basic/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "basic", + "title": "gopeed extension basic test", + "version": "0.0.1", + "scripts": [ + { + "event": "onResolve", + "matches": [ + "*://github.com/*" + ], + "entry": "index.js" + } + ], + "settings": [ + { + "name": "ua", + "title": "User-Agent", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/pkg/download/testdata/extensions/settings_all/index.js b/pkg/download/testdata/extensions/settings_all/index.js new file mode 100644 index 00000000..4454268b --- /dev/null +++ b/pkg/download/testdata/extensions/settings_all/index.js @@ -0,0 +1,43 @@ +gopeed.events.onResolve(async function (ctx) { + if (ctx.settings.string != null) { + throw new Error("string is not null"); + } + if (ctx.settings.number != null) { + throw new Error("number is not null"); + } + if (ctx.settings.boolean != null) { + throw new Error("boolean is not null"); + } + + if (ctx.settings.stringDefault !== "default") { + throw new Error("string default value is incorrect"); + } + if (ctx.settings.numberDefault !== 1) { + throw new Error("number default value is incorrect"); + } + if (ctx.settings.booleanDefault !== true) { + throw new Error("boolean default value is incorrect"); + } + + if (ctx.settings.stringValued !== "valued") { + throw new Error("string value is incorrect"); + } + if (ctx.settings.numberValued !== 1.1) { + throw new Error("number value is incorrect"); + } + if (ctx.settings.booleanValued !== true) { + throw new Error("boolean value is incorrect"); + } + + ctx.res = { + name: "test", + files: Array(2).fill(true).map((_, i) => ({ + name: `test-${i}.txt`, + size: 1024, + req: { + url: ctx.req.url + "/" + i, + } + }), + ), + }; +}); diff --git a/pkg/download/testdata/extensions/settings_all/manifest.json b/pkg/download/testdata/extensions/settings_all/manifest.json new file mode 100644 index 00000000..ae6b9540 --- /dev/null +++ b/pkg/download/testdata/extensions/settings_all/manifest.json @@ -0,0 +1,64 @@ +{ + "name": "settings-all", + "title": "gopeed extension settings all type test", + "version": "0.0.1", + "scripts": [ + { + "event": "onResolve", + "matches": [ + "*://*/*" + ], + "entry": "index.js" + } + ], + "settings": [ + { + "name": "string", + "title": "string null test", + "type": "string" + }, + { + "name": "number", + "title": "number null test", + "type": "number" + }, + { + "name": "boolean", + "title": "boolean null test", + "type": "boolean" + }, + { + "name": "stringDefault", + "title": "string default test", + "type": "string", + "value": "default" + }, + { + "name": "numberDefault", + "title": "number default test", + "type": "number", + "value": 1 + }, + { + "name": "booleanDefault", + "title": "boolean default test", + "type": "boolean", + "value": true + }, + { + "name": "stringValued", + "title": "string valued test", + "type": "string" + }, + { + "name": "numberValued", + "title": "number valued test", + "type": "number" + }, + { + "name": "booleanValued", + "title": "boolean valued test", + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/pkg/download/testdata/extensions/settings_empty/index.js b/pkg/download/testdata/extensions/settings_empty/index.js new file mode 100644 index 00000000..4d47d019 --- /dev/null +++ b/pkg/download/testdata/extensions/settings_empty/index.js @@ -0,0 +1,17 @@ +gopeed.events.onResolve(async function (ctx) { + if (Object.keys(ctx.settings).length > 0){ + throw new Error("settings is not empty"); + } + + ctx.res = { + name: "test", + files: Array(2).fill(true).map((_, i) => ({ + name: `test-${i}.txt`, + size: 1024, + req: { + url: ctx.req.url + "/" + i, + } + }), + ), + }; +}); diff --git a/pkg/download/testdata/extensions/settings_empty/manifest.json b/pkg/download/testdata/extensions/settings_empty/manifest.json new file mode 100644 index 00000000..0ae921ae --- /dev/null +++ b/pkg/download/testdata/extensions/settings_empty/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "settings-empty", + "title": "gopeed extension settings empty test", + "version": "0.0.1", + "scripts": [ + { + "event": "onResolve", + "matches": [ + "*://*/*" + ], + "entry": "index.js" + } + ] +} \ No newline at end of file diff --git a/pkg/download/testdata/extensions/update/index.js b/pkg/download/testdata/extensions/update/index.js new file mode 100644 index 00000000..4376e703 --- /dev/null +++ b/pkg/download/testdata/extensions/update/index.js @@ -0,0 +1,3 @@ +gopeed.events.onResolve(async function (ctx) { + // do nothing for test +}); diff --git a/pkg/download/testdata/extensions/update/manifest.json b/pkg/download/testdata/extensions/update/manifest.json new file mode 100644 index 00000000..30522af3 --- /dev/null +++ b/pkg/download/testdata/extensions/update/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "github-release", + "author": "gopeed", + "title": "Github Release", + "description": "Download github release assets", + "version": "0.0.1", + "homepage": "https://gopeed.com", + "repository": { + "url": "https://github.com/GopeedLab/gopeed-extension-samples", + "directory": "github-release-sample" + }, + "scripts": [ + { + "event": "onResolve", + "matches": [ + "*://github.com/*/releases/*" + ], + "entry": "index.js" + } + ] +} \ No newline at end of file diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 4443e7b3..a0fc3ea8 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "net/url" - "strings" ) func Resolve(w http.ResponseWriter, r *http.Request) { @@ -31,9 +30,9 @@ func CreateTask(w http.ResponseWriter, r *http.Request) { err error ) if req.Rid != "" { - taskId, err = Downloader.Create(req.Rid, req.Opts) + taskId, err = Downloader.Create(req.Rid, req.Opt) } else if req.Req != nil { - taskId, err = Downloader.CreateDirect(req.Req, req.Opts) + taskId, err = Downloader.CreateDirect(req.Req, req.Opt) } else { WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam)) return @@ -121,21 +120,16 @@ func GetTask(w http.ResponseWriter, r *http.Request) { } func GetTasks(w http.ResponseWriter, r *http.Request) { - status := r.FormValue("status") - if status == "" { - status = strings.Join([]string{ - string(base.DownloadStatusReady), - string(base.DownloadStatusRunning), - string(base.DownloadStatusPause), - string(base.DownloadStatusError), - string(base.DownloadStatusDone), - }, ",") - } - statusArr := strings.Split(status, ",") + r.ParseForm() + status := r.Form["status"] tasks := Downloader.GetTasks() + if len(status) == 0 { + WriteJson(w, model.NewOkResult(tasks)) + return + } result := make([]*download.Task, 0) for _, task := range tasks { - for _, s := range statusArr { + for _, s := range status { if task.Status == base.Status(s) { result = append(result, task) } @@ -159,6 +153,101 @@ func PutConfig(w http.ResponseWriter, r *http.Request) { WriteJson(w, model.NewNilResult()) } +func InstallExtension(w http.ResponseWriter, r *http.Request) { + var req model.InstallExtension + if ReadJson(r, w, &req) { + var ( + installedExt *download.Extension + err error + ) + if req.DevMode { + installedExt, err = Downloader.InstallExtensionByFolder(req.URL, true) + } else { + installedExt, err = Downloader.InstallExtensionByGit(req.URL) + } + if err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewOkResult(installedExt.Identity)) + } +} + +func GetExtensions(w http.ResponseWriter, r *http.Request) { + list := Downloader.GetExtensions() + WriteJson(w, model.NewOkResult(list)) +} + +func GetExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + ext, err := Downloader.GetExtension(identity) + if err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewOkResult(ext)) +} + +func UpdateExtensionSettings(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + var req model.UpdateExtensionSettings + if ReadJson(r, w, &req) { + if err := Downloader.UpdateExtensionSettings(identity, req.Settings); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + } + WriteJson(w, model.NewNilResult()) +} + +func SwitchExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + var switchExtension model.SwitchExtension + if ReadJson(r, w, &switchExtension) { + if err := Downloader.SwitchExtension(identity, switchExtension.Status); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + } + WriteJson(w, model.NewNilResult()) +} + +func DeleteExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + if err := Downloader.DeleteExtension(identity); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) +} + +func UpdateCheckExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + newVersion, err := Downloader.UpgradeCheckExtension(identity) + if err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewOkResult(&model.UpdateCheckExtensionResp{ + NewVersion: newVersion, + })) +} + +func UpdateExtension(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + identity := vars["identity"] + if err := Downloader.UpgradeExtension(identity); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) +} + func DoProxy(w http.ResponseWriter, r *http.Request) { target := r.Header.Get("X-Target-Uri") if target == "" { @@ -191,18 +280,6 @@ func DoProxy(w http.ResponseWriter, r *http.Request) { return } w.Write(buf) - //var reader io.ReadCloser - //switch resp.Header.Get("Content-Encoding") { - //case "gzip": - // reader, err = gzip.NewReader(resp.Body) - // defer reader.Close() - //default: - // reader = resp.Body - //} - //if _, err := io.Copy(w, resp.Body); err != nil { - // writeError(w, err.Error()) - // return - //} } func writeError(w http.ResponseWriter, msg string) { diff --git a/pkg/rest/model/extension.go b/pkg/rest/model/extension.go new file mode 100644 index 00000000..2c67e567 --- /dev/null +++ b/pkg/rest/model/extension.go @@ -0,0 +1,18 @@ +package model + +type InstallExtension struct { + DevMode bool `json:"devMode"` + URL string `json:"url"` +} + +type UpdateExtensionSettings struct { + Settings map[string]any `json:"settings"` +} + +type SwitchExtension struct { + Status bool `json:"status"` +} + +type UpdateCheckExtensionResp struct { + NewVersion string `json:"newVersion"` +} diff --git a/pkg/rest/model/server.go b/pkg/rest/model/server.go index 6eb6e87d..9f8b363c 100644 --- a/pkg/rest/model/server.go +++ b/pkg/rest/model/server.go @@ -20,6 +20,8 @@ type StartConfig struct { StorageDir string `json:"storageDir"` ApiToken string `json:"apiToken"` + ProductionMode bool + WebEnable bool WebFS fs.FS WebBasicAuth *WebBasicAuth diff --git a/pkg/rest/model/task.go b/pkg/rest/model/task.go index f3d2081d..4cd9e656 100644 --- a/pkg/rest/model/task.go +++ b/pkg/rest/model/task.go @@ -3,7 +3,7 @@ package model import "github.com/GopeedLab/gopeed/pkg/base" type CreateTask struct { - Rid string `json:"rid"` - Req *base.Request `json:"req"` - Opts *base.Options `json:"opts"` + Rid string `json:"rid"` + Req *base.Request `json:"req"` + Opt *base.Options `json:"opt"` } diff --git a/pkg/rest/server.go b/pkg/rest/server.go index cbcaccf8..326a9e59 100644 --- a/pkg/rest/server.go +++ b/pkg/rest/server.go @@ -10,6 +10,10 @@ import ( "github.com/gorilla/mux" "net" "net/http" + "os" + "path/filepath" + "regexp" + "strings" ) var ( @@ -26,7 +30,6 @@ func Start(startCfg *model.StartConfig) (int, error) { go func() { if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { - // TODO log panic(err) } }() @@ -41,12 +44,12 @@ func Start(startCfg *model.StartConfig) (int, error) { func Stop() { if srv != nil { if err := srv.Shutdown(context.TODO()); err != nil { - // TODO log + Downloader.Logger.Warn().Err(err).Msg("shutdown server failed") } } if Downloader != nil { if err := Downloader.Close(); err != nil { - // TODO log + Downloader.Logger.Warn().Err(err).Msg("close downloader failed") } } } @@ -58,6 +61,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error startCfg.Init() downloadCfg := &download.DownloaderConfig{ + ProductionMode: startCfg.ProductionMode, RefreshInterval: startCfg.RefreshInterval, } if startCfg.Storage == model.StorageBolt { @@ -93,6 +97,15 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks) r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig) r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig) + r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension) + r.Methods(http.MethodGet).Path("/api/v1/extensions").HandlerFunc(GetExtensions) + r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}").HandlerFunc(GetExtension) + r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/settings").HandlerFunc(UpdateExtensionSettings) + r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/switch").HandlerFunc(SwitchExtension) + r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension) + r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension) + r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension) + r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem))) r.Path("/api/v1/proxy").HandlerFunc(DoProxy) if startCfg.WebEnable { r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS))) @@ -125,6 +138,32 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error return srv, listener, nil } +// handle extension file resource +type extensionFileSystem struct { +} + +func (e *extensionFileSystem) Open(name string) (http.File, error) { + // remove prefix + path := strings.TrimPrefix(name, "/fs/extensions") + // match extension identity, eg: /fs/extensions/identity/xxx + reg := regexp.MustCompile(`^/([^/]+)/(.*)$`) + if !reg.MatchString(path) { + return nil, os.ErrNotExist + } + matched := reg.FindStringSubmatch(path) + if len(matched) != 3 { + return nil, os.ErrNotExist + } + // get extension identity + identity := matched[1] + extension, err := Downloader.GetExtension(identity) + if err != nil { + return nil, os.ErrNotExist + } + extensionPath := Downloader.ExtensionPath(extension) + return os.Open(filepath.Join(extensionPath, matched[2])) +} + func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool { if err := json.NewDecoder(r.Body).Decode(v); err != nil { WriteJson(w, model.NewErrorResult(err.Error())) diff --git a/pkg/rest/server_test.go b/pkg/rest/server_test.go index 9235cdc0..b34fb346 100644 --- a/pkg/rest/server_test.go +++ b/pkg/rest/server_test.go @@ -33,7 +33,6 @@ var ( }, } taskRes = &base.Resource{ - Name: test.BuildName, Size: test.BuildSize, Range: true, Files: []*base.FileInfo{ @@ -44,15 +43,23 @@ var ( }, }, } + createOpts = &base.Options{ + Path: test.Dir, + Name: test.DownloadName, + Extra: map[string]any{ + "connections": 2, + }, + } createReq = &model.CreateTask{ Req: taskReq, - Opts: &base.Options{ - Path: test.Dir, - Name: test.DownloadName, - Extra: map[string]any{ - "connections": 2, - }, - }, + Opt: createOpts, + } + createResoledReq = &model.CreateTask{ + Req: taskReq, + Opt: createOpts, + } + installExtensionReq = &model.InstallExtension{ + URL: "https://github.com/GopeedLab/gopeed-extension-samples#github-contributor-avatars-sample", } ) @@ -66,6 +73,35 @@ func TestResolve(t *testing.T) { } func TestCreateTask(t *testing.T) { + doTest(func() { + resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq) + + var wg sync.WaitGroup + wg.Add(1) + Downloader.Listener(func(event *download.Event) { + if event.Key == download.EventKeyFinally { + wg.Done() + } + }) + + taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", &model.CreateTask{ + Rid: resp.ID, + Opt: createOpts, + }) + if taskId == "" { + t.Fatal("create task failed") + } + + wg.Wait() + want := test.FileMd5(test.BuildFile) + got := test.FileMd5(test.DownloadFile) + if want != got { + t.Errorf("CreateTask() got = %v, want %v", got, want) + } + }) +} + +func TestCreateDirectTask(t *testing.T) { doTest(func() { var wg sync.WaitGroup wg.Add(1) @@ -84,32 +120,41 @@ func TestCreateTask(t *testing.T) { want := test.FileMd5(test.BuildFile) got := test.FileMd5(test.DownloadFile) if want != got { - t.Errorf("CreateTask() got = %v, want %v", got, want) + t.Errorf("CreateDirectTask() got = %v, want %v", got, want) + } + }) +} + +func TestCreateDirectTaskWithResoled(t *testing.T) { + doTest(func() { + var wg sync.WaitGroup + wg.Add(1) + Downloader.Listener(func(event *download.Event) { + if event.Key == download.EventKeyFinally { + wg.Done() + } + }) + + taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createResoledReq) + if taskId == "" { + t.Fatal("create task failed") + } + + wg.Wait() + want := test.FileMd5(test.BuildFile) + got := test.FileMd5(test.DownloadFile) + if want != got { + t.Errorf("CreateDirectTaskWithResoled() got = %v, want %v", got, want) } }) } func TestPauseAndContinueTask(t *testing.T) { doTest(func() { - type result struct { - pauseCount int - continueCount int - md5 string - } - - var got = result{ - pauseCount: 0, - continueCount: 0, - md5: "", - } var wg sync.WaitGroup wg.Add(1) Downloader.Listener(func(event *download.Event) { switch event.Key { - case download.EventKeyPause: - got.pauseCount++ - case download.EventKeyContinue: - got.continueCount++ case download.EventKeyFinally: wg.Done() } @@ -132,12 +177,8 @@ func TestPauseAndContinueTask(t *testing.T) { } wg.Wait() - want := result{ - pauseCount: 1, - continueCount: 1, - md5: test.FileMd5(test.BuildFile), - } - got.md5 = test.FileMd5(test.DownloadFile) + want := test.FileMd5(test.BuildFile) + got := test.FileMd5(test.DownloadFile) if !reflect.DeepEqual(got, want) { t.Errorf("PauseAndContinueTask() got = %v, want %v", got, want) } @@ -163,13 +204,15 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) { // continue all httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil) + time.Sleep(time.Millisecond * 100) tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil) if len(tasks) != cfg.MaxRunning { t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning) } // pause all httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil) - tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s,%s", base.DownloadStatusReady, base.DownloadStatusPause), nil) + time.Sleep(time.Millisecond * 500) + tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil) if len(tasks) != total { t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total) } @@ -210,7 +253,7 @@ func TestGetTasks(t *testing.T) { } }) - httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s,%s", + httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s&status=%s", base.DownloadStatusReady, base.DownloadStatusRunning), createReq) httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil) @@ -248,6 +291,115 @@ func TestGetAndPutConfig(t *testing.T) { }) } +func TestInstallExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + if identity == "" { + t.Errorf("InstallExtension() got = %v, want %v", identity, "not empty") + } + + // not a valid extension repository + code, _ := httpRequest[string](http.MethodPost, "/api/v1/extensions", &model.InstallExtension{ + URL: "https://github.com/GopeedLab/gopeed", + }) + checkCode(code, model.CodeError) + + // not a git repository + code, _ = httpRequest[string](http.MethodPost, "/api/v1/extensions", &model.InstallExtension{ + URL: "https://github.com", + }) + checkCode(code, model.CodeError) + }) +} + +func TestGetExtensions(t *testing.T) { + doTest(func() { + httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil) + if len(extensions) == 0 { + t.Errorf("GetExtensions() got = %v, want %v", len(extensions), "not empty") + } + }) +} + +func TestUpdateExtensionSettings(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + + httpRequestCheckOk[any](http.MethodPut, "/api/v1/extensions/"+identity+"/settings", &model.UpdateExtensionSettings{ + Settings: map[string]any{ + "undefined": "test", + "ua": "test", + }, + }) + + settings := httpRequestCheckOk[*download.Extension](http.MethodGet, "/api/v1/extensions/"+identity, nil).Settings + if len(settings) != 1 { + t.Errorf("UpdateExtensionSettings() got = %v, want %v", len(settings), 1) + } + + if settings[0].Name != "ua" || settings[0].Value != "test" { + t.Errorf("UpdateExtensionSettings() got = %v, want %v", settings[0].Value, "test") + } + }) +} + +func TestSwitchExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + httpRequestCheckOk[any](http.MethodPut, "/api/v1/extensions/"+identity+"/switch", &model.SwitchExtension{ + Status: false, + }) + extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil) + if !extensions[0].Disabled { + t.Errorf("TestSwitchExtension() got = %v, want %v", extensions[0].Disabled, true) + } + }) +} + +func TestDeleteExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + httpRequestCheckOk[any](http.MethodDelete, "/api/v1/extensions/"+identity, nil) + extensions := httpRequestCheckOk[[]*download.Extension](http.MethodGet, "/api/v1/extensions", nil) + if len(extensions) != 0 { + t.Errorf("TestDeleteExtension() got = %v, want %v", len(extensions), 0) + } + }) +} + +func TestUpdateCheckExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + resp := httpRequestCheckOk[*model.UpdateCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/update", nil) + // no new version + if resp.NewVersion != "" { + t.Errorf("UpdateCheckExtension() got = %v, want %v", resp.NewVersion, "") + } + // force update + httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/update", nil) + }) +} + +func TestFsExtension(t *testing.T) { + doTest(func() { + identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq) + statusCode, _ := doHttpRequest0(http.MethodGet, "/fs/extensions/"+identity+"/icon.png", nil, nil) + if statusCode != http.StatusOK { + t.Errorf("FsExtension() got = %v, want %v", statusCode, http.StatusOK) + } + }) +} + +func TestFsExtensionFail(t *testing.T) { + doTest(func() { + statusCode, _ := doHttpRequest0(http.MethodGet, "/fs/extensions/not_exist/icon.png", nil, nil) + if statusCode != http.StatusNotFound { + t.Errorf("TestFsExtensionFail() got = %v, want %v", statusCode, http.StatusNotFound) + } + }) +} + func TestDoProxy(t *testing.T) { doTest(func() { code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{ @@ -341,15 +493,18 @@ func doTest(handler func()) { var cfg = &model.StartConfig{} cfg.Init() cfg.Storage = storage + cfg.StorageDir = ".test_storage" fileListener := doStart(cfg) defer func() { if err := fileListener.Close(); err != nil { panic(err) } Stop() + Downloader.Clear() }() - defer Downloader.Clear() defer func() { + Downloader.PauseAll() + tasks := Downloader.GetTasks() taskIds := make([]string, len(tasks)) for i, task := range tasks { @@ -358,6 +513,7 @@ func doTest(handler func()) { for _, id := range taskIds { Downloader.Delete(id, true) } + os.RemoveAll(cfg.StorageDir) }() taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName handler() diff --git a/pkg/util/matcher.go b/pkg/util/matcher.go new file mode 100644 index 00000000..02f609c9 --- /dev/null +++ b/pkg/util/matcher.go @@ -0,0 +1,75 @@ +package util + +import ( + "net/url" + "regexp" + "strings" +) + +// Match url with pattern by chrome extension match pattern style +// https://developer.chrome.com/docs/extensions/mv3/match_patterns/ +func Match(pattern string, u string) bool { + scheme, host, path := parsePattern(pattern) + url, err := url.Parse(u) + if err != nil { + return false + } + if scheme != "*" && scheme != url.Scheme { + return false + } + if !matchHost(host, url.Hostname()) { + return false + } + if !matchPath(path, url.Path) { + return false + } + return true +} + +func parsePattern(pattern string) (scheme string, host string, path string) { + parts := strings.Split(pattern, "://") + if len(parts) == 2 { + scheme = parts[0] + pattern = parts[1] + } else { + scheme = "" + } + parts = strings.SplitN(pattern, "/", 2) + if len(parts) == 2 { + host = parts[0] + path = "/" + parts[1] + } else { + host = pattern + path = "/" + } + return +} + +func matchHost(pattern string, host string) bool { + if pattern == "*" { + return true + } + if strings.HasPrefix(pattern, "*.") { + return strings.HasSuffix(host, pattern[1:]) + } + return pattern == host +} + +func matchPath(pattern string, path string) bool { + if pattern == "*" { + return true + } + if !strings.HasSuffix(pattern, "*") && !strings.HasSuffix(pattern, "/") { + pattern += "/" + } + if !strings.HasSuffix(path, "/") { + path += "/" + } + + if strings.Contains(pattern, "*") { + pattern = strings.Replace(pattern, "*", ".*", -1) + matched, _ := regexp.MatchString("^"+pattern+"$", path) + return matched + } + return pattern == path +} diff --git a/pkg/util/matcher_test.go b/pkg/util/matcher_test.go new file mode 100644 index 00000000..5f24442e --- /dev/null +++ b/pkg/util/matcher_test.go @@ -0,0 +1,36 @@ +package util + +import "testing" + +func TestMatch(t *testing.T) { + tests := []struct { + pattern string + urls []string + want bool + }{ + {"*://*/*", []string{"https://www.google.com/", "http://example.org/foo/bar.html"}, true}, + {"https://*/*", []string{"https://www.google.com", "https://example.org/foo/bar.html"}, true}, + {"*://www.google.com", []string{"https://www.google.com/", "https://www.google.com"}, true}, + {"*://*.google.com/", []string{"https://a.www.google.com/", "https://c.www.google.com/", "https://www.google.com/"}, true}, + {"https://*/foo*", []string{"https://www.google.com/foo", "https://example.com/foo/bar.html"}, true}, + {"https://www.google.com/*/b/*", []string{"https://www.google.com/a/b", "https://www.google.com/a/b/c"}, true}, + {"https://*.google.com/foo*bar", []string{"https://www.google.com/foo/baz/bar", "https://docs.google.com/foobar"}, true}, + {"https://www.google.com/*abc*", []string{"https://www.google.com/abc", "https://www.google.com/123abc", "https://www.google.com/abc456", "https://www.google.com/123abc456"}, true}, + {"https://example.org/foo/bar.html", []string{"https://example.org/foo/bar.html"}, true}, + {"http://127.0.0.1/*", []string{"http://127.0.0.1/", "http://127.0.0.1/foo/bar.html"}, true}, + {"*://mail.google.com/*", []string{"http://mail.google.com/foo/baz/bar", "https://mail.google.com/foobar"}, true}, + {"https://www.google.com/", []string{"http://www.google.com/"}, false}, + {"www.google.com/", []string{"http://www.google.com/", "https://www.google.com/"}, false}, + {"www.google.com/*c", []string{"https://www.google.com/a", "https://www.google.com/b"}, false}, + {"https://*.example.org/*", []string{"https://www.google.com", "https://docs.google.com"}, false}, + } + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + for _, url := range tt.urls { + if got := Match(tt.pattern, url); got != tt.want { + t.Errorf("Match() = %v, want %v", got, tt.want) + } + } + }) + } +} diff --git a/pkg/util/path.go b/pkg/util/path.go index e0572f37..d7080694 100644 --- a/pkg/util/path.go +++ b/pkg/util/path.go @@ -3,8 +3,10 @@ package util import ( "errors" "fmt" + "io" "os" syspath "path" + "path/filepath" "strings" ) @@ -64,22 +66,105 @@ func CheckDuplicateAndRename(path string) (string, error) { } } -// GetSingleDir get the top level single folder name,if not exist, return empty string -func GetSingleDir(paths []string) string { - if len(paths) == 0 { - return "" +// CopyDir Copy all files to the target directory, if the file already exists, it will be overwritten. +// Remove target file if the source file is not exist. +func CopyDir(source string, target string, excludeDir ...string) error { + if err := os.MkdirAll(target, 0755); err != nil { + return err } - split := strings.Split(paths[0], "/") - if len(split) == 0 || split[0] == "" { - return "" - } - dir := split[0] - for i := 1; i < len(paths); i++ { - if !strings.HasPrefix(paths[i], dir) { - return "" + if err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err } + if info.IsDir() { + if len(excludeDir) > 0 { + for _, dir := range excludeDir { + if info.IsDir() && info.Name() == dir { + return filepath.SkipDir + } + } + } + return nil + } + relPath, err := filepath.Rel(source, path) + if err != nil { + return err + } + targetPath := filepath.Join(target, relPath) + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + return err + } + if err := copyForce(path, targetPath); err != nil { + return err + } + return nil + }); err != nil { + return err } - return dir + + if err := filepath.Walk(target, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if len(excludeDir) > 0 { + for _, dir := range excludeDir { + if info.IsDir() && info.Name() == dir { + return filepath.SkipDir + } + } + } + return nil + } + relPath, err := filepath.Rel(target, path) + if err != nil { + return err + } + targetPath := filepath.Join(target, relPath) + sourcePath := filepath.Join(source, relPath) + // if source file is not exist, remove target file + if _, err := os.Stat(sourcePath); os.IsNotExist(err) { + if err := SafeRemove(targetPath); err != nil { + return err + } + } + return nil + }); err != nil { + return err + } + return nil +} + +// RmAndMkDirAll remove and create directory, if the directory already exists, it will be overwritten. +func RmAndMkDirAll(path string) error { + if err := os.RemoveAll(path); err != nil { + return err + } + if err := os.MkdirAll(path, 0755); err != nil { + return err + } + return nil +} + +// copy file, if the target file already exists, it will be overwritten. +func copyForce(source string, target string) error { + sourceFile, err := os.Open(source) + if err != nil { + return err + } + defer sourceFile.Close() + + targetFile, err := os.Create(target) + if err != nil { + return err + } + defer targetFile.Close() + + _, err = io.Copy(targetFile, sourceFile) + if err != nil { + return err + } + return nil } func CreateDirIfNotExist(dir string) error { @@ -92,7 +177,7 @@ func CreateDirIfNotExist(dir string) error { // IsExistsFile check file exists and is a file func IsExistsFile(path string) bool { info, err := os.Stat(path) - // 判断路径是否存在,并且是一个文件 + // if file exists and is a file if err == nil && !info.IsDir() { return true } diff --git a/pkg/util/path_test.go b/pkg/util/path_test.go index 91c013c7..0f8f4c37 100644 --- a/pkg/util/path_test.go +++ b/pkg/util/path_test.go @@ -78,7 +78,7 @@ func TestFilepath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Filepath(tt.args.path, tt.args.originName, tt.args.customName); got != tt.want { - t.Errorf("Filepath() = %v, want %v", got, tt.want) + t.Errorf("SingleFilepath() = %v, want %v", got, tt.want) } }) } @@ -105,67 +105,6 @@ func TestSafeRemove(t *testing.T) { } } -func TestGetSingleDir(t *testing.T) { - type args struct { - paths []string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "empty", - args: args{ - paths: []string{}, - }, - want: "", - }, - { - name: "blank", - args: args{ - paths: []string{""}, - }, - want: "", - }, - { - name: "blank multi", - args: args{ - paths: []string{"", "a"}, - }, - want: "", - }, - { - name: "multi dir not single", - args: args{ - paths: []string{"a", "b"}, - }, - want: "", - }, - { - name: "single dir", - args: args{ - paths: []string{"a"}, - }, - want: "a", - }, - { - name: "multi dir single", - args: args{ - paths: []string{"a/b", "a/c"}, - }, - want: "a", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := GetSingleDir(tt.args.paths); got != tt.want { - t.Errorf("IsSingleDir() = %v, want %v", got, tt.want) - } - }) - } -} - func TestCheckDuplicateAndRename(t *testing.T) { doCheckDuplicateAndRename(t, []string{}, "a.txt", "a.txt") doCheckDuplicateAndRename(t, []string{"a.txt"}, "a.txt", "a (1).txt") diff --git a/ui/flutter/.gitignore b/ui/flutter/.gitignore index 74fa3cc9..b1f0661d 100644 --- a/ui/flutter/.gitignore +++ b/ui/flutter/.gitignore @@ -62,3 +62,5 @@ macos/Flutter/GeneratedPluginRegistrant.swift windows/flutter/generated_plugin_registrant.cc windows/flutter/generated_plugin_registrant.h windows/flutter/generated_plugins.cmake + +/extensions \ No newline at end of file diff --git a/ui/flutter/assets/extension/default_icon.png b/ui/flutter/assets/extension/default_icon.png new file mode 100644 index 00000000..d8b2dbc3 Binary files /dev/null and b/ui/flutter/assets/extension/default_icon.png differ diff --git a/ui/flutter/assets/locales/en_US.json b/ui/flutter/assets/locales/en_US.json index 55b49212..0306dbbc 100644 --- a/ui/flutter/assets/locales/en_US.json +++ b/ui/flutter/assets/locales/en_US.json @@ -58,7 +58,13 @@ "deleteTaskTip": "Keep downloaded files", "delete": "Delete", "newVersionTitle": "Discover new version @version", - "newVersionUpdate": "Download Now", - "newVersionLater": "Later" + "newVersionUpdate": "Update Now", + "newVersionLater": "Later", + "extensions": "Extensions", + "extensionInstallUrl": "Install URL", + "extensionInstallSuccess": "Installed successfully", + "extensionUpdateSuccess": "Updated successfully", + "extensionDelete": "Delete Extension", + "extensionAlreadyLatest": "It's already the latest version" } diff --git a/ui/flutter/assets/locales/fa_IR.json b/ui/flutter/assets/locales/fa_IR.json index f3e08920..6256c729 100644 --- a/ui/flutter/assets/locales/fa_IR.json +++ b/ui/flutter/assets/locales/fa_IR.json @@ -53,6 +53,6 @@ "deleteTaskTip": "فایل های دانلود شده را نگه دارد", "delete": "پاک کردن", "newVersionTitle": "@version عنوان: کشف نسخه جدید", - "newVersionUpdate": "دریافت اکنون", + "newVersionUpdate": "بروزرسانی", "newVersionLater": "بعداً" } \ No newline at end of file diff --git a/ui/flutter/assets/locales/ja_JP.json b/ui/flutter/assets/locales/ja_JP.json index 93932415..abd556ef 100644 --- a/ui/flutter/assets/locales/ja_JP.json +++ b/ui/flutter/assets/locales/ja_JP.json @@ -56,6 +56,6 @@ "deleteTaskTip": "ダウンロードしたファイルを保持", "delete": "削除", "newVersionTitle": "新しいバージョン @version を発見する", - "newVersionUpdate": "今すぐダウンロード", + "newVersionUpdate": "アップデート", "newVersionLater": "後で" } diff --git a/ui/flutter/assets/locales/ru_RU.json b/ui/flutter/assets/locales/ru_RU.json index 72db02d1..65e72a4d 100644 --- a/ui/flutter/assets/locales/ru_RU.json +++ b/ui/flutter/assets/locales/ru_RU.json @@ -56,6 +56,6 @@ "deleteTaskTip": "Сохранить загруженные файлы", "delete": "Удалить", "newVersionTitle": "Обнаружена новая версия @version", - "newVersionUpdate": "Скачать сейчас", + "newVersionUpdate": "Обновить", "newVersionLater": "позже" } \ No newline at end of file diff --git a/ui/flutter/assets/locales/zh_CN.json b/ui/flutter/assets/locales/zh_CN.json index 0a3dad97..50dc8271 100644 --- a/ui/flutter/assets/locales/zh_CN.json +++ b/ui/flutter/assets/locales/zh_CN.json @@ -58,6 +58,12 @@ "deleteTaskTip": "保留已下载的文件", "delete": "删除", "newVersionTitle": "发现新版本 @version", - "newVersionUpdate": "立即下载", - "newVersionLater": "稍后再说" + "newVersionUpdate": "立即更新", + "newVersionLater": "稍后再说", + "extensions": "扩展", + "extensionInstallUrl": "安装链接", + "extensionInstallSuccess": "安装成功", + "extensionUpdateSuccess": "更新成功", + "extensionDelete": "删除扩展", + "extensionAlreadyLatest": "已经是最新版本" } diff --git a/ui/flutter/lib/api/api.dart b/ui/flutter/lib/api/api.dart index 3a65a856..a3040820 100644 --- a/ui/flutter/lib/api/api.dart +++ b/ui/flutter/lib/api/api.dart @@ -3,14 +3,19 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:flutter/foundation.dart'; -import 'model/resolve_result.dart'; +import 'package:gopeed/api/model/extension.dart'; +import 'package:gopeed/api/model/install_extension.dart'; +import 'package:gopeed/api/model/switch_extension.dart'; + import '../util/util.dart'; import 'model/create_task.dart'; +import 'model/downloader_config.dart'; import 'model/request.dart'; +import 'model/resolve_result.dart'; import 'model/result.dart'; import 'model/task.dart'; - -import 'model/downloader_config.dart'; +import 'model/update_extension_settings.dart'; +import 'model/update_check_extension_resp.dart'; class _Client { static _Client? _instance; @@ -62,6 +67,12 @@ class _Client { } } +class TimeoutException implements Exception { + final String message; + + TimeoutException(this.message); +} + late _Client _client; void init(String network, String address, String apiToken) { @@ -85,7 +96,7 @@ Future _parse( if (e.type == DioExceptionType.sendTimeout || e.type == DioExceptionType.receiveTimeout || e.type == DioExceptionType.connectionTimeout) { - throw Exception(Result(code: 1000, msg: "request timeout")); + throw TimeoutException("request timeout"); } throw Exception(Result(code: 1000, msg: e.message)); } @@ -105,8 +116,8 @@ Future createTask(CreateTask createTask) async { Future> getTasks(List statuses) async { return _parse>( - () => _client.dio - .get("/api/v1/tasks?status=${statuses.map((e) => e.name).join(",")}"), + () => _client.dio.get( + "/api/v1/tasks?${statuses.map((e) => "status=${e.name}").join("&")}"), (data) => (data as List).map((e) => Task.fromJson(e)).toList()); } @@ -140,6 +151,47 @@ Future putConfig(DownloaderConfig config) async { return _parse(() => _client.dio.put("/api/v1/config", data: config), null); } +Future installExtension(InstallExtension installExtension) async { + return _parse( + () => _client.dio.post("/api/v1/extensions", data: installExtension), + null); +} + +Future> getExtensions() async { + return _parse>(() => _client.dio.get("/api/v1/extensions"), + (data) => (data as List).map((e) => Extension.fromJson(e)).toList()); +} + +Future updateExtensionSettings( + String identity, UpdateExtensionSettings updateExtensionSettings) async { + return _parse( + () => _client.dio.put("/api/v1/extensions/$identity/settings", + data: updateExtensionSettings), + null); +} + +Future switchExtension( + String identity, SwitchExtension switchExtension) async { + return _parse( + () => _client.dio + .put("/api/v1/extensions/$identity/switch", data: switchExtension), + null); +} + +Future deleteExtension(String identity) async { + return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null); +} + +Future upgradeCheckExtension(String identity) async { + return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"), + (data) => UpdateCheckExtensionResp.fromJson(data)); +} + +Future updateExtension(String identity) async { + return _parse( + () => _client.dio.post("/api/v1/extensions/$identity/update"), null); +} + Future> proxyRequest(String uri, {data, Options? options}) async { options ??= Options(); @@ -152,3 +204,7 @@ Future> proxyRequest(String uri, data: data, options: options); } + +String join(String path) { + return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}"; +} diff --git a/ui/flutter/lib/api/model/create_task.dart b/ui/flutter/lib/api/model/create_task.dart index 5ddbe999..92bcbcdf 100644 --- a/ui/flutter/lib/api/model/create_task.dart +++ b/ui/flutter/lib/api/model/create_task.dart @@ -9,12 +9,12 @@ part 'create_task.g.dart'; class CreateTask { String? rid; Request? req; - Options? opts; + Options? opt; CreateTask({ this.rid, this.req, - this.opts, + this.opt, }); factory CreateTask.fromJson( diff --git a/ui/flutter/lib/api/model/create_task.g.dart b/ui/flutter/lib/api/model/create_task.g.dart index f36e9432..3e147035 100644 --- a/ui/flutter/lib/api/model/create_task.g.dart +++ b/ui/flutter/lib/api/model/create_task.g.dart @@ -11,9 +11,9 @@ CreateTask _$CreateTaskFromJson(Map json) => CreateTask( req: json['req'] == null ? null : Request.fromJson(json['req'] as Map), - opts: json['opts'] == null + opt: json['opt'] == null ? null - : Options.fromJson(json['opts'] as Map), + : Options.fromJson(json['opt'] as Map), ); Map _$CreateTaskToJson(CreateTask instance) { @@ -27,6 +27,6 @@ Map _$CreateTaskToJson(CreateTask instance) { writeNotNull('rid', instance.rid); writeNotNull('req', instance.req?.toJson()); - writeNotNull('opts', instance.opts?.toJson()); + writeNotNull('opt', instance.opt?.toJson()); return val; } diff --git a/ui/flutter/lib/api/model/extension.dart b/ui/flutter/lib/api/model/extension.dart new file mode 100644 index 00000000..da13b403 --- /dev/null +++ b/ui/flutter/lib/api/model/extension.dart @@ -0,0 +1,95 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'extension.g.dart'; + +@JsonSerializable(explicitToJson: true) +class Extension { + String identity; + String name; + String author; + String title; + String description; + String icon; + String version; + String homepage; + Repository? repository; + List? settings; + bool disabled; + bool devMode; + + Extension({ + required this.identity, + required this.name, + required this.author, + required this.title, + required this.description, + required this.icon, + required this.version, + required this.homepage, + required this.repository, + required this.disabled, + required this.devMode, + }); + + factory Extension.fromJson(Map json) => + _$ExtensionFromJson(json); + Map toJson() => _$ExtensionToJson(this); +} + +@JsonSerializable() +class Repository { + String url; + String directory; + + Repository({ + required this.url, + required this.directory, + }); + + factory Repository.fromJson(Map json) => + _$RepositoryFromJson(json); + Map toJson() => _$RepositoryToJson(this); +} + +@JsonSerializable() +class Setting { + String name; + String title; + String description; + bool required; + SettingType type; + Object? value; + List