Compare commits

...
76 Commits
Author SHA1 Message Date
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
219 changed files with 11236 additions and 379 deletions

No files matched your search

+2 -2
View File
@@ -1,5 +1,5 @@
name-template: 'v$NEXT_PATCH_VERSION 🌈' name-template: 'v1.0.0 🌈'
tag-template: 'v$NEXT_PATCH_VERSION' tag-template: 'v1.0.0'
categories: categories:
- title: '🆕 Features' - title: '🆕 Features'
labels: labels:
+293 -10
View File
@@ -3,37 +3,319 @@ name: release
on: on:
push: push:
branches: branches:
- master - main
paths-ignore: paths-ignore:
- '_docs/**' - '_docs/**'
- 'README.md' - '_examples/**'
- 'README*.md'
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 }}
build-windows:
runs-on: windows-latest
needs: [ 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 - name: Build
env: env:
VERSION: ${{ steps.create_release.outputs.tag_name }} VERSION: ${{ needs.release.outputs.tag_name }}
run: | 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
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.exe"
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: ui/flutter/build/windows/Output/*
overwrite: true
build-macos-arm64-lib:
runs-on: ubuntu-latest
needs: [ 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: [ 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.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.release.outputs.upload_url }}
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
overwrite: true
build-linux:
runs-on: ubuntu-latest
needs: [ 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.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.release.outputs.upload_url }}
asset_path: ui/flutter/debian/packages/*
overwrite: true
build-android:
runs-on: ubuntu-latest
needs: [ 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.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.release.outputs.upload_url }}
asset_path: ui/flutter/dist/*
overwrite: true
build-web:
runs-on: ubuntu-latest
needs: [ 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.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 mkdir -p dist/zip
goos_arr=(windows darwin linux) goos_arr=(windows darwin linux)
goarch_arr=(386 amd64 arm arm64) goarch_arr=(386 amd64 arm64)
export CGO_ENABLED=0 export CGO_ENABLED=0
for goos in "${goos_arr[@]}"; do for goos in "${goos_arr[@]}"; do
for goarch in "${goarch_arr[@]}"; do for goarch in "${goarch_arr[@]}"; do
dir="dist/gopeed-$VERSION-$goos-$goarch/" name=gopeed-web-$VERSION-$goos-$goarch
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite -ldflags="-s -w" -o $dir github.com/monkeyWie/gopeed-core/cmd/gopeed \ 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 \ && cd $dir \
&& zip -r ../zip/gopeed-$VERSION-$goos-$goarch.zip * \ && file=$(ls -AU | head -1) \
&& mkdir $name \
&& mv $file $name/$(echo $file | sed -e "s/web/gopeed/g") \
&& zip -r ../zip/$name.zip * \
&& cd ../..) \ && cd ../..) \
|| true || true
done done
@@ -41,5 +323,6 @@ jobs:
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: dist/zip/* asset_path: dist/zip/*
overwrite: true
+23 -15
View File
@@ -3,16 +3,22 @@ name: test
on: on:
pull_request: pull_request:
branches: branches:
- master - main
paths-ignore: paths-ignore:
- '.github/workflows/release.yml'
- '.github/release-drafter.yml'
- '_docs/**' - '_docs/**'
- 'README.md' - '_examples/**'
- 'README*.md'
push: push:
branches: branches:
- master - main
paths-ignore: paths-ignore:
- '.github/workflows/release.yml'
- '.github/release-drafter.yml'
- '_docs/**' - '_docs/**'
- 'README.md' - '_examples/**'
- 'README*.md'
jobs: jobs:
# lint: # lint:
@@ -21,7 +27,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 +38,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 +55,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/
+674
View File
File diff suppressed because it is too large. Load diff
+104 -31
View File
@@ -1,47 +1,120 @@
[![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)
## gopeed ![](_docs/img/banner.png)
支持多协议(HTTP、BitTorrent)下载的客户端,提供命令行、RESTful API、WebSocket、Go 类库方式来使用。 ## 介绍
## 安装 Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
```sh ## 下载
go install github.com/monkeyWie/gopeed-core
**已支持平台**
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] web
[点击前往](https://github.com/monkeyWie/gopeed/releases/latest)
### 命令行工具
使用`go install`安装:
```bash
go install github.com/monkeyWie/gopeed/cmd/gopeed
``` ```
## 示例 ## 界面展示
可以在本仓库的[\_examples](_examples)目录下查看 ![](_docs/img/ui-demo.png)
## 命令行工具 ## 开发
### 预览 本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
![](_docs/img/cli-demo.gif) > 前端代码位于`ui/flutter`目录下。
### 下载 ### 环境要求
前往[releases](https://github.com/monkeyWie/gopeed-core/releases)页面 1. Golang 1.9+
2. Flutter 3.0+
## TODO ### 克隆项目
- [x] HTTP 下载实现 ```bash
- [x] BitTorrent 下载实现 git clone [email protected]:monkeyWie/gopeed.git
- [x] .torrent 文件解析 ```
- [x] tracker 协议实现
- [x] peer wire protocol 协议实现
- [x] DHT 协议实现
- [x] 磁力链接支持
- [x] uTP 协议实现
- [x] 下载接口抽象(不关心具体协议)
- [ ] 支持自定义配置
- [ ] 限速功能实现
- [x] 命令行工具提供
- [ ] RESTful 服务提供
## 参与 ### 编译
由于项目目前还未定型,代码可能随时有大的调整,所以暂不接受 PR,当然如果有什么好的想法可以在 issue 区提出来。 #### 桌面端
首先需要按照[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()
}
+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: "127.0.0.1:9999",
Storage: model.StorageMem,
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
} }
+59
View File
@@ -0,0 +1,59 @@
# 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
generated_plugin_registrant.cc
generated_plugin_registrant.h
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
+13
View File
@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
+78
View File
@@ -0,0 +1,78 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 33
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.gopeed"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 19
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
repositories {
flatDir {
dirs 'libs'
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation(name: 'libgopeed', ext: 'aar')
}
@@ -0,0 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gopeed">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<application android:requestLegacyExternalStorage="true"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>
@@ -0,0 +1,39 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gopeed">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:requestLegacyExternalStorage="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:label="Gopeed">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
Loaded 50 of 219 files, more files were not shown because too many files have changed in this diff. Show more