mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54fc0f7289 | ||
|
|
917fc64046 | ||
|
|
4ffb172259 | ||
|
|
a34383a7e2 | ||
|
|
98879569f0 | ||
|
|
8ef3b63e8d | ||
|
|
d873cd411c | ||
|
|
9e9bcd4b01 | ||
|
|
5bd57608d2 | ||
|
|
ebfd113360 | ||
|
|
d69874e13e | ||
|
|
b0d45226d7 | ||
|
|
cd19772099 | ||
|
|
1e36599d52 | ||
|
|
fc0a1e0ef9 | ||
|
|
8719166dbf | ||
|
|
c0b70d319d | ||
|
|
e0951de270 | ||
|
|
be0fb05edd | ||
|
|
91edba9af6 | ||
|
|
7d423e2182 | ||
|
|
903c5c46c4 | ||
|
|
b8ff5ff97f | ||
|
|
c51c5b284d | ||
|
|
4d65948934 | ||
|
|
c275bc2e46 | ||
|
|
9aae13453a | ||
|
|
d125d4ab75 | ||
|
|
4214765d39 | ||
|
|
47cafb7575 | ||
|
|
de65149b29 | ||
|
|
5462b14705 | ||
|
|
eee39e012f | ||
|
|
12860bb207 | ||
|
|
b86c2c18e7 |
No files matched your search
@@ -106,17 +106,32 @@ jobs:
|
|||||||
if ("${{ matrix.arch }}" -eq "amd64") {
|
if ("${{ matrix.arch }}" -eq "amd64") {
|
||||||
# for amd64
|
# for amd64
|
||||||
$release = "build\windows\x64\runner\Release\"
|
$release = "build\windows\x64\runner\Release\"
|
||||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
# Use the same toolchain as cgo instead of Git's bundled DLLs.
|
||||||
cp $mingw\libstdc++-6.dll $release
|
$cc = (Get-Command -Name (go env CC) -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source
|
||||||
cp $mingw\libgcc_s_seh-1.dll $release
|
foreach ($dll in @("libstdc++-6.dll", "libgcc_s_seh-1.dll", "libwinpthread-1.dll")) {
|
||||||
|
$runtimePath = [string](& $cc "-print-file-name=$dll")
|
||||||
|
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($runtimePath)) {
|
||||||
|
throw "Failed to locate $dll using $cc"
|
||||||
|
}
|
||||||
|
$runtimePath = $runtimePath.Trim()
|
||||||
|
# Some MinGW distributions keep runtime DLLs beside gcc.exe.
|
||||||
|
if ($runtimePath -eq $dll) {
|
||||||
|
$runtimePath = Join-Path (Split-Path -Parent $cc) $dll
|
||||||
|
}
|
||||||
|
if (!(Test-Path -LiteralPath $runtimePath -PathType Leaf)) {
|
||||||
|
throw "MinGW runtime $dll was not found by $cc (reported path: $runtimePath)"
|
||||||
|
}
|
||||||
|
Write-Host "Copying MinGW runtime: $runtimePath"
|
||||||
|
Copy-Item -LiteralPath $runtimePath -Destination $release
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
# for ARM64
|
# for ARM64
|
||||||
$release = "build\windows\arm64\runner\Release\"
|
$release = "build\windows\arm64\runner\Release\"
|
||||||
$mingw = "$env:CLANGARM64_ROOT\aarch64-w64-mingw32\bin"
|
$mingw = "$env:CLANGARM64_ROOT\aarch64-w64-mingw32\bin"
|
||||||
cp $mingw\libc++.dll $release
|
cp $mingw\libc++.dll $release
|
||||||
cp $mingw\libunwind.dll $release
|
cp $mingw\libunwind.dll $release
|
||||||
}
|
|
||||||
cp $mingw\libwinpthread-1.dll $release
|
cp $mingw\libwinpthread-1.dll $release
|
||||||
|
}
|
||||||
cp $system\msvcp140.dll $release
|
cp $system\msvcp140.dll $release
|
||||||
cp $system\vcruntime140.dll $release
|
cp $system\vcruntime140.dll $release
|
||||||
cp $system\vcruntime140_1.dll $release
|
cp $system\vcruntime140_1.dll $release
|
||||||
@@ -783,5 +798,5 @@ jobs:
|
|||||||
linux/arm64
|
linux/arm64
|
||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
tags: |
|
tags: |
|
||||||
liwei2633/gopeed:latest
|
${{ !contains(env.VERSION, 'beta') && 'liwei2633/gopeed:latest' || '' }}
|
||||||
liwei2633/gopeed:v${{ env.VERSION }}
|
liwei2633/gopeed:v${{ env.VERSION }}
|
||||||
@@ -69,6 +69,49 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||||
|
test-flutter-category:
|
||||||
|
# The settings editor and the backend have to read an extension list the
|
||||||
|
# same way, so both sides run the shared rule table. The Go side is covered
|
||||||
|
# by the test job above, this runs the ui side of it.
|
||||||
|
name: Test shared category rules
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Run the shared category rule table on the ui side
|
||||||
|
working-directory: ui/flutter
|
||||||
|
run: |
|
||||||
|
flutter pub get
|
||||||
|
flutter gen-l10n
|
||||||
|
flutter test test/api/downloader_config_test.dart
|
||||||
|
test-flutter-task-context-menu:
|
||||||
|
name: Test task context menus and text inputs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
env:
|
||||||
|
CHROME_EXECUTABLE: /usr/bin/google-chrome
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Prepare Flutter tests
|
||||||
|
working-directory: ui/flutter
|
||||||
|
run: |
|
||||||
|
flutter pub get
|
||||||
|
flutter gen-l10n
|
||||||
|
- name: Test task menu actions and native input policy
|
||||||
|
working-directory: ui/flutter
|
||||||
|
run: |
|
||||||
|
flutter test --no-pub test/features/tasks/presentation/widgets/task_context_menu_test.dart test/shared/widgets/app_text_field_test.dart
|
||||||
|
flutter test --no-pub test/widget_test.dart --plain-name 'task context menu exposes'
|
||||||
|
- name: Test browser menu cancellation and input policy
|
||||||
|
working-directory: ui/flutter
|
||||||
|
run: |
|
||||||
|
"$CHROME_EXECUTABLE" --version
|
||||||
|
flutter test --no-pub --platform chrome test/features/tasks/presentation/widgets/task_context_menu_test.dart test/shared/widgets/app_text_field_test.dart
|
||||||
test-webview-integration:
|
test-webview-integration:
|
||||||
name: Test webview integration (${{ matrix.name }})
|
name: Test webview integration (${{ matrix.name }})
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Beyond core download management, Gopeed offers browser integration, JavaScript e
|
|||||||
|
|
||||||
Visit ✈ [Official Website](https://gopeed.com)
|
Visit ✈ [Official Website](https://gopeed.com)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ Gopeed(**Go Speed** の略)は、Go と Flutter で開発された高速で
|
|||||||
|
|
||||||
✈ [公式サイト](https://gopeed.com)
|
✈ [公式サイト](https://gopeed.com)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## ✨ 主な機能
|
## ✨ 主な機能
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ Ngoài việc quản lý tải xuống hằng ngày, Gopeed còn hỗ trợ tíc
|
|||||||
|
|
||||||
Truy cập ✈ [Trang web chính thức](https://gopeed.com)
|
Truy cập ✈ [Trang web chính thức](https://gopeed.com)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## ✨ Tính năng chính
|
## ✨ Tính năng chính
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ Gopeed(全称 **Go Speed**,也叫“够快下载器”)是一款使用 Go
|
|||||||
|
|
||||||
访问 ✈ [官方网站](https://gopeed.com)
|
访问 ✈ [官方网站](https://gopeed.com)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## ✨ 主要特性
|
## ✨ 主要特性
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ Gopeed(**Go Speed** 的縮寫)是一款以 Go 與 Flutter 開發的高速、
|
|||||||
|
|
||||||
瀏覽 ✈ [官方網站](https://gopeed.com)
|
瀏覽 ✈ [官方網站](https://gopeed.com)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## ✨ 主要功能
|
## ✨ 主要功能
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 792 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 787 KiB |
@@ -0,0 +1,52 @@
|
|||||||
|
# Batch task creation
|
||||||
|
|
||||||
|
`POST /api/v1/tasks/batch` accepts ordinary and grouped batches:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"group": true,
|
||||||
|
"groupName": "album",
|
||||||
|
"opts": {"path": "/downloads"},
|
||||||
|
"reqs": [
|
||||||
|
{"rid": "resolved-resource-id", "opts": {"selectFiles": [1]}},
|
||||||
|
{"req": {"url": "https://example.com/cover.jpg"}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each member accepts `rid` or `req`. A nonempty `rid` takes priority and creates a
|
||||||
|
task from the cached resolve result. A member's `opts`, when present, replaces
|
||||||
|
batch `opts` as a whole; omitted fields do not inherit individual batch values.
|
||||||
|
Members without `opts` use batch `opts`. BT file selections use the indexes from
|
||||||
|
the resolve result. Requests without `rid` continue to work.
|
||||||
|
|
||||||
|
For groups, batch `opts.path` selects the parent directory. Members inheriting
|
||||||
|
batch options, or with an empty path, use the reserved group directory. Explicit
|
||||||
|
relative member paths are resolved under that directory; do not prefix them with
|
||||||
|
the batch parent path. For example, parent `downloads` and member path `nested`
|
||||||
|
place the member in `downloads/<group name>/nested`. Absolute paths under the
|
||||||
|
parent directory preserve their relative structure within the group. In ordinary
|
||||||
|
batches, an empty member path uses the configured default download directory.
|
||||||
|
|
||||||
|
All members are attempted. If at least one task is persisted, the response is
|
||||||
|
successful and contains the created task IDs. A task that fails to start after
|
||||||
|
being persisted is included; use the existing task controls to retry it.
|
||||||
|
Successful tasks are kept when other members fail. If every member fails, the
|
||||||
|
API returns an error and removes any newly reserved empty group and directory.
|
||||||
|
|
||||||
|
Both ordinary and grouped batches now return an object in `data`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"code": 0, "msg": "", "data": {"taskIds": ["task-1", "task-2"], "parentId": "group-id"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ordinary batches omit `parentId`. **This replaces the former array response for
|
||||||
|
ordinary batches.** The outer `code`/`msg`/`data` envelope is unchanged. Clients
|
||||||
|
must read `data.taskIds`; there are no per-member results. A successful response
|
||||||
|
completes the batch, including partial success, so clients must not automatically
|
||||||
|
resubmit missing members.
|
||||||
|
|
||||||
|
With `group: true`, the server generates the group ID and reserves its directory.
|
||||||
|
Creation does not accept a client group ID or append to an existing group. Group
|
||||||
|
metadata remains available through the existing task list and detail responses;
|
||||||
|
`parentId` identifies membership for group operations.
|
||||||
+7
-1
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/GopeedLab/gopeed/cmd"
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
)
|
)
|
||||||
@@ -13,5 +16,8 @@ func main() {
|
|||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
WebEnable: true,
|
WebEnable: true,
|
||||||
}
|
}
|
||||||
cmd.Start(cfg)
|
if err := cmd.Start(cfg); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -166,7 +166,7 @@ func runResolve(args []string) error {
|
|||||||
result, err := d.Resolve(&base.Request{
|
result, err := d.Resolve(&base.Request{
|
||||||
URL: url,
|
URL: url,
|
||||||
Labels: map[string]string(labels),
|
Labels: map[string]string(labels),
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@ func runDownload(args []string) error {
|
|||||||
result, err := d.Resolve(&base.Request{
|
result, err := d.Resolve(&base.Request{
|
||||||
URL: url,
|
URL: url,
|
||||||
Labels: map[string]string(labels),
|
Labels: map[string]string(labels),
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -16,17 +16,18 @@ import (
|
|||||||
//go:embed banner.txt
|
//go:embed banner.txt
|
||||||
var banner string
|
var banner string
|
||||||
|
|
||||||
func Start(cfg *model.StartConfig) {
|
func Start(cfg *model.StartConfig) error {
|
||||||
fmt.Println(banner)
|
|
||||||
srv, listener, err := rest.BuildServer(cfg)
|
srv, listener, err := rest.BuildServer(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
fmt.Println(banner)
|
||||||
downloadCfg, err := rest.Downloader.GetConfig()
|
downloadCfg, err := rest.Downloader.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
listener.Close()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
if downloadCfg.FirstLoad {
|
if rest.Downloader.IsFirstLoad() {
|
||||||
// Set default download config
|
// Set default download config
|
||||||
if cfg.DownloadConfig != nil {
|
if cfg.DownloadConfig != nil {
|
||||||
cfg.DownloadConfig.Merge(downloadCfg)
|
cfg.DownloadConfig.Merge(downloadCfg)
|
||||||
@@ -51,13 +52,20 @@ func Start(cfg *model.StartConfig) {
|
|||||||
rest.Downloader.PutConfig(downloadCfg)
|
rest.Downloader.PutConfig(downloadCfg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The default download dir is resolved only after the core starts, so
|
||||||
|
// seed the built-in categories now that the dir is known. Init is
|
||||||
|
// idempotent and leaves an existing category list untouched.
|
||||||
|
downloadCfg.Init()
|
||||||
|
rest.Downloader.PutConfig(downloadCfg)
|
||||||
}
|
}
|
||||||
watchExit()
|
watchExit()
|
||||||
|
|
||||||
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
||||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func watchExit() {
|
func watchExit() {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStartInvalidWebAPITokenReturnsError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
webAuth *model.WebAuth
|
||||||
|
}{
|
||||||
|
{name: "no credentials"},
|
||||||
|
{name: "missing username", webAuth: &model.WebAuth{Password: "secret"}},
|
||||||
|
{name: "missing password", webAuth: &model.WebAuth{Username: "gopeed"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := Start(&model.StartConfig{
|
||||||
|
WebEnable: true,
|
||||||
|
ApiToken: "test-token",
|
||||||
|
WebAuth: tt.webAuth,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Start() error = nil, want web authentication validation error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "web authentication username and password are required") {
|
||||||
|
t.Fatalf("Start() error = %q, want missing web authentication credentials", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -52,8 +52,8 @@ func loadCliArgs() *args {
|
|||||||
flag.StringVar(cfg.Username, "username", "gopeed", "Web Authentication Username")
|
flag.StringVar(cfg.Username, "username", "gopeed", "Web Authentication Username")
|
||||||
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||||
flag.StringVar(cfg.Password, "password", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
flag.StringVar(cfg.Password, "password", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||||
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
cfg.ApiToken = flag.String("T", "", "API token (requires web authentication with username and password)")
|
||||||
flag.StringVar(cfg.ApiToken, "api-token", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
flag.StringVar(cfg.ApiToken, "api-token", "", "API token (requires web authentication with username and password)")
|
||||||
flag.BoolVar(&cfg.MCPEnable, "mcp-enable", false, "Enable MCP endpoint")
|
flag.BoolVar(&cfg.MCPEnable, "mcp-enable", false, "Enable MCP endpoint")
|
||||||
cfg.TempDir = flag.String("temp-dir", "", "Temporary directory (defaults to the OS temporary directory)")
|
cfg.TempDir = flag.String("temp-dir", "", "Temporary directory (defaults to the OS temporary directory)")
|
||||||
cfg.StorageDir = flag.String("d", "", "Storage directory")
|
cfg.StorageDir = flag.String("d", "", "Storage directory")
|
||||||
|
|||||||
+4
-1
@@ -62,7 +62,10 @@ func main() {
|
|||||||
WebFS: sub,
|
WebFS: sub,
|
||||||
WebAuth: webAuth,
|
WebAuth: webAuth,
|
||||||
}
|
}
|
||||||
cmd.Start(cfg)
|
if err := cmd.Start(cfg); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isNotBlank(str *string) bool {
|
func isNotBlank(str *string) bool {
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ require (
|
|||||||
github.com/bodgit/sevenzip v1.6.1
|
github.com/bodgit/sevenzip v1.6.1
|
||||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
|
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||||
github.com/go-git/go-git/v5 v5.8.1
|
github.com/fclairamb/ftpserverlib v0.31.0
|
||||||
|
github.com/go-git/go-git/v5 v5.19.2
|
||||||
github.com/google/jsonschema-go v0.4.3
|
github.com/google/jsonschema-go v0.4.3
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/imroc/req/v3 v3.52.2
|
github.com/imroc/req/v3 v3.58.0
|
||||||
|
github.com/jlaffaye/ftp v0.2.4
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||||
github.com/mattn/go-ieproxy v0.0.12
|
github.com/mattn/go-ieproxy v0.0.12
|
||||||
github.com/mholt/archives v0.1.5
|
github.com/mholt/archives v0.1.5
|
||||||
@@ -23,10 +25,17 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/tetratelabs/wazero v1.12.0
|
github.com/tetratelabs/wazero v1.12.0
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/onsi/gomega v1.36.3 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -39,48 +48,40 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||||
github.com/felixge/fgprof v0.9.5 // indirect
|
github.com/felixge/fgprof v0.9.5 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/icholy/digest v1.1.0 // indirect
|
github.com/icholy/digest v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.2 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||||
github.com/minio/minlz v1.0.1 // indirect
|
github.com/minio/minlz v1.0.1 // indirect
|
||||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
|
||||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
github.com/pion/transport/v4 v4.0.2 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.51.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||||
github.com/segmentio/asm v1.1.3 // indirect
|
github.com/segmentio/asm v1.1.3 // indirect
|
||||||
github.com/segmentio/encoding v0.5.4 // indirect
|
github.com/segmentio/encoding v0.5.4 // indirect
|
||||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
|
||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
|
||||||
go.uber.org/mock v0.5.1 // indirect
|
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||||
golang.org/x/oauth2 v0.35.0 // indirect
|
golang.org/x/oauth2 v0.35.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1
|
github.com/Microsoft/go-winio v0.6.2
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.9.4
|
github.com/RoaringBitmap/roaring v1.9.4
|
||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
github.com/anacrolix/chansync v0.7.0 // indirect
|
github.com/anacrolix/chansync v0.7.0 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.23.0 // indirect
|
github.com/anacrolix/dht/v2 v2.23.0 // indirect
|
||||||
github.com/anacrolix/envpprof v1.5.0 // indirect
|
github.com/anacrolix/envpprof v1.5.0 // indirect
|
||||||
github.com/anacrolix/generics v0.2.0 // indirect
|
github.com/anacrolix/generics v0.2.0
|
||||||
github.com/anacrolix/go-libutp v1.3.2 // indirect
|
github.com/anacrolix/go-libutp v1.3.2 // indirect
|
||||||
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // indirect
|
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // indirect
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
@@ -96,7 +97,7 @@ require (
|
|||||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -104,13 +105,13 @@ require (
|
|||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.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/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
github.com/go-git/go-billy/v5 v5.9.2 // indirect
|
||||||
github.com/go-llsqlite/adapter v0.2.0 // indirect
|
github.com/go-llsqlite/adapter v0.2.0 // indirect
|
||||||
github.com/go-llsqlite/crawshaw v0.6.0 // indirect
|
github.com/go-llsqlite/crawshaw v0.6.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
|
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
@@ -128,7 +129,7 @@ require (
|
|||||||
github.com/multiformats/go-varint v0.1.0 // indirect
|
github.com/multiformats/go-varint v0.1.0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pion/datachannel v1.6.0 // indirect
|
github.com/pion/datachannel v1.6.0 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
github.com/pion/dtls/v3 v3.1.4 // indirect
|
||||||
github.com/pion/ice/v4 v4.2.0 // indirect
|
github.com/pion/ice/v4 v4.2.0 // indirect
|
||||||
github.com/pion/interceptor v0.1.42 // indirect
|
github.com/pion/interceptor v0.1.42 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
@@ -139,15 +140,15 @@ require (
|
|||||||
github.com/pion/sctp v1.9.1 // indirect
|
github.com/pion/sctp v1.9.1 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.17 // indirect
|
github.com/pion/sdp/v3 v3.0.17 // indirect
|
||||||
github.com/pion/srtp/v3 v3.0.10 // indirect
|
github.com/pion/srtp/v3 v3.0.10 // indirect
|
||||||
github.com/pion/stun/v3 v3.1.1 // indirect
|
github.com/pion/stun/v3 v3.1.5 // indirect
|
||||||
github.com/pion/turn/v4 v4.1.4 // indirect
|
github.com/pion/turn/v4 v4.1.4 // indirect
|
||||||
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 // indirect
|
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||||
github.com/protolambda/ctxlock v0.1.0 // indirect
|
github.com/protolambda/ctxlock v0.1.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/tidwall/btree v1.8.1 // indirect
|
github.com/tidwall/btree v1.8.1 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
@@ -155,13 +156,11 @@ require (
|
|||||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.55.0 // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
golang.org/x/net v0.58.0
|
||||||
golang.org/x/net v0.50.0
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/text v0.41.0
|
||||||
golang.org/x/text v0.34.0
|
golang.org/x/time v0.15.0
|
||||||
golang.org/x/time v0.15.0 // indirect
|
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
lukechampine.com/blake3 v1.4.1 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
modernc.org/libc v1.67.4 // indirect
|
modernc.org/libc v1.67.4 // indirect
|
||||||
|
|||||||
@@ -25,29 +25,17 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmG
|
|||||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260915010614-2567e492369a h1:Vzz+8aMVX9ZPPpkxgxa7mCorrAgdXCZL5EunOpOBvao=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260915010614-2567e492369a/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917153919-dcf00737f32a h1:nz1bC6TGwQ1kv/OVLahNZ0EEtb7v5LHfceAkW4MvS6I=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917153919-dcf00737f32a/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917155039-1ee7ffb4169a h1:yudcfXpipKLnMaaw1losn6YeOBjvj2rAFicfjqYJexY=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917155039-1ee7ffb4169a/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917155218-bd16da0d6f53 h1:nMSHKhbrEaWPE/x3mYl/270GPjswbUmS+E7yYjly4xk=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917155218-bd16da0d6f53/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917161756-2a4f989dcb1d h1:4fjlhJpBWa6IcdKVPkEMMIDd6Xo9JM09dJHdKU+1V7k=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917161756-2a4f989dcb1d/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917163929-ca26c3e6bcfa h1:VkdBMcNHYTqCMwOOCimA7b3xZKV/qFIGrhh3+vm9n3s=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260917163929-ca26c3e6bcfa/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
|
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914 h1:9VJQZIZ1R6T4m+4vlNQYNXSPNUv2gf2PiTfIXiQ3MTw=
|
github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914 h1:9VJQZIZ1R6T4m+4vlNQYNXSPNUv2gf2PiTfIXiQ3MTw=
|
||||||
github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
|
github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914/go.mod h1:oNO07SCX8xgoHLds2OBwML8TU4xils88vMQ4gadKUHQ=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
|
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
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.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
||||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||||
@@ -57,8 +45,6 @@ github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
|
|||||||
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
||||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
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/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/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
|
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
|
||||||
github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o=
|
github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o=
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
|
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
|
||||||
@@ -155,7 +141,6 @@ 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-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 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
|
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
@@ -172,11 +157,12 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
|||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -199,12 +185,14 @@ 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/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
|
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
|
||||||
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
||||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/fclairamb/ftpserverlib v0.31.0 h1:ws4fLJvuU/mgg1uIgRjCJfc6CrFSgIBAOxVsnawXegE=
|
||||||
|
github.com/fclairamb/ftpserverlib v0.31.0/go.mod h1:C5hUT0sd9P5XsHMxsmpcrT6wdZexu3qtUu1z0Ih6aG0=
|
||||||
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||||
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
||||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||||
@@ -214,8 +202,8 @@ github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
|
|||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
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-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-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/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
@@ -224,12 +212,12 @@ github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1T
|
|||||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
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.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
|
||||||
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
|
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
|
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||||
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
|
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
@@ -250,8 +238,6 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
@@ -264,8 +250,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
|
|||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/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-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-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
@@ -290,10 +276,13 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
|||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
||||||
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||||
@@ -321,11 +310,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
@@ -343,10 +327,12 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||||
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||||
github.com/imroc/req/v3 v3.52.2 h1:xJocr1aIv0a2K9knfBQ4JnZHk+kWTITdjf0mgDg229I=
|
github.com/imroc/req/v3 v3.58.0 h1:rBqJhv2gZjMymGot4vKmNSUs/mAITMpGASqElhuMvS4=
|
||||||
github.com/imroc/req/v3 v3.52.2/go.mod h1:dBGsDloOSZJcFs6PnTjZXYBJK70OXbZpizHBLNqcH2k=
|
github.com/imroc/req/v3 v3.58.0/go.mod h1:rC7NTGgfMSkVWR4GO46rMeJsUfaeOpa6pLsgpq9q3zI=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
|
github.com/jlaffaye/ftp v0.2.4 h1:JqI85DdkfZj8ntaHk8W9U2SC3jNfiPUU70+wtIWmlfE=
|
||||||
|
github.com/jlaffaye/ftp v0.2.4/go.mod h1:Y1ZnkzxownGIuX7xQ1mQzzkZ21+DbjVIyeKL/V+IIz4=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
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/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
@@ -359,8 +345,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
|
|||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
@@ -370,7 +356,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
|
|||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
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.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -383,8 +368,6 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
|
|||||||
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
|
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 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
|
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M=
|
github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M=
|
||||||
@@ -422,13 +405,10 @@ github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJh
|
|||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
|
||||||
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
|
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
|
||||||
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
@@ -440,8 +420,8 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU
|
|||||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||||
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||||
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
|
||||||
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
|
||||||
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
|
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
|
||||||
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
|
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
|
||||||
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
||||||
@@ -462,18 +442,18 @@ github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
|||||||
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||||
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||||
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8=
|
||||||
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
|
||||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
|
||||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
|
||||||
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
|
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
|
||||||
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
|
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
|
||||||
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 h1:gnkvpxCSzeTKovDsxeFFl+W/i6WydzSHhu5lH/RpFfM=
|
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 h1:gnkvpxCSzeTKovDsxeFFl+W/i6WydzSHhu5lH/RpFfM=
|
||||||
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7/go.mod h1:/td+qaV3q20qTbRjYV9ABWjmCkvt+Pog/S+NqLMoUKk=
|
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7/go.mod h1:/td+qaV3q20qTbRjYV9ABWjmCkvt+Pog/S+NqLMoUKk=
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
@@ -481,10 +461,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
|
||||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
@@ -504,13 +481,13 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
|
|||||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
||||||
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
@@ -527,17 +504,19 @@ github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR
|
|||||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
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/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||||
|
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||||
|
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||||
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
|
||||||
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
|
||||||
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
|
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
|
||||||
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
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.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
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.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||||
github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
|
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
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/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
@@ -564,8 +543,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
||||||
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
||||||
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
|
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
|
||||||
@@ -606,12 +585,12 @@ go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF
|
|||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
@@ -621,11 +600,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
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.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -635,8 +612,8 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -658,8 +635,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
|||||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -681,14 +658,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
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/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -705,8 +680,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -739,26 +714,20 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.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.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.5.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -766,13 +735,11 @@ 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
@@ -805,8 +772,8 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
|
|||||||
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -851,8 +818,6 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
|
|||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"github.com/GopeedLab/gopeed/internal/tempfiles"
|
"github.com/GopeedLab/gopeed/internal/tempfiles"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,6 +16,9 @@ type Controller struct {
|
|||||||
TempDir string
|
TempDir string
|
||||||
GetConfig func(v any)
|
GetConfig func(v any)
|
||||||
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
|
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
|
||||||
|
// Logger is the downloader's core logger. It may be nil, in which case
|
||||||
|
// fetchers log nothing.
|
||||||
|
Logger *zerolog.Logger
|
||||||
FileController
|
FileController
|
||||||
//ContextDialer() (proxy.Dialer, error)
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
// Each download task will have a corresponding Fetcher instance for the management of the download task
|
// Each download task will have a corresponding Fetcher instance for the management of the download task
|
||||||
type Fetcher interface {
|
type Fetcher interface {
|
||||||
Setup(ctl *controller.Controller)
|
Setup(ctl *controller.Controller)
|
||||||
Resolve(req *base.Request, opts *base.Options) error
|
Resolve(req *base.Request) error
|
||||||
Start() error
|
Start() error
|
||||||
// Patch modifies task-specific data based on the protocol.
|
// Patch modifies task-specific data based on the protocol.
|
||||||
// For HTTP: can modify Request info (URL, headers, etc.)
|
// For HTTP: can modify Request info (URL, headers, etc.)
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package fetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TriggerSource string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TriggerScheduled TriggerSource = "scheduled"
|
||||||
|
TriggerManual TriggerSource = "manual"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrJobSkipped = errors.New("protocol job skipped")
|
||||||
|
var ErrConfigChanged = errors.New("protocol configuration changed during execution")
|
||||||
|
|
||||||
|
// ConfigState embeds the common backend-owned state section in a protocol config.
|
||||||
|
// Public config PUTs always retain this section from the latest stored config.
|
||||||
|
type ConfigState[T any] struct {
|
||||||
|
State T `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtocolHost is scoped to one manager. UpdateState runs against the latest
|
||||||
|
// config under Downloader's lock; callbacks must not perform network I/O.
|
||||||
|
type ProtocolHost struct {
|
||||||
|
Read func(any) error
|
||||||
|
UpdateState func(func(config, state json.RawMessage) (any, error)) error
|
||||||
|
Fetch func(context.Context, string, int64, func([]byte) error) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig[T any](host ProtocolHost) (*T, error) {
|
||||||
|
var cfg T
|
||||||
|
if err := host.Read(&cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateState gives the callback a detached config snapshot and mutable state.
|
||||||
|
// Only the state is committed; edits to the config snapshot are discarded.
|
||||||
|
func UpdateState[C, S any](host ProtocolHost, update func(*C, *S) error) error {
|
||||||
|
return host.UpdateState(func(rawConfig, rawState json.RawMessage) (any, error) {
|
||||||
|
var cfg C
|
||||||
|
var state S
|
||||||
|
if err := json.Unmarshal(rawConfig, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rawState, &state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := update(&cfg, &state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &state, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type CronJob struct {
|
||||||
|
Name string
|
||||||
|
Every time.Duration
|
||||||
|
Timeout time.Duration
|
||||||
|
Run func(context.Context, TriggerSource) error
|
||||||
|
}
|
||||||
|
type CronProvider interface{ CronJobs(ProtocolHost) []CronJob }
|
||||||
|
|
||||||
|
// ConfigNormalizer is optional. It owns protocol validation and derived fields.
|
||||||
|
type ConfigNormalizer interface {
|
||||||
|
NormalizeConfig(json.RawMessage) (any, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# GitHub mirror execution
|
||||||
|
|
||||||
|
This package owns GitHub URL rewriting and execution strategies. It depends only
|
||||||
|
on the standard library and does not depend on Downloader, HTTP clients, Git, or
|
||||||
|
storage. Downloader persists `Config` at its top-level `githubMirror` field;
|
||||||
|
`pkg/base` preserves the existing configuration names through aliases.
|
||||||
|
|
||||||
|
## Race: resource reads
|
||||||
|
|
||||||
|
```go
|
||||||
|
body, err := githubmirror.Race(ctx, cfg.GithubMirror, rawURL,
|
||||||
|
githubmirror.Options[[]byte]{
|
||||||
|
Kind: githubmirror.Resource,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
func(ctx context.Context, candidate string) ([]byte, error) {
|
||||||
|
return fetchAndValidate(ctx, client, candidate)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The callback owns HTTP requests, size limits, and content validation. The first
|
||||||
|
completed valid result wins. Other operations are canceled and drained. There is
|
||||||
|
no HEAD-probe assumption or client-specific HTTP behavior inside the executor.
|
||||||
|
|
||||||
|
## Fallback: repository operations
|
||||||
|
|
||||||
|
```go
|
||||||
|
repo, err := githubmirror.Fallback(ctx, cfg.GithubMirror, officialURL,
|
||||||
|
githubmirror.Options[*git.Repository]{
|
||||||
|
Kind: githubmirror.Repository,
|
||||||
|
},
|
||||||
|
func(ctx context.Context, candidate string) (*git.Repository, error) {
|
||||||
|
// Use a clean temporary checkout for each attempt.
|
||||||
|
return clone(ctx, candidate)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The official URL is attempted first; mirrors follow their configured order.
|
||||||
|
jsDelivr is excluded for repositories. Stop on the first success. Perform
|
||||||
|
persistent installation or other business state changes after success, rather
|
||||||
|
than inside a potentially repeated attempt.
|
||||||
|
|
||||||
|
## Results with resources
|
||||||
|
|
||||||
|
For temporary files, set `Options[*Download].Discard` to remove the file. Return
|
||||||
|
the partial `*Download` alongside errors, so cleanup also covers failed reads or
|
||||||
|
validation. The executor discards every failure and every race loser exactly
|
||||||
|
once. The caller owns and must track/release the winning result.
|
||||||
|
|
||||||
|
- `Timeout == 0` uses 30 seconds per resource attempt and no overall deadline
|
||||||
|
for repository attempts; a negative timeout is invalid. Extension Git HTTP
|
||||||
|
transport limits connections/TLS handshakes to 10 seconds and resets a 60-second
|
||||||
|
read deadline as data arrives. Slow continuous transfers can finish, while
|
||||||
|
stalled sources fail and allow ordered fallback. Parent cancellation still applies.
|
||||||
|
- Attempts must honor the supplied context; draining waits for them to return.
|
||||||
|
- Attempt contexts are canceled after return, so callbacks must finish reading
|
||||||
|
HTTP bodies before returning. Returning a still-streaming response is unsupported.
|
||||||
|
- Discard callbacks must accept zero values, must not panic, and run serially.
|
||||||
|
- Parent cancellation wins over a result selected while the parent is canceled;
|
||||||
|
the selected result is then discarded as well.
|
||||||
|
- Errors preserve their causes through wrapping/joining for `errors.Is`.
|
||||||
|
- `github.com` and `raw.githubusercontent.com` expand into eligible mirrors.
|
||||||
|
jsDelivr is only eligible for raw source resources. `api.github.com` is never
|
||||||
|
mirrored and is rejected by the public mirror candidates API.
|
||||||
|
- Client application update checks request the official GitHub API directly,
|
||||||
|
falling back to the independent Gopeed website API on request or validation
|
||||||
|
failure. Only package downloads obtain URLs from
|
||||||
|
`POST /api/v1/mirrors/github/candidates`. Candidate generation never performs
|
||||||
|
HTTP requests.
|
||||||
|
- Disabled mirrors and other URLs execute only the original URL.
|
||||||
|
- Config is a caller-owned immutable snapshot for the duration of a call.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// Package githubmirror rewrites GitHub URLs and executes mirrored operations.
|
||||||
|
package githubmirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mirror struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
IsBuiltIn bool `json:"isBuiltIn"`
|
||||||
|
IsDeleted bool `json:"isDeleted"`
|
||||||
|
}
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Mirrors []Mirror `json:"mirrors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{Enabled: true, Mirrors: []Mirror{
|
||||||
|
{Type: "jsdelivr", URL: "https://fastly.jsdelivr.net/gh", IsBuiltIn: true},
|
||||||
|
{Type: "ghProxy", URL: "https://fastgit.cc", IsBuiltIn: true},
|
||||||
|
{Type: "ghProxy", URL: "https://gh-proxy.com", IsBuiltIn: true},
|
||||||
|
{Type: "ghProxy", URL: "https://ghfast.top", IsBuiltIn: true},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
if len(c.Mirrors) > 32 {
|
||||||
|
return fmt.Errorf("too many GitHub mirrors")
|
||||||
|
}
|
||||||
|
for _, mirror := range c.Mirrors {
|
||||||
|
u, err := url.Parse(mirror.URL)
|
||||||
|
if err != nil || u == nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
|
||||||
|
return fmt.Errorf("invalid GitHub mirror URL")
|
||||||
|
}
|
||||||
|
if mirror.Type != "jsdelivr" && mirror.Type != "ghProxy" {
|
||||||
|
return fmt.Errorf("invalid GitHub mirror type: %s", mirror.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind describes what a candidate endpoint must support.
|
||||||
|
type Kind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
Resource Kind = iota
|
||||||
|
Repository
|
||||||
|
)
|
||||||
|
|
||||||
|
// Candidates preserves the official URL first. jsDelivr is only eligible for
|
||||||
|
// raw source resources; Git operations use mirrors supporting Git Smart HTTP.
|
||||||
|
// GitHub API URLs are never mirrored.
|
||||||
|
func Candidates(c *Config, raw string, kind Kind) []string {
|
||||||
|
result := []string{raw}
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil || c == nil || !c.Enabled || u.User != nil || (u.Scheme != "https" && u.Scheme != "http") {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if u.Host != "github.com" && u.Host != "raw.githubusercontent.com" {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
seen := map[string]bool{raw: true}
|
||||||
|
for _, mirror := range c.Mirrors {
|
||||||
|
if mirror.IsDeleted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var candidate string
|
||||||
|
switch mirror.Type {
|
||||||
|
case "ghProxy":
|
||||||
|
candidate = strings.TrimRight(mirror.URL, "/") + "/" + raw
|
||||||
|
case "jsdelivr":
|
||||||
|
if kind == Repository || u.Host != "raw.githubusercontent.com" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(strings.TrimPrefix(u.EscapedPath(), "/"), "/", 4)
|
||||||
|
if len(parts) != 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidate = strings.TrimRight(mirror.URL, "/") + "/" + parts[0] + "/" + parts[1] + "@" + parts[2] + "/" + parts[3]
|
||||||
|
}
|
||||||
|
if candidate != "" && !seen[candidate] {
|
||||||
|
result = append(result, candidate)
|
||||||
|
seen[candidate] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package githubmirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMirrorCandidates(t *testing.T) {
|
||||||
|
cfg := &Config{Enabled: true, Mirrors: []Mirror{
|
||||||
|
{Type: "jsdelivr", URL: "https://cdn.example/gh/"},
|
||||||
|
{Type: "ghProxy", URL: "https://mirror.example/"},
|
||||||
|
{Type: "ghProxy", URL: "https://deleted.example", IsDeleted: true},
|
||||||
|
}}
|
||||||
|
source := "https://raw.githubusercontent.com/owner/repo/main/a.txt"
|
||||||
|
want := []string{source, "https://cdn.example/gh/owner/repo@main/a.txt", "https://mirror.example/" + source}
|
||||||
|
if got := Candidates(cfg, source, Resource); !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("got %v", got)
|
||||||
|
}
|
||||||
|
repo := "https://github.com/owner/repo.git"
|
||||||
|
if got := Candidates(cfg, repo, Repository); !reflect.DeepEqual(got, []string{repo, "https://mirror.example/" + repo}) {
|
||||||
|
t.Fatalf("git candidates %v", got)
|
||||||
|
}
|
||||||
|
for _, raw := range []string{"https://github.com.evil.example/a", "https://example.com/a", "https://user:[email protected]/a"} {
|
||||||
|
if got := Candidates(cfg, raw, Resource); !reflect.DeepEqual(got, []string{raw}) {
|
||||||
|
t.Fatalf("unexpected mirror: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiURL := "https://api.github.com/repos/GopeedLab/gopeed/releases?per_page=10"
|
||||||
|
for _, kind := range []Kind{Resource, Repository} {
|
||||||
|
if got := Candidates(cfg, apiURL, kind); !reflect.DeepEqual(got, []string{apiURL}) {
|
||||||
|
t.Fatalf("GitHub API must never use mirrors: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.Enabled = false
|
||||||
|
if got := Candidates(cfg, source, Resource); !reflect.DeepEqual(got, []string{source}) {
|
||||||
|
t.Fatalf("disabled mirror: %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package githubmirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options configures execution, not URL persistence. Discard releases results
|
||||||
|
// from failed attempts and successful race losers, including partial results
|
||||||
|
// returned together with an error. The caller owns the winning result.
|
||||||
|
type Options[T any] struct {
|
||||||
|
Kind Kind
|
||||||
|
// Timeout limits each attempt. Zero uses 30 seconds for resources and no
|
||||||
|
// overall deadline for repositories, whose transport owns idle timeouts.
|
||||||
|
Timeout time.Duration
|
||||||
|
Discard func(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt must honor ctx and return a completed, validated result. Its context
|
||||||
|
// is canceled after it returns, so live response streams must not be returned.
|
||||||
|
// Return any partially acquired resources along with errors so Discard can
|
||||||
|
// release them. Failed attempts must not mutate persistent business state.
|
||||||
|
type Attempt[T any] func(context.Context, string) (T, error)
|
||||||
|
|
||||||
|
const defaultTimeout = 30 * time.Second
|
||||||
|
|
||||||
|
func prepare[T any](ctx context.Context, config *Config, raw string, options Options[T], attempt Attempt[T]) ([]string, time.Duration, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if raw == "" {
|
||||||
|
return nil, 0, errors.New("empty GitHub source URL")
|
||||||
|
}
|
||||||
|
if options.Kind != Resource && options.Kind != Repository {
|
||||||
|
return nil, 0, errors.New("invalid GitHub resource kind")
|
||||||
|
}
|
||||||
|
if options.Timeout < 0 {
|
||||||
|
return nil, 0, errors.New("invalid GitHub attempt timeout")
|
||||||
|
}
|
||||||
|
if attempt == nil {
|
||||||
|
return nil, 0, errors.New("GitHub mirror attempt is required")
|
||||||
|
}
|
||||||
|
if config != nil {
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timeout := options.Timeout
|
||||||
|
if timeout == 0 && options.Kind == Resource {
|
||||||
|
timeout = defaultTimeout
|
||||||
|
}
|
||||||
|
return Candidates(config, raw, options.Kind), timeout, nil
|
||||||
|
}
|
||||||
|
func invoke[T any](ctx context.Context, timeout time.Duration, candidate string, attempt Attempt[T]) (value T, err error) {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
if timeout > 0 {
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
|
} else {
|
||||||
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
defer func() {
|
||||||
|
if recovered := recover(); recovered != nil {
|
||||||
|
err = fmt.Errorf("GitHub mirror attempt panicked: %v", recovered)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
value, err = attempt(ctx, candidate)
|
||||||
|
if err == nil {
|
||||||
|
err = ctx.Err()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func discard[T any](options Options[T], value T) {
|
||||||
|
if options.Discard != nil {
|
||||||
|
options.Discard(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Race executes the official URL and eligible mirrors concurrently. The first
|
||||||
|
// completed successful result wins; losing attempts are canceled and drained
|
||||||
|
// before return so no temporary files or other resources outlive the call.
|
||||||
|
// HTTP status/content validation is the attempt's responsibility.
|
||||||
|
func Race[T any](ctx context.Context, config *Config, raw string, options Options[T], attempt Attempt[T]) (T, error) {
|
||||||
|
var zero T
|
||||||
|
candidates, timeout, err := prepare(ctx, config, raw, options, attempt)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
raceCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
type outcome struct {
|
||||||
|
index int
|
||||||
|
value T
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
outcomes := make(chan outcome, len(candidates))
|
||||||
|
for index, candidate := range candidates {
|
||||||
|
go func(index int, candidate string) {
|
||||||
|
value, err := invoke(raceCtx, timeout, candidate, attempt)
|
||||||
|
outcomes <- outcome{index, value, err}
|
||||||
|
}(index, candidate)
|
||||||
|
}
|
||||||
|
var winner T
|
||||||
|
won := false
|
||||||
|
failures := make([]error, len(candidates))
|
||||||
|
for range candidates {
|
||||||
|
outcome := <-outcomes
|
||||||
|
if !won && outcome.err == nil && ctx.Err() == nil {
|
||||||
|
winner = outcome.value
|
||||||
|
won = true
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
discard(options, outcome.value)
|
||||||
|
if outcome.err != nil {
|
||||||
|
failures[outcome.index] = fmt.Errorf("GitHub source %d: %w", outcome.index+1, outcome.err)
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
if won {
|
||||||
|
discard(options, winner)
|
||||||
|
}
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
if won {
|
||||||
|
return winner, nil
|
||||||
|
}
|
||||||
|
return zero, fmt.Errorf("all GitHub sources failed: %w", errors.Join(failures...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback executes the official URL first, then eligible mirrors in their
|
||||||
|
// configured order. It stops immediately on success and cleans every failure.
|
||||||
|
func Fallback[T any](ctx context.Context, config *Config, raw string, options Options[T], attempt Attempt[T]) (T, error) {
|
||||||
|
var zero T
|
||||||
|
candidates, timeout, err := prepare(ctx, config, raw, options, attempt)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
failures := make([]error, 0, len(candidates))
|
||||||
|
for index, candidate := range candidates {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
value, err := invoke(ctx, timeout, candidate, attempt)
|
||||||
|
if err == nil && ctx.Err() == nil {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
discard(options, value)
|
||||||
|
if parentErr := ctx.Err(); parentErr != nil {
|
||||||
|
return zero, parentErr
|
||||||
|
}
|
||||||
|
failures = append(failures, fmt.Errorf("GitHub source %d: %w", index+1, err))
|
||||||
|
}
|
||||||
|
return zero, fmt.Errorf("all GitHub sources failed: %w", errors.Join(failures...))
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package githubmirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testSource = "https://github.com/owner/repo.git"
|
||||||
|
|
||||||
|
func testConfig() *Config {
|
||||||
|
return &Config{Enabled: true, Mirrors: []Mirror{
|
||||||
|
{Type: "ghProxy", URL: "https://first.example"},
|
||||||
|
{Type: "ghProxy", URL: "https://second.example"},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
func TestRaceWinsAndDrainsOwnedResults(t *testing.T) {
|
||||||
|
config := testConfig()
|
||||||
|
candidates := Candidates(config, testSource, Resource)
|
||||||
|
var started sync.WaitGroup
|
||||||
|
started.Add(len(candidates))
|
||||||
|
discarded := []string{}
|
||||||
|
winner, err := Race(context.Background(), config, testSource, Options[string]{
|
||||||
|
Discard: func(value string) { discarded = append(discarded, value) },
|
||||||
|
}, func(ctx context.Context, address string) (string, error) {
|
||||||
|
started.Done()
|
||||||
|
started.Wait()
|
||||||
|
switch address {
|
||||||
|
case candidates[0]:
|
||||||
|
return address, errors.New("official offline")
|
||||||
|
case candidates[1]:
|
||||||
|
return address, nil
|
||||||
|
default:
|
||||||
|
<-ctx.Done()
|
||||||
|
return address, ctx.Err()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil || winner != candidates[1] {
|
||||||
|
t.Fatalf("winner=%s err=%v", winner, err)
|
||||||
|
}
|
||||||
|
if len(discarded) != 2 || !contains(discarded, candidates[0]) || !contains(discarded, candidates[2]) {
|
||||||
|
t.Fatalf("discarded %v", discarded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func contains(values []string, value string) bool {
|
||||||
|
for _, item := range values {
|
||||||
|
if item == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func TestRaceCleansSimultaneousSuccessfulLosers(t *testing.T) {
|
||||||
|
config := testConfig()
|
||||||
|
var started sync.WaitGroup
|
||||||
|
started.Add(3)
|
||||||
|
cleaned := []string{}
|
||||||
|
winner, err := Race(context.Background(), config, testSource, Options[string]{Discard: func(value string) { cleaned = append(cleaned, value) }}, func(_ context.Context, address string) (string, error) {
|
||||||
|
started.Done()
|
||||||
|
started.Wait()
|
||||||
|
return address, nil
|
||||||
|
})
|
||||||
|
if err != nil || winner == "" || len(cleaned) != 2 || contains(cleaned, winner) {
|
||||||
|
t.Fatalf("winner=%s discarded=%v err=%v", winner, cleaned, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestRaceParentCancellationCleansWinnerToo(t *testing.T) {
|
||||||
|
config := testConfig()
|
||||||
|
config.Mirrors = config.Mirrors[:1]
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
cleaned := []string{}
|
||||||
|
winner, err := Race(ctx, config, testSource, Options[string]{Discard: func(value string) { cleaned = append(cleaned, value) }}, func(ctx context.Context, address string) (string, error) {
|
||||||
|
if address == testSource {
|
||||||
|
return address, nil
|
||||||
|
}
|
||||||
|
<-ctx.Done()
|
||||||
|
cancel()
|
||||||
|
return address, ctx.Err()
|
||||||
|
})
|
||||||
|
if !errors.Is(err, context.Canceled) || winner != "" || len(cleaned) != 2 || !contains(cleaned, testSource) {
|
||||||
|
t.Fatalf("winner=%s cleanup=%v err=%v", winner, cleaned, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestRaceTimeoutAndErrorAggregation(t *testing.T) {
|
||||||
|
expected := errors.New("offline")
|
||||||
|
config := testConfig()
|
||||||
|
config.Mirrors = config.Mirrors[:1]
|
||||||
|
discarded := 0
|
||||||
|
_, err := Race(context.Background(), config, testSource, Options[string]{Timeout: 10 * time.Millisecond, Discard: func(string) { discarded++ }}, func(ctx context.Context, address string) (string, error) {
|
||||||
|
if address == testSource {
|
||||||
|
return address, expected
|
||||||
|
}
|
||||||
|
<-ctx.Done()
|
||||||
|
return address, ctx.Err()
|
||||||
|
})
|
||||||
|
if !errors.Is(err, expected) || !errors.Is(err, context.DeadlineExceeded) || discarded != 2 {
|
||||||
|
t.Fatalf("discarded=%d err=%v", discarded, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestFallbackPreservesOrderAndStopsOnSuccess(t *testing.T) {
|
||||||
|
config := testConfig()
|
||||||
|
candidates := Candidates(config, testSource, Repository)
|
||||||
|
tried := []string{}
|
||||||
|
cleaned := []string{}
|
||||||
|
winner, err := Fallback(context.Background(), config, testSource, Options[string]{Kind: Repository, Discard: func(value string) { cleaned = append(cleaned, value) }}, func(_ context.Context, address string) (string, error) {
|
||||||
|
tried = append(tried, address)
|
||||||
|
if address == candidates[1] {
|
||||||
|
return address, nil
|
||||||
|
}
|
||||||
|
return address, errors.New("offline")
|
||||||
|
})
|
||||||
|
if err != nil || winner != candidates[1] || !reflect.DeepEqual(tried, candidates[:2]) || !reflect.DeepEqual(cleaned, candidates[:1]) {
|
||||||
|
t.Fatalf("winner=%s tried=%v discarded=%v err=%v", winner, tried, cleaned, err)
|
||||||
|
}
|
||||||
|
tried = nil
|
||||||
|
_, err = Fallback(context.Background(), config, testSource, Options[string]{Kind: Repository}, func(_ context.Context, address string) (string, error) {
|
||||||
|
tried = append(tried, address)
|
||||||
|
return address, nil
|
||||||
|
})
|
||||||
|
if err != nil || !reflect.DeepEqual(tried, candidates[:1]) {
|
||||||
|
t.Fatalf("tried=%v err=%v", tried, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestFallbackTimeoutContinuesWithFreshContext(t *testing.T) {
|
||||||
|
discarded := []string{}
|
||||||
|
winner, err := Fallback(context.Background(), testConfig(), testSource, Options[string]{Kind: Repository, Timeout: 10 * time.Millisecond, Discard: func(value string) { discarded = append(discarded, value) }}, func(ctx context.Context, address string) (string, error) {
|
||||||
|
if address == testSource {
|
||||||
|
<-ctx.Done()
|
||||||
|
return address, ctx.Err()
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return address, err
|
||||||
|
}
|
||||||
|
return address, nil
|
||||||
|
})
|
||||||
|
if err != nil || !strings.HasPrefix(winner, "https://first.example/") || !reflect.DeepEqual(discarded, []string{testSource}) {
|
||||||
|
t.Fatalf("winner=%s cleanup=%v err=%v", winner, discarded, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestFallbackCancellationStopsBeforeNextMirror(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
calls := 0
|
||||||
|
cleaned := 0
|
||||||
|
_, err := Fallback(ctx, testConfig(), testSource, Options[string]{Kind: Repository, Discard: func(string) { cleaned++ }}, func(ctx context.Context, address string) (string, error) {
|
||||||
|
calls++
|
||||||
|
cancel()
|
||||||
|
return address, ctx.Err()
|
||||||
|
})
|
||||||
|
if !errors.Is(err, context.Canceled) || calls != 1 || cleaned != 1 {
|
||||||
|
t.Fatalf("calls=%d discarded=%d err=%v", calls, cleaned, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestExecutorsDoNotStartInvalidOrCanceledRequests(t *testing.T) {
|
||||||
|
canceled, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
for _, test := range []struct {
|
||||||
|
ctx context.Context
|
||||||
|
config *Config
|
||||||
|
options Options[string]
|
||||||
|
attempt Attempt[string]
|
||||||
|
}{
|
||||||
|
{ctx: canceled, options: Options[string]{}, attempt: func(context.Context, string) (string, error) { t.Fatal("called after cancellation"); return "", nil }},
|
||||||
|
{ctx: context.Background(), options: Options[string]{Kind: 99}, attempt: func(context.Context, string) (string, error) { t.Fatal("invalid kind"); return "", nil }},
|
||||||
|
{ctx: context.Background(), options: Options[string]{Timeout: -time.Second}, attempt: func(context.Context, string) (string, error) { t.Fatal("invalid timeout"); return "", nil }},
|
||||||
|
{ctx: context.Background(), options: Options[string]{}, attempt: nil},
|
||||||
|
{ctx: context.Background(), config: &Config{Mirrors: []Mirror{{Type: "invalid", URL: "https://mirror.example"}}}, attempt: func(context.Context, string) (string, error) { t.Fatal("invalid config"); return "", nil }},
|
||||||
|
} {
|
||||||
|
if _, err := Race(test.ctx, test.config, testSource, test.options, test.attempt); err == nil {
|
||||||
|
t.Fatal("invalid race accepted")
|
||||||
|
}
|
||||||
|
if _, err := Fallback(test.ctx, test.config, testSource, test.options, test.attempt); err == nil {
|
||||||
|
t.Fatal("invalid fallback accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestAttemptPanicBecomesError(t *testing.T) {
|
||||||
|
attempt := func(context.Context, string) (string, error) { panic("broken operation") }
|
||||||
|
if _, err := Race(context.Background(), nil, testSource, Options[string]{}, attempt); err == nil || !strings.Contains(err.Error(), "broken operation") {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := Fallback(context.Background(), nil, testSource, Options[string]{}, attempt); err == nil || !strings.Contains(err.Error(), "broken operation") {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestNonGithubResourcesAndDisabledMirrorsUseOnlyOriginal(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
config *Config
|
||||||
|
raw string
|
||||||
|
}{{testConfig(), "https://example.com/resource"}, {&Config{Enabled: false, Mirrors: testConfig().Mirrors}, testSource}} {
|
||||||
|
calls := 0
|
||||||
|
got, err := Race(context.Background(), test.config, test.raw, Options[string]{}, func(_ context.Context, address string) (string, error) { calls++; return address, nil })
|
||||||
|
if err != nil || got != test.raw || calls != 1 {
|
||||||
|
t.Fatalf("got=%s calls=%d err=%v", got, calls, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepositoryAttemptsHaveNoDefaultDeadline(t *testing.T) {
|
||||||
|
value, err := Fallback(context.Background(), nil, testSource, Options[string]{Kind: Repository}, func(ctx context.Context, raw string) (string, error) {
|
||||||
|
if _, ok := ctx.Deadline(); ok {
|
||||||
|
t.Fatal("repository attempt has a total deadline")
|
||||||
|
}
|
||||||
|
return raw, nil
|
||||||
|
})
|
||||||
|
if err != nil || value != testSource {
|
||||||
|
t.Fatalf("value=%s error=%v", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,37 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
type trackerSubscriptionStatus struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
TrackerCount int `json:"trackerCount"`
|
||||||
|
LastFetchTime time.Time `json:"lastFetchTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
TrackerSubscriptionStatuses map[string]trackerSubscriptionStatus `json:"trackerSubscriptionStatuses"`
|
||||||
|
TrackerSources map[string][]string `json:"trackerSources"`
|
||||||
|
LastTrackerUpdateTime time.Time `json:"lastTrackerUpdateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
fetcher.ConfigState[state]
|
||||||
|
TrackerSubscribeURLs []string `json:"trackerSubscribeUrls"`
|
||||||
|
AutoUpdateTrackers bool `json:"autoUpdateTrackers"`
|
||||||
|
CustomTrackers []string `json:"customTrackers"`
|
||||||
ListenPort int `json:"listenPort"`
|
ListenPort int `json:"listenPort"`
|
||||||
Trackers []string `json:"trackers"`
|
|
||||||
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
||||||
SeedKeep bool `json:"seedKeep"`
|
SeedKeep bool `json:"seedKeep"`
|
||||||
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
||||||
SeedRatio float64 `json:"seedRatio"`
|
SeedRatio float64 `json:"seedRatio"`
|
||||||
// SeedTime is the time in seconds to seed after downloading is complete.
|
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||||
SeedTime int64 `json:"seedTime"`
|
SeedTime int64 `json:"seedTime"`
|
||||||
|
// DisableDHT and DisablePEX turn off DHT and PEX peer discovery. Like ListenPort,
|
||||||
|
// they are read when the shared client is built.
|
||||||
|
DisableDHT bool `json:"disableDht"`
|
||||||
|
DisablePEX bool `json:"disablePex"`
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
btprotocol "github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
pp "github.com/anacrolix/torrent/peer_protocol"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type btDownloadHarness struct {
|
||||||
|
*Fetcher
|
||||||
|
pendingRequests atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// These tests exercise the BT fetcher, native storage and real peer protocol.
|
||||||
|
// All content is generated locally and served by a loopback seeder.
|
||||||
|
func btDownloadFetcher(t *testing.T, f fetcher.Fetcher) *btDownloadHarness {
|
||||||
|
t.Helper()
|
||||||
|
if f == nil {
|
||||||
|
f = new(FetcherManager).Build()
|
||||||
|
}
|
||||||
|
ctl := controller.NewController()
|
||||||
|
ctl.GetConfig = func(v any) {
|
||||||
|
encoded, err := json.Marshal(new(FetcherManager).DefaultConfig())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(encoded, v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Setup(ctl)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if err := closeClient(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
h := &btDownloadHarness{Fetcher: f.(*Fetcher)}
|
||||||
|
// Keep the test client on loopback and observe real cancellation replies.
|
||||||
|
// Waiting for them avoids the pinned library's immediate cancel/resume
|
||||||
|
// request scheduling bug, without relying on arbitrary sleep durations.
|
||||||
|
cfg = torrent.TestingConfig(t)
|
||||||
|
cfg.DisableTrackers = false
|
||||||
|
cfg.Seed = true
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
cfg.DisableIPv6 = true
|
||||||
|
cfg.DisableWebtorrent = true
|
||||||
|
cfg.KeepAliveTimeout = time.Minute
|
||||||
|
cfg.MaxAllocPeerRequestDataPerConn = 1 << 20
|
||||||
|
cfg.Callbacks.SentRequest = []func(torrent.PeerRequestEvent){func(torrent.PeerRequestEvent) {
|
||||||
|
h.pendingRequests.Add(1)
|
||||||
|
}}
|
||||||
|
cfg.Callbacks.ReadMessage = func(_ *torrent.PeerConn, msg *pp.Message) {
|
||||||
|
if !msg.Keepalive && (msg.Type == pp.Piece || msg.Type == pp.Reject) {
|
||||||
|
h.pendingRequests.Add(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
client, err = torrent.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func btResolve(t *testing.T, f *btDownloadHarness, source string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := f.Resolve(&base.Request{URL: source}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func btStart(t *testing.T, f *btDownloadHarness) {
|
||||||
|
t.Helper()
|
||||||
|
if err := f.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func btAwaitDone(t *testing.T, f *btDownloadHarness) {
|
||||||
|
t.Helper()
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- f.Wait() }()
|
||||||
|
timeout := time.NewTimer(20 * time.Second)
|
||||||
|
defer timeout.Stop()
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BT download failed: %v", err)
|
||||||
|
}
|
||||||
|
if !f.isDone() {
|
||||||
|
t.Fatal("Wait returned before selected files were verified")
|
||||||
|
}
|
||||||
|
case <-timeout.C:
|
||||||
|
var status strings.Builder
|
||||||
|
client.WriteStatus(&status)
|
||||||
|
t.Log(status.String())
|
||||||
|
t.Fatalf("BT download did not finish: progress=%v stats=%+v pieces=%+v peers=%+v", f.Progress(), f.torrent.Stats(), f.torrent.PieceStateRuns(), f.Stats().Runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func btAwaitPartial(t *testing.T, f *btDownloadHarness) {
|
||||||
|
t.Helper()
|
||||||
|
timeout := time.NewTimer(15 * time.Second)
|
||||||
|
defer timeout.Stop()
|
||||||
|
ticker := time.NewTicker(20 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
progress := f.Progress().TotalDownloaded()
|
||||||
|
if progress >= 32*1024 && progress < f.Meta().Res.Size {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-timeout.C:
|
||||||
|
t.Fatal("BT transfer did not reach partial progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func btPause(t *testing.T, f *btDownloadHarness) {
|
||||||
|
t.Helper()
|
||||||
|
if err := f.Pause(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
timeout := time.NewTimer(5 * time.Second)
|
||||||
|
defer timeout.Stop()
|
||||||
|
ticker := time.NewTicker(10 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
if f.pendingRequests.Load() == 0 {
|
||||||
|
// The message callback runs under the client lock. Acquiring that
|
||||||
|
// lock ensures the final cancellation reply has finished processing.
|
||||||
|
client.Stats()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
case <-timeout.C:
|
||||||
|
t.Fatal("peer did not acknowledge cancelled requests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTDownloadSources(t *testing.T) {
|
||||||
|
for _, kind := range []string{"magnet", "torrent", "file URI", "data URI"} {
|
||||||
|
t.Run(kind, func(t *testing.T) {
|
||||||
|
fixture := newBTFixture(t, map[string][]byte{"": btPayload(96*1024, kind)}, 0)
|
||||||
|
source := fixture.Magnet
|
||||||
|
switch kind {
|
||||||
|
case "torrent":
|
||||||
|
source = fixture.TorrentFile
|
||||||
|
case "file URI":
|
||||||
|
source = fixture.FileURI()
|
||||||
|
case "data URI":
|
||||||
|
source = fixture.DataURI
|
||||||
|
}
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
btResolve(t, f, source)
|
||||||
|
if f.Meta().Opts != nil {
|
||||||
|
t.Fatal("Resolve assigned download options")
|
||||||
|
}
|
||||||
|
f.storage.mu.RLock()
|
||||||
|
bound := f.storage.owner != nil
|
||||||
|
f.storage.mu.RUnlock()
|
||||||
|
if bound {
|
||||||
|
t.Fatal("Resolve bound download storage")
|
||||||
|
}
|
||||||
|
resolvedTorrent := f.torrent
|
||||||
|
dir := filepath.Join(t.TempDir(), "chosen-after-resolve")
|
||||||
|
f.Meta().Opts = &base.Options{Path: dir}
|
||||||
|
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("output directory exists before Start: %v", err)
|
||||||
|
}
|
||||||
|
btStart(t, f)
|
||||||
|
if f.torrent != resolvedTorrent {
|
||||||
|
t.Fatal("Start replaced the resolved torrent")
|
||||||
|
}
|
||||||
|
btAwaitDone(t, f)
|
||||||
|
fixture.AssertFiles(t, dir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTDirectDownloadWithExtraTrackers(t *testing.T) {
|
||||||
|
for _, source := range []string{"request", "config", "subscription"} {
|
||||||
|
t.Run(source, func(t *testing.T) {
|
||||||
|
fixture := makeBTFixture(t, map[string][]byte{
|
||||||
|
"a.bin": btPayload(20*1024, "direct-a"),
|
||||||
|
"nested/b.bin": btPayload(48*1024, "direct-b"),
|
||||||
|
}, 0, false, false)
|
||||||
|
magnet, err := url.Parse(fixture.Magnet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
query := magnet.Query()
|
||||||
|
query.Del("tr")
|
||||||
|
query.Del("x.pe")
|
||||||
|
magnet.RawQuery = query.Encode()
|
||||||
|
req := &base.Request{URL: magnet.String()}
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
if source == "request" {
|
||||||
|
req.Extra = &btprotocol.ReqExtra{Trackers: []string{fixture.TrackerURL}}
|
||||||
|
} else {
|
||||||
|
backendConfig := new(FetcherManager).DefaultConfig().(*config)
|
||||||
|
if source == "config" {
|
||||||
|
backendConfig.CustomTrackers = []string{fixture.TrackerURL}
|
||||||
|
} else {
|
||||||
|
backendConfig.TrackerSubscribeURLs = []string{"https://subscription.example/list"}
|
||||||
|
backendConfig.State.TrackerSources[backendConfig.TrackerSubscribeURLs[0]] = []string{fixture.TrackerURL}
|
||||||
|
}
|
||||||
|
f.ctl.GetConfig = func(dst any) {
|
||||||
|
raw, err := json.Marshal(backendConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, dst); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// CreateDirect supplies options before Downloader resolves the task.
|
||||||
|
dir := filepath.Join(t.TempDir(), "direct-download")
|
||||||
|
opts := &base.Options{Path: dir, SelectFiles: []int{}}
|
||||||
|
f.Meta().Opts = opts
|
||||||
|
resolved := make(chan error, 1)
|
||||||
|
go func() { resolved <- f.Resolve(req) }()
|
||||||
|
select {
|
||||||
|
case err := <-resolved:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
f.torrentDropFunc()
|
||||||
|
<-resolved
|
||||||
|
t.Fatal("direct magnet download did not obtain metadata from its extra tracker")
|
||||||
|
}
|
||||||
|
if f.Meta().Opts != opts || len(opts.SelectFiles) != len(fixture.Files) {
|
||||||
|
t.Fatal("resolution lost direct download options or did not select all files")
|
||||||
|
}
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitDone(t, f)
|
||||||
|
fixture.AssertFiles(t, dir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTDownloadSelectedFiles(t *testing.T) {
|
||||||
|
fixture := newBTFixture(t, map[string][]byte{
|
||||||
|
"a.bin": btPayload(20*1024, "a"), "nested/b.bin": btPayload(48*1024, "b"),
|
||||||
|
}, 0)
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
dir := t.TempDir()
|
||||||
|
btResolve(t, f, fixture.Magnet)
|
||||||
|
selected := -1
|
||||||
|
for i, file := range f.Meta().Res.Files {
|
||||||
|
if file.Name == "b.bin" {
|
||||||
|
selected = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected < 0 {
|
||||||
|
t.Fatal("selected file absent from metadata")
|
||||||
|
}
|
||||||
|
f.Meta().Opts = &base.Options{Path: dir}
|
||||||
|
if err := f.Patch(nil, &base.Options{SelectFiles: []int{selected}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitDone(t, f)
|
||||||
|
fixture.AssertFiles(t, dir, filepath.Join("bundle", "nested", "b.bin"))
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "bundle", "a.bin")); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("unselected output remains: %v", err)
|
||||||
|
}
|
||||||
|
if f.Meta().Res.Size != 48*1024 {
|
||||||
|
t.Fatal("selected resource size is incorrect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTPauseAndResume(t *testing.T) {
|
||||||
|
fixture := newBTFixture(t, map[string][]byte{"": btPayload(512*1024, "pause")}, 64*1024)
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
dir := t.TempDir()
|
||||||
|
btResolve(t, f, fixture.Magnet)
|
||||||
|
f.Meta().Opts = &base.Options{Path: dir}
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitPartial(t, f)
|
||||||
|
btPause(t, f)
|
||||||
|
resolvedTorrent := f.torrent
|
||||||
|
fixture.Limiter.SetLimit(rate.Inf)
|
||||||
|
// Once cancelled requests have settled, no more data should arrive.
|
||||||
|
stats := f.torrent.Stats()
|
||||||
|
readWhilePaused := stats.BytesReadData.Int64()
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
stats = f.torrent.Stats()
|
||||||
|
if stats.BytesReadData.Int64() != readWhilePaused || f.isDone() {
|
||||||
|
t.Fatal("data download continued while paused")
|
||||||
|
}
|
||||||
|
btStart(t, f)
|
||||||
|
if f.torrent != resolvedTorrent {
|
||||||
|
t.Fatal("resume replaced the resolved torrent")
|
||||||
|
}
|
||||||
|
btAwaitDone(t, f)
|
||||||
|
fixture.AssertFiles(t, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
type btFetcherCheckpoint struct {
|
||||||
|
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func btStore(t *testing.T, f *btDownloadHarness) []byte {
|
||||||
|
t.Helper()
|
||||||
|
data, err := new(FetcherManager).Store(f.Fetcher)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
encodedData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(btFetcherCheckpoint{Meta: f.Meta(), Data: encodedData})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func btRestore(t *testing.T, encoded []byte) *btDownloadHarness {
|
||||||
|
t.Helper()
|
||||||
|
var checkpoint btFetcherCheckpoint
|
||||||
|
if err := json.Unmarshal(encoded, &checkpoint); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, restore := new(FetcherManager).Restore()
|
||||||
|
if err := json.Unmarshal(checkpoint.Data, data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return btDownloadFetcher(t, restore(checkpoint.Meta, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func btCloseForRestart(t *testing.T, f *btDownloadHarness) {
|
||||||
|
t.Helper()
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := new(FetcherManager).Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTColdStartResume(t *testing.T) {
|
||||||
|
for _, kind := range []string{"v1", "hybrid"} {
|
||||||
|
t.Run(kind, func(t *testing.T) {
|
||||||
|
var fixture *btFixture
|
||||||
|
if kind == "hybrid" {
|
||||||
|
fixture = newBTHybridFixture(t, btPayload(512*1024, "restart-hybrid"))
|
||||||
|
fixture.Limiter.SetLimit(64 * 1024)
|
||||||
|
} else {
|
||||||
|
fixture = newBTFixture(t, map[string][]byte{"": btPayload(512*1024, "restart")}, 64*1024)
|
||||||
|
}
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
dir := t.TempDir()
|
||||||
|
btResolve(t, f, fixture.Magnet)
|
||||||
|
f.Meta().Opts = &base.Options{Path: dir}
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitPartial(t, f)
|
||||||
|
btPause(t, f)
|
||||||
|
checkpoint := btStore(t, f)
|
||||||
|
hash := f.Meta().Res.Hash
|
||||||
|
btCloseForRestart(t, f)
|
||||||
|
seedStats := fixture.Torrent.Stats()
|
||||||
|
sentBeforeRestart := seedStats.BytesWrittenData.Int64()
|
||||||
|
restored := btRestore(t, checkpoint)
|
||||||
|
if restored.Meta().Opts.Path != dir || restored.Meta().Res.Hash != hash {
|
||||||
|
t.Fatal("download directory or hash was lost")
|
||||||
|
}
|
||||||
|
fixture.Limiter.SetLimit(rate.Inf)
|
||||||
|
btStart(t, restored)
|
||||||
|
btAwaitDone(t, restored)
|
||||||
|
fixture.AssertFiles(t, dir)
|
||||||
|
seedStats = fixture.Torrent.Stats()
|
||||||
|
if sent := seedStats.BytesWrittenData.Int64() - sentBeforeRestart; sent >= 512*1024 {
|
||||||
|
t.Fatalf("restart redownloaded the entire file: %d bytes", sent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTDownloadHybrid(t *testing.T) {
|
||||||
|
fixture := newBTHybridFixture(t, btPayload(80*1024, "hybrid-v2"))
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
dir := t.TempDir()
|
||||||
|
btResolve(t, f, fixture.Magnet)
|
||||||
|
f.Meta().Opts = &base.Options{Path: dir}
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitDone(t, f)
|
||||||
|
fixture.AssertFiles(t, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTRepairCorruptedExistingFile(t *testing.T) {
|
||||||
|
fixture := newBTFixture(t, map[string][]byte{"": btPayload(128*1024, "repair")}, 0)
|
||||||
|
dir := t.TempDir()
|
||||||
|
damaged := append([]byte(nil), fixture.Files["payload.bin"]...)
|
||||||
|
damaged[20000] ^= 0xff
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "payload.bin"), damaged, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
btResolve(t, f, fixture.Magnet)
|
||||||
|
f.Meta().Opts = &base.Options{Path: dir}
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitDone(t, f)
|
||||||
|
fixture.AssertFiles(t, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTPatchSelectionWhilePaused(t *testing.T) {
|
||||||
|
fixture := newBTFixture(t, map[string][]byte{"a.bin": btPayload(256*1024, "patch-a"), "b.bin": btPayload(256*1024, "patch-b")}, 64*1024)
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
dir := t.TempDir()
|
||||||
|
btResolve(t, f, fixture.Magnet)
|
||||||
|
f.Meta().Opts = &base.Options{Path: dir, SelectFiles: []int{0}}
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitPartial(t, f)
|
||||||
|
btPause(t, f)
|
||||||
|
if err := f.Patch(nil, &base.Options{SelectFiles: []int{1}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fixture.Limiter.SetLimit(rate.Inf)
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitDone(t, f)
|
||||||
|
fixture.AssertFiles(t, dir, filepath.Join("bundle", "b.bin"))
|
||||||
|
for _, suffix := range []string{"", ".part"} {
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "bundle", "a.bin") + suffix); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("unselected output remains: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBTDownloadMultiFileWithEmptyFile(t *testing.T) {
|
||||||
|
fixture := newBTFixture(t, map[string][]byte{"empty.txt": {}, "nested/data.bin": btPayload(48*1024, "empty-file")}, 0)
|
||||||
|
f := btDownloadFetcher(t, nil)
|
||||||
|
dir := t.TempDir()
|
||||||
|
btResolve(t, f, fixture.Magnet)
|
||||||
|
f.Meta().Opts = &base.Options{Path: dir}
|
||||||
|
btStart(t, f)
|
||||||
|
btAwaitDone(t, f)
|
||||||
|
fixture.AssertFiles(t, dir)
|
||||||
|
}
|
||||||
+241
-114
File diff suppressed because it is too large.
Load diff
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
@@ -18,11 +19,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||||
doResolve(t, buildFetcher())
|
doResolve(t, buildFetcher(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
||||||
fetcher := buildFetcher()
|
fetcher := buildFetcher(t)
|
||||||
buf, err := os.ReadFile("./testdata/ubuntu-22.04-live-server-amd64.iso.torrent")
|
buf, err := os.ReadFile("./testdata/ubuntu-22.04-live-server-amd64.iso.torrent")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -31,7 +32,7 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
|||||||
dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf)
|
dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf)
|
||||||
err = fetcher.Resolve(&base.Request{
|
err = fetcher.Resolve(&base.Request{
|
||||||
URL: dataUri,
|
URL: dataUri,
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_Config(t *testing.T) {
|
func TestFetcher_Config(t *testing.T) {
|
||||||
doResolve(t, buildConfigFetcher(nil))
|
doResolve(t, buildConfigFetcher(t, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_ResolveWithProxy(t *testing.T) {
|
func TestFetcher_ResolveWithProxy(t *testing.T) {
|
||||||
@@ -61,7 +62,7 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
|||||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
doResolve(t, buildConfigFetcher(&base.DownloaderProxyConfig{
|
doResolve(t, buildConfigFetcher(t, &base.DownloaderProxyConfig{
|
||||||
Enable: true,
|
Enable: true,
|
||||||
System: false,
|
System: false,
|
||||||
Scheme: "socks5",
|
Scheme: "socks5",
|
||||||
@@ -71,6 +72,82 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_CloseAfterInit(t *testing.T) {
|
||||||
|
if err := closeClient(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
f := buildFetcher(t).(*Fetcher)
|
||||||
|
f.meta.Req = &base.Request{}
|
||||||
|
lock.Lock()
|
||||||
|
err := f.initClient()
|
||||||
|
lock.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := closeClient(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_CloseWithoutClient(t *testing.T) {
|
||||||
|
if err := closeClient(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("restored task", func(t *testing.T) {
|
||||||
|
_, build := new(FetcherManager).Restore()
|
||||||
|
f := build(&fetcher.FetcherMeta{
|
||||||
|
Req: &base.Request{URL: "./testdata/test.torrent"},
|
||||||
|
Opts: &base.Options{},
|
||||||
|
}, &fetcherData{})
|
||||||
|
f.Setup(controller.NewController())
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
t.Errorf("Close() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed resolve", func(t *testing.T) {
|
||||||
|
f := buildFetcher(t)
|
||||||
|
if err := f.Resolve(&base.Request{URL: "./testdata/not-exist.torrent"}); err == nil {
|
||||||
|
t.Fatal("Resolve() got = nil, want error")
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
t.Errorf("Close() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ResolveWhileClientCloses(t *testing.T) {
|
||||||
|
if err := closeClient(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { closeClient() })
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, u := range []string{"./testdata/test.torrent", "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent"} {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
f := buildFetcher(t)
|
||||||
|
// The other goroutine may close the client after the torrent was added.
|
||||||
|
if err := f.Resolve(&base.Request{URL: u}); err != nil && err.Error() != "torrent closed during resolution" {
|
||||||
|
t.Errorf("Resolve(%s) got = %v", u, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
if err := closeClient(); err != nil {
|
||||||
|
t.Errorf("closeClient() got = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
t.Run("Resolve Single File", func(t *testing.T) {
|
t.Run("Resolve Single File", func(t *testing.T) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
err := fetcher.Resolve(&base.Request{
|
||||||
@@ -81,7 +158,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -111,7 +188,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -145,7 +222,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
|
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
err := fetcher.Resolve(&base.Request{
|
||||||
URL: "./testdata/test.unclean.torrent",
|
URL: "./testdata/test.unclean.torrent",
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
|
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
|
||||||
}
|
}
|
||||||
@@ -156,7 +233,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
uri := "file:///" + file
|
uri := "file:///" + file
|
||||||
err := fetcher.Resolve(&base.Request{
|
err := fetcher.Resolve(&base.Request{
|
||||||
URL: uri,
|
URL: uri,
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
|
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
|
||||||
}
|
}
|
||||||
@@ -211,7 +288,7 @@ func TestFetcherManager_ParseName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFetcher() fetcher.Fetcher {
|
func buildFetcher(t testing.TB) fetcher.Fetcher {
|
||||||
fb := new(FetcherManager)
|
fb := new(FetcherManager)
|
||||||
fetcher := fb.Build()
|
fetcher := fb.Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
@@ -219,14 +296,15 @@ func buildFetcher() fetcher.Fetcher {
|
|||||||
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
||||||
}
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
|
t.Cleanup(func() { fetcher.Close(); closeClient() })
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
|
func buildConfigFetcher(t testing.TB, proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherManager).Build()
|
fetcher := new(FetcherManager).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
mockCfg := config{
|
mockCfg := config{
|
||||||
Trackers: []string{
|
CustomTrackers: []string{
|
||||||
"udp://tracker.birkenwald.de:6969/announce",
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
}}
|
}}
|
||||||
@@ -237,29 +315,30 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher
|
|||||||
return proxyConfig.ToHandler()
|
return proxyConfig.ToHandler()
|
||||||
}
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
|
t.Cleanup(func() { fetcher.Close(); closeClient() })
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFetcher_Patch tests the Patch functionality for BT fetcher.
|
// TestFetcher_Patch tests the Patch functionality for BT fetcher.
|
||||||
// It tests modifying selected files after Resolve (without downloading).
|
// It tests modifying selected files after Resolve (without downloading).
|
||||||
func TestFetcher_Patch(t *testing.T) {
|
func TestFetcher_Patch(t *testing.T) {
|
||||||
f := buildFetcher()
|
f := buildFetcher(t)
|
||||||
|
|
||||||
// Resolve a multi-file torrent
|
// Resolve a multi-file torrent
|
||||||
err := f.Resolve(&base.Request{
|
err := f.Resolve(&base.Request{
|
||||||
URL: "./testdata/test.torrent",
|
URL: "./testdata/test.torrent",
|
||||||
}, nil)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resolve failed: %v", err)
|
t.Fatalf("Resolve failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify initial state: 3 files, all selected by default
|
// Resolution describes files without choosing download options.
|
||||||
meta := f.Meta()
|
meta := f.Meta()
|
||||||
if len(meta.Res.Files) != 3 {
|
if len(meta.Res.Files) != 3 {
|
||||||
t.Fatalf("Expected 3 files, got %d", len(meta.Res.Files))
|
t.Fatalf("Expected 3 files, got %d", len(meta.Res.Files))
|
||||||
}
|
}
|
||||||
if len(meta.Opts.SelectFiles) != 3 {
|
if meta.Opts != nil {
|
||||||
t.Fatalf("Expected 3 selected files, got %d", len(meta.Opts.SelectFiles))
|
t.Fatal("Resolve should not assign download options")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Total size of all files: 107484864
|
// Total size of all files: 107484864
|
||||||
@@ -366,3 +445,52 @@ func TestFetcher_Patch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_PeerDiscoveryConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
stored any
|
||||||
|
wantDisableDHT bool
|
||||||
|
wantDisablePEX bool
|
||||||
|
}{
|
||||||
|
{"default config", new(FetcherManager).DefaultConfig(), false, false},
|
||||||
|
{"stored config without the fields", map[string]any{"listenPort": 0, "seedRatio": 1.0}, false, false},
|
||||||
|
{"both disabled", map[string]any{"disableDht": true, "disablePex": true}, true, true},
|
||||||
|
{"only DHT disabled", map[string]any{"disableDht": true}, true, false},
|
||||||
|
{"only PEX disabled", map[string]any{"disablePex": true}, false, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
f := buildFetcherWithConfig(tt.stored).(*Fetcher)
|
||||||
|
if f.config.DisableDHT != tt.wantDisableDHT || f.config.DisablePEX != tt.wantDisablePEX {
|
||||||
|
t.Errorf("config got DisableDHT = %v, DisablePEX = %v, want %v, %v", f.config.DisableDHT, f.config.DisablePEX, tt.wantDisableDHT, tt.wantDisablePEX)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_PeerDiscoveryDisabledOnClient(t *testing.T) {
|
||||||
|
if err := closeClient(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f := buildFetcherWithConfig(map[string]any{"disableDht": true, "disablePex": true})
|
||||||
|
if err := f.Resolve(&base.Request{URL: "./testdata/test.torrent"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !cfg.NoDHT || !cfg.DisablePEX {
|
||||||
|
t.Errorf("client config got NoDHT = %v, DisablePEX = %v, want true", cfg.NoDHT, cfg.DisablePEX)
|
||||||
|
}
|
||||||
|
if n := len(client.DhtServers()); n != 0 {
|
||||||
|
t.Errorf("client got %d DHT servers, want 0", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFetcherWithConfig(stored any) fetcher.Fetcher {
|
||||||
|
fetcher := new(FetcherManager).Build()
|
||||||
|
newController := controller.NewController()
|
||||||
|
newController.GetConfig = func(v any) {
|
||||||
|
json.Unmarshal([]byte(test.ToJson(stored)), v)
|
||||||
|
}
|
||||||
|
fetcher.Setup(newController)
|
||||||
|
return fetcher
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/bencode"
|
||||||
|
"github.com/anacrolix/torrent/merkle"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// btFixture serves generated, trackerless content over a real loopback TCP peer.
|
||||||
|
// Its HTTP tracker exercises .torrent downloads; magnet links also carry the
|
||||||
|
// explicit x.pe address.
|
||||||
|
type btFixture struct {
|
||||||
|
MetaInfo *metainfo.MetaInfo
|
||||||
|
Magnet string
|
||||||
|
TorrentFile string
|
||||||
|
DataURI string
|
||||||
|
TrackerURL string
|
||||||
|
Files map[string][]byte // Paths relative to the selected download directory.
|
||||||
|
Seeder *torrent.Client
|
||||||
|
Torrent *torrent.Torrent
|
||||||
|
Limiter *rate.Limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBTFixture(t testing.TB, files map[string][]byte, bytesPerSecond int) *btFixture {
|
||||||
|
return makeBTFixture(t, files, bytesPerSecond, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBTHybridFixture(t testing.TB, payload []byte) *btFixture {
|
||||||
|
return makeBTFixture(t, map[string][]byte{"": payload}, 0, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBTFixture(t testing.TB, files map[string][]byte, bytesPerSecond int, v2, private bool) *btFixture {
|
||||||
|
t.Helper()
|
||||||
|
source := t.TempDir()
|
||||||
|
name := "payload.bin"
|
||||||
|
if _, single := files[""]; !single {
|
||||||
|
name = "bundle"
|
||||||
|
}
|
||||||
|
root := filepath.Join(source, name)
|
||||||
|
expected := make(map[string][]byte, len(files))
|
||||||
|
for path, data := range files {
|
||||||
|
target := filepath.Join(root, filepath.FromSlash(path))
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(target, data, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expected[filepath.Join(name, filepath.FromSlash(path))] = append([]byte(nil), data...)
|
||||||
|
}
|
||||||
|
info := metainfo.Info{PieceLength: 16 * 1024}
|
||||||
|
if err := info.BuildFromFilePath(root); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var layers map[string]string
|
||||||
|
if v2 {
|
||||||
|
data := files[""]
|
||||||
|
h := merkle.NewHash()
|
||||||
|
h.Write(data)
|
||||||
|
root := h.Sum(nil)
|
||||||
|
var pieceHashes []byte
|
||||||
|
for off := 0; off < len(data); off += int(info.PieceLength) {
|
||||||
|
end := min(off+int(info.PieceLength), len(data))
|
||||||
|
ph := merkle.NewHash()
|
||||||
|
ph.Write(data[off:end])
|
||||||
|
pieceHashes = append(pieceHashes, ph.SumMinLength(nil, int(info.PieceLength))...)
|
||||||
|
}
|
||||||
|
info = metainfo.Info{Name: name, PieceLength: info.PieceLength, Length: int64(len(data)), Pieces: info.Pieces, MetaVersion: 2, FileTree: metainfo.FileTree{File: metainfo.FileTreeFile{Length: int64(len(data)), PiecesRoot: string(root)}}}
|
||||||
|
layers = map[string]string{string(root): string(pieceHashes)}
|
||||||
|
}
|
||||||
|
// Private torrents avoid public DHT discovery once metadata is available.
|
||||||
|
info.Private = &private
|
||||||
|
raw, err := bencode.Marshal(&info)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mi := &metainfo.MetaInfo{InfoBytes: raw, PieceLayers: layers}
|
||||||
|
cfg := torrent.TestingConfig(t)
|
||||||
|
cfg.DataDir = source
|
||||||
|
cfg.Seed = true
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
cfg.DisableIPv6 = true
|
||||||
|
cfg.DisableWebtorrent = true
|
||||||
|
cfg.KeepAliveTimeout = time.Minute
|
||||||
|
cfg.MaxAllocPeerRequestDataPerConn = 1 << 20
|
||||||
|
limiter := rate.NewLimiter(rate.Inf, 16*1024)
|
||||||
|
if bytesPerSecond > 0 {
|
||||||
|
limiter.SetLimit(rate.Limit(bytesPerSecond))
|
||||||
|
}
|
||||||
|
cfg.UploadRateLimiter = limiter
|
||||||
|
seed, err := torrent.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { seed.Close() })
|
||||||
|
seeded, _, err := seed.AddTorrentSpec(torrent.TorrentSpecFromMetaInfo(mi))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := seeded.VerifyDataContext(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-seeded.Complete().On():
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal("local seeder did not finish verification")
|
||||||
|
}
|
||||||
|
addr := seed.ListenAddrs()[0].String()
|
||||||
|
host, portString, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resolved, err := net.ResolveTCPAddr("tcp4", net.JoinHostPort(host, portString))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
compact := make([]byte, 6)
|
||||||
|
copy(compact, resolved.IP.To4())
|
||||||
|
binary.BigEndian.PutUint16(compact[4:], uint16(resolved.Port))
|
||||||
|
tracker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response, err := bencode.Marshal(map[string]any{"interval": 3600, "peers": string(compact)})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(response)
|
||||||
|
}))
|
||||||
|
t.Cleanup(tracker.Close)
|
||||||
|
// The pinned library has a hybrid tracker-dispatch assertion bug.
|
||||||
|
// Hybrid fixtures use their explicit loopback peer rather than a tracker.
|
||||||
|
if !v2 {
|
||||||
|
mi.Announce = tracker.URL
|
||||||
|
}
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
if err := mi.Write(&encoded); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
torrentFile := filepath.Join(t.TempDir(), "fixture.torrent")
|
||||||
|
if err := os.WriteFile(torrentFile, encoded.Bytes(), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m, err := mi.MagnetV2()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
magnet := m.String()
|
||||||
|
u, err := url.Parse(magnet)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
q.Add("x.pe", addr)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
return &btFixture{MetaInfo: mi, Magnet: u.String(), TorrentFile: torrentFile, DataURI: "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(encoded.Bytes()), TrackerURL: tracker.URL, Files: expected, Seeder: seed, Torrent: seeded, Limiter: limiter}
|
||||||
|
}
|
||||||
|
|
||||||
|
func btPayload(size int, label string) []byte {
|
||||||
|
data := make([]byte, size)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte((i*31 + i/251) % 251)
|
||||||
|
}
|
||||||
|
copy(data, label)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *btFixture) AssertFiles(t testing.TB, dir string, paths ...string) {
|
||||||
|
t.Helper()
|
||||||
|
if len(paths) == 0 {
|
||||||
|
for path := range f.Files {
|
||||||
|
paths = append(paths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, path := range paths {
|
||||||
|
got, err := os.ReadFile(filepath.Join(dir, path))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want, ok := f.Files[path]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unknown fixture file %q", path)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("BT file %s differs: received %d bytes, want %d", path, len(got), len(want))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *btFixture) FileURI() string {
|
||||||
|
path := filepath.ToSlash(f.TorrentFile)
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
return (&url.URL{Scheme: "file", Path: path}).String()
|
||||||
|
}
|
||||||
@@ -17,25 +17,85 @@ import (
|
|||||||
"github.com/anacrolix/torrent/storage"
|
"github.com/anacrolix/torrent/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStartUsesSelectionSubmittedAfterResolve(t *testing.T) {
|
func TestStartDoesNotPublishUnverifiedCompletion(t *testing.T) {
|
||||||
config := torrent.TestingConfig(t)
|
cfg := torrent.TestingConfig(t)
|
||||||
config.DataDir = t.TempDir()
|
cfg.DisableTCP = true
|
||||||
config.DisableTCP = true
|
cfg.DisableUTP = true
|
||||||
config.DisableUTP = true
|
c, err := torrent.NewClient(cfg)
|
||||||
c, err := torrent.NewClient(config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
tor, err := c.AddTorrentFromFile("testdata/test.torrent")
|
payload := btPayload(32*1024, "unverified")
|
||||||
|
info := metainfo.Info{Name: "payload.bin", Length: int64(len(payload)), PieceLength: 16 * 1024}
|
||||||
|
for offset := 0; offset < len(payload); offset += int(info.PieceLength) {
|
||||||
|
hash := sha1.Sum(payload[offset : offset+int(info.PieceLength)])
|
||||||
|
info.Pieces = append(info.Pieces, hash[:]...)
|
||||||
|
}
|
||||||
|
raw, err := bencode.Marshal(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
f := &Fetcher{torrent: tor, meta: &fetcher.FetcherMeta{Opts: &base.Options{}}, data: &fetcherData{}}
|
store := &deferredFileStorage{}
|
||||||
|
spec := torrent.TorrentSpecFromMetaInfo(&metainfo.MetaInfo{InfoBytes: raw})
|
||||||
|
spec.Storage = store
|
||||||
|
tor, _, err := c.AddTorrentSpec(spec)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tor.DisallowDataDownload()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
f := &Fetcher{torrent: tor, storage: store, meta: &fetcher.FetcherMeta{}, data: &fetcherData{}, torrentDropCtx: ctx, torrentDropFunc: cancel, config: &config{SeedKeep: true}}
|
||||||
f.torrentReady.Store(true)
|
f.torrentReady.Store(true)
|
||||||
f.updateRes()
|
f.updateRes()
|
||||||
// Resolve initializes all files; creation replaces the selection before Start.
|
dir := t.TempDir()
|
||||||
f.meta.Opts = &base.Options{SelectFiles: []int{2}}
|
payload[20000] ^= 0xff
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, info.Name), payload, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.meta.Opts = &base.Options{Path: dir}
|
||||||
|
// Retain the original event channel to detect even a transient completion.
|
||||||
|
// With no network transports, the damaged piece cannot have been repaired.
|
||||||
|
completed := tor.Complete().On()
|
||||||
|
if err := f.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-completed:
|
||||||
|
t.Fatal("Start published completion before verifying the existing file")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStartUsesSelectionSubmittedAfterResolve(t *testing.T) {
|
||||||
|
cfg := torrent.TestingConfig(t)
|
||||||
|
cfg.DataDir = t.TempDir()
|
||||||
|
cfg.DisableTCP = true
|
||||||
|
cfg.DisableUTP = true
|
||||||
|
c, err := torrent.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
store := &deferredFileStorage{}
|
||||||
|
mi, err := metainfo.LoadFromFile("testdata/test.torrent")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
spec := torrent.TorrentSpecFromMetaInfo(mi)
|
||||||
|
spec.Storage = store
|
||||||
|
tor, _, err := c.AddTorrentSpec(spec)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tor.DisallowDataDownload()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
f := &Fetcher{torrent: tor, storage: store, meta: &fetcher.FetcherMeta{}, data: &fetcherData{}, torrentDropCtx: ctx, torrentDropFunc: cancel, config: &config{SeedKeep: true}}
|
||||||
|
f.torrentReady.Store(true)
|
||||||
|
f.updateRes()
|
||||||
|
f.meta.Opts = &base.Options{Path: t.TempDir(), SelectFiles: []int{2}}
|
||||||
if err := f.Start(); err != nil {
|
if err := f.Start(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
g "github.com/anacrolix/generics"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// deferredFileStorage lets metadata resolve without opening the user's files.
|
||||||
|
// It binds once. After binding, pieces are supplied directly by file storage.
|
||||||
|
type deferredFileStorage struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
ctx context.Context
|
||||||
|
info *metainfo.Info
|
||||||
|
hash metainfo.Hash
|
||||||
|
knownHashes []bool
|
||||||
|
owner storage.ClientImplCloser
|
||||||
|
torrent storage.TorrentImpl
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deferredFileStorage) OpenTorrent(ctx context.Context, info *metainfo.Info, hash metainfo.Hash) (storage.TorrentImpl, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed || s.info != nil {
|
||||||
|
return storage.TorrentImpl{}, errors.New("torrent storage already opened or closed")
|
||||||
|
}
|
||||||
|
s.ctx, s.info, s.hash = ctx, info, hash
|
||||||
|
s.knownHashes = make([]bool, info.NumPieces())
|
||||||
|
return storage.TorrentImpl{
|
||||||
|
PieceWithHash: func(p metainfo.Piece, hash g.Option[[]byte]) storage.PieceImpl {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
return metadataPiece{err: os.ErrClosed}
|
||||||
|
}
|
||||||
|
if s.owner == nil {
|
||||||
|
s.knownHashes[p.Index()] = hash.Ok
|
||||||
|
// A missing v2 hash must remain unknown until its arrival changes
|
||||||
|
// completion and updates the library's piece request priorities.
|
||||||
|
return metadataPiece{known: hash.Ok}
|
||||||
|
}
|
||||||
|
if s.torrent.PieceWithHash != nil {
|
||||||
|
return s.torrent.PieceWithHash(p, hash)
|
||||||
|
}
|
||||||
|
return s.torrent.Piece(p)
|
||||||
|
},
|
||||||
|
Close: s.Close,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deferredFileStorage) bind(opts storage.NewFileClientOpts) ([]bool, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
return nil, os.ErrClosed
|
||||||
|
}
|
||||||
|
if s.info == nil {
|
||||||
|
return nil, errors.New("torrent metadata is not ready")
|
||||||
|
}
|
||||||
|
if s.owner != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if opts.FilePathMaker == nil {
|
||||||
|
opts.FilePathMaker = func(o storage.FilePathMakerOpts) string { _, name := torrentFileLayout(o.Info, *o.File); return name }
|
||||||
|
}
|
||||||
|
owner := storage.NewFileOpts(opts)
|
||||||
|
t, err := owner.OpenTorrent(s.ctx, s.info, s.hash)
|
||||||
|
if err != nil {
|
||||||
|
owner.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.owner, s.torrent = owner, t
|
||||||
|
return append([]bool(nil), s.knownHashes...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *deferredFileStorage) Close() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.closed = true
|
||||||
|
if s.owner == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if s.torrent.Close != nil {
|
||||||
|
err = s.torrent.Close()
|
||||||
|
}
|
||||||
|
return errors.Join(err, s.owner.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
type metadataPiece struct {
|
||||||
|
known bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p metadataPiece) Completion() storage.Completion {
|
||||||
|
return storage.Completion{Ok: p.known, Err: p.err}
|
||||||
|
}
|
||||||
|
func (p metadataPiece) ReadAt([]byte, int64) (int, error) {
|
||||||
|
return 0, errors.New("download storage is not initialized")
|
||||||
|
}
|
||||||
|
func (p metadataPiece) WriteAt([]byte, int64) (int, error) {
|
||||||
|
return 0, errors.New("download storage is not initialized")
|
||||||
|
}
|
||||||
|
func (p metadataPiece) MarkComplete() error { return errors.New("download storage is not initialized") }
|
||||||
|
func (p metadataPiece) MarkNotComplete() error {
|
||||||
|
return errors.New("download storage is not initialized")
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultTrackerSubscribeURLs = []string{
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt",
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_http.txt",
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_https.txt",
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt",
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_udp.txt",
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ws.txt",
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt",
|
||||||
|
"https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt",
|
||||||
|
"https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt",
|
||||||
|
"https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt",
|
||||||
|
"https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/http.txt",
|
||||||
|
}
|
||||||
|
|
||||||
|
func validTracker(raw string) bool {
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil || u.Hostname() == "" || u.User != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch u.Scheme {
|
||||||
|
case "udp", "http", "https", "ws", "wss":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func uniqueTrackers(groups ...[]string) []string {
|
||||||
|
result := []string{}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, raw := range group {
|
||||||
|
value := strings.TrimSpace(raw)
|
||||||
|
if value != "" && !seen[value] {
|
||||||
|
seen[value] = true
|
||||||
|
result = append(result, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// effectiveTrackers derives the client list without persisting duplicate lists.
|
||||||
|
func (c *config) effectiveTrackers() []string {
|
||||||
|
groups := [][]string{c.CustomTrackers}
|
||||||
|
for _, source := range c.TrackerSubscribeURLs {
|
||||||
|
groups = append(groups, c.State.TrackerSources[source])
|
||||||
|
}
|
||||||
|
return uniqueTrackers(groups...)
|
||||||
|
}
|
||||||
|
func (fm *FetcherManager) NormalizeConfig(raw json.RawMessage) (any, error) {
|
||||||
|
c := fm.DefaultConfig().(*config)
|
||||||
|
if err := json.Unmarshal(raw, c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.ListenPort < 0 || c.ListenPort > 65535 || c.SeedRatio < 0 || c.SeedTime < 0 {
|
||||||
|
return nil, errors.New("invalid BT configuration")
|
||||||
|
}
|
||||||
|
if len(c.TrackerSubscribeURLs) > 64 {
|
||||||
|
return nil, errors.New("too many tracker subscriptions")
|
||||||
|
}
|
||||||
|
c.TrackerSubscribeURLs = uniqueTrackers(c.TrackerSubscribeURLs)
|
||||||
|
for _, source := range c.TrackerSubscribeURLs {
|
||||||
|
u, err := url.Parse(source)
|
||||||
|
if err != nil || u == nil || (u.Scheme != "https" && u.Scheme != "http") || u.Hostname() == "" || u.User != nil {
|
||||||
|
return nil, errors.New("invalid tracker subscription URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.CustomTrackers = uniqueTrackers(c.CustomTrackers)
|
||||||
|
for _, tracker := range c.CustomTrackers {
|
||||||
|
if !validTracker(tracker) {
|
||||||
|
return nil, fmt.Errorf("invalid tracker URL: %s", tracker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.State.TrackerSources == nil {
|
||||||
|
c.State.TrackerSources = map[string][]string{}
|
||||||
|
}
|
||||||
|
for source := range c.State.TrackerSources {
|
||||||
|
if !slices.Contains(c.TrackerSubscribeURLs, source) {
|
||||||
|
delete(c.State.TrackerSources, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.State.TrackerSubscriptionStatuses == nil {
|
||||||
|
c.State.TrackerSubscriptionStatuses = map[string]trackerSubscriptionStatus{}
|
||||||
|
}
|
||||||
|
for source := range c.State.TrackerSubscriptionStatuses {
|
||||||
|
if !slices.Contains(c.TrackerSubscribeURLs, source) {
|
||||||
|
delete(c.State.TrackerSubscriptionStatuses, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
func (fm *FetcherManager) CronJobs(host fetcher.ProtocolHost) []fetcher.CronJob {
|
||||||
|
return []fetcher.CronJob{{
|
||||||
|
Name: "update-trackers",
|
||||||
|
Every: 24 * time.Hour,
|
||||||
|
Timeout: 90 * time.Second,
|
||||||
|
Run: func(ctx context.Context, source fetcher.TriggerSource) error {
|
||||||
|
return fm.updateTrackers(ctx, host, source)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
func parseTrackers(data []byte) ([]string, error) {
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||||
|
scanner.Buffer(make([]byte, 4096), 64*1024)
|
||||||
|
result := []string{}
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(strings.TrimPrefix(scanner.Text(), "\ufeff"))
|
||||||
|
if validTracker(line) {
|
||||||
|
result = append(result, line)
|
||||||
|
}
|
||||||
|
if len(result) > 20000 {
|
||||||
|
return nil, errors.New("too many trackers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, errors.New("subscription contains no valid trackers")
|
||||||
|
}
|
||||||
|
return uniqueTrackers(result), nil
|
||||||
|
}
|
||||||
|
func (fm *FetcherManager) updateTrackers(ctx context.Context, host fetcher.ProtocolHost, source fetcher.TriggerSource) error {
|
||||||
|
snapshot, err := fetcher.ReadConfig[config](host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if (source == fetcher.TriggerScheduled && !snapshot.AutoUpdateTrackers) || len(snapshot.TrackerSubscribeURLs) == 0 {
|
||||||
|
return fetcher.ErrJobSkipped
|
||||||
|
}
|
||||||
|
fetched := map[string][]string{}
|
||||||
|
statuses := map[string]trackerSubscriptionStatus{}
|
||||||
|
var failures []error
|
||||||
|
var mu sync.Mutex
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
slots := make(chan struct{}, 4)
|
||||||
|
for _, address := range snapshot.TrackerSubscribeURLs {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(address string) {
|
||||||
|
defer wg.Done()
|
||||||
|
select {
|
||||||
|
case slots <- struct{}{}:
|
||||||
|
defer func() { <-slots }()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := host.Fetch(ctx, address, 2<<20, func(body []byte) error { _, err := parseTrackers(body); return err })
|
||||||
|
var trackers []string
|
||||||
|
if err == nil {
|
||||||
|
trackers, err = parseTrackers(data)
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
statuses[address] = trackerSubscriptionStatus{Success: err == nil, TrackerCount: len(trackers), LastFetchTime: time.Now().UTC()}
|
||||||
|
if err != nil {
|
||||||
|
failures = append(failures, fmt.Errorf("subscription %s: %w", address, err))
|
||||||
|
} else {
|
||||||
|
fetched[address] = trackers
|
||||||
|
}
|
||||||
|
}(address)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
failures = append(failures, err)
|
||||||
|
}
|
||||||
|
if len(statuses) == 0 {
|
||||||
|
return errors.Join(failures...)
|
||||||
|
}
|
||||||
|
err = fetcher.UpdateState[config, state](host, func(current *config, state *state) error {
|
||||||
|
if !slices.Equal(current.TrackerSubscribeURLs, snapshot.TrackerSubscribeURLs) {
|
||||||
|
return fetcher.ErrConfigChanged
|
||||||
|
}
|
||||||
|
if state.TrackerSources == nil {
|
||||||
|
state.TrackerSources = map[string][]string{}
|
||||||
|
}
|
||||||
|
if state.TrackerSubscriptionStatuses == nil {
|
||||||
|
state.TrackerSubscriptionStatuses = map[string]trackerSubscriptionStatus{}
|
||||||
|
}
|
||||||
|
for address, status := range statuses {
|
||||||
|
state.TrackerSubscriptionStatuses[address] = status
|
||||||
|
}
|
||||||
|
for address, trackers := range fetched {
|
||||||
|
state.TrackerSources[address] = trackers
|
||||||
|
}
|
||||||
|
if len(failures) == 0 {
|
||||||
|
state.LastTrackerUpdateTime = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current, err := fetcher.ReadConfig[config](host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fm.applyTrackers(current.effectiveTrackers())
|
||||||
|
return errors.Join(failures...)
|
||||||
|
}
|
||||||
|
func (fm *FetcherManager) applyTrackers(trackers []string) {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
announce := make([][]string, 0, len(trackers))
|
||||||
|
for _, tracker := range trackers {
|
||||||
|
announce = append(announce, []string{tracker})
|
||||||
|
}
|
||||||
|
for _, t := range client.Torrents() {
|
||||||
|
info := t.Info()
|
||||||
|
if info == nil || (info.Private != nil && *info.Private) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.AddTrackers(announce)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
func trackerTestHost(t *testing.T, c *config, fetch func(context.Context, string, int64, func([]byte) error) ([]byte, error)) (fetcher.ProtocolHost, *sync.Mutex) {
|
||||||
|
t.Helper()
|
||||||
|
mu := &sync.Mutex{}
|
||||||
|
return fetcher.ProtocolHost{
|
||||||
|
Read: func(dst any) error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
raw, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(raw, dst)
|
||||||
|
},
|
||||||
|
UpdateState: func(change func(config, state json.RawMessage) (any, error)) error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
raw, _ := json.Marshal(c)
|
||||||
|
currentState, _ := json.Marshal(c.State)
|
||||||
|
next, err := change(raw, currentState)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
raw, err = json.Marshal(next)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(raw, &c.State)
|
||||||
|
},
|
||||||
|
Fetch: fetch,
|
||||||
|
}, mu
|
||||||
|
}
|
||||||
|
func TestTrackerRefreshPartialSuccessUsesLatestConfig(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
cfg := fm.DefaultConfig().(*config)
|
||||||
|
cfg.TrackerSubscribeURLs = []string{"https://a.example/list", "https://b.example/list"}
|
||||||
|
cfg.CustomTrackers = []string{"udp://custom.example:80/announce"}
|
||||||
|
cfg.State.TrackerSources = map[string][]string{cfg.TrackerSubscribeURLs[0]: {"udp://old.example:80/announce"}, cfg.TrackerSubscribeURLs[1]: {"udp://cached.example:80/announce"}}
|
||||||
|
host, mu := trackerTestHost(t, cfg, nil)
|
||||||
|
host.Fetch = func(ctx context.Context, address string, _ int64, validate func([]byte) error) ([]byte, error) {
|
||||||
|
if address == "https://b.example/list" {
|
||||||
|
return nil, errors.New("offline")
|
||||||
|
}
|
||||||
|
// Simulate a frontend update while the network request is in flight.
|
||||||
|
mu.Lock()
|
||||||
|
cfg.CustomTrackers = []string{"udp://new-custom.example:80/announce"}
|
||||||
|
mu.Unlock()
|
||||||
|
data := []byte("udp://fresh.example:80/announce\n# ignored\nudp://fresh.example:80/announce\n")
|
||||||
|
return data, validate(data)
|
||||||
|
}
|
||||||
|
err := fm.updateTrackers(context.Background(), host, fetcher.TriggerManual)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("partial failure must be reported")
|
||||||
|
}
|
||||||
|
want := []string{"udp://new-custom.example:80/announce", "udp://fresh.example:80/announce", "udp://cached.example:80/announce"}
|
||||||
|
if !reflect.DeepEqual(cfg.effectiveTrackers(), want) {
|
||||||
|
t.Fatalf("got %v", cfg.effectiveTrackers())
|
||||||
|
}
|
||||||
|
success := cfg.State.TrackerSubscriptionStatuses["https://a.example/list"]
|
||||||
|
failure := cfg.State.TrackerSubscriptionStatuses["https://b.example/list"]
|
||||||
|
if !success.Success || success.TrackerCount != 1 || success.LastFetchTime.IsZero() || failure.Success || failure.TrackerCount != 0 || failure.LastFetchTime.IsZero() {
|
||||||
|
t.Fatalf("unexpected subscription statuses: %+v", cfg.State.TrackerSubscriptionStatuses)
|
||||||
|
}
|
||||||
|
if !cfg.State.LastTrackerUpdateTime.IsZero() {
|
||||||
|
t.Fatal("partial refresh marked fully successful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestTrackerRefreshDiscardsChangedSubscriptions(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
cfg := fm.DefaultConfig().(*config)
|
||||||
|
cfg.TrackerSubscribeURLs = []string{"https://a.example/list"}
|
||||||
|
host, mu := trackerTestHost(t, cfg, nil)
|
||||||
|
host.Fetch = func(context.Context, string, int64, func([]byte) error) ([]byte, error) {
|
||||||
|
mu.Lock()
|
||||||
|
cfg.TrackerSubscribeURLs = []string{}
|
||||||
|
mu.Unlock()
|
||||||
|
return []byte("udp://fresh.example:80/announce"), nil
|
||||||
|
}
|
||||||
|
if err := fm.updateTrackers(context.Background(), host, fetcher.TriggerManual); !errors.Is(err, fetcher.ErrConfigChanged) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(cfg.State.TrackerSources) != 0 {
|
||||||
|
t.Fatal("outdated source was committed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestTrackerRefreshManualWorksWhenAutomaticDisabled(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
cfg := fm.DefaultConfig().(*config)
|
||||||
|
cfg.AutoUpdateTrackers = false
|
||||||
|
cfg.TrackerSubscribeURLs = []string{"https://a.example/list"}
|
||||||
|
calls := 0
|
||||||
|
host, _ := trackerTestHost(t, cfg, func(context.Context, string, int64, func([]byte) error) ([]byte, error) {
|
||||||
|
calls++
|
||||||
|
return []byte("udp://fresh.example:80/announce"), nil
|
||||||
|
})
|
||||||
|
if err := fm.updateTrackers(context.Background(), host, fetcher.TriggerScheduled); !errors.Is(err, fetcher.ErrJobSkipped) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if calls != 0 {
|
||||||
|
t.Fatal("scheduled job fetched while disabled")
|
||||||
|
}
|
||||||
|
if err := fm.updateTrackers(context.Background(), host, fetcher.TriggerManual); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if calls != 1 || cfg.State.LastTrackerUpdateTime.IsZero() {
|
||||||
|
t.Fatal("manual refresh did not succeed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestTrackerConfigPreservesExplicitSettings(t *testing.T) {
|
||||||
|
value, err := (&FetcherManager{}).NormalizeConfig(json.RawMessage(`{
|
||||||
|
"listenPort":6881, "trackerSubscribeUrls":[], "autoUpdateTrackers":false,
|
||||||
|
"customTrackers":["udp://custom.example:80/announce"]
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg := value.(*config)
|
||||||
|
if cfg.AutoUpdateTrackers || len(cfg.TrackerSubscribeURLs) != 0 || cfg.ListenPort != 6881 {
|
||||||
|
t.Fatalf("%+v", cfg)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.effectiveTrackers(), cfg.CustomTrackers) {
|
||||||
|
t.Fatalf("derived trackers: %v", cfg.effectiveTrackers())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTrackerList(t *testing.T) {
|
||||||
|
got, err := parseTrackers([]byte("\ufeffudp://tracker.example:80/announce\r\n\n<html>error</html>\nhttps://tracker.example/announce\n"))
|
||||||
|
if err != nil || len(got) != 2 {
|
||||||
|
t.Fatalf("%v %v", got, err)
|
||||||
|
}
|
||||||
|
if _, err := parseTrackers([]byte("<html>mirror error</html>")); err == nil {
|
||||||
|
t.Fatal("invalid mirror body accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestTrackerDefaultsRegisterDailyJob(t *testing.T) {
|
||||||
|
jobs := (&FetcherManager{}).CronJobs(fetcher.ProtocolHost{})
|
||||||
|
if len(jobs) != 1 || jobs[0].Name != "update-trackers" || jobs[0].Every != 24*time.Hour {
|
||||||
|
t.Fatal(jobs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackerRefreshAllFailuresPersistStatusesAndKeepCache(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
cfg := fm.DefaultConfig().(*config)
|
||||||
|
address := "https://custom.example/trackers.txt"
|
||||||
|
cfg.TrackerSubscribeURLs = []string{address}
|
||||||
|
cfg.State.TrackerSources[address] = []string{"udp://cached.example:80/announce"}
|
||||||
|
cfg.State.TrackerSubscriptionStatuses[address] = trackerSubscriptionStatus{Success: true, TrackerCount: 1, LastFetchTime: time.Now().Add(-time.Hour)}
|
||||||
|
host, _ := trackerTestHost(t, cfg, func(context.Context, string, int64, func([]byte) error) ([]byte, error) {
|
||||||
|
return nil, errors.New("offline")
|
||||||
|
})
|
||||||
|
before := time.Now()
|
||||||
|
if err := fm.updateTrackers(context.Background(), host, fetcher.TriggerManual); err == nil {
|
||||||
|
t.Fatal("failure was not reported")
|
||||||
|
}
|
||||||
|
status, ok := cfg.State.TrackerSubscriptionStatuses[address]
|
||||||
|
if !ok || status.Success || status.TrackerCount != 0 || status.LastFetchTime.Before(before) {
|
||||||
|
t.Fatalf("failed fetch status not stored: %+v", status)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.State.TrackerSources[address], []string{"udp://cached.example:80/announce"}) {
|
||||||
|
t.Fatal("last good cache lost")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(cfg.effectiveTrackers(), cfg.State.TrackerSources[address]) {
|
||||||
|
t.Fatal("cached trackers not retained")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackerConfigPrunesRemovedSubscriptions(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
cfg := fm.DefaultConfig().(*config)
|
||||||
|
address := "https://custom.example/trackers.txt"
|
||||||
|
cfg.TrackerSubscribeURLs = []string{}
|
||||||
|
cfg.State.TrackerSources[address] = []string{"udp://cached.example:80/announce"}
|
||||||
|
cfg.State.TrackerSubscriptionStatuses[address] = trackerSubscriptionStatus{Success: true, TrackerCount: 1, LastFetchTime: time.Now()}
|
||||||
|
raw, _ := json.Marshal(cfg)
|
||||||
|
value, err := fm.NormalizeConfig(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
next := value.(*config)
|
||||||
|
if len(next.State.TrackerSources) != 0 || len(next.State.TrackerSubscriptionStatuses) != 0 || len(next.effectiveTrackers()) != 0 {
|
||||||
|
t.Fatal("removed subscription still owns cached results")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackerRefreshTimeoutPersistsCompletedAttemptStatus(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
cfg := fm.DefaultConfig().(*config)
|
||||||
|
address := "https://custom.example/trackers.txt"
|
||||||
|
cfg.TrackerSubscribeURLs = []string{address}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
host, _ := trackerTestHost(t, cfg, func(context.Context, string, int64, func([]byte) error) ([]byte, error) {
|
||||||
|
cancel()
|
||||||
|
return nil, context.DeadlineExceeded
|
||||||
|
})
|
||||||
|
if err := fm.updateTrackers(ctx, host, fetcher.TriggerManual); !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
status, ok := cfg.State.TrackerSubscriptionStatuses[address]
|
||||||
|
if !ok || status.Success || status.TrackerCount != 0 || status.LastFetchTime.IsZero() {
|
||||||
|
t.Fatalf("timeout status lost: %+v", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,17 +67,14 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
|||||||
f.ctl.GetConfig(&f.config)
|
f.ctl.GetConfig(&f.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) error {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
link, err := parseLink(req.URL)
|
link, err := parseLink(req.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f.meta.Req = req
|
f.meta.Req = req
|
||||||
f.meta.Opts = opts
|
|
||||||
if f.meta.Opts == nil {
|
|
||||||
f.meta.Opts = &base.Options{}
|
|
||||||
}
|
|
||||||
f.meta.Res = buildResource(link)
|
f.meta.Res = buildResource(link)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
f := (&FetcherManager{}).Build()
|
f := (&FetcherManager{}).Build()
|
||||||
f.Setup(controller.NewController())
|
f.Setup(controller.NewController())
|
||||||
|
|
||||||
err := f.Resolve(&base.Request{URL: testLink}, &base.Options{Path: t.TempDir()})
|
f.Meta().Opts = &base.Options{Path: t.TempDir()}
|
||||||
|
err := f.Resolve(&base.Request{URL: testLink})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resolve() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,404 @@
|
|||||||
|
package ftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jlaffaye/ftp"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
|
||||||
|
pftp "github.com/GopeedLab/gopeed/pkg/protocol/ftp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
connectTimeout = 15 * time.Second
|
||||||
|
readTimeout = 15 * time.Second
|
||||||
|
quitTimeout = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// target is a parsed FTP URL. It holds the password, so it must never be
|
||||||
|
// formatted into an error or a log line.
|
||||||
|
type target struct {
|
||||||
|
host string // host name or IP, the TLS server name
|
||||||
|
addr string // host:port
|
||||||
|
user string
|
||||||
|
pass string
|
||||||
|
path string // path relative to the login directory, "" for the directory itself
|
||||||
|
tlsMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTarget reads an ftp, ftps or ftpes URL. The path follows RFC 1738: it
|
||||||
|
// is relative to the login directory, and an encoded leading slash
|
||||||
|
// (ftp://host/%2Fpub) makes it absolute.
|
||||||
|
func parseTarget(rawURL, optTLS string) (*target, error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
// url.Error repeats the whole URL, password included.
|
||||||
|
return nil, errors.New("ftp: invalid URL")
|
||||||
|
}
|
||||||
|
t := &target{}
|
||||||
|
switch strings.ToLower(u.Scheme) {
|
||||||
|
case "ftps":
|
||||||
|
t.tlsMode = pftp.TLSImplicit
|
||||||
|
case "ftpes":
|
||||||
|
t.tlsMode = pftp.TLSExplicit
|
||||||
|
case "ftp":
|
||||||
|
switch strings.ToLower(optTLS) {
|
||||||
|
case "", pftp.TLSNone:
|
||||||
|
t.tlsMode = pftp.TLSNone
|
||||||
|
case pftp.TLSExplicit:
|
||||||
|
t.tlsMode = pftp.TLSExplicit
|
||||||
|
case pftp.TLSImplicit:
|
||||||
|
t.tlsMode = pftp.TLSImplicit
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("ftp: unknown tls mode %q", optTLS)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("ftp: unsupported scheme %q", u.Scheme)
|
||||||
|
}
|
||||||
|
t.host = u.Hostname()
|
||||||
|
if t.host == "" {
|
||||||
|
return nil, errors.New("ftp: URL has no host")
|
||||||
|
}
|
||||||
|
port := u.Port()
|
||||||
|
if port == "" {
|
||||||
|
port = "21"
|
||||||
|
if t.tlsMode == pftp.TLSImplicit {
|
||||||
|
port = "990"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.addr = net.JoinHostPort(t.host, port)
|
||||||
|
if u.User != nil {
|
||||||
|
// Username and Password are already percent-decoded.
|
||||||
|
t.user = u.User.Username()
|
||||||
|
t.pass, _ = u.User.Password()
|
||||||
|
}
|
||||||
|
if t.user == "" {
|
||||||
|
t.user, t.pass = "anonymous", "anonymous@"
|
||||||
|
}
|
||||||
|
t.path = strings.TrimPrefix(u.Path, "/")
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dialContextFunc func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
|
|
||||||
|
// session is one logged-in control connection. It owns the network
|
||||||
|
// connections it opens, so abort can unblock any read or write in progress.
|
||||||
|
type session struct {
|
||||||
|
*ftp.ServerConn
|
||||||
|
|
||||||
|
ctrl net.Conn // control connection as dialed, under any TLS layer
|
||||||
|
dataHost string // where data connections go
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
data net.Conn // current data connection as dialed
|
||||||
|
closed bool
|
||||||
|
stop func() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// deadline bounds the next control-connection exchange. The control
|
||||||
|
// connection is idle during a transfer, so every exchange sets a new one.
|
||||||
|
func (s *session) deadline() {
|
||||||
|
_ = s.ctrl.SetDeadline(time.Now().Add(readTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearDeadline lets the control connection stay idle through a transfer.
|
||||||
|
func (s *session) clearDeadline() {
|
||||||
|
_ = s.ctrl.SetDeadline(time.Time{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// abort closes every connection at once. Pending calls fail immediately.
|
||||||
|
func (s *session) abort() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.closed = true
|
||||||
|
_ = s.ctrl.Close()
|
||||||
|
if s.data != nil {
|
||||||
|
_ = s.data.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quit logs out and waits, briefly, for the server to hang up. Servers that
|
||||||
|
// cap logins count a session until they have seen it end, so the next login
|
||||||
|
// would otherwise be refused.
|
||||||
|
func (s *session) quit() {
|
||||||
|
if s.stop != nil {
|
||||||
|
s.stop()
|
||||||
|
}
|
||||||
|
_ = s.ctrl.SetDeadline(time.Now().Add(quitTimeout))
|
||||||
|
if s.ServerConn != nil {
|
||||||
|
_ = s.ServerConn.Quit()
|
||||||
|
}
|
||||||
|
s.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) setData(c net.Conn) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
_ = c.Close()
|
||||||
|
return net.ErrClosed
|
||||||
|
}
|
||||||
|
s.data = c
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctrlConn is the control connection handed to the ftp library.
|
||||||
|
type ctrlConn struct {
|
||||||
|
net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteAddr must be a *net.TCPAddr: the library asserts it. A connection
|
||||||
|
// through a proxy may report something else, and its address is not used
|
||||||
|
// for data connections anyway (see dialData).
|
||||||
|
func (c ctrlConn) RemoteAddr() net.Addr {
|
||||||
|
if a, ok := c.Conn.RemoteAddr().(*net.TCPAddr); ok {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return &net.TCPAddr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is called by the library's Quit right after it sends QUIT. Reading
|
||||||
|
// until the server hangs up (bounded by the deadline quit set) tells us the
|
||||||
|
// server has ended the session.
|
||||||
|
func (c ctrlConn) Close() error {
|
||||||
|
_, _ = io.Copy(io.Discard, c.Conn)
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// dial opens a control connection, upgrades it to TLS when asked, and logs in.
|
||||||
|
//
|
||||||
|
// The library is given a dial function for every connection. That is how
|
||||||
|
// data connections go through the same proxy as the control connection, but
|
||||||
|
// it also means the library does no TLS on them: with a dial function it
|
||||||
|
// neither wraps an implicit-TLS control connection nor any data connection.
|
||||||
|
// We do both here. Explicit TLS (AUTH TLS) is still done by the library, and
|
||||||
|
// so are PBSZ and PROT P after login. For the same reason the library's
|
||||||
|
// DialWithTimeout and DialWithContext would have no effect: connectTimeout
|
||||||
|
// and ctx are applied to our own dials instead.
|
||||||
|
func (f *Fetcher) dial(ctx context.Context, t *target) (*session, error) {
|
||||||
|
netDial, proxied, err := f.netDialer(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tlsConf *tls.Config
|
||||||
|
if t.tlsMode != pftp.TLSNone {
|
||||||
|
tlsConf = f.tlsConfig(t.host)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialCtx, cancel := context.WithTimeout(ctx, connectTimeout)
|
||||||
|
defer cancel()
|
||||||
|
raw, err := netDial(dialCtx, "tcp", t.addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctrl := raw
|
||||||
|
if t.tlsMode == pftp.TLSImplicit {
|
||||||
|
tc := tls.Client(raw, tlsConf)
|
||||||
|
if err := tc.HandshakeContext(dialCtx); err != nil {
|
||||||
|
_ = raw.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctrl = tc
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &session{ctrl: raw, dataHost: t.host}
|
||||||
|
if a, ok := raw.RemoteAddr().(*net.TCPAddr); ok && !proxied {
|
||||||
|
// Same IP as the control connection, as the library itself does,
|
||||||
|
// so a name with several addresses cannot send us elsewhere.
|
||||||
|
s.dataHost = a.IP.String()
|
||||||
|
}
|
||||||
|
s.stop = context.AfterFunc(ctx, s.abort)
|
||||||
|
s.deadline()
|
||||||
|
|
||||||
|
handedOut := false
|
||||||
|
opts := []ftp.DialOption{
|
||||||
|
ftp.DialWithShutTimeout(readTimeout),
|
||||||
|
ftp.DialWithDialFunc(func(network, addr string) (net.Conn, error) {
|
||||||
|
if !handedOut {
|
||||||
|
// Dial asks for the control connection first, once.
|
||||||
|
handedOut = true
|
||||||
|
return ctrlConn{ctrl}, nil
|
||||||
|
}
|
||||||
|
return s.dialData(ctx, netDial, tlsConf, addr)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
switch t.tlsMode {
|
||||||
|
case pftp.TLSImplicit:
|
||||||
|
opts = append(opts, ftp.DialWithTLS(tlsConf))
|
||||||
|
case pftp.TLSExplicit:
|
||||||
|
opts = append(opts, ftp.DialWithExplicitTLS(tlsConf))
|
||||||
|
}
|
||||||
|
c, err := ftp.Dial(t.addr, opts...)
|
||||||
|
if err != nil {
|
||||||
|
s.stop()
|
||||||
|
s.abort()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.ServerConn = c
|
||||||
|
s.deadline()
|
||||||
|
if err := c.Login(t.user, t.pass); err != nil {
|
||||||
|
s.stop()
|
||||||
|
s.abort()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dialData opens a passive data connection. The library takes the host from
|
||||||
|
// the control connection's peer, which through a proxy is the proxy, so only
|
||||||
|
// the port is used here.
|
||||||
|
func (s *session) dialData(ctx context.Context, netDial dialContextFunc, tlsConf *tls.Config, addr string) (net.Conn, error) {
|
||||||
|
_, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dialCtx, cancel := context.WithTimeout(ctx, connectTimeout)
|
||||||
|
defer cancel()
|
||||||
|
raw, err := netDial(dialCtx, "tcp", net.JoinHostPort(s.dataHost, port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.setData(raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tlsConf != nil {
|
||||||
|
// The handshake runs on the first read, as in the library, because
|
||||||
|
// some servers only start it once the transfer command is answered.
|
||||||
|
return tls.Client(raw, tlsConf), nil
|
||||||
|
}
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsConfig returns the TLS settings for one control connection and its data
|
||||||
|
// connections. Certificates are verified unless the request sets
|
||||||
|
// skipVerifyCert, as for HTTP.
|
||||||
|
//
|
||||||
|
// Each call gets its own one-entry session cache, so a data connection
|
||||||
|
// resumes the TLS session of its own control connection. vsftpd
|
||||||
|
// (require_ssl_reuse) only checks that some session was resumed, but ProFTPD's
|
||||||
|
// mod_tls also checks that it is the control connection's; a cache shared by
|
||||||
|
// several workers would hand a data connection another worker's session.
|
||||||
|
// A cache in the manager's base config is kept as given.
|
||||||
|
func (f *Fetcher) tlsConfig(host string) *tls.Config {
|
||||||
|
cfg := &tls.Config{}
|
||||||
|
if f.manager != nil && f.manager.baseTLS != nil {
|
||||||
|
cfg = f.manager.baseTLS.Clone()
|
||||||
|
}
|
||||||
|
cfg.ServerName = host
|
||||||
|
if f.meta.Req != nil && f.meta.Req.SkipVerifyCert {
|
||||||
|
cfg.InsecureSkipVerify = true
|
||||||
|
}
|
||||||
|
if cfg.ClientSessionCache == nil {
|
||||||
|
cfg.ClientSessionCache = tls.NewLRUClientSessionCache(1)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// netDialer picks how to reach the server, from the same proxy settings the
|
||||||
|
// HTTP protocol reads (the task's proxy, otherwise the global one).
|
||||||
|
func (f *Fetcher) netDialer(t *target) (dialContextFunc, bool, error) {
|
||||||
|
direct := (&net.Dialer{Timeout: connectTimeout}).DialContext
|
||||||
|
pu, err := f.proxyURL(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if pu == nil {
|
||||||
|
return direct, false, nil
|
||||||
|
}
|
||||||
|
switch strings.ToLower(pu.Scheme) {
|
||||||
|
case "socks5", "socks5h":
|
||||||
|
d, err := proxy.FromURL(pu, &net.Dialer{Timeout: connectTimeout})
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("ftp: socks5 proxy: %w", err)
|
||||||
|
}
|
||||||
|
if cd, ok := d.(proxy.ContextDialer); ok {
|
||||||
|
return cd.DialContext, true, nil
|
||||||
|
}
|
||||||
|
return func(_ context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return d.Dial(network, addr)
|
||||||
|
}, true, nil
|
||||||
|
default:
|
||||||
|
// An HTTP proxy is not used. Every passive data connection goes to
|
||||||
|
// a port the server picks, and HTTP proxies usually allow CONNECT to
|
||||||
|
// port 443 only, so the transfer would fail after the login worked.
|
||||||
|
f.warnOnce.Do(func() {
|
||||||
|
if l := f.logger(); l != nil {
|
||||||
|
l.Warn().Msgf("ftp: the %s proxy at %s cannot carry FTP data connections (HTTP proxy), connecting directly; use a SOCKS5 proxy for FTP", strings.ToLower(pu.Scheme), pu.Host)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return direct, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) proxyURL(t *target) (*url.URL, error) {
|
||||||
|
if f.ctl == nil || f.ctl.GetProxy == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
handler := f.ctl.GetProxy(f.meta.Req.Proxy)
|
||||||
|
if handler == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// The proxy chooser reads only the URL; the credentials stay out.
|
||||||
|
req := &http.Request{URL: &url.URL{Scheme: "ftp", Host: t.addr, Path: "/" + t.path}, Header: http.Header{}}
|
||||||
|
pu, err := handler(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("ftp: cannot read the proxy settings")
|
||||||
|
}
|
||||||
|
return pu, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLoginRefusal tells a server that is full apart from bad credentials.
|
||||||
|
// 421 always means "not now". 530 means a refused login; it is only a
|
||||||
|
// capacity limit when these credentials have worked before. The library
|
||||||
|
// drops the code when USER itself is refused, so a "too many" message counts
|
||||||
|
// as well.
|
||||||
|
func isLoginRefusal(err error, credentialsWorked bool) bool {
|
||||||
|
var te *textproto.Error
|
||||||
|
if errors.As(err, &te) {
|
||||||
|
switch te.Code {
|
||||||
|
case ftp.StatusNotAvailable:
|
||||||
|
return true
|
||||||
|
case ftp.StatusNotLoggedIn:
|
||||||
|
return credentialsWorked || strings.Contains(strings.ToLower(te.Msg), "too many")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(strings.ToLower(err.Error()), "too many")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPermanent reports errors a retry cannot fix: a 5xx reply and a
|
||||||
|
// certificate that does not verify.
|
||||||
|
func isPermanent(err error) bool {
|
||||||
|
var te *textproto.Error
|
||||||
|
if errors.As(err, &te) {
|
||||||
|
return te.Code >= 500
|
||||||
|
}
|
||||||
|
var verifyErr *tls.CertificateVerificationError
|
||||||
|
if errors.As(err, &verifyErr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var recordErr tls.RecordHeaderError
|
||||||
|
if errors.As(err, &recordErr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return errors.Is(err, errSizeChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replyCode(err error) int {
|
||||||
|
var te *textproto.Error
|
||||||
|
if errors.As(err, &te) {
|
||||||
|
return te.Code
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package ftp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/logger"
|
||||||
|
iftp "github.com/GopeedLab/gopeed/internal/protocol/ftp"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
|
pftp "github.com/GopeedLab/gopeed/pkg/protocol/ftp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// syncBuffer is a log sink that the downloader's goroutines can share.
|
||||||
|
type syncBuffer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
buf bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *syncBuffer) Write(p []byte) (int, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.buf.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *syncBuffer) String() string {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// taskEvents records the first done or error event of every task, so the
|
||||||
|
// test never reads a task's status while the downloader writes it.
|
||||||
|
type taskEvents struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
final map[string]download.EventKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *taskEvents) listen(ev *download.Event) {
|
||||||
|
if ev.Key != download.EventKeyDone && ev.Key != download.EventKeyError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
if _, seen := e.final[ev.Task.ID]; !seen {
|
||||||
|
e.final[ev.Task.ID] = ev.Key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *taskEvents) wait(t *testing.T, id string) download.EventKey {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(60 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
e.mu.Lock()
|
||||||
|
key, ok := e.final[id]
|
||||||
|
e.mu.Unlock()
|
||||||
|
if ok {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatalf("task %s did not finish", id)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPasswordNeverLogged runs FTP tasks through the downloader with its core
|
||||||
|
// logger captured: a success, a missing file and a wrong password, all behind
|
||||||
|
// an HTTP proxy that FTP must skip with a warning.
|
||||||
|
func TestPasswordNeverLogged(t *testing.T) {
|
||||||
|
srv := iftp.StartFTPServer(t, iftp.ServerOptions{User: "u", Pass: "p@ss"})
|
||||||
|
want := srv.WriteFile(t, "secret.bin", iftp.RandomBytes(300000, 21))
|
||||||
|
|
||||||
|
d := download.NewDownloader(&download.DownloaderConfig{Storage: download.NewMemStorage(), StorageDir: t.TempDir()})
|
||||||
|
logs := &syncBuffer{}
|
||||||
|
d.Logger = &logger.Logger{Logger: zerolog.New(logs)}
|
||||||
|
events := &taskEvents{final: map[string]download.EventKey{}}
|
||||||
|
d.Listener(events.listen)
|
||||||
|
if err := d.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Close, not Clear: Clear resets the extension list without a lock while
|
||||||
|
// the last task's done hooks may still read it.
|
||||||
|
defer d.Close()
|
||||||
|
cfg, err := d.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg.Proxy = &base.DownloaderProxyConfig{Enable: true, Scheme: "http", Host: "127.0.0.1:1", Usr: "proxyuser", Pwd: "proxysecret"}
|
||||||
|
if err := d.PutConfig(cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
create := func(rawURL string) string {
|
||||||
|
id, err := d.CreateDirect(&base.Request{URL: rawURL}, &base.Options{Path: dir, Extra: &pftp.OptsExtra{Connections: 2}})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "p@ss") || strings.Contains(err.Error(), "p%40ss") {
|
||||||
|
t.Fatalf("the password leaked into an error: %v", err)
|
||||||
|
}
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
root := "ftp://u:p%40ss@" + srv.Addr + "/"
|
||||||
|
|
||||||
|
okID := create(root + "secret.bin")
|
||||||
|
if key := events.wait(t, okID); key != download.EventKeyDone {
|
||||||
|
t.Fatalf("download ended with %q, logs:\n%s", key, logs.String())
|
||||||
|
}
|
||||||
|
ok := d.GetTask(okID)
|
||||||
|
if ok.Protocol != "ftp" {
|
||||||
|
t.Fatalf("protocol %q, want ftp", ok.Protocol)
|
||||||
|
}
|
||||||
|
if got := iftp.FileSHA256(t, ok.Meta.SingleFilepath()); got != want {
|
||||||
|
t.Fatalf("sha256 %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
missingID := create(root + "missing.bin")
|
||||||
|
if key := events.wait(t, missingID); key != download.EventKeyError {
|
||||||
|
t.Fatalf("a missing file must fail, got %q", key)
|
||||||
|
}
|
||||||
|
wrongID := create("ftp://u:wr0ng%40pw@" + srv.Addr + "/secret.bin")
|
||||||
|
if key := events.wait(t, wrongID); key != download.EventKeyError {
|
||||||
|
t.Fatalf("a wrong password must fail, got %q", key)
|
||||||
|
}
|
||||||
|
missing, wrong := d.GetTask(missingID), d.GetTask(wrongID)
|
||||||
|
for _, task := range []*download.Task{ok, missing, wrong} {
|
||||||
|
if name := task.Name(); strings.Contains(name, "p@ss") || strings.Contains(name, "p%40ss") || strings.Contains(name, "wr0ng") {
|
||||||
|
t.Fatalf("task name %q shows the password", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := logs.String()
|
||||||
|
if !strings.Contains(out, "task id") || !strings.Contains(out, "HTTP proxy") {
|
||||||
|
t.Fatalf("the captured log misses the expected lines, so it proves nothing:\n%s", out)
|
||||||
|
}
|
||||||
|
for _, secret := range []string{"p@ss", "p%40ss", "wr0ng", "proxysecret"} {
|
||||||
|
if strings.Contains(out, secret) {
|
||||||
|
t.Fatalf("%q appears in the log:\n%s", secret, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,109 @@
|
|||||||
|
package ftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
pftp "github.com/GopeedLab/gopeed/pkg/protocol/ftp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fetcherData is what a task saves to resume later. Segments has the same
|
||||||
|
// Begin, End and Downloaded fields every version writes; the other fields are
|
||||||
|
// optional, so a save without them still restores.
|
||||||
|
type fetcherData struct {
|
||||||
|
Segments []*segment
|
||||||
|
// MaxConns is the login ceiling the server enforced, 0 when none was hit.
|
||||||
|
MaxConns int `json:",omitempty"`
|
||||||
|
// NoRest records that the server refused REST, so the task downloads each
|
||||||
|
// file in one sequential stream.
|
||||||
|
NoRest bool `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetcherManager handles ftp://, ftps:// (implicit TLS) and ftpes:// (explicit
|
||||||
|
// TLS) URLs.
|
||||||
|
type FetcherManager struct {
|
||||||
|
// baseTLS is the base for every TLS connection. Nil means the system
|
||||||
|
// roots; the package's tests set it to trust their own certificate. The
|
||||||
|
// task's skipVerifyCert still turns verification off.
|
||||||
|
baseTLS *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Name() string {
|
||||||
|
return "ftp"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
||||||
|
return []*fetcher.SchemeFilter{
|
||||||
|
{Type: fetcher.FilterTypeUrl, Pattern: "FTP"},
|
||||||
|
{Type: fetcher.FilterTypeUrl, Pattern: "FTPS"},
|
||||||
|
{Type: fetcher.FilterTypeUrl, Pattern: "FTPES"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
||||||
|
return &Fetcher{manager: fm}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseName names a task before it is resolved. It never includes the
|
||||||
|
// userinfo of the URL.
|
||||||
|
func (fm *FetcherManager) ParseName(u string) string {
|
||||||
|
parsed, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
name := path.Base(strings.TrimSuffix(parsed.Path, "/"))
|
||||||
|
if name == "" || name == "/" || name == "." {
|
||||||
|
name = parsed.Hostname()
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) AutoRename() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
|
return &config{Connections: defaultConnections}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Store(f fetcher.Fetcher) (any, error) {
|
||||||
|
ff := f.(*Fetcher)
|
||||||
|
ff.mu.Lock()
|
||||||
|
defer ff.mu.Unlock()
|
||||||
|
segments := make([]*segment, len(ff.segments))
|
||||||
|
for i, s := range ff.segments {
|
||||||
|
c := *s
|
||||||
|
segments[i] = &c
|
||||||
|
}
|
||||||
|
return &fetcherData{
|
||||||
|
Segments: segments,
|
||||||
|
MaxConns: ff.maxConns,
|
||||||
|
NoRest: ff.noRest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||||
|
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||||
|
fd := v.(*fetcherData)
|
||||||
|
ff := fm.Build().(*Fetcher)
|
||||||
|
ff.meta = meta
|
||||||
|
if meta != nil && meta.Req != nil {
|
||||||
|
_ = base.ParseReqExtra[pftp.ReqExtra](meta.Req)
|
||||||
|
}
|
||||||
|
if meta != nil && meta.Opts != nil {
|
||||||
|
_ = base.ParseOptExtra[pftp.OptsExtra](meta.Opts)
|
||||||
|
}
|
||||||
|
ff.segments = fd.Segments
|
||||||
|
ff.maxConns = fd.MaxConns
|
||||||
|
ff.noRest = fd.NoRest
|
||||||
|
return ff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,187 @@
|
|||||||
|
package ftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jlaffaye/ftp"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Limits of a directory download. A tree past either one is refused with an
|
||||||
|
// error that names the limit; it is never cut short.
|
||||||
|
const maxWalkDepth = 8
|
||||||
|
|
||||||
|
// maxWalkFiles is a variable so tests can lower it.
|
||||||
|
var maxWalkFiles = 10000
|
||||||
|
|
||||||
|
// resolveTarget describes the URL's path: one file, or a directory walked
|
||||||
|
// into a multi-file resource whose paths are relative to it.
|
||||||
|
func (f *Fetcher) resolveTarget(s *session, t *target) (*base.Resource, error) {
|
||||||
|
isDir, size, mtime, err := f.stat(s, t.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isDir {
|
||||||
|
return &base.Resource{
|
||||||
|
Size: size,
|
||||||
|
Range: true,
|
||||||
|
Files: []*base.FileInfo{{
|
||||||
|
Name: path.Base(t.path),
|
||||||
|
Size: size,
|
||||||
|
Ctime: mtime,
|
||||||
|
}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := f.walk(s, t.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name := path.Base(strings.TrimSuffix(t.path, "/"))
|
||||||
|
if name == "" || name == "." || name == "/" {
|
||||||
|
name = t.host
|
||||||
|
}
|
||||||
|
res := &base.Resource{Name: name, Range: true, Files: files}
|
||||||
|
res.CalcSize(nil)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stat tells a file from a directory. MLST answers both at once. Without
|
||||||
|
// it, SIZE answers for a file, and a directory is one we can change into.
|
||||||
|
func (f *Fetcher) stat(s *session, p string) (isDir bool, size int64, mtime *time.Time, err error) {
|
||||||
|
if p == "" || strings.HasSuffix(p, "/") {
|
||||||
|
return true, 0, nil, nil
|
||||||
|
}
|
||||||
|
s.deadline()
|
||||||
|
if entry, mlstErr := s.GetEntry(p); mlstErr == nil {
|
||||||
|
switch entry.Type {
|
||||||
|
case ftp.EntryTypeFolder:
|
||||||
|
return true, 0, nil, nil
|
||||||
|
case ftp.EntryTypeFile:
|
||||||
|
if !entry.Time.IsZero() {
|
||||||
|
t := entry.Time
|
||||||
|
mtime = &t
|
||||||
|
}
|
||||||
|
return false, int64(entry.Size), mtime, nil
|
||||||
|
}
|
||||||
|
// A link: SIZE below follows it on the server.
|
||||||
|
}
|
||||||
|
// MLST is missing or refused the path: the older commands decide. A
|
||||||
|
// broken connection fails them too, with its own error.
|
||||||
|
|
||||||
|
s.deadline()
|
||||||
|
size, sizeErr := s.FileSize(p)
|
||||||
|
if sizeErr == nil {
|
||||||
|
if s.IsGetTimeSupported() {
|
||||||
|
s.deadline()
|
||||||
|
if t, err := s.GetTime(p); err == nil {
|
||||||
|
mtime = &t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, size, mtime, nil
|
||||||
|
}
|
||||||
|
if replyCode(sizeErr) == 0 {
|
||||||
|
return false, 0, nil, sizeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIZE failed: a directory, or a missing path.
|
||||||
|
s.deadline()
|
||||||
|
cwd, err := s.CurrentDir()
|
||||||
|
if err != nil {
|
||||||
|
return false, 0, nil, err
|
||||||
|
}
|
||||||
|
s.deadline()
|
||||||
|
if err := s.ChangeDir(p); err != nil {
|
||||||
|
return false, 0, nil, sizeErr
|
||||||
|
}
|
||||||
|
s.deadline()
|
||||||
|
if err := s.ChangeDir(cwd); err != nil {
|
||||||
|
return false, 0, nil, err
|
||||||
|
}
|
||||||
|
return true, 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeEntryName reports whether a name from a listing can become a local
|
||||||
|
// path element. A hostile server could otherwise list "../x" and write
|
||||||
|
// outside the task directory.
|
||||||
|
func safeEntryName(name string) bool {
|
||||||
|
if name == "" || name == "." || name == ".." {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !strings.ContainsAny(name, "/\\\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk lists a directory tree, at most maxWalkDepth levels below root and
|
||||||
|
// maxWalkFiles files. Files come back sorted by path, then name.
|
||||||
|
func (f *Fetcher) walk(s *session, root string) ([]*base.FileInfo, error) {
|
||||||
|
type dir struct {
|
||||||
|
remote string // path on the server
|
||||||
|
rel string // path below root, "" for root itself
|
||||||
|
depth int
|
||||||
|
}
|
||||||
|
var files []*base.FileInfo
|
||||||
|
stack := []dir{{remote: strings.TrimSuffix(root, "/"), rel: "", depth: 0}}
|
||||||
|
for len(stack) > 0 {
|
||||||
|
d := stack[len(stack)-1]
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
|
||||||
|
s.deadline()
|
||||||
|
entries, err := s.List(d.remote)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.Name == "." || e.Name == ".." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !safeEntryName(e.Name) {
|
||||||
|
f.warn("ftp: skipping a listed name that is not a safe file name: %q", e.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remote := path.Join(d.remote, e.Name)
|
||||||
|
rel := path.Join(d.rel, e.Name)
|
||||||
|
if d.remote == "" {
|
||||||
|
remote = e.Name
|
||||||
|
}
|
||||||
|
entryType, size := e.Type, int64(e.Size)
|
||||||
|
if entryType == ftp.EntryTypeLink {
|
||||||
|
// A link to a file has a size; a link to a directory is
|
||||||
|
// not followed, so a loop cannot make the walk endless.
|
||||||
|
s.deadline()
|
||||||
|
linkSize, err := s.FileSize(remote)
|
||||||
|
if err != nil {
|
||||||
|
f.warn("ftp: skipping link %q: %v", rel, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entryType, size = ftp.EntryTypeFile, linkSize
|
||||||
|
}
|
||||||
|
switch entryType {
|
||||||
|
case ftp.EntryTypeFolder:
|
||||||
|
if d.depth+1 > maxWalkDepth {
|
||||||
|
return nil, fmt.Errorf("ftp: the folder is deeper than %d levels, the limit for a folder download", maxWalkDepth)
|
||||||
|
}
|
||||||
|
stack = append(stack, dir{remote: remote, rel: rel, depth: d.depth + 1})
|
||||||
|
case ftp.EntryTypeFile:
|
||||||
|
if len(files) >= maxWalkFiles {
|
||||||
|
return nil, fmt.Errorf("ftp: the folder has more than %d files, the limit for a folder download", maxWalkFiles)
|
||||||
|
}
|
||||||
|
fi := &base.FileInfo{Name: e.Name, Path: d.rel, Size: size}
|
||||||
|
if !e.Time.IsZero() {
|
||||||
|
t := e.Time
|
||||||
|
fi.Ctime = &t
|
||||||
|
}
|
||||||
|
files = append(files, fi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(files, func(a, b *base.FileInfo) int {
|
||||||
|
return cmp.Or(strings.Compare(a.Path, b.Path), strings.Compare(a.Name, b.Name))
|
||||||
|
})
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
@@ -172,11 +172,11 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
return f.meta
|
return f.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) (err error) {
|
func (f *Fetcher) Resolve(req *base.Request) (err error) {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
f.meta.Req = req
|
f.meta.Req = req
|
||||||
f.meta.Opts = opts
|
|
||||||
f.extra = &ReqExtra{}
|
f.extra = &ReqExtra{}
|
||||||
if err = base.ParseReqExtra[ReqExtra](req); err != nil {
|
if err = base.ParseReqExtra[ReqExtra](req); err != nil {
|
||||||
return
|
return
|
||||||
@@ -194,10 +194,6 @@ func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) (err error) {
|
|||||||
}
|
}
|
||||||
state.StagingID = randomStagingID()
|
state.StagingID = randomStagingID()
|
||||||
f.state = state
|
f.state = state
|
||||||
// Pin the staging folder now so the very first persisted state already
|
|
||||||
// carries it: a crash right after create still resumes into the same
|
|
||||||
// folder.
|
|
||||||
f.resolveStagingDirLocked()
|
|
||||||
size := planTotalSize(state.Segments)
|
size := planTotalSize(state.Segments)
|
||||||
f.meta.Res = &base.Resource{
|
f.meta.Res = &base.Resource{
|
||||||
Range: false,
|
Range: false,
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ func mustResolve(t *testing.T, f *Fetcher, playlistURL, referer string) {
|
|||||||
if referer != "" {
|
if referer != "" {
|
||||||
req.Extra = &ReqExtra{Header: map[string]string{"Referer": referer}}
|
req.Extra = &ReqExtra{Header: map[string]string{"Referer": referer}}
|
||||||
}
|
}
|
||||||
if err := f.Resolve(req, &base.Options{Path: t.TempDir()}); err != nil {
|
f.Meta().Opts = &base.Options{Path: t.TempDir()}
|
||||||
|
if err := f.Resolve(req); err != nil {
|
||||||
t.Fatalf("resolve: %v", err)
|
t.Fatalf("resolve: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +208,8 @@ func TestFetcherE2E_RefererEnforced(t *testing.T) {
|
|||||||
// Without Referer the resolve itself must fail (403 on the playlist).
|
// Without Referer the resolve itself must fail (403 on the playlist).
|
||||||
blocked := newTestFetcher(t, testConfig())
|
blocked := newTestFetcher(t, testConfig())
|
||||||
req := &base.Request{URL: server.URL + "/video.m3u8"}
|
req := &base.Request{URL: server.URL + "/video.m3u8"}
|
||||||
if err := blocked.Resolve(req, &base.Options{Path: t.TempDir()}); err == nil {
|
blocked.Meta().Opts = &base.Options{Path: t.TempDir()}
|
||||||
|
if err := blocked.Resolve(req); err == nil {
|
||||||
t.Fatal("resolve without Referer should fail")
|
t.Fatal("resolve without Referer should fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +219,8 @@ func TestFetcherE2E_RefererEnforced(t *testing.T) {
|
|||||||
URL: server.URL + "/video.m3u8",
|
URL: server.URL + "/video.m3u8",
|
||||||
Extra: &ReqExtra{Header: map[string]string{"Referer": page}},
|
Extra: &ReqExtra{Header: map[string]string{"Referer": page}},
|
||||||
}
|
}
|
||||||
if err := allowed.Resolve(allowedReq, &base.Options{Path: t.TempDir()}); err != nil {
|
allowed.Meta().Opts = &base.Options{Path: t.TempDir()}
|
||||||
|
if err := allowed.Resolve(allowedReq); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
mustRun(t, allowed)
|
mustRun(t, allowed)
|
||||||
@@ -560,7 +563,8 @@ func TestFetcherE2E_StoreRestoreAuthenticated(t *testing.T) {
|
|||||||
URL: server.URL + "/video.m3u8",
|
URL: server.URL + "/video.m3u8",
|
||||||
Extra: &ReqExtra{Header: map[string]string{"Referer": page}},
|
Extra: &ReqExtra{Header: map[string]string{"Referer": page}},
|
||||||
}
|
}
|
||||||
if err := f.Resolve(req, &base.Options{Path: t.TempDir()}); err != nil {
|
f.Meta().Opts = &base.Options{Path: t.TempDir()}
|
||||||
|
if err := f.Resolve(req); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := f.Start(); err != nil {
|
if err := f.Start(); err != nil {
|
||||||
@@ -630,7 +634,8 @@ func TestFetcherE2E_PatchPartialUpdate(t *testing.T) {
|
|||||||
Extra: &ReqExtra{Header: map[string]string{"Referer": page}},
|
Extra: &ReqExtra{Header: map[string]string{"Referer": page}},
|
||||||
}
|
}
|
||||||
opts := &base.Options{Path: t.TempDir()}
|
opts := &base.Options{Path: t.TempDir()}
|
||||||
if err := f.Resolve(req, opts); err != nil {
|
f.Meta().Opts = opts
|
||||||
|
if err := f.Resolve(req); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,7 +718,8 @@ func TestFetcherE2E_PatchWhileRunningRejected(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
f := newTestFetcher(t, testConfig())
|
f := newTestFetcher(t, testConfig())
|
||||||
if err := f.Resolve(&base.Request{URL: server.URL + "/video.m3u8"}, &base.Options{Path: t.TempDir()}); err != nil {
|
f.Meta().Opts = &base.Options{Path: t.TempDir()}
|
||||||
|
if err := f.Resolve(&base.Request{URL: server.URL + "/video.m3u8"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := f.Start(); err != nil {
|
if err := f.Start(); err != nil {
|
||||||
@@ -767,7 +773,8 @@ func TestFetcherE2E_RetryAfterFailure(t *testing.T) {
|
|||||||
cfg.MaxRetries = 0 // fail the run on the first bad segment
|
cfg.MaxRetries = 0 // fail the run on the first bad segment
|
||||||
cfg.PrefetchContentLength = false
|
cfg.PrefetchContentLength = false
|
||||||
f := newTestFetcher(t, cfg)
|
f := newTestFetcher(t, cfg)
|
||||||
if err := f.Resolve(&base.Request{URL: server.URL + "/video.m3u8"}, &base.Options{Path: t.TempDir()}); err != nil {
|
f.Meta().Opts = &base.Options{Path: t.TempDir()}
|
||||||
|
if err := f.Resolve(&base.Request{URL: server.URL + "/video.m3u8"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := f.Start(); err != nil {
|
if err := f.Start(); err != nil {
|
||||||
@@ -828,7 +835,8 @@ func TestFetcherE2E_StateMissingSelfHeal(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
f := newTestFetcher(t, testConfig())
|
f := newTestFetcher(t, testConfig())
|
||||||
if err := f.Resolve(&base.Request{URL: server.URL + "/video.m3u8"}, &base.Options{Path: t.TempDir()}); err != nil {
|
f.Meta().Opts = &base.Options{Path: t.TempDir()}
|
||||||
|
if err := f.Resolve(&base.Request{URL: server.URL + "/video.m3u8"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Simulate the crash window where the engine persisted the task with its
|
// Simulate the crash window where the engine persisted the task with its
|
||||||
|
|||||||
Loaded 50 of 283 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user