Compare commits

...
109 Commits
Author SHA1 Message Date
liwei 7c6c361d30 fix: docker dist copy 2022-11-08 22:55:56 +08:00
liwei 872ba0ba87 doc: fix docker run command 2022-11-08 22:36:47 +08:00
monkeyWie 93c14dcee5 fix: ip bind (#50) 2022-11-08 22:29:33 +08:00
liwei 1af0b2175c ci: path include Dockerfile 2022-11-08 21:50:45 +08:00
monkeyWie ee81ebffef fix: docker executable (#49) 2022-11-08 21:49:02 +08:00
levi dc00fd2543 doc: fix 2022-11-08 15:02:58 +08:00
levi 11015c7be1 doc: mac repair tip 2022-11-08 14:35:51 +08:00
monkeyWie 57befd2c03 fix: web download dir setting (#46) 2022-11-08 14:31:49 +08:00
monkeyWie 35156ed968 ci: remove windows zip root folder 2022-11-07 20:29:10 +08:00
levi ee6c36174f ci: fix version 2022-11-07 17:25:23 +08:00
levi fdfd95555d ci: fix docker build 2022-11-07 17:20:31 +08:00
levi 7ec824edda ci: fix 2022-11-07 17:13:00 +08:00
levi 1f8b1be669 ci: fix action version 2022-11-07 16:49:58 +08:00
levi b0bcfeae34 ci: fix action version 2022-11-07 16:48:02 +08:00
levi 10b346a326 ci: fix 2022-11-07 16:45:44 +08:00
levi a85af39b35 ci: fix build 2022-11-07 16:43:55 +08:00
levi 576f0d2e24 ci: fix docker tag 2022-11-07 16:41:52 +08:00
monkeyWie 95eb3a427c feat: docker support (#43) 2022-11-07 16:32:52 +08:00
monkeyWie e9442edd94 feat: support drag torrent file (#42) 2022-11-07 14:12:12 +08:00
monkeyWie 77e59f7376 feat: i18n support (#40) 2022-11-05 17:04:22 +08:00
monkeyWie 135fba1b54 doc: fix md 2022-11-05 13:16:57 +08:00
liwei ff813e3bc9 ci: build on published 2022-11-05 11:27:56 +08:00
Trim21 babfe79023 Update flags.go 2022-11-05 10:43:07 +08:00
liwei 89ac52bebd ci: path ignore 2022-11-04 21:48:22 +08:00
liwei 15e1c2f2d5 chore: git ignore generated_plugin 2022-11-04 21:43:31 +08:00
levi 203f0ccc82 chore: fix go version 2022-11-04 17:13:32 +08:00
levi 2422bd2c66 chore: update md 2022-11-04 16:38:07 +08:00
levi 0306a1ed6b chore: en link 2022-11-04 16:26:19 +08:00
levi 8a94650370 chore: en readme 2022-11-04 16:25:57 +08:00
levi 837a193e96 chore: add discord link 2022-11-04 15:40:31 +08:00
levi fcf0746f9a ci: fix version 2022-11-04 15:12:38 +08:00
levi 9eb466ccd3 ci: path ignore 2022-11-04 15:11:06 +08:00
levi c54f22613e chore: issue tpl 2022-11-04 15:06:54 +08:00
liwei 3e1cfa0a27 ci: release v1.0.0 again 2022-11-03 23:31:48 +08:00
liwei 9d22b49e41 ci: fix windows build 2022-11-03 23:23:23 +08:00
liwei 1c6df55975 ci: debug 2022-11-03 23:17:40 +08:00
liwei 237616ded2 ci: debug 2022-11-03 23:15:35 +08:00
liwei cebdda564e ci: debug windows 2022-11-03 22:18:15 +08:00
liwei 08ffb51946 ci: build windows 2022-11-03 21:21:39 +08:00
levi 8378f51f8e ci: fix windows 2022-11-03 20:11:28 +08:00
levi 978d786f99 ci: fix windows 2022-11-03 19:56:17 +08:00
levi 8b997fa7f2 ci: fix windows build 2022-11-03 19:38:00 +08:00
levi 745afb600c ci: debug windows 2022-11-03 19:21:59 +08:00
levi b45b840afb ci: fix windows build 2022-11-03 19:03:12 +08:00
levi 2a7a136203 ci: debug 2022-11-03 18:13:09 +08:00
levi dfb8958999 ci: debug 2022-11-03 18:05:24 +08:00
levi 1568f4d6ff ci: debug windows 2022-11-03 17:56:55 +08:00
levi 12ebceecb0 ci: debug windows 2022-11-03 17:34:23 +08:00
levi 8cfe46cb71 ci: debug windows mingw dir 2022-11-03 17:01:49 +08:00
liwei 2f1b929bcd ci: fix windows build 2022-11-02 22:55:11 +08:00
liwei 2144cd8c3a ci: release v1.0.0 2022-11-02 22:40:35 +08:00
liwei 080b34d070 ci: fix windows 2022-11-02 22:14:06 +08:00
liwei de73d2a29f ci: fix windows build 2022-11-02 22:04:45 +08:00
liwei 9de2b765fb chore: add windows build step 2022-11-02 21:58:49 +08:00
liwei be75b50521 ci: build windows 2022-11-02 21:58:34 +08:00
levi 39aec19471 chore: update md 2022-11-02 17:55:00 +08:00
monkeyWie 9884001d54 chore: update md 2022-11-02 17:35:08 +08:00
levi 29c834a070 chore: add linux build step 2022-11-02 17:26:03 +08:00
levi b16f348676 chore: update md 2022-11-02 17:23:10 +08:00
monkeyWie 7b46c2a2f6 chore: create LICENSE 2022-11-02 17:17:54 +08:00
levi 1716d8b33e chore: update md 2022-11-02 17:02:45 +08:00
levi 855f0b4002 chore: update md 2022-11-02 17:01:58 +08:00
monkeyWie ee0d081b83 ci: fix linux desktop icon 2022-11-02 16:41:31 +00:00
monkeyWie d2f91a49ad ci: fix android name 2022-11-02 16:05:51 +00:00
monkeyWie fb6f889ddb ci: fix linux name 2022-11-02 15:53:16 +00:00
monkeyWie 0e6f1013cb ci: fix linux 2022-11-02 15:48:54 +00:00
monkeyWie fd10b18707 ci: fix 2022-11-02 15:42:27 +00:00
monkeyWie f423cf5b6d ci: fix linux version 2022-11-02 15:37:39 +00:00
monkeyWie 37b54b0c0f ci: fix linux build 2022-11-02 15:27:04 +00:00
monkeyWie cd74c895d3 ci: fix config 2022-11-02 15:22:38 +00:00
monkeyWie 450027a8b0 ci: fix linux build env 2022-11-02 15:21:04 +00:00
monkeyWie 48821a519e ci: build linux 2022-11-02 15:16:24 +00:00
levi 423ee5c3d1 chore: remove test code 2022-11-02 12:12:31 +08:00
levi 96c8945ac8 chore: update md 2022-11-02 12:05:22 +08:00
levi b2b714ae88 chore: change banner 2022-11-02 12:04:01 +08:00
levi 2793ff696e chore: refactor md 2022-11-02 12:02:58 +08:00
levi 2f81c5ef04 ci: fix web build 2022-11-02 12:02:19 +08:00
levi 2eed470d5c feat: icon 2022-11-02 10:26:22 +08:00
levi 34f8df1999 ci: fix web zip 2022-11-01 16:39:57 +08:00
levi e901e08534 fix: android lib 2022-11-01 16:30:15 +08:00
levi 56561ba764 ci: fix web build name 2022-11-01 16:05:57 +08:00
levi cf60e5a60a ci: fix web embed 2022-11-01 15:30:45 +08:00
levi bf8e24a6af ci: build web 2022-11-01 15:13:59 +08:00
levi ff4e6f448c ci: improve name 2022-11-01 13:38:37 +08:00
levi dccf080fbb fix: mac build 2022-11-01 10:33:17 +08:00
levi 9d7094133d chore: update md 2022-11-01 10:17:27 +08:00
levi ea6277805c ci: move to main branch 2022-11-01 10:15:09 +08:00
levi 3ad3060d71 ci: fix macos build 2022-11-01 10:07:23 +08:00
levi c1545adeb2 fix: andriod unix socket 2022-11-01 10:05:04 +08:00
levi 9659176752 ci: fix gomobile init 2022-10-31 15:36:58 +08:00
levi 4371345c7d ci: apk build 2022-10-31 15:33:54 +08:00
levi 655f9d99c7 chore: git ignore 2022-10-31 15:25:21 +08:00
levi a9c6705bd3 ci: add xgo cache 2022-10-28 14:55:25 +08:00
levi 46e0c6258f ci: upgrade go version to 1.19 2022-10-28 14:46:16 +08:00
levi 7c596c6b91 ci: fix 2022-10-28 14:40:40 +08:00
levi 5e92379aa2 ci: fix macos arm 2022-10-28 14:39:41 +08:00
levi 8a47b6cfb5 ci: macos arm 2022-10-28 14:26:31 +08:00
levi 97eca4ec9c ci: fix overwrite 2022-10-28 11:11:28 +08:00
levi b0a7a5569a feat: add logger 2022-10-28 10:57:39 +08:00
levi 5073ae6166 ci: fix macos asset path 2022-10-27 16:26:49 +08:00
levi 81c6fcf15d fix: ci node install cache 2022-10-27 16:16:19 +08:00
levi 2c6f821c4b chore: update md 2022-10-27 16:11:30 +08:00
levi 236423fe3c ci: fix 2022-10-27 16:07:59 +08:00
levi a8136afc31 ci: flutter build 2022-10-27 15:57:56 +08:00
levi 4d4d3b446a feat: support flutter web 2022-10-27 13:58:43 +08:00
levi c4a30dc4c2 feat: flutter ui integration 2022-10-27 13:58:43 +08:00
levi 0fc133e46d feat: bt improve 2022-10-26 17:18:13 +08:00
monkeyWie c6aa83194f feat: support RESTful api (#33) 2022-09-09 18:31:57 +08:00
monkeyWie e292926697 fix: http download resume task (#31) 2022-08-12 13:06:58 +08:00
217 changed files with 11521 additions and 402 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
<!--
请根据以下模版提issue
-->
### 问题描述(必要)
### 版本号(必要)
### 操作系统(必要)
### 相关截图
### 相关日志
+336
View File
@@ -0,0 +1,336 @@
name: build
on:
workflow_dispatch:
release:
types: [published]
jobs:
get-release:
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.get-release.outputs.tag_name }}
upload_url: ${{ steps.get-release.outputs.upload_url }}
steps:
- uses: actions/checkout@v3
- uses: thebritican/[email protected]
id: get-release
build-windows:
runs-on: windows-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: "^1.19"
cache: true
- uses: subosito/flutter-action@v2
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop
cd ui/flutter
flutter build windows
$mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin"
$system = "C:\Windows\System32"
$release = "build\windows\runner\Release\"
cp $mingw\libstdc++-6.dll $release
cp $mingw\libgcc_s_seh-1.dll $release
cp $mingw\libwinpthread-1.dll $release
cp $system\msvcp140.dll $release
cp $system\vcruntime140.dll $release
cp $system\vcruntime140_1.dll $release
New-Item -Path build\windows\Output -ItemType Directory
Compress-Archive -Path "$release*" -DestinationPath build\windows\Output\Gopeed-$env:VERSION-windows-amd64.zip
cd build/windows
echo @"
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Gopeed"
#define MyAppVersion "$($env:VERSION.substring(1))"
#define MyAppPublisher "monkeyWie"
#define MyAppURL "https://gopeed.com"
#define MyAppExeName "gopeed.exe"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{5960F34D-1E42-402C-8C85-DE2FF24CBAE4}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\gopeed
DisableProgramGroupPage=yes
LicenseFile=..\..\..\..\LICENSE
; Remove the following line to run in administrative install mode (install for all users.)
PrivilegesRequired=lowest
OutputBaseFilename=gopeed
SetupIconFile=..\..\assets\icon\icon.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: ".\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
"@ > setup.iss
iscc.exe setup.iss
mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64-install.exe"
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/windows/Output/*
overwrite: true
build-macos-arm64-lib:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: "^1.19"
cache: true
- run: mkdir -p ~/image-cache
- id: image-cache
uses: actions/cache@v3
with:
path: ~/image-cache
key: image-cache-xgo-v1.19
- if: steps.image-cache.outputs.cache-hit != 'true'
run: |
docker pull techknowlogick/xgo:go-1.19.x
docker save -o ~/image-cache/xgo.tar techknowlogick/xgo:go-1.19.x
- if: steps.image-cache.outputs.cache-hit == 'true'
run: docker load -i ~/image-cache/xgo.tar
- name: Build
run: |
go install src.techknowlogick.com/xgo@latest
xgo --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
mv libgopeed-*.dylib libgopeed.dylib
- uses: actions/upload-artifact@v3
with:
name: macos-arm64-lib
path: libgopeed.dylib
build-macos:
runs-on: macos-latest
needs: [get-release, build-macos-arm64-lib]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: "^1.19"
cache: true
- uses: actions/setup-node@v3
with:
node-version: 16
- uses: subosito/flutter-action@v2
- name: Install appdmg
run: npm install -g appdmg
- uses: actions/download-artifact@v3
with:
name: macos-arm64-lib
path: ui/flutter/macos/Frameworks
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
PROJECT_DIR=$(pwd)
cd ui/flutter
flutter build macos
cd build/macos/Build/Products/Release
cat>appdmg.json<<EOF
{
"title": "Gopeed",
"icon": "Gopeed.app/Contents/Resources/AppIcon.icns",
"contents": [
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" },
{ "x": 192, "y": 344, "type": "file", "path": "Gopeed.app" }
]
}
EOF
mkdir dist
appdmg appdmg.json dist/Gopeed-$VERSION-macos-arm64.dmg
# amd64
cd $PROJECT_DIR
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o bin/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
cp bin/libgopeed.dylib ui/flutter/build/macos/Build/Products/Release/Gopeed.app/Contents/Frameworks/
cd ui/flutter/build/macos/Build/Products/Release
appdmg appdmg.json dist/Gopeed-$VERSION-macos-amd64.dmg
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
overwrite: true
build-linux:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: "^1.19"
cache: true
- uses: subosito/flutter-action@v2
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/monkeyWie/gopeed/bind/desktop
cd ui/flutter
flutter build linux
mkdir -p debian/gui
cp assets/icon/icon_1024.png debian/gui/gopeed.png
cat>debian/debian.yaml<<EOF
flutter_app:
command: gopeed
arch: x64
parent: /usr/local/lib
control:
Package: gopeed
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
Depends:
Maintainer: monkeyWie
Description: High speed downloader that supports all platforms.
EOF
cat>debian/gui/gopeed.desktop<<EOF
[Desktop Entry]
Version=${VERSION:1}
Name=Gopeed
GenericName=Gopeed
Comment=High speed downloader that supports all platforms.
Terminal=false
Type=Application
Categories=Utility
Keywords=Flutter;Downloader;
EOF
dart pub global activate flutter_to_debian
flutter_to_debian
mv debian/packages/gopeed*.deb debian/packages/Gopeed-$VERSION-linux-amd64.deb
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/debian/packages/*
overwrite: true
build-android:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: "^1.19"
cache: true
- uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "11"
- uses: subosito/flutter-action@v2
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
mkdir ui/flutter/android/app/libs
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/monkeyWie/gopeed/bind/mobile
cd ui/flutter
flutter build apk
mkdir dist
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-$VERSION-android.apk
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/dist/*
overwrite: true
build-web:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: "^1.19"
cache: true
- uses: subosito/flutter-action@v2
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
cd ui/flutter
flutter build web
cd ../../
cp -r ui/flutter/build/web cmd/web/dist
mkdir -p dist/zip
goos_arr=(windows darwin linux)
goarch_arr=(386 amd64 arm64)
export CGO_ENABLED=0
for goos in "${goos_arr[@]}"; do
for goarch in "${goarch_arr[@]}"; do
name=gopeed-web-$VERSION-$goos-$goarch
dir="dist/$name/"
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w" -o $dir github.com/monkeyWie/gopeed/cmd/web \
&& cd $dir \
&& file=$(ls -AU | head -1) \
&& mkdir $name \
&& mv $file $name/$(echo $file | sed -e "s/web/gopeed/g") \
&& zip -r ../zip/$name.zip * \
&& cd ../..) \
|| true
done
done
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: dist/zip/*
overwrite: true
build-docker:
runs-on: ubuntu-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
tag=liwei2633/gopeed:${VERSION:1}
docker build -t $tag .
docker login -u liwei2633 -p ${{ secrets.DOCKER_PASSWORD }}
docker push $tag
docker tag $tag liwei2633/gopeed:latest
docker push liwei2633/gopeed:latest
+17 -29
View File
@@ -3,43 +3,31 @@ name: release
on: on:
push: push:
branches: branches:
- master - main
paths-ignore: paths:
- '_docs/**' - "bind/**"
- 'README.md' - "cmd/**"
- "internal/**"
- "pkg/**"
- "ui/**"
- ".github/workflows/release.yml"
- "go.mod"
- "go.sum"
- "Dockerfile"
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.create_release.outputs.tag_name }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps: steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: '^1.18' go-version: "^1.19"
- uses: actions/checkout@v3
- uses: release-drafter/release-drafter@v5 - uses: release-drafter/release-drafter@v5
id: create_release id: create_release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build
env:
VERSION: ${{ steps.create_release.outputs.tag_name }}
run: |
mkdir -p dist/zip
goos_arr=(windows darwin linux)
goarch_arr=(386 amd64 arm arm64)
export CGO_ENABLED=0
for goos in "${goos_arr[@]}"; do
for goarch in "${goarch_arr[@]}"; do
dir="dist/gopeed-$VERSION-$goos-$goarch/"
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite -ldflags="-s -w" -o $dir github.com/monkeyWie/gopeed-core/cmd/gopeed \
&& cd $dir \
&& zip -r ../zip/gopeed-$VERSION-$goos-$goarch.zip * \
&& cd ../..) \
|| true
done
done
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: dist/zip/*
+33 -19
View File
@@ -3,16 +3,28 @@ name: test
on: on:
pull_request: pull_request:
branches: branches:
- master - main
paths-ignore: paths:
- '_docs/**' - "bind/**"
- 'README.md' - "cmd/**"
- "internal/**"
- "pkg/**"
- "ui/**"
- ".github/workflows/test.yml"
- "go.mod"
- "go.sum"
push: push:
branches: branches:
- master - main
paths-ignore: paths:
- '_docs/**' - "bind/**"
- 'README.md' - "cmd/**"
- "internal/**"
- "pkg/**"
- "ui/**"
- ".github/workflows/test.yml"
- "go.mod"
- "go.sum"
jobs: jobs:
# lint: # lint:
@@ -21,7 +33,7 @@ jobs:
# steps: # steps:
# - uses: actions/setup-go@v2 # - uses: actions/setup-go@v2
# with: # with:
# go-version: '^1.18' # go-version: '^1.19'
# - uses: actions/checkout@v2 # - uses: actions/checkout@v2
# - name: Lint Go Code # - name: Lint Go Code
# run: | # run: |
@@ -32,15 +44,16 @@ jobs:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v2 - uses: actions/checkout@v3
- uses: actions/setup-go@v3
with: with:
go-version: '^1.18' go-version: "^1.19"
- uses: actions/checkout@v2 cache: true
- name: Run Unit tests. - name: Run Unit tests.
run: go test ./... -v -covermode=count -coverprofile=coverage.txt run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
- uses: codecov/codecov-action@v1 - uses: codecov/codecov-action@v3
with: with:
file: ./coverage.txt files: ./coverage.txt
fail_ci_if_error: true fail_ci_if_error: true
build: build:
name: Build name: Build
@@ -48,9 +61,10 @@ jobs:
# needs: [lint, test] # needs: [lint, test]
needs: [test] needs: [test]
steps: steps:
- uses: actions/setup-go@v2 - uses: actions/checkout@v3
- uses: actions/setup-go@v3
with: with:
go-version: '^1.18' go-version: "^1.19"
- uses: actions/checkout@v2 cache: true
- name: Build - name: Build
run: go build github.com/monkeyWie/gopeed-core/cmd/gopeed run: go build github.com/monkeyWie/gopeed/cmd/gopeed
+7 -2
View File
@@ -11,8 +11,6 @@
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
debug
.idea/ .idea/
bin/ bin/
@@ -20,3 +18,10 @@ bin/
.torrent.db .torrent.db
.torrent.db-shm .torrent.db-shm
.torrent.db-wal .torrent.db-wal
*.data
*.db
.DS_Store
dist/
+21
View File
@@ -0,0 +1,21 @@
FROM cirrusci/flutter:3.3.7 AS flutter
WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get
COPY ./ui/flutter ./
RUN flutter build web
FROM golang:1.19.3 AS go
WORKDIR /app
COPY ./go.mod ./go.sum ./
RUN go mod download
COPY . .
COPY --from=flutter /app/build/web ./cmd/web/dist
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/monkeyWie/gopeed/cmd/web
FROM alpine:3.14.2
LABEL maintainer="monkeyWie"
WORKDIR /app
COPY --from=go /app/dist/gopeed ./
EXPOSE 9999
ENTRYPOINT ["./gopeed"]
+674
View File
File diff suppressed because it is too large. Load diff
+119 -31
View File
@@ -1,47 +1,135 @@
[![Test Status](https://github.com/monkeyWie/gopeed-core/workflows/test/badge.svg)](https://github.com/monkeyWie/gopeed-core/actions?query=workflow%3Atest) [![Test Status](https://github.com/monkeyWie/gopeed/workflows/test/badge.svg)](https://github.com/monkeyWie/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/monkeyWie/gopeed-core/branch/master/graph/badge.svg)](https://codecov.io/gh/monkeyWie/gopeed-core) [![Codecov](https://codecov.io/gh/monkeyWie/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/monkeyWie/gopeed)
[![Release](https://img.shields.io/github/release/monkeyWie/gopeed-core.svg?style=flat-square)](https://github.com/monkeyWie/gopeed-core/releases) [![Release](https://img.shields.io/github/release/monkeyWie/gopeed.svg?style=flat-square)](https://github.com/monkeyWie/gopeed/releases)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
## gopeed ![](_docs/img/banner.png)
支持多协议(HTTP、BitTorrent)下载的客户端,提供命令行、RESTful API、WebSocket、Go 类库方式来使用。 [English](/README.md) | [中文](/README_zh-CN.md)
## 安装 ## Introduction
```sh Gopeed is a high-speed downloader developed by `Golang`+`Flutter`, which supports (HTTP, BitTorrent, Magnet) protocol downloads and supports all platforms.
go install github.com/monkeyWie/gopeed-core
## Installation
**Supported platforms**
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] web
- [x] docker
[To Release](https://github.com/monkeyWie/gopeed/releases/latest)
> Tips: If the macos open failed, please execute the `xattr -d com.apple.quarantine /Applications/Gopeed.app` command in the terminal
### Command tool
use `go install`:
```bash
go install github.com/monkeyWie/gopeed/cmd/gopeed
``` ```
## 示例 ### Docker
可以在本仓库的[\_examples](_examples)目录下查看 ```bash
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
```
## 命令行工具 When the docker container is running, you can access the web page through `http://localhost:9999`.
> Tip: Modify the download path to `/download` on the setting page to access the downloaded files on the host.
### 预览 ## Showcase
![](_docs/img/cli-demo.gif) ![](_docs/img/ui-demo.png)
### 下载 ## Development
前往[releases](https://github.com/monkeyWie/gopeed-core/releases)页面 This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used.
## TODO > The front code is located in the `ui/flutter` directory.
- [x] HTTP 下载实现 ### Environment
- [x] BitTorrent 下载实现
- [x] .torrent 文件解析
- [x] tracker 协议实现
- [x] peer wire protocol 协议实现
- [x] DHT 协议实现
- [x] 磁力链接支持
- [x] uTP 协议实现
- [x] 下载接口抽象(不关心具体协议)
- [ ] 支持自定义配置
- [ ] 限速功能实现
- [x] 命令行工具提供
- [ ] RESTful 服务提供
## 参与 1. Golang 1.19+
2. Flutter 3.0+
由于项目目前还未定型,代码可能随时有大的调整,所以暂不接受 PR,当然如果有什么好的想法可以在 issue 区提出来。 ### Clone
```bash
git clone [email protected]:monkeyWie/gopeed.git
```
### Build
#### Desktop
First, you need to configure the environment according to the [flutter desktop official website document](https://docs.flutter.dev/development/platform-integration/desktop), and then you need to prepare the `cgo` environment, which can be searched for yourself.
command:
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
- linux
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/monkeyWie/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
#### Mobile
Same as before, you also need to prepare the `cgo` environment, and then install `gomobile`:
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
```
command:
- android
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/monkeyWie/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
#### Web
Web platform communicates directly with the backend http server, no additional environment is required.
command:
```bash
cd ui/flutter
flutter build web
cd ../../
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/monkeyWie/gopeed/cmd/web
````
## License
[GPLv3](LICENSE)
+135
View File
@@ -0,0 +1,135 @@
[![Test Status](https://github.com/monkeyWie/gopeed/workflows/test/badge.svg)](https://github.com/monkeyWie/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/monkeyWie/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/monkeyWie/gopeed)
[![Release](https://img.shields.io/github/release/monkeyWie/gopeed.svg?style=flat-square)](https://github.com/monkeyWie/gopeed/releases)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[English](/README.md) | [中文](/README_zh-CN.md)
## 介绍
Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
## 下载
**已支持平台**
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] web
- [x] docker
[点击前往](https://github.com/monkeyWie/gopeed/releases/latest)
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令
### 命令行工具
使用`go install`安装:
```bash
go install github.com/monkeyWie/gopeed/cmd/gopeed
```
### Docker
```bash
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
```
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
## 界面展示
![](_docs/img/ui-demo.png)
## 开发
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
> 前端代码位于`ui/flutter`目录下。
### 环境要求
1. Golang 1.19+
2. Flutter 3.0+
### 克隆项目
```bash
git clone [email protected]:monkeyWie/gopeed.git
```
### 编译
#### 桌面端
首先需要按照[flutter desktop 官网文档](https://docs.flutter.dev/development/platform-integration/desktop)进行环境配置,然后需要准备好`cgo`环境,具体可以自行搜索。
构建命令:
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
- linux
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/monkeyWie/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
#### 移动端
同样的也是需要准备好`cgo`环境,接着安装`gomobile`:
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
```
构建命令:
- android
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/monkeyWie/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
#### Web 端(推荐本地调试使用)
Web 端直接与后端 http 服务通讯,不需要额外准备环境。
构建命令:
```bash
cd ui/flutter
flutter build web
cd ../../
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/monkeyWie/gopeed/cmd/web
```
## 开源许可
基于 [GPLv3](LICENSE) 协议开源。
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+3 -3
View File
@@ -2,13 +2,13 @@ package main
import ( import (
"fmt" "fmt"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed-core/pkg/download" "github.com/monkeyWie/gopeed/pkg/download"
) )
func main() { func main() {
finallyCh := make(chan error) finallyCh := make(chan error)
err := download.Boot(). _, err := download.Boot().
URL("https://www.baidu.com/index.html"). URL("https://www.baidu.com/index.html").
Listener(func(event *download.Event) { Listener(func(event *download.Event) {
if event.Key == download.EventKeyFinally { if event.Key == download.EventKeyFinally {
+28
View File
@@ -0,0 +1,28 @@
package main
import "C"
import (
"encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model"
)
func main() {}
//export Start
func Start(cfg *C.char) (int, *C.char) {
var config model.StartConfig
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
return 0, C.CString(err.Error())
}
realPort, err := rest.Start(&config)
if err != nil {
return 0, C.CString(err.Error())
}
return realPort, nil
}
//export Stop
func Stop() {
rest.Stop()
}
+21
View File
@@ -0,0 +1,21 @@
package libgopeed
// #cgo LDFLAGS: -static-libstdc++
import "C"
import (
"encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model"
)
func Start(cfg string) (int, error) {
var config model.StartConfig
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
return 0, err
}
return rest.Start(&config)
}
func Stop() {
rest.Stop()
}
+1 -2
View File
@@ -4,7 +4,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath"
) )
type args struct { type args struct {
@@ -14,7 +13,7 @@ type args struct {
} }
func parse() *args { func parse() *args {
dir, err := filepath.Abs(filepath.Dir(os.Args[0])) dir, err := os.Getwd()
if err != nil { if err != nil {
panic(err) panic(err)
} }
+6 -6
View File
@@ -2,9 +2,9 @@ package main
import ( import (
"fmt" "fmt"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed-core/pkg/download" "github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed-core/pkg/util" "github.com/monkeyWie/gopeed/pkg/util"
"strings" "strings"
"sync" "sync"
) )
@@ -15,7 +15,7 @@ func main() {
args := parse() args := parse()
var wg sync.WaitGroup var wg sync.WaitGroup
err := download.Boot(). _, err := download.Boot().
URL(args.url). URL(args.url).
Listener(func(event *download.Event) { Listener(func(event *download.Event) {
if event.Key == download.EventKeyProgress { if event.Key == download.EventKeyProgress {
@@ -55,10 +55,10 @@ var (
) )
func printProgress(task *download.Task, title string) { func printProgress(task *download.Task, title string) {
rate := float64(task.Progress.Downloaded) / float64(task.Res.Length) rate := float64(task.Progress.Downloaded) / float64(task.Res.Size)
completeWidth := int(progressWidth * rate) completeWidth := int(progressWidth * rate)
speed := util.ByteFmt(task.Progress.Speed) speed := util.ByteFmt(task.Progress.Speed)
totalSize := util.ByteFmt(task.Res.Length) totalSize := util.ByteFmt(task.Res.Size)
sb.WriteString(fmt.Sprintf("\r%s [", title)) sb.WriteString(fmt.Sprintf("\r%s [", title))
for i := 0; i < progressWidth; i++ { for i := 0; i < progressWidth; i++ {
if i < completeWidth { if i < completeWidth {
+7
View File
@@ -0,0 +1,7 @@
_______ ______ .______ _______ _______ _______
/ _____| / __ \ | _ \ | ____|| ____|| \
| | __ | | | | | |_) | | |__ | |__ | .--. |
| | |_ | | | | | | ___/ | __| | __| | | | |
| |__| | | `--' | | | | |____ | |____ | '--' |
\______| \______/ | _| |_______||_______||_______/
+42
View File
@@ -0,0 +1,42 @@
//go:build web
// +build web
package main
import (
"embed"
"fmt"
"github.com/monkeyWie/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"io/fs"
"net/http"
)
//go:embed banner.txt
var banner string
//go:embed dist/*
var dist embed.FS
func main() {
sub, err := fs.Sub(dist, "dist")
if err != nil {
panic(err)
}
cfg := &model.StartConfig{
Network: "tcp",
Address: "0.0.0.0:9999",
Storage: model.StorageBolt,
WebEnable: true,
WebFS: sub,
}
fmt.Println(banner)
srv, listener, err := rest.BuildServer(cfg)
if err != nil {
panic(err)
}
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err)
}
}
+13 -6
View File
@@ -1,12 +1,13 @@
module github.com/monkeyWie/gopeed-core module github.com/monkeyWie/gopeed
go 1.18 go 1.19
require ( require (
github.com/anacrolix/torrent v1.46.0 github.com/anacrolix/torrent v1.46.0
github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd github.com/matoous/go-nanoid/v2 v2.0.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c go.etcd.io/bbolt v1.3.6
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
) )
require ( require (
@@ -35,7 +36,10 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/google/btree v1.0.1 // indirect github.com/google/btree v1.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/huandu/xstrings v1.3.2 // indirect github.com/huandu/xstrings v1.3.2 // indirect
github.com/lispad/go-generics-tools v1.0.0 // indirect github.com/lispad/go-generics-tools v1.0.0 // indirect
@@ -60,10 +64,13 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect
github.com/tidwall/btree v1.3.1 // indirect github.com/tidwall/btree v1.3.1 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835 // indirect
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
) )
+30 -1
View File
@@ -7,6 +7,7 @@ crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 h1:fILCBBFnjnrQ0whVJlGhf
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
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/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=
@@ -105,6 +106,8 @@ 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.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
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=
@@ -158,7 +161,11 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORR
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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=
@@ -187,6 +194,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lispad/go-generics-tools v1.0.0 h1:pPtt9QERE5sGQfwvLakKe2sjhcNKKsDSnr3939ofj1E= github.com/lispad/go-generics-tools v1.0.0 h1:pPtt9QERE5sGQfwvLakKe2sjhcNKKsDSnr3939ofj1E=
github.com/lispad/go-generics-tools v1.0.0/go.mod h1:stn7X24ZIyFvaSyttafq3VlJzGJJJkUtLYdbgi/gopM= github.com/lispad/go-generics-tools v1.0.0/go.mod h1:stn7X24ZIyFvaSyttafq3VlJzGJJJkUtLYdbgi/gopM=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -306,6 +316,7 @@ github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@@ -313,19 +324,30 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4= golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
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/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/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=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835 h1:v6yt1xSu+4xV0fPEmB8s2BHdOaoU0SSZrVQzj6pRvqk=
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835/go.mod h1:M32cGdzp91A8Ex9qQtyZinr19EYxzkFqDjW2oyHzTDQ=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/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-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=
@@ -343,6 +365,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/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-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -357,8 +380,9 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
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-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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=
@@ -381,6 +405,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -397,9 +422,13 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
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=
+13 -32
View File
@@ -1,61 +1,42 @@
package controller package controller
import ( import (
"golang.org/x/net/proxy" "errors"
"net"
"os" "os"
"time" "path/filepath"
) )
type Controller interface { type Controller interface {
Touch(name string, size int64) (file *os.File, err error) Touch(name string, size int64) (file *os.File, err error)
Open(name string) (file *os.File, err error) //ContextDialer() (proxy.Dialer, error)
Write(name string, offset int64, buf []byte) (int, error)
Close(name string) error
ContextDialer() (proxy.Dialer, error)
} }
type DefaultController struct { type DefaultController struct {
Files map[string]*os.File
} }
func NewController() *DefaultController { func NewController() *DefaultController {
return &DefaultController{Files: make(map[string]*os.File)} return &DefaultController{}
} }
func (c *DefaultController) Touch(name string, size int64) (file *os.File, err error) { func (c *DefaultController) Touch(name string, size int64) (file *os.File, err error) {
dir := filepath.Dir(name)
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return
}
file, err = os.Create(name) file, err = os.Create(name)
if err != nil && !errors.Is(err, os.ErrExist) {
return
}
if size > 0 { if size > 0 {
err = os.Truncate(name, size) err = os.Truncate(name, size)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if err == nil {
c.Files[name] = file
}
return return
} }
func (c *DefaultController) Open(name string) (file *os.File, err error) { /*func (c *DefaultController) ContextDialer() (proxy.Dialer, error) {
file, err = os.OpenFile(name, os.O_RDWR, os.ModePerm)
if err == nil {
c.Files[name] = file
}
return
}
func (c *DefaultController) Write(name string, offset int64, buf []byte) (int, error) {
return c.Files[name].WriteAt(buf, offset)
}
func (c *DefaultController) Close(name string) error {
err := c.Files[name].Close()
delete(c.Files, name)
return err
}
func (c *DefaultController) ContextDialer() (proxy.Dialer, error) {
// return proxy.SOCKS5("tpc", "127.0.0.1:9999", nil, nil) // return proxy.SOCKS5("tpc", "127.0.0.1:9999", nil, nil)
var dialer proxy.Dialer var dialer proxy.Dialer
return &DialerWarp{dialer: dialer}, nil return &DialerWarp{dialer: dialer}, nil
@@ -107,4 +88,4 @@ func (d *DialerWarp) Dial(network, addr string) (c net.Conn, err error) {
return nil, err return nil, err
} }
return &ConnWarp{conn: conn}, nil return &ConnWarp{conn: conn}, nil
} }*/
+6 -7
View File
@@ -1,8 +1,8 @@
package fetcher package fetcher
import ( import (
"github.com/monkeyWie/gopeed-core/internal/controller" "github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/pkg/base"
) )
// 对应协议的下载支持 // 对应协议的下载支持
@@ -15,6 +15,7 @@ type Fetcher interface {
Start() (err error) Start() (err error)
Pause() (err error) Pause() (err error)
Continue() (err error) Continue() (err error)
Close() (err error)
// 获取任务各个文件下载进度 // 获取任务各个文件下载进度
Progress() Progress Progress() Progress
@@ -25,13 +26,11 @@ type Fetcher interface {
type FetcherBuilder interface { type FetcherBuilder interface {
Schemes() []string Schemes() []string
Build() Fetcher Build() Fetcher
}
type FetcherSaver interface {
// Store 存储任务 // Store 存储任务
Store(fetcher Fetcher) interface{} Store(fetcher Fetcher) (any, error)
// Resume 恢复任务 // Restore 恢复任务
Resume(res *base.Resource, opts *base.Options, data interface{}) Fetcher Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) Fetcher)
} }
type DefaultFetcher struct { type DefaultFetcher struct {
+137 -30
View File
@@ -2,12 +2,16 @@ package bt
import ( import (
"github.com/anacrolix/torrent" "github.com/anacrolix/torrent"
"github.com/monkeyWie/gopeed-core/internal/controller" "github.com/anacrolix/torrent/metainfo"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "github.com/anacrolix/torrent/storage"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed-core/pkg/util" "github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util"
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic"
"time"
) )
var client *torrent.Client var client *torrent.Client
@@ -16,89 +20,184 @@ type Fetcher struct {
Ctl controller.Controller Ctl controller.Controller
torrent *torrent.Torrent torrent *torrent.Torrent
res *base.Resource
opts *base.Options opts *base.Options
ready atomic.Bool
progress fetcher.Progress
torrentPaths map[string]string
} }
func (f *Fetcher) Setup(ctl controller.Controller) (err error) { func (f *Fetcher) Setup(ctl controller.Controller) (err error) {
f.Ctl = ctl f.Ctl = ctl
var once sync.Once var once sync.Once
once.Do(func() { once.Do(func() {
client, err = torrent.NewClient(nil) cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0
// Support custom download path for each torrent
pieceCompletion, err := storage.NewDefaultPieceCompletionForDir(cfg.DataDir)
if err != nil {
pieceCompletion = storage.NewMapPieceCompletion()
}
clientImplCloser := storage.NewFileOpts(storage.NewFileClientOpts{
ClientBaseDir: cfg.DataDir,
TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
if dir, ok := f.torrentPaths[infoHash.String()]; ok {
return dir
}
return baseDir
},
PieceCompletion: pieceCompletion,
})
cfg.DefaultStorage = clientImplCloser
client, err = torrent.NewClient(cfg)
if err != nil {
return
}
f.torrentPaths = make(map[string]string)
}) })
return return
} }
func (f *Fetcher) Wait() (err error) { func (f *Fetcher) Wait() (err error) {
client.WaitAll() for {
if f.ready.Load() {
done := true
for _, selectIndex := range f.opts.SelectFiles {
file := f.torrent.Files()[selectIndex]
if file.BytesCompleted() < file.Length() {
done = false
break
}
}
if done {
// remove unselected files
for i, file := range f.torrent.Files() {
selected := false
for _, selectIndex := range f.opts.SelectFiles {
if i == selectIndex {
selected = true
break
}
}
if !selected {
util.SafeRemove(filepath.Join(f.opts.Path, file.Path()))
}
}
break
}
}
time.Sleep(time.Millisecond * 500)
}
return nil return nil
} }
func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) { func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
schema := util.ParseSchema(req.URL) if err = f.addTorrent(req.URL, true); err != nil {
if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(req.URL)
} else {
f.torrent, err = client.AddTorrentFromFile(req.URL)
}
if err != nil {
return return
} }
<-f.torrent.GotInfo() defer f.torrent.Drop()
res = &base.Resource{ res = &base.Resource{
Req: req, Req: req,
Length: f.torrent.Length(), Name: f.torrent.Name(),
Range: true, Range: true,
Files: make([]*base.FileInfo, len(f.torrent.Files())), Files: make([]*base.FileInfo, len(f.torrent.Files())),
Extra: map[string]any{
"infoHash": f.torrent.InfoHash().String(),
},
} }
for i, file := range f.torrent.Files() { for i, file := range f.torrent.Files() {
res.Files[i] = &base.FileInfo{ res.Files[i] = &base.FileInfo{
Name: filepath.Base(file.DisplayPath()), Name: filepath.Base(file.DisplayPath()),
Path: filepath.Dir(file.Path()), Path: util.Dir(file.Path()),
Size: file.Length(), Size: file.Length(),
} }
res.Size += file.Length()
} }
return return
} }
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) (err error) { func (f *Fetcher) Create(res *base.Resource, opts *base.Options) (err error) {
f.res = res
f.opts = opts f.opts = opts
if len(f.opts.SelectFiles) == 0 { if len(f.opts.SelectFiles) == 0 {
f.opts.SelectFiles = make([]int, len(res.Files)) f.opts.SelectFiles = make([]int, 0)
for i := range f.opts.SelectFiles { for i := range f.torrent.Files() {
f.opts.SelectFiles[i] = i f.opts.SelectFiles = append(f.opts.SelectFiles, i)
} }
} }
if f.opts.Path != "" {
if extra, ok := res.Extra.(map[string]any); ok {
f.torrentPaths[extra["infoHash"].(string)] = f.opts.Path
}
}
f.progress = make(fetcher.Progress, len(f.opts.SelectFiles))
f.ready.Store(false)
return nil return nil
} }
func (f *Fetcher) Start() (err error) { func (f *Fetcher) Start() (err error) {
if len(f.opts.SelectFiles) == 0 { if err = f.addTorrent(f.res.Req.URL, false); err != nil {
return
}
files := f.torrent.Files()
if len(f.opts.SelectFiles) == len(files) {
f.torrent.DownloadAll() f.torrent.DownloadAll()
} else { } else {
for _, index := range f.opts.SelectFiles { for _, selectIndex := range f.opts.SelectFiles {
f.torrent.Files()[index].Download() file := files[selectIndex]
file.Download()
} }
} }
return return
} }
func (f *Fetcher) Pause() (err error) { func (f *Fetcher) Pause() (err error) {
f.torrent.DisallowDataDownload() f.ready.Store(false)
f.torrent.Drop()
return return
} }
func (f *Fetcher) Continue() (err error) { func (f *Fetcher) Continue() (err error) {
f.torrent.AllowDataDownload() return f.Start()
return }
func (f *Fetcher) Close() (err error) {
f.torrent.Drop()
return nil
} }
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, len(f.opts.SelectFiles)) if !f.ready.Load() {
for i := range p { return f.progress
file := f.torrent.Files()[i]
p[i] = file.BytesCompleted()
} }
return p for i := range f.progress {
selectIndex := f.opts.SelectFiles[i]
file := f.torrent.Files()[selectIndex]
f.progress[i] = file.BytesCompleted()
}
return f.progress
}
func (f *Fetcher) addTorrent(url string, resolve bool) (err error) {
schema := util.ParseSchema(url)
if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(url)
} else {
f.torrent, err = client.AddTorrentFromFile(url)
}
if err != nil {
return
}
<-f.torrent.GotInfo()
if resolve {
return
}
f.ready.Store(true)
return
} }
type FetcherBuilder struct { type FetcherBuilder struct {
@@ -113,3 +212,11 @@ func (fb *FetcherBuilder) Schemes() []string {
func (fb *FetcherBuilder) Build() fetcher.Fetcher { func (fb *FetcherBuilder) Build() fetcher.Fetcher {
return &Fetcher{} return &Fetcher{}
} }
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
return nil, nil
}
func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) {
return nil, nil
}
+9 -6
View File
@@ -1,9 +1,9 @@
package bt package bt
import ( import (
"github.com/monkeyWie/gopeed-core/internal/controller" "github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/pkg/base"
"reflect" "reflect"
"testing" "testing"
) )
@@ -19,18 +19,21 @@ func TestFetcher_Resolve_Torrent(t *testing.T) {
want := &base.Resource{ want := &base.Resource{
Req: res.Req, Req: res.Req,
Length: 1466714112, Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
Name: "ubuntu-22.04-live-server-amd64.iso", Name: "ubuntu-22.04-live-server-amd64.iso",
Path: ".",
Size: 1466714112, Size: 1466714112,
}, },
}, },
Extra: map[string]any{
"infoHash": "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
},
} }
if !reflect.DeepEqual(want, res) { if !reflect.DeepEqual(want, res) {
t.Errorf("Resolve error = %v, want %v", res, want) t.Errorf("Resolve() got = %v, want %v", res, want)
} }
} }
+88 -73
View File
@@ -4,15 +4,16 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"io" "io"
"io/ioutil"
"mime" "mime"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"os"
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -39,9 +40,9 @@ type Fetcher struct {
res *base.Resource res *base.Resource
opts *base.Options opts *base.Options
status base.Status status base.Status
chunks []*Chunk chunks []*chunk
clients []*http.Response
file *os.File
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
pauseCh chan interface{} pauseCh chan interface{}
@@ -76,7 +77,7 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
res.Length = parse res.Size = parse
} }
} else if base.HttpCodeOK == httpResp.StatusCode { } else if base.HttpCodeOK == httpResp.StatusCode {
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码 // 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
@@ -86,31 +87,32 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
res.Length = parse res.Size = parse
} }
} else { } else {
return nil, NewRequestError(httpResp.StatusCode, httpResp.Status) return nil, NewRequestError(httpResp.StatusCode, httpResp.Status)
} }
file := &base.FileInfo{ file := &base.FileInfo{
Size: res.Length, Size: res.Size,
} }
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition) contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
if contentDisposition != "" { if contentDisposition != "" {
_, params, _ := mime.ParseMediaType(contentDisposition) _, params, _ := mime.ParseMediaType(contentDisposition)
filename := params["filename"] filename := params["filePath"]
if filename != "" { if filename != "" {
file.Name = filename file.Name = filename
} }
} }
// Get file filename by URL // Get file filePath by URL
if file.Name == "" && strings.Count(req.URL, "/") > 2 { if file.Name == "" && strings.Count(req.URL, "/") > 2 {
file.Name = filepath.Base(req.URL) file.Name = filepath.Base(req.URL)
} }
// unknown file filename // unknown file filePath
if file.Name == "" { if file.Name == "" {
file.Name = "unknown" file.Name = "unknown"
} }
res.Files = append(res.Files, file) res.Files = append(res.Files, file)
res.Name = file.Name
return res, nil return res, nil
} }
@@ -123,58 +125,36 @@ func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
func (f *Fetcher) Start() (err error) { func (f *Fetcher) Start() (err error) {
// 创建文件 // 创建文件
name := f.filename() name := f.filepath()
_, err = f.Ctl.Touch(name, f.res.Length) f.file, err = f.Ctl.Touch(name, f.res.Size)
if err != nil { if err != nil {
return err return err
} }
f.status = base.DownloadStatusStart f.status = base.DownloadStatusRunning
if f.res.Range { f.chunks = splitChunk(f.res, f.opts)
// 每个连接平均需要下载的分块大小
chunkSize := f.res.Length / int64(f.opts.Connections)
f.chunks = make([]*Chunk, f.opts.Connections)
f.clients = make([]*http.Response, f.opts.Connections)
for i := 0; i < f.opts.Connections; i++ {
var (
begin = chunkSize * int64(i)
end int64
)
if i == f.opts.Connections-1 {
// 最后一个分块需要保证把文件下载完
end = f.res.Length - 1
} else {
end = begin + chunkSize - 1
}
chunk := NewChunk(begin, end)
f.chunks[i] = chunk
}
} else {
// 只支持单连接下载
f.chunks = make([]*Chunk, 1)
f.clients = make([]*http.Response, 1)
f.chunks[0] = NewChunk(0, 0)
}
f.fetch() f.fetch()
return return
} }
func (f *Fetcher) Pause() (err error) { func (f *Fetcher) Pause() (err error) {
if base.DownloadStatusStart != f.status { if base.DownloadStatusRunning != f.status {
return return
} }
f.status = base.DownloadStatusPause f.status = base.DownloadStatusPause
if f.cancel != nil {
f.cancel() f.cancel()
<-f.pauseCh <-f.pauseCh
}
return return
} }
func (f *Fetcher) Continue() (err error) { func (f *Fetcher) Continue() (err error) {
if base.DownloadStatusStart == f.status || base.DownloadStatusDone == f.status { if base.DownloadStatusRunning == f.status || base.DownloadStatusDone == f.status {
return return
} }
f.status = base.DownloadStatusStart f.status = base.DownloadStatusRunning
var name = f.filename() f.file, err = os.OpenFile(f.filepath(), os.O_RDWR, os.ModeAppend)
_, err = f.Ctl.Open(name)
if err != nil { if err != nil {
return err return err
} }
@@ -182,6 +162,13 @@ func (f *Fetcher) Continue() (err error) {
return return
} }
func (f *Fetcher) Close() (err error) {
if err = f.Pause(); err != nil {
return
}
return
}
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, 0) p := make(fetcher.Progress, 0)
if len(f.chunks) > 0 { if len(f.chunks) > 0 {
@@ -194,13 +181,8 @@ func (f *Fetcher) Progress() fetcher.Progress {
return p return p
} }
func (f *Fetcher) filename() string { func (f *Fetcher) filepath() string {
// 创建文件 return util.Filepath(f.opts.Path, f.res.Files[0].Name, f.opts.Name)
var filename = f.opts.Name
if filename == "" {
filename = f.res.Files[0].Name
}
return filepath.Join(f.opts.Path, filename)
} }
func (f *Fetcher) fetch() { func (f *Fetcher) fetch() {
@@ -216,7 +198,7 @@ func (f *Fetcher) fetch() {
go func() { go func() {
err := eg.Wait() err := eg.Wait()
// 下载停止,关闭文件句柄 // 下载停止,关闭文件句柄
f.Ctl.Close(f.filename()) f.file.Close()
if f.status == base.DownloadStatusPause { if f.status == base.DownloadStatusPause {
f.pauseCh <- nil f.pauseCh <- nil
} else { } else {
@@ -231,7 +213,6 @@ func (f *Fetcher) fetch() {
} }
func (f *Fetcher) fetchChunk(index int) (err error) { func (f *Fetcher) fetchChunk(index int) (err error) {
filename := f.filename()
chunk := f.chunks[index] chunk := f.chunks[index]
httpReq, err := buildRequest(f.ctx, f.res.Req) httpReq, err := buildRequest(f.ctx, f.res.Req)
@@ -267,7 +248,6 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
if err != nil { if err != nil {
return err return err
} }
f.clients[index] = resp
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent { if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status) err = NewRequestError(resp.StatusCode, resp.Status)
return err return err
@@ -285,7 +265,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
for { for {
n, err := resp.Body.Read(buf) n, err := resp.Body.Read(buf)
if n > 0 { if n > 0 {
_, err := f.Ctl.Write(filename, chunk.Begin+chunk.Downloaded, buf[:n]) _, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -306,16 +286,44 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
} }
} }
if f.status == base.DownloadStatusPause {
chunk.Status = base.DownloadStatusPause
} else if chunk.Downloaded >= chunk.End-chunk.Begin+1 {
chunk.Status = base.DownloadStatusDone
} else {
if err != nil { if err != nil {
chunk.Status = base.DownloadStatusError chunk.Status = base.DownloadStatusError
} else { return
chunk.Status = base.DownloadStatusDone
} }
isComplete := f.res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
if f.status == base.DownloadStatusPause && !isComplete {
chunk.Status = base.DownloadStatusPause
return
}
chunk.Status = base.DownloadStatusDone
return
}
func splitChunk(res *base.Resource, opts *base.Options) (chunks []*chunk) {
if res.Range {
// 每个连接平均需要下载的分块大小
chunkSize := res.Size / int64(opts.Connections)
chunks = make([]*chunk, opts.Connections)
for i := 0; i < opts.Connections; i++ {
var (
begin = chunkSize * int64(i)
end int64
)
if i == opts.Connections-1 {
// 最后一个分块需要保证把文件下载完
end = res.Size - 1
} else {
end = begin + chunkSize - 1
}
chunk := newChunk(begin, end)
chunks[i] = chunk
}
} else {
// 只支持单连接下载
chunks = make([]*chunk, 1)
chunks[0] = newChunk(0, 0)
} }
return return
} }
@@ -343,7 +351,7 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
if req.Extra == nil { if req.Extra == nil {
method = http.MethodGet method = http.MethodGet
} else { } else {
extra := req.Extra.(Extra) extra := req.Extra.(extra)
if extra.Method != "" { if extra.Method != "" {
method = extra.Method method = extra.Method
} else { } else {
@@ -355,7 +363,7 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
} }
} }
if extra.Body != "" { if extra.Body != "" {
body = ioutil.NopCloser(bytes.NewBufferString(extra.Body)) body = bytes.NewBufferString(extra.Body)
} }
} }
@@ -371,8 +379,8 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
return httpReq, nil return httpReq, nil
} }
type FetcherData struct { type fetcherData struct {
Chunks []*Chunk Chunks []*chunk
} }
type FetcherBuilder struct { type FetcherBuilder struct {
@@ -391,19 +399,26 @@ func (fb *FetcherBuilder) Build() fetcher.Fetcher {
} }
} }
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) interface{} { func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
_f := f.(*Fetcher) _f := f.(*Fetcher)
return &FetcherData{ return &fetcherData{
Chunks: _f.chunks, Chunks: _f.chunks,
} }, nil
} }
func (fb *FetcherBuilder) Resume(res *base.Resource, opts *base.Options, data interface{}) fetcher.Fetcher { func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) {
fetcher := new(FetcherBuilder).Build().(*Fetcher) return &fetcherData{}, func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher {
fs := data.(*FetcherData) fd := v.(*fetcherData)
fb := &FetcherBuilder{}
fetcher := fb.Build().(*Fetcher)
fetcher.res = res fetcher.res = res
fetcher.opts = opts fetcher.opts = opts
fetcher.chunks = fs.Chunks
fetcher.status = base.DownloadStatusPause fetcher.status = base.DownloadStatusPause
if len(fd.Chunks) == 0 {
fetcher.chunks = splitChunk(res, opts)
} else {
fetcher.chunks = fd.Chunks
}
return fetcher return fetcher
} }
}
+58 -18
View File
@@ -1,10 +1,11 @@
package http package http
import ( import (
"github.com/monkeyWie/gopeed-core/internal/controller" "fmt"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "github.com/monkeyWie/gopeed/internal/controller"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed-core/pkg/test" "github.com/monkeyWie/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base"
"net" "net"
"reflect" "reflect"
"testing" "testing"
@@ -13,7 +14,8 @@ import (
func TestFetcher_Resolve(t *testing.T) { func TestFetcher_Resolve(t *testing.T) {
testResolve(test.StartTestFileServer, &base.Resource{ testResolve(test.StartTestFileServer, &base.Resource{
Length: test.BuildSize, Name: test.BuildName,
Size: test.BuildSize,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
@@ -23,7 +25,8 @@ func TestFetcher_Resolve(t *testing.T) {
}, },
}, t) }, t)
testResolve(test.StartTestChunkedServer, &base.Resource{ testResolve(test.StartTestChunkedServer, &base.Resource{
Length: 0, Name: test.BuildName,
Size: 0,
Range: false, Range: false,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
@@ -46,7 +49,7 @@ func testResolve(startTestServer func() net.Listener, want *base.Resource, t *te
} }
res.Req = nil res.Req = nil
if !reflect.DeepEqual(want, res) { if !reflect.DeepEqual(want, res) {
t.Errorf("Resolve error = %v, want %v", res, want) t.Errorf("Resolve() got = %v, want %v", res, want)
} }
} }
@@ -75,7 +78,13 @@ func TestFetcher_DownloadChunked(t *testing.T) {
defer listener.Close() defer listener.Close()
// chunked编码下载 // chunked编码下载
downloadNormal(listener, 1, t) downloadNormal(listener, 1, t)
downloadContinue(listener, 1, t) }
func TestFetcher_DownloadPost(t *testing.T) {
listener := test.StartTestPostServer()
defer listener.Close()
// post下载
downloadPost(listener, 1, t)
} }
func TestFetcher_DownloadRetry(t *testing.T) { func TestFetcher_DownloadRetry(t *testing.T) {
@@ -135,7 +144,31 @@ func downloadNormal(listener net.Listener, connections int, t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download error = %v, want %v", got, want) t.Errorf("Download() got = %v, want %v", got, want)
}
}
func downloadPost(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t)
fetcher.(*Fetcher).res.Req.Extra = extra{
Method: "POST",
Header: map[string]string{
"Authorization": "Bearer 123456",
},
Body: fmt.Sprintf(`{"name":"%s"}`, test.BuildName),
}
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
} }
} }
@@ -145,11 +178,11 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 50)
if err := fetcher.Pause(); err != nil { if err := fetcher.Pause(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 50)
if err := fetcher.Continue(); err != nil { if err := fetcher.Continue(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -160,7 +193,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download error = %v, want %v", got, want) t.Errorf("Download() got = %v, want %v", got, want)
} }
} }
@@ -172,7 +205,7 @@ func downloadError(listener net.Listener, connections int, t *testing.T) {
} }
err = fetcher.Wait() err = fetcher.Wait()
if err == nil { if err == nil {
t.Errorf("Download error = %v, want %v", err, nil) t.Errorf("Download() got = %v, want %v", err, nil)
} }
} }
@@ -184,12 +217,19 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
} }
fb := new(FetcherBuilder) fb := new(FetcherBuilder)
time.Sleep(time.Second * 2) time.Sleep(time.Millisecond * 50)
data := fb.Store(fetcher) data, err := fb.Store(fetcher)
time.Sleep(time.Second * 1) if err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 50)
fetcher.Pause() fetcher.Pause()
fetcher = fb.Resume(res, opts, data) _, f := fb.Restore()
f(res, opts, data)
if err != nil {
t.Fatal(err)
}
fetcher.Setup(controller.NewController()) fetcher.Setup(controller.NewController())
fetcher.Continue() fetcher.Continue()
@@ -200,7 +240,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download error = %v, want %v", got, want) t.Errorf("Download() got = %v, want %v", got, want)
} }
} }
+5 -5
View File
@@ -1,23 +1,23 @@
package http package http
import "github.com/monkeyWie/gopeed-core/pkg/base" import "github.com/monkeyWie/gopeed/pkg/base"
type Chunk struct { type chunk struct {
Status base.Status Status base.Status
Begin int64 Begin int64
End int64 End int64
Downloaded int64 Downloaded int64
} }
func NewChunk(begin int64, end int64) *Chunk { func newChunk(begin int64, end int64) *chunk {
return &Chunk{ return &chunk{
Status: base.DownloadStatusReady, Status: base.DownloadStatusReady,
Begin: begin, Begin: begin,
End: end, End: end,
} }
} }
type Extra struct { type extra struct {
Method string Method string
Header map[string]string Header map[string]string
Body string Body string
@@ -1,12 +1,14 @@
package test package test
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
"os" "os"
"time"
) )
const ( const (
@@ -25,6 +27,25 @@ func StartTestFileServer() net.Listener {
}) })
} }
type SlowFileServer struct {
delay time.Duration
handler http.Handler
}
func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
time.Sleep(s.delay)
s.handler.ServeHTTP(w, r)
}
func StartTestSlowFileServer(delay time.Duration) net.Listener {
return startTestServer(func() http.Handler {
return &SlowFileServer{
delay: delay,
handler: http.FileServer(http.Dir(Dir)),
}
})
}
func StartTestChunkedServer() net.Listener { func StartTestChunkedServer() net.Listener {
return startTestServer(func() http.Handler { return startTestServer(func() http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -61,6 +82,29 @@ func StartTestRetryServer() net.Listener {
}) })
} }
func StartTestPostServer() net.Listener {
return startTestServer(func() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
var data map[string]interface{}
if err := json.NewDecoder(request.Body).Decode(&data); err != nil {
panic(err)
}
if data["name"] == BuildName {
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
}
}
})
return mux
})
}
func StartTestErrorServer() net.Listener { func StartTestErrorServer() net.Listener {
counter := 0 counter := 0
return startTestServer(func() http.Handler { return startTestServer(func() http.Handler {
@@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"os" "os"
"path/filepath"
) )
func FileMd5(filePath string) string { func FileMd5(filePath string) string {
@@ -26,6 +27,24 @@ func FileMd5(filePath string) string {
return hex.EncodeToString(hash.Sum(nil)) return hex.EncodeToString(hash.Sum(nil))
} }
func DirMd5(dirPath string) string {
hash := md5.New()
filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
if _, err := io.Copy(hash, file); err != nil {
return err
}
return nil
})
return hex.EncodeToString(hash.Sum(nil))
}
func ToJson(v interface{}) string { func ToJson(v interface{}) string {
buf, _ := json.Marshal(v) buf, _ := json.Marshal(v)
return string(buf) return string(buf)
+6 -13
View File
@@ -1,16 +1,13 @@
package base package base
import "errors" type Status string
type Status int
const ( const (
DownloadStatusPrepare Status = iota DownloadStatusReady Status = "ready"
DownloadStatusReady DownloadStatusRunning Status = "running"
DownloadStatusStart DownloadStatusPause Status = "pause"
DownloadStatusPause DownloadStatusError Status = "error"
DownloadStatusError DownloadStatusDone Status = "done"
DownloadStatusDone
) )
const ( const (
@@ -24,7 +21,3 @@ const (
HttpHeaderRangeFormat = "bytes=%d-%d" HttpHeaderRangeFormat = "bytes=%d-%d"
) )
var (
DeleteErr = errors.New("delete")
)
+18 -16
View File
@@ -3,38 +3,40 @@ package base
// 下载请求 // 下载请求
type Request struct { type Request struct {
// 下载链接 // 下载链接
Schema string URL string `json:"url"`
// 下载链接
URL string
// 附加信息 // 附加信息
Extra interface{} Extra interface{} `json:"extra"`
} }
// 资源信息 // 资源信息
type Resource struct { type Resource struct {
Req *Request Req *Request `json:"req"`
// 资源名称
Name string `json:"name"`
// 资源总大小 // 资源总大小
Length int64 Size int64 `json:"size"`
// 是否支持断点下载 // 是否支持断点下载
Range bool Range bool `json:"range"`
// 资源所包含的文件列表 // 资源所包含的文件列表
Files []*FileInfo Files []*FileInfo `json:"files"`
// 附加信息
Extra interface{} `json:"extra"`
} }
type FileInfo struct { type FileInfo struct {
Name string Name string `json:"name"`
Path string Path string `json:"path"`
Size int64 Size int64 `json:"size"`
} }
// 下载选项 // 下载选项
type Options struct { type Options struct {
// 保存文件名 // 保存文件名
Name string Name string `json:"name"`
// 保存目录 // 保存目录
Path string Path string `json:"path"`
// 并发连接数 // 并发连接数
Connections int Connections int `json:"connections"`
// 下载的文件下标列表 // 选择下载的文件下标列表
SelectFiles []int SelectFiles []int `json:"selectFiles"`
} }
File diff suppressed because it is too large. Load diff
+87 -12
View File
@@ -1,29 +1,34 @@
package download package download
import ( import (
"github.com/monkeyWie/gopeed-core/internal/protocol/http" "github.com/monkeyWie/gopeed/internal/test"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed-core/pkg/test"
"reflect" "reflect"
"sync" "sync"
"testing" "testing"
"time"
) )
func TestDownloader_Resolve(t *testing.T) { func TestDownloader_Resolve(t *testing.T) {
listener := test.StartTestFileServer() listener := test.StartTestFileServer()
defer listener.Close() defer listener.Close()
downloader := NewDownloader(new(http.FetcherBuilder)) downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
_, res, err := downloader.Resolve(req) res, err := downloader.Resolve(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := &base.Resource{ want := &base.Resource{
Name: test.BuildName,
Req: req, Req: req,
Length: test.BuildSize, Size: test.BuildSize,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
@@ -33,8 +38,8 @@ func TestDownloader_Resolve(t *testing.T) {
}, },
}, },
} }
if !reflect.DeepEqual(res, want) { if !reflect.DeepEqual(want, res) {
t.Errorf("Resolve error = %s, want %s", test.ToJson(res), test.ToJson(want)) t.Errorf("Resolve() got = %v, want %v", res, want)
} }
} }
@@ -42,11 +47,15 @@ func TestDownloader_Create(t *testing.T) {
listener := test.StartTestFileServer() listener := test.StartTestFileServer()
defer listener.Close() defer listener.Close()
downloader := NewDownloader(new(http.FetcherBuilder)) downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
taskID, res, err := downloader.Resolve(req) res, err := downloader.Resolve(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -58,7 +67,7 @@ func TestDownloader_Create(t *testing.T) {
wg.Done() wg.Done()
} }
}) })
err = downloader.Create(taskID, res, &base.Options{ _, err = downloader.Create(res, &base.Options{
Path: test.Dir, Path: test.Dir,
Name: test.DownloadName, Name: test.DownloadName,
Connections: 4, Connections: 4,
@@ -70,6 +79,72 @@ func TestDownloader_Create(t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download error = %v, want %v", got, want) t.Errorf("Download() got = %v, want %v", got, want)
} }
} }
func TestDownloader_StoreAndRestore(t *testing.T) {
listener := test.StartTestSlowFileServer(time.Millisecond * 2000)
defer listener.Close()
downloader := NewDownloader(&DownloaderConfig{
Storage: NewBoltStorage("./"),
})
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
res, err := downloader.Resolve(req)
if err != nil {
t.Fatal(err)
}
id, err := downloader.Create(res, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Connections: 4,
})
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 1001)
err = downloader.Pause(id)
if err != nil {
t.Fatal(err)
}
downloader.Close()
downloader = NewDownloader(&DownloaderConfig{
Storage: NewBoltStorage("./"),
})
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
task := downloader.GetTask(id)
if task == nil {
t.Fatal("task is nil")
}
var wg sync.WaitGroup
wg.Add(1)
downloader.Listener(func(event *Event) {
if event.Key == EventKeyDone {
wg.Done()
}
})
err = downloader.Continue(id)
wg.Wait()
if err != nil {
t.Fatal(err)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("StoreAndResume() got = %v, want %v", got, want)
}
downloader.Clear()
}
+1
View File
@@ -8,6 +8,7 @@ const (
EventKeyContinue = "continue" EventKeyContinue = "continue"
EventKeyProgress = "progress" EventKeyProgress = "progress"
EventKeyError = "error" EventKeyError = "error"
EventKeyDelete = "delete"
EventKeyDone = "done" EventKeyDone = "done"
EventKeyFinally = "finally" EventKeyFinally = "finally"
) )
+228
View File
@@ -0,0 +1,228 @@
package download
import (
"encoding/json"
"go.etcd.io/bbolt"
"os"
"path/filepath"
"reflect"
)
type Storage interface {
Setup(buckets []string) error
Put(bucket string, key string, v any) error
Get(bucket string, key string, v any) (bool, error)
List(bucket string, v any) error
Pop(bucket string, key string, v any) error
Delete(bucket string, key string) error
Close() error
Clear() error
}
func changeValue(p any, v any) {
if v == nil {
return
}
rp := reflect.ValueOf(p)
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice {
if rv.Len() == 0 {
return
}
// get underlying type
tp := reflect.TypeOf(p).Elem().Elem()
for i := 0; i < rv.Len(); i++ {
// convert to underlying type
vv := rv.Index(i).Elem().Convert(tp)
rp.Elem().Set(reflect.Append(rp.Elem(), vv))
}
} else if rv.Kind() == reflect.Ptr {
rp.Elem().Set(rv.Elem())
} else {
rp.Elem().Set(rv)
}
}
var memData = make(map[string]map[string]any)
type MemStorage struct {
}
func NewMemStorage() *MemStorage {
return &MemStorage{}
}
func (n *MemStorage) Setup(buckets []string) error {
for _, bucket := range buckets {
if _, ok := memData[bucket]; !ok {
memData[bucket] = make(map[string]any)
}
}
return nil
}
func (n *MemStorage) Put(bucket string, key string, v any) error {
memData[bucket][key] = v
return nil
}
func (n *MemStorage) Get(bucket string, key string, v any) (bool, error) {
if dv, ok := memData[bucket][key]; ok {
changeValue(v, dv)
return true, nil
}
return false, nil
}
func (n *MemStorage) List(bucket string, v any) error {
data := memData[bucket]
list := make([]any, 0)
for _, v := range data {
list = append(list, v)
}
changeValue(v, list)
return nil
}
func (n *MemStorage) Pop(bucket string, key string, v any) error {
data := memData[bucket]
changeValue(v, data[key])
delete(data, key)
return nil
}
func (n *MemStorage) Delete(bucket string, key string) error {
delete(memData[bucket], key)
return nil
}
func (n *MemStorage) Close() error {
return nil
}
func (n *MemStorage) Clear() error {
memData = make(map[string]map[string]any)
return nil
}
const (
dbFile = "gopeed.db"
)
type BoltStorage struct {
db *bbolt.DB
path string
}
func NewBoltStorage(dir string) *BoltStorage {
path := filepath.Join(dir, dbFile)
db, err := bbolt.Open(path, 0600, nil)
if err != nil {
panic(err)
}
return &BoltStorage{
db: db,
path: path,
}
}
func (b *BoltStorage) Setup(buckets []string) error {
return b.db.Update(func(tx *bbolt.Tx) error {
for _, bucket := range buckets {
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
if err != nil {
return err
}
}
return nil
})
}
func (b *BoltStorage) Put(bucket string, key string, v any) error {
buf, err := json.Marshal(v)
if err != nil {
return err
}
return b.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
return b.Put([]byte(key), buf)
})
}
func (b *BoltStorage) Get(bucket string, key string, v any) (bool, error) {
var data []byte
err := b.db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
data = b.Get([]byte(key))
return nil
})
if err != nil {
return false, err
}
if data == nil {
return false, nil
}
if err := json.Unmarshal(data, v); err != nil {
return false, err
}
return true, nil
}
func (b *BoltStorage) List(bucket string, v any) error {
list := make([]any, 0)
tv := reflect.TypeOf(v).Elem().Elem()
if err := b.db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
if err := b.ForEach(func(k, v []byte) error {
data := reflect.New(tv.Elem()).Interface()
if err := json.Unmarshal(v, &data); err != nil {
return err
}
list = append(list, data)
return nil
}); err != nil {
return err
}
return nil
}); err != nil {
return err
}
changeValue(v, list)
return nil
}
func (b *BoltStorage) Pop(bucket string, key string, v any) error {
var data []byte
err := b.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
kb := []byte(key)
data = b.Get(kb)
return b.Delete(kb)
})
if err != nil {
return err
}
return json.Unmarshal(data, v)
}
func (b *BoltStorage) Delete(bucket string, key string) error {
return b.db.Update(func(tx *bbolt.Tx) error {
b := tx.Bucket([]byte(bucket))
return b.Delete([]byte(key))
})
}
func (b *BoltStorage) Close() error {
return b.db.Close()
}
func (b *BoltStorage) Clear() error {
if err := b.Close(); err != nil {
return err
}
if err := os.Remove(b.path); err != nil {
return err
}
return nil
}
+33 -13
View File
@@ -1,29 +1,49 @@
package download package download
import ( import (
"github.com/google/uuid" gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/monkeyWie/gopeed-core/internal/fetcher" "github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed-core/pkg/base" "github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed-core/pkg/util" "github.com/monkeyWie/gopeed/pkg/util"
"sync" "sync"
"time"
) )
type Task struct { type Task struct {
ID string ID string `json:"id"`
Res *base.Resource Res *base.Resource `json:"res"`
Opts *base.Options Opts *base.Options `json:"opts"`
Status base.Status Status base.Status `json:"status"`
Files map[string]*base.FileInfo Progress *Progress `json:"progress"`
Progress *Progress Size int64 `json:"size"`
CreatedAt time.Time `json:"createdAt"`
fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher fetcher fetcher.Fetcher
timer *util.Timer timer *util.Timer
locker *sync.Mutex lock *sync.Mutex
} }
func NewTask() *Task { func NewTask() *Task {
id, err := gonanoid.New()
if err != nil {
panic(err)
}
return &Task{ return &Task{
ID: uuid.New().String(), ID: id,
Status: base.DownloadStatusPrepare, Status: base.DownloadStatusReady,
CreatedAt: time.Now(),
}
}
func (t *Task) clone() *Task {
return &Task{
ID: t.ID,
Res: t.Res,
Opts: t.Opts,
Status: t.Status,
Progress: t.Progress,
Size: t.Size,
CreatedAt: t.CreatedAt,
} }
} }
+123
View File
@@ -0,0 +1,123 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"github.com/monkeyWie/gopeed/pkg/rest/util"
"net/http"
"strings"
)
func Resolve(w http.ResponseWriter, r *http.Request) {
var req base.Request
if util.ReadJson(w, r, &req) {
resource, err := Downloader.Resolve(&req)
if err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
return
}
util.WriteJsonOk(w, model.NewResultWithData(resource))
}
}
func CreateTask(w http.ResponseWriter, r *http.Request) {
var req model.CreateTask
if util.ReadJson(w, r, &req) {
if req.Opts.Connections == 0 {
req.Opts.Connections = getServerConfig().Connections
}
if req.Opts.Path == "" {
req.Opts.Path = getServerConfig().DownloadDir
}
taskId, err := Downloader.Create(req.Res, req.Opts)
if err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
return
}
util.WriteJsonOk(w, model.NewResultWithData(taskId))
}
}
func PauseTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
if err := Downloader.Pause(taskId); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
return
}
util.WriteJsonOk(w, nil)
}
func ContinueTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
if err := Downloader.Continue(taskId); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
return
}
util.WriteJsonOk(w, nil)
}
func DeleteTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
force := r.FormValue("force")
if err := Downloader.Delete(taskId, force == "true"); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
return
}
util.WriteJsonOk(w, nil)
}
func GetTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
task := Downloader.GetTask(taskId)
if task == nil {
util.WriteJson(w, http.StatusNotFound, model.NewResultWithMsg("task not found"))
return
}
util.WriteJsonOk(w, model.NewResultWithData(task))
}
func GetTasks(w http.ResponseWriter, r *http.Request) {
status := r.FormValue("status")
if status == "" {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("param is required: status"))
return
}
statusArr := strings.Split(status, ",")
tasks := Downloader.GetTasks()
result := make([]*download.Task, 0)
for _, task := range tasks {
for _, s := range statusArr {
if task.Status == base.Status(s) {
result = append(result, task)
}
}
}
util.WriteJsonOk(w, model.NewResultWithData(result))
}
func GetConfig(w http.ResponseWriter, r *http.Request) {
util.WriteJsonOk(w, model.NewResultWithData(getServerConfig()))
}
func PutConfig(w http.ResponseWriter, r *http.Request) {
var cfg model.ServerConfig
if util.ReadJson(w, r, &cfg) {
if err := Downloader.PutConfig(&cfg); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
return
}
}
util.WriteJsonOk(w, nil)
}
func getServerConfig() *model.ServerConfig {
var cfg model.ServerConfig
Downloader.GetConfig(&cfg)
return &cfg
}
+21
View File
@@ -0,0 +1,21 @@
package model
type Result[T any] struct {
Msg string `json:"msg"`
Data T `json:"data"`
}
func NewResult[T any](msg string, data T) *Result[T] {
return &Result[T]{
Msg: msg,
Data: data,
}
}
func NewResultWithMsg(msg string) *Result[any] {
return NewResult[any](msg, nil)
}
func NewResultWithData[T any](data T) *Result[T] {
return NewResult[T]("", data)
}
+58
View File
@@ -0,0 +1,58 @@
package model
import "io/fs"
type Storage string
const (
StorageMem Storage = "mem"
StorageBolt Storage = "bolt"
)
type StartConfig struct {
Network string `json:"network"`
Address string `json:"address"`
Storage Storage `json:"storage"`
StorageDir string `json:"storageDir"`
RefreshInterval int `json:"refreshInterval"`
WebEnable bool
WebFS fs.FS
}
func (cfg *StartConfig) Init() *StartConfig {
if cfg.Network == "" {
cfg.Network = "tcp"
}
if cfg.Storage == "" {
cfg.Storage = StorageBolt
}
if cfg.StorageDir == "" {
cfg.StorageDir = "./"
}
return cfg
}
// ServerConfig is present in the database
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
Connections int `json:"connections"`
DownloadDir string `json:"downloadDir"`
Extra map[string]any `json:"extra"`
}
func (cfg *ServerConfig) Init() *ServerConfig {
if cfg.Host == "" {
cfg.Host = "127.0.0.1"
}
if cfg.Port < 0 {
cfg.Port = 0
}
if cfg.Connections <= 0 {
cfg.Connections = 16
}
return cfg
}
+8
View File
@@ -0,0 +1,8 @@
package model
import "github.com/monkeyWie/gopeed/pkg/base"
type CreateTask struct {
Res *base.Resource `json:"res"`
Opts *base.Options `json:"opts"`
}
+125
View File
@@ -0,0 +1,125 @@
package rest
import (
"context"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"github.com/monkeyWie/gopeed/pkg/util"
"net"
"net/http"
"strconv"
)
var (
srv *http.Server
Downloader *download.Downloader
)
func Start(startCfg *model.StartConfig) (int, error) {
srv, listener, err := BuildServer(startCfg)
if err != nil {
return 0, err
}
go func() {
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
// TODO log
panic(err)
}
}()
port := 0
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
port = addr.Port
}
return port, nil
}
func Stop() {
if srv != nil {
if err := srv.Shutdown(context.TODO()); err != nil {
// TODO log
}
}
if Downloader != nil {
if err := Downloader.Close(); err != nil {
// TODO log
}
}
}
func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error) {
if startCfg == nil {
startCfg = &model.StartConfig{}
}
startCfg.Init()
downloadCfg := &download.DownloaderConfig{}
if startCfg.Storage == model.StorageBolt {
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
} else {
downloadCfg.Storage = download.NewMemStorage()
}
downloadCfg.RefreshInterval = startCfg.RefreshInterval
Downloader = download.NewDownloader(downloadCfg.Init())
if err := Downloader.Setup(); err != nil {
return nil, nil, err
}
serverConfig := getAndPutServerConfig(startCfg)
var address string
if startCfg.Network == "unix" {
address = startCfg.Address
util.SafeRemove(address)
} else {
address = net.JoinHostPort(serverConfig.Host, strconv.Itoa(serverConfig.Port))
}
listener, err := net.Listen(startCfg.Network, address)
if err != nil {
return nil, nil, err
}
var r = mux.NewRouter()
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig)
r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
if startCfg.WebEnable {
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
}
srv = &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
handlers.AllowedOrigins([]string{"*"}),
)(r)}
return srv, listener, nil
}
func getAndPutServerConfig(startCfg *model.StartConfig) *model.ServerConfig {
var serverConfig model.ServerConfig
exist, err := Downloader.GetConfig(&serverConfig)
if err != nil {
// TODO log
}
// first start
if !exist {
if startCfg.Network == "tcp" {
host, port, _ := net.SplitHostPort(startCfg.Address)
serverConfig.Host = host
serverConfig.Port, _ = strconv.Atoi(port)
}
serverConfig.Init()
Downloader.PutConfig(serverConfig)
}
return &serverConfig
}
+273
View File
@@ -0,0 +1,273 @@
package rest
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/monkeyWie/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"io"
"net"
"net/http"
"os"
"reflect"
"sync"
"testing"
"time"
)
var (
restPort int
taskReq = &base.Request{}
taskRes = &base.Resource{
Name: test.BuildName,
Req: taskReq,
Size: test.BuildSize,
Range: true,
Files: []*base.FileInfo{
{
Name: test.BuildName,
Path: "",
Size: test.BuildSize,
},
},
}
createReq = &model.CreateTask{
Res: taskRes,
Opts: &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Connections: 2,
},
}
)
func TestResolve(t *testing.T) {
doTest(func() {
resp := httpRequestCheckOk[*base.Resource](http.MethodPost, "/api/v1/resolve", taskReq)
if !reflect.DeepEqual(taskRes, resp) {
t.Errorf("Resolve() got = %v, want %v", resp, taskRes)
}
})
}
func TestCreateTask(t *testing.T) {
doTest(func() {
var wg sync.WaitGroup
wg.Add(1)
Downloader.Listener(func(event *download.Event) {
if event.Key == download.EventKeyFinally {
wg.Done()
}
})
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
if taskId == "" {
t.Fatal("create task failed")
}
wg.Wait()
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("CreateTask() got = %v, want %v", got, want)
}
})
}
func TestPauseAndContinueTask(t *testing.T) {
doTest(func() {
type result struct {
pauseCount int
continueCount int
md5 string
}
var got = result{
pauseCount: 0,
continueCount: 0,
md5: "",
}
var wg sync.WaitGroup
wg.Add(1)
Downloader.Listener(func(event *download.Event) {
switch event.Key {
case download.EventKeyPause:
got.pauseCount++
case download.EventKeyContinue:
got.continueCount++
case download.EventKeyFinally:
wg.Done()
}
})
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
t1 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
if t1.Status != base.DownloadStatusRunning {
t.Errorf("CreateTask() got = %v, want %v", t1.Status, base.DownloadStatusRunning)
}
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/"+taskId+"/pause", nil)
t2 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
if t2.Status != base.DownloadStatusPause {
t.Errorf("PauseTask() got = %v, want %v", t2.Status, base.DownloadStatusPause)
}
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/"+taskId+"/continue", nil)
t3 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
if t3.Status != base.DownloadStatusRunning {
t.Errorf("ContinueTask() got = %v, want %v", t3.Status, base.DownloadStatusRunning)
}
wg.Wait()
want := result{
pauseCount: 1,
continueCount: 1,
md5: test.FileMd5(test.BuildFile),
}
got.md5 = test.FileMd5(test.DownloadFile)
if !reflect.DeepEqual(got, want) {
t.Errorf("PauseAndContinueTask() got = %v, want %v", got, want)
}
})
}
func TestDeleteTask(t *testing.T) {
doTest(func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId, nil)
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
checkCode(code, http.StatusNotFound)
})
}
func TestDeleteTaskForce(t *testing.T) {
doTest(func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
time.Sleep(time.Millisecond * 500)
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId+"?force=true", nil)
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
checkCode(code, http.StatusNotFound)
if _, err := os.Stat(test.DownloadFile); !errors.Is(err, os.ErrNotExist) {
t.Errorf("DeleteTaskForce() got = %v, want %v", err, os.ErrNotExist)
}
})
}
func TestGetTasks(t *testing.T) {
doTest(func() {
var wg sync.WaitGroup
wg.Add(1)
Downloader.Listener(func(event *download.Event) {
if event.Key == download.EventKeyFinally {
wg.Done()
}
})
httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s,%s",
base.DownloadStatusReady, base.DownloadStatusRunning), createReq)
code, _ := httpRequest[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
checkCode(code, http.StatusBadRequest)
wg.Wait()
r := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s",
base.DownloadStatusDone), nil)
if r[0].Status != base.DownloadStatusDone {
t.Errorf("GetTasks() got = %v, want %v", r[0].Status, base.DownloadStatusDone)
}
r = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s,%s",
base.DownloadStatusReady, base.DownloadStatusRunning), nil)
if len(r) > 0 {
t.Errorf("GetTasks() got = %v, want %v", len(r), 0)
}
})
}
func TestGetAndPutConfig(t *testing.T) {
doTest(func() {
cfg := httpRequestCheckOk[*model.ServerConfig](http.MethodGet, "/api/v1/config", nil)
cfg.Port = 8888
cfg.Connections = 32
cfg.DownloadDir = "./download"
cfg.Extra = map[string]any{
"theme": "dark",
}
httpRequestCheckOk[any](http.MethodPut, "/api/v1/config", cfg)
newCfg := httpRequestCheckOk[*model.ServerConfig](http.MethodGet, "/api/v1/config", nil)
if !reflect.DeepEqual(newCfg, cfg) {
t.Errorf("GetAndPutConfig() got = %v, want %v", newCfg, cfg)
}
})
}
func doTest(handler func()) {
testFunc := func(storage model.Storage) {
fileListener := doStart(storage)
defer func() {
if err := fileListener.Close(); err != nil {
panic(err)
}
Stop()
}()
defer Downloader.Clear()
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
handler()
}
testFunc(model.StorageMem)
testFunc(model.StorageBolt)
}
func doStart(storage model.Storage) net.Listener {
port, err := Start((&model.StartConfig{
Storage: storage,
}).Init())
if err != nil {
panic(err)
}
restPort = port
return test.StartTestFileServer()
}
func httpRequest[T any](method string, path string, req any) (int, *model.Result[T]) {
var body io.Reader
if req != nil {
buf, _ := json.Marshal(req)
body = bytes.NewBuffer(buf)
}
request, err := http.NewRequest(method, fmt.Sprintf("http://127.0.0.1:%d%s", restPort, path), body)
if err != nil {
panic(err)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
var r model.Result[T]
if err := json.NewDecoder(response.Body).Decode(&r); err != nil {
panic(err)
}
return response.StatusCode, &r
}
func httpRequestCheckOk[T any](method string, path string, req any) T {
code, result := httpRequest[T](method, path, req)
checkOk(code)
return result.Data
}
func checkOk(code int) {
checkCode(code, 200)
}
func checkCode(code int, exceptCode int) {
if code != exceptCode {
panic(fmt.Sprintf("code got = %d, want %d", code, exceptCode))
}
}
+25
View File
@@ -0,0 +1,25 @@
package util
import (
"encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"net/http"
)
func ReadJson(w http.ResponseWriter, r *http.Request, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
WriteJson(w, 500, model.NewResultWithMsg(err.Error()))
return false
}
return true
}
func WriteJsonOk(w http.ResponseWriter, v any) {
WriteJson(w, 200, v)
}
func WriteJson(w http.ResponseWriter, code int, v any) {
w.WriteHeader(code)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
+81
View File
@@ -0,0 +1,81 @@
package util
import (
"errors"
"io"
"os"
"path/filepath"
)
func Dir(path string) string {
dir := filepath.Dir(path)
if dir == "." {
return ""
}
return dir
}
func Filepath(path string, originName string, customName string) string {
if customName == "" {
customName = originName
}
return filepath.Join(path, customName)
}
// SafeRemove remove file safely, ignoring errors if the path does not exist.
func SafeRemove(name string) error {
if err := os.Remove(name); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
// SafeRemoveAll remove file and parent directories safely
func SafeRemoveAll(path string, names []string) error {
for _, name := range names {
err := SafeRemove(filepath.Join(path, name))
if err != nil {
return err
}
if err := safeRemoveParent(path, name); err != nil {
return err
}
}
return nil
}
func safeRemoveParent(path string, subPath string) error {
currPath := filepath.Dir(subPath)
if currPath == "." {
return nil
}
// if directory is empty, remove it
dir := filepath.Join(path, filepath.Dir(subPath))
empty, err := isEmpty(dir)
if err != nil {
return err
}
if empty {
err = SafeRemove(dir)
if err != nil {
return err
}
return safeRemoveParent(path, currPath)
}
return nil
}
// check directory is empty
func isEmpty(name string) (bool, error) {
f, err := os.Open(name)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdirnames(1) // Or f.Readdir(1)
if err == io.EOF {
return true, nil
}
return false, err // Either not empty or error, suits both cases
}
+198
View File
@@ -0,0 +1,198 @@
package util
import (
"os"
"path/filepath"
"testing"
)
func TestDir(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty path",
args: args{
path: ".",
},
want: "",
},
{
name: "normal path case 1",
args: args{
path: "./a/b/c/1.txt",
},
want: "a/b/c",
},
{
name: "normal path case 2",
args: args{
path: "a/b/c/1.txt",
},
want: "a/b/c",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Dir(tt.args.path); got != tt.want {
t.Errorf("Dir() = %v, want %v", got, tt.want)
}
})
}
}
func TestFilepath(t *testing.T) {
type args struct {
path string
originName string
customName string
}
tests := []struct {
name string
args args
want string
}{
{
name: "origin name",
args: args{
path: "/Downloads",
originName: "1.txt",
customName: "",
},
want: "/Downloads/1.txt",
},
{
name: "origin name",
args: args{
path: "/Downloads",
originName: "1.txt",
customName: "2.txt",
},
want: "/Downloads/2.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Filepath(tt.args.path, tt.args.originName, tt.args.customName); got != tt.want {
t.Errorf("Filepath() = %v, want %v", got, tt.want)
}
})
}
}
func TestSafeRemove(t *testing.T) {
name := "test_safe_remove.data"
_, err := os.Create(name)
if err != nil {
t.Fatal(err)
}
if err := SafeRemove(name); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(name); err == nil {
t.Fatal(err)
}
if err := SafeRemove("test_safe_remove_not_exist.data"); err != nil {
t.Fatal(err)
}
}
func TestSafeRemoveAll(t *testing.T) {
testDir := "test_dir"
err := os.Mkdir(testDir, 0755)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(testDir)
doSafeRemoveAll(t, testDir, []string{
"1.txt",
}, []string{})
doSafeRemoveAll(t, testDir, []string{
"1.txt",
}, []string{
"2.txt",
}, "2.txt")
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{})
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{
"a/b/2.txt",
}, "a/b/2.txt")
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{
"a/2.txt",
}, "a/2.txt")
}
func doSafeRemoveAll(t *testing.T, path string, downloadNames []string, otherNames []string, exist ...string) {
preCreate(t, path, downloadNames)
preCreate(t, path, otherNames)
if err := SafeRemoveAll(path, downloadNames); err != nil {
t.Fatal(err)
}
if len(exist) == 0 {
for _, name := range downloadNames {
filePath := filepath.Join(path, name)
if isExist(filePath) {
t.Fatalf("file %s should not exist", filePath)
}
subDirPath := filepath.Dir(name)
if subDirPath != "." {
dirPath := filepath.Join(path, subDirPath)
if isExist(dirPath) {
t.Fatalf("dir %s should not exist", dirPath)
}
}
}
return
}
for _, name := range exist {
if !isExist(filepath.Join(path, name)) {
t.Fatalf("file %s should exist", name)
}
}
}
func preCreate(t *testing.T, path string, names []string) {
for _, name := range names {
fullPath := filepath.Join(path, name)
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
t.Fatal(err)
}
_, err := os.Create(fullPath)
if err != nil {
t.Fatal(err)
}
}
}
func isExist(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}
-4
View File
@@ -16,10 +16,6 @@ func (t *Timer) Pause() {
t.used += time.Now().UnixNano() - t.t t.used += time.Now().UnixNano() - t.t
} }
func (t *Timer) Continue() {
t.t = time.Now().UnixNano()
}
func (t *Timer) Used() int64 { func (t *Timer) Used() int64 {
return (time.Now().UnixNano() - t.t) + t.used return (time.Now().UnixNano() - t.t) + t.used
} }
+63
View File
@@ -0,0 +1,63 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# Libgopeed
windows/libgopeed.h
windows/libgopeed.dll
macos/Frameworks/libgopeed.h
macos/Frameworks/libgopeed.dylib
linux/bundle/lib/libgopeed.h
linux/bundle/lib/libgopeed.dylib
android/app/libs/libgopeed.aar
android/app/libs/libgopeed-sources.jar
debian/packages
linux/flutter/generated_plugin_registrant.cc
linux/flutter/generated_plugin_registrant.h
linux/flutter/generated_plugins.cmake
macos/Flutter/GeneratedPluginRegistrant.swift
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.h
windows/flutter/generated_plugins.cmake
+45
View File
@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: f1875d570e39de09040c8f79aa13cc56baab8db1
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: android
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: ios
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: linux
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: macos
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: web
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: windows
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+16
View File
@@ -0,0 +1,16 @@
# gopeed
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+29
View File
@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
Loaded 50 of 217 files, more files were not shown because too many files have changed in this diff. Show more