mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 03:43:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39202b44a1 | ||
|
|
457d9e85d6 | ||
|
|
858ad74339 | ||
|
|
755d7ce814 | ||
|
|
854f600d3f | ||
|
|
28da89f08c | ||
|
|
dddefb68a2 | ||
|
|
e882b663a8 | ||
|
|
6f8e9fc68f | ||
|
|
7799a4032c | ||
|
|
6316e9986a | ||
|
|
39d1028ee9 | ||
|
|
107f90649b | ||
|
|
c8b66501b7 | ||
|
|
398e3f7adc | ||
|
|
158018c248 | ||
|
|
c56447ad5f | ||
|
|
c073fc0a67 | ||
|
|
82d316af83 | ||
|
|
150953a033 | ||
|
|
f30ad6641f | ||
|
|
7019f18635 | ||
|
|
c0338c3d8e | ||
|
|
fe55f1bd54 | ||
|
|
48f8c60672 | ||
|
|
cc97cedf0f | ||
|
|
a7d855f65a | ||
|
|
f0f16b3c80 | ||
|
|
3459b474db | ||
|
|
d28fcef0a7 | ||
|
|
d694c08635 | ||
|
|
a7c61a668e | ||
|
|
09aea55b49 | ||
|
|
c0cd2d7255 | ||
|
|
b00f4a1ed0 | ||
|
|
06766eea56 | ||
|
|
0f0c8f27bd | ||
|
|
45cf62f88d | ||
|
|
6e9d3499d9 | ||
|
|
b6d60f6bdf | ||
|
|
e1684e755a | ||
|
|
e63cfca82a | ||
|
|
85ef8d3be4 | ||
|
|
bf2ccdb5a7 | ||
|
|
a75bfd6118 | ||
|
|
a5d8713ff7 | ||
|
|
81518d794f | ||
|
|
7c2b81caf7 | ||
|
|
2221332243 | ||
|
|
dc407434d0 | ||
|
|
1c2e86a991 | ||
|
|
c78dc5f18f | ||
|
|
8ea5abb5c3 | ||
|
|
d1a4067dfa | ||
|
|
a1928d202f | ||
|
|
7376e85895 | ||
|
|
eb79dd5ac5 | ||
|
|
5b945b16c1 | ||
|
|
80e60f4190 | ||
|
|
3087796ef5 | ||
|
|
99de0aaf9b | ||
|
|
6bea61cf0a | ||
|
|
6c3553805e | ||
|
|
adc7c7766d | ||
|
|
15d0296307 | ||
|
|
32d2e46118 | ||
|
|
abe3cec13d | ||
|
|
9249613601 | ||
|
|
5bafb40599 | ||
|
|
5968d3c14a | ||
|
|
fcb54d3acc | ||
|
|
e2c4567ed0 | ||
|
|
c3b28c7034 | ||
|
|
32061200a5 | ||
|
|
19aad9bc9c | ||
|
|
2d0cc1c877 | ||
|
|
dc94a1004c | ||
|
|
1385f62fb0 | ||
|
|
34fc886662 | ||
|
|
c651263747 | ||
|
|
96a4526bc8 | ||
|
|
97cf8ff0c5 |
No files matched your search
@@ -1,27 +0,0 @@
|
|||||||
## Donate
|
|
||||||
|
|
||||||
Thank you for your support! 🙏
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>微信</th>
|
|
||||||
<th>支付宝</th>
|
|
||||||
<th>PayPal</th>
|
|
||||||
<th>Ko-fi</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="/.donate/qr/wechatpay.jpg" alt="wechatpay" width="300" /></td>
|
|
||||||
<td><img src="/.donate/qr/alipay.jpg" alt="alipay" width="300" /></td>
|
|
||||||
<td><a href="https://paypal.me/monkeyWie" target="_blank">Click Me</a></td>
|
|
||||||
<td><a href="https://ko-fi.com/gopeed" target="_blank">Click Me</a></td>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Crypto
|
|
||||||
|
|
||||||
- **BTC**: 35jYkPN8bgkh7AWjt3B1VgMQHqtGYcvF6p
|
|
||||||
- **ETH:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
|
|
||||||
- **Matic:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
|
|
||||||
- **BNB:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
|
|
||||||
- **USDT(ERC20)**: 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
+1
-1
@@ -12,4 +12,4 @@
|
|||||||
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
# github: monkeyWie
|
# github: monkeyWie
|
||||||
ko_fi: gopeed
|
ko_fi: gopeed
|
||||||
custom: https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate
|
custom: https://docs.gopeed.com/donate.html
|
||||||
@@ -11,6 +11,8 @@ categories:
|
|||||||
- 'fix'
|
- 'fix'
|
||||||
- 'bugfix'
|
- 'bugfix'
|
||||||
- 'bug'
|
- 'bug'
|
||||||
|
- title: '🧪 Tests'
|
||||||
|
label: 'test'
|
||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
label: 'chore'
|
label: 'chore'
|
||||||
- title: '📖 Document'
|
- title: '📖 Document'
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.19
|
GO_VERSION: "1.19"
|
||||||
FLUTTER_VERSION: 3.7.12
|
FLUTTER_VERSION: "3.10.6"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: "1.20"
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build windows
|
flutter build windows
|
||||||
$mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin"
|
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||||
$system = "C:\Windows\System32"
|
$system = "C:\Windows\System32"
|
||||||
$release = "build\windows\runner\Release\"
|
$release = "build\windows\runner\Release\"
|
||||||
cp $mingw\libstdc++-6.dll $release
|
cp $mingw\libstdc++-6.dll $release
|
||||||
@@ -133,9 +133,8 @@ jobs:
|
|||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
docker pull techknowlogick/xgo:go-1.19.x
|
|
||||||
go install src.techknowlogick.com/xgo@latest
|
go install src.techknowlogick.com/xgo@latest
|
||||||
xgo --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
xgo -go go-1.19.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
||||||
mv libgopeed-*.dylib libgopeed.dylib
|
mv libgopeed-*.dylib libgopeed.dylib
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -294,6 +293,38 @@ jobs:
|
|||||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
asset_path: ui/flutter/dist/*
|
asset_path: ui/flutter/dist/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
build-ios:
|
||||||
|
runs-on: macos-latest
|
||||||
|
needs: [ get-release ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build ios --no-codesign
|
||||||
|
mkdir Payload
|
||||||
|
cp -r build/ios/iphoneos/Runner.app Payload
|
||||||
|
zip -r -y Payload.zip Payload/Runner.app
|
||||||
|
mkdir dist
|
||||||
|
mv Payload.zip dist/Gopeed-$VERSION-ios.ipa
|
||||||
|
- name: Upload
|
||||||
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
|
asset_path: ui/flutter/dist/*
|
||||||
|
overwrite: true
|
||||||
build-web:
|
build-web:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
@@ -310,7 +341,7 @@ jobs:
|
|||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||||
run: |
|
run: |
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web
|
flutter build web --web-renderer html
|
||||||
cd ../../
|
cd ../../
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
mkdir -p dist/zip
|
mkdir -p dist/zip
|
||||||
@@ -345,6 +376,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
|
- name: Remove unnecessary files
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
@@ -365,6 +400,8 @@ jobs:
|
|||||||
linux/386
|
linux/386
|
||||||
linux/amd64
|
linux/amd64
|
||||||
linux/arm64
|
linux/arm64
|
||||||
|
linux/arm/v6
|
||||||
|
linux/arm/v7
|
||||||
tags: |
|
tags: |
|
||||||
liwei2633/gopeed:latest
|
liwei2633/gopeed:latest
|
||||||
liwei2633/gopeed:${{ env.VERSION }}
|
liwei2633/gopeed:${{ env.VERSION }}
|
||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.19
|
GO_VERSION: "1.19"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.19
|
GO_VERSION: "1.19"
|
||||||
FLUTTER_VERSION: 3.7.12
|
FLUTTER_VERSION: "3.10.6"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
# lint:
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# name: 'translator'
|
||||||
|
|
||||||
|
# on:
|
||||||
|
# issues:
|
||||||
|
# types: [opened, edited]
|
||||||
|
# issue_comment:
|
||||||
|
# types: [created, edited]
|
||||||
|
# discussion:
|
||||||
|
# types: [created, edited]
|
||||||
|
# discussion_comment:
|
||||||
|
# types: [created, edited]
|
||||||
|
# pull_request_target:
|
||||||
|
# types: [opened, edited]
|
||||||
|
# pull_request_review_comment:
|
||||||
|
# types: [created, edited]
|
||||||
|
|
||||||
|
# jobs:
|
||||||
|
# translate:
|
||||||
|
# permissions:
|
||||||
|
# issues: write
|
||||||
|
# discussions: write
|
||||||
|
# pull-requests: write
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: lizheming/[email protected]
|
||||||
|
# env:
|
||||||
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# with:
|
||||||
|
# IS_MODIFY_TITLE: true
|
||||||
|
# APPEND_TRANSLATION: true
|
||||||
+6
-1
@@ -21,7 +21,12 @@ bin/
|
|||||||
|
|
||||||
*.data
|
*.data
|
||||||
*.db
|
*.db
|
||||||
|
*.log
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
dist/
|
node_modules/
|
||||||
|
cmd/web/dist/
|
||||||
|
.test_storage/
|
||||||
|
.test_download/
|
||||||
|
/extensions
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Gopeed コントリビューターガイド
|
||||||
|
|
||||||
|
まず最初に、Gopeed への貢献に興味を持っていただきありがとうございます。このガイドは、あなたが Gopeed の
|
||||||
|
開発に参加するための手助けとなるでしょう。
|
||||||
|
|
||||||
|
## ブランチの説明
|
||||||
|
|
||||||
|
このプロジェクトのメインブランチは `main` ブランチのみです。Gopeed の開発に参加したい場合は、
|
||||||
|
まずこのプロジェクトをフォークし、フォークしたプロジェクトで開発を行ってください。開発が完了したら、
|
||||||
|
このプロジェクトに PR を提出し、`main` ブランチにマージしてください。
|
||||||
|
|
||||||
|
## ローカル開発
|
||||||
|
|
||||||
|
開発およびデバッグはウェブ上で行うことを推奨する。まずバックエンドのサービスを起動し、
|
||||||
|
コマンドライン `go run cmd/api/main.go` で起動する。サービスのデフォルトポートは `9999` で、
|
||||||
|
次にフロントエンドの flutter プロジェクトを `debug` モードで起動して実行します。
|
||||||
|
|
||||||
|
## 翻訳
|
||||||
|
|
||||||
|
Gopeed の国際化ファイルは `ui/flutter/assets/locales` ディレクトリにあります。
|
||||||
|
このディレクトリに対応する言語ファイルを追加するだけでよいです。
|
||||||
|
|
||||||
|
|
||||||
|
ロケール編集後にロケールを生成:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
get generate locales
|
||||||
|
```
|
||||||
|
|
||||||
|
翻訳については `en_us.dart` を参照してください。
|
||||||
|
|
||||||
|
## flutter での開発
|
||||||
|
|
||||||
|
コミットする前に `dart format ./ui/flutter` を実行し、コードを標準の dart フォーマットにしておくことを忘れないでください
|
||||||
|
|
||||||
|
api/models を編集したい場合は build_runner watcher をオンにします:
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter pub run build_runner watch
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
get-cli コマンドの使用法:
|
||||||
|
|
||||||
|
```
|
||||||
|
create:
|
||||||
|
controller: Generate controller
|
||||||
|
page: Use to generate pages
|
||||||
|
view: Generate view
|
||||||
|
generate:
|
||||||
|
locales: Generate translation file from json files
|
||||||
|
model: generate Class model from json
|
||||||
|
```
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
FROM cirrusci/flutter:3.7.7 AS flutter
|
FROM instrumentisto/flutter:3.10.6 AS flutter
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
||||||
RUN flutter pub get
|
RUN flutter pub get
|
||||||
COPY ./ui/flutter ./
|
COPY ./ui/flutter ./
|
||||||
RUN flutter build web
|
RUN flutter build web --web-renderer html
|
||||||
|
|
||||||
FROM golang:1.19.3 AS go
|
FROM golang:1.19.3 AS go
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
[](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://d
|
|||||||
- [x] macos
|
- [x] macos
|
||||||
- [x] linux
|
- [x] linux
|
||||||
- [x] android
|
- [x] android
|
||||||
- [ ] ios
|
- [x] ios
|
||||||
- [x] web
|
- [x] web
|
||||||
- [x] docker
|
- [x] docker
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|||||||
#### Start directly
|
#### Start directly
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
|
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using Docker Compose
|
#### Using Docker Compose
|
||||||
@@ -56,11 +59,9 @@ docker-compose up -d
|
|||||||
|
|
||||||
When the docker container is running, you can access the web page through `http://localhost:9999`.
|
When the docker container is running, you can access the web page through `http://localhost:9999`.
|
||||||
|
|
||||||
> Tip: Modify the download path to `/download` on the setting page to access the downloaded files on the host.
|
|
||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
|
|
||||||
If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you!
|
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
|
||||||
|
|
||||||
## Showcase
|
## Showcase
|
||||||
|
|
||||||
@@ -75,12 +76,12 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
|||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.19+
|
||||||
2. Flutter 3.7+
|
2. Flutter 3.10+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:monkeyWie/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
@@ -91,7 +92,7 @@ Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
|
|||||||
|
|
||||||
#### Desktop
|
#### Desktop
|
||||||
|
|
||||||
First, you need to configure the environment according to the [flutter desktop official website document](https://docs.flutter.dev/development/platform-integration/desktop), and then you need to prepare the `cgo` environment, which can be searched for yourself.
|
First, you need to configure the environment according to the official [Flutter desktop website documention](https://docs.flutter.dev/development/platform-integration/desktop), then you will need to ensure the cgo environment is set up accordingly. For detailed instructions on setting up the cgo environment, please refer to relevant resources available online.
|
||||||
|
|
||||||
command:
|
command:
|
||||||
|
|
||||||
@@ -139,6 +140,14 @@ cd ui/flutter
|
|||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- ios
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build ios --no-codesign
|
||||||
|
```
|
||||||
|
|
||||||
#### Web
|
#### Web
|
||||||
|
|
||||||
command:
|
command:
|
||||||
@@ -153,7 +162,7 @@ go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed
|
|||||||
go run cmd/web/main.go
|
go run cmd/web/main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
## Thanks
|
## Credits
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
|
|
||||||
|
|||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/donate.html)
|
||||||
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||||
|
|
||||||
|
## はじめに
|
||||||
|
|
||||||
|
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。
|
||||||
|
|
||||||
|
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
|
||||||
|
|
||||||
|
## インストール
|
||||||
|
|
||||||
|
**サポートするプラットフォーム**
|
||||||
|
|
||||||
|
- [x] windows
|
||||||
|
- [x] macos
|
||||||
|
- [x] linux
|
||||||
|
- [x] android
|
||||||
|
- [x] ios
|
||||||
|
- [x] web
|
||||||
|
- [x] docker
|
||||||
|
|
||||||
|
[ダウンロード](https://github.com/GopeedLab/gopeed/releases/latest)
|
||||||
|
|
||||||
|
> ヒント: macos を開くのに失敗した場合は、ターミナルで `xattr -d com.apple.quarantine /Applications/Gopeed.app` コマンドを実行してください
|
||||||
|
|
||||||
|
### コマンドツール
|
||||||
|
|
||||||
|
`go install` を使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
#### 直接始める
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Compose の使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Gopeed へのアクセス
|
||||||
|
|
||||||
|
docker コンテナが起動しているときは、`http://localhost:9999` からウェブページにアクセスできます。
|
||||||
|
|
||||||
|
## 寄付
|
||||||
|
|
||||||
|
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
|
||||||
|
|
||||||
|
## ショーケース
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 開発
|
||||||
|
|
||||||
|
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
|
||||||
|
|
||||||
|
> フロントコードは `ui/flutter` ディレクトリにあります。
|
||||||
|
|
||||||
|
### 環境
|
||||||
|
|
||||||
|
1. Go 言語 1.19+
|
||||||
|
2. Flutter 3.10+
|
||||||
|
|
||||||
|
### クローン
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### コントリビュート
|
||||||
|
|
||||||
|
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
|
||||||
|
|
||||||
|
### ビルド
|
||||||
|
|
||||||
|
#### デスクトップ
|
||||||
|
|
||||||
|
まず、[flutter デスクトップ公式サイトドキュメント](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/GopeedLab/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/GopeedLab/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/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build linux
|
||||||
|
```
|
||||||
|
|
||||||
|
#### モバイル
|
||||||
|
|
||||||
|
先ほどと同じように、`cgo` 環境を準備し、`gomobile` をインストールする必要があります:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
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/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build apk
|
||||||
|
```
|
||||||
|
|
||||||
|
- ios
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build ios --no-codesign
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Web
|
||||||
|
|
||||||
|
コマンド:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
go run cmd/web/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 感謝
|
||||||
|
|
||||||
|
### コントリビューター
|
||||||
|
|
||||||
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### JetBrains
|
||||||
|
|
||||||
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
[GPLv3](LICENSE)
|
||||||
+20
-10
@@ -1,12 +1,15 @@
|
|||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
[](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/zh/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||||
|
|
||||||
## 介绍
|
## 介绍
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下
|
|||||||
- [x] macos
|
- [x] macos
|
||||||
- [x] linux
|
- [x] linux
|
||||||
- [x] android
|
- [x] android
|
||||||
- [ ] ios
|
- [x] ios
|
||||||
- [x] web
|
- [x] web
|
||||||
- [x] docker
|
- [x] docker
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|||||||
#### 直接运行
|
#### 直接运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
|
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 使用 Docker Compose
|
#### 使用 Docker Compose
|
||||||
@@ -55,11 +58,10 @@ docker-compose up -d
|
|||||||
#### 访问服务
|
#### 访问服务
|
||||||
|
|
||||||
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
|
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
|
||||||
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
|
|
||||||
|
|
||||||
## 打赏
|
## 赞助
|
||||||
|
|
||||||
如果觉得项目对你有帮助,请考虑[打赏](/.donate/index.md#donate)以支持这个项目的发展,非常感谢!
|
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
||||||
|
|
||||||
## 界面展示
|
## 界面展示
|
||||||
|
|
||||||
@@ -74,12 +76,12 @@ docker-compose up -d
|
|||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.19+
|
||||||
2. Flutter 3.7+
|
2. Flutter 3.10+
|
||||||
|
|
||||||
### 克隆项目
|
### 克隆项目
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:monkeyWie/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### 贡献代码
|
### 贡献代码
|
||||||
@@ -138,6 +140,14 @@ cd ui/flutter
|
|||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- ios
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build ios --no-codesign
|
||||||
|
```
|
||||||
|
|
||||||
#### Web 端
|
#### Web 端
|
||||||
|
|
||||||
构建命令:
|
构建命令:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ func Start(cfg *C.char) (int, *C.char) {
|
|||||||
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
|
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
|
||||||
return 0, C.CString(err.Error())
|
return 0, C.CString(err.Error())
|
||||||
}
|
}
|
||||||
|
config.ProductionMode = true
|
||||||
realPort, err := rest.Start(&config)
|
realPort, err := rest.Start(&config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, C.CString(err.Error())
|
return 0, C.CString(err.Error())
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ func Start(cfg string) (int, error) {
|
|||||||
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
|
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
config.ProductionMode = true
|
||||||
return rest.Start(&config)
|
return rest.Start(&config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,11 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
|
||||||
"github.com/GopeedLab/gopeed/cmd"
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// only for local development
|
||||||
func main() {
|
func main() {
|
||||||
cfg := &model.StartConfig{
|
cfg := &model.StartConfig{
|
||||||
Network: "tcp",
|
Network: "tcp",
|
||||||
|
|||||||
+1
-8
@@ -13,14 +13,7 @@ import (
|
|||||||
const progressWidth = 20
|
const progressWidth = 20
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
//args := parse()
|
args := parse()
|
||||||
var dir = "./"
|
|
||||||
var connections = 8
|
|
||||||
args := &args{
|
|
||||||
url: "https://sel-kor-ping.vultr.com/vultr.com.100MB.bin",
|
|
||||||
dir: &dir,
|
|
||||||
connections: &connections,
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Address *string `json:"address"`
|
||||||
|
Port *int `json:"port"`
|
||||||
|
Username *string `json:"username"`
|
||||||
|
Password *string `json:"password"`
|
||||||
|
ApiToken *string `json:"apiToken"`
|
||||||
|
|
||||||
|
configPath *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse() *args {
|
||||||
|
var cliArgs args
|
||||||
|
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||||
|
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||||
|
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||||
|
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Password")
|
||||||
|
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
|
||||||
|
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// args priority: config file > cli args
|
||||||
|
cfgArgs := loadConfig(*cliArgs.configPath)
|
||||||
|
if cfgArgs.Address == nil {
|
||||||
|
cfgArgs.Address = cliArgs.Address
|
||||||
|
}
|
||||||
|
if cfgArgs.Port == nil {
|
||||||
|
cfgArgs.Port = cliArgs.Port
|
||||||
|
}
|
||||||
|
if cfgArgs.Username == nil {
|
||||||
|
cfgArgs.Username = cliArgs.Username
|
||||||
|
}
|
||||||
|
if cfgArgs.Password == nil {
|
||||||
|
cfgArgs.Password = cliArgs.Password
|
||||||
|
}
|
||||||
|
if cfgArgs.ApiToken == nil {
|
||||||
|
cfgArgs.ApiToken = cliArgs.ApiToken
|
||||||
|
}
|
||||||
|
return cfgArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(path string) *args {
|
||||||
|
var args args
|
||||||
|
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||||
|
if err != nil {
|
||||||
|
return &args
|
||||||
|
}
|
||||||
|
path = filepath.Join(dir, path)
|
||||||
|
}
|
||||||
|
file, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &args
|
||||||
|
}
|
||||||
|
fmt.Println("config file read failed, reason:" + err.Error())
|
||||||
|
return &args
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(file, &args); err != nil {
|
||||||
|
fmt.Println("config file parse failed, reason:" + err.Error())
|
||||||
|
return &args
|
||||||
|
}
|
||||||
|
fmt.Printf("config file loaded: %s\n", path)
|
||||||
|
return &args
|
||||||
|
}
|
||||||
+19
-1
@@ -5,6 +5,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/cmd"
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -18,12 +19,29 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args := parse()
|
||||||
|
var webBasicAuth *model.WebBasicAuth
|
||||||
|
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
||||||
|
webBasicAuth = &model.WebBasicAuth{
|
||||||
|
Username: *args.Username,
|
||||||
|
Password: *args.Password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cfg := &model.StartConfig{
|
cfg := &model.StartConfig{
|
||||||
Network: "tcp",
|
Network: "tcp",
|
||||||
Address: "0.0.0.0:9999",
|
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
|
ApiToken: *args.ApiToken,
|
||||||
|
ProductionMode: true,
|
||||||
WebEnable: true,
|
WebEnable: true,
|
||||||
WebFS: sub,
|
WebFS: sub,
|
||||||
|
WebBasicAuth: webBasicAuth,
|
||||||
}
|
}
|
||||||
cmd.Start(cfg)
|
cmd.Start(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isNotBlank(str *string) bool {
|
||||||
|
return str != nil && *str != ""
|
||||||
|
}
|
||||||
+1
-1
@@ -7,5 +7,5 @@ services:
|
|||||||
- 9999:9999 # HTTP port (host:container)
|
- 9999:9999 # HTTP port (host:container)
|
||||||
image: liwei2633/gopeed
|
image: liwei2633/gopeed
|
||||||
volumes:
|
volumes:
|
||||||
- ~/Downloads:/download # replace with your prefer path
|
- ~/Downloads:/root/Downloads # replace with your prefer path
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -3,71 +3,105 @@ module github.com/GopeedLab/gopeed
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/torrent v1.46.0
|
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
|
||||||
|
github.com/anacrolix/torrent v1.52.5
|
||||||
|
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e
|
||||||
|
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562
|
||||||
|
github.com/go-git/go-git/v5 v5.8.1
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||||
go.etcd.io/bbolt v1.3.6
|
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
||||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6
|
||||||
|
golang.org/x/sync v0.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 // indirect
|
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||||
|
github.com/RoaringBitmap/roaring v1.2.3 // indirect
|
||||||
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||||
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
github.com/anacrolix/chansync v0.3.0 // indirect
|
github.com/anacrolix/chansync v0.3.0 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51 // indirect
|
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // indirect
|
||||||
github.com/anacrolix/envpprof v1.2.1 // indirect
|
github.com/anacrolix/envpprof v1.2.1 // indirect
|
||||||
github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60 // indirect
|
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 // indirect
|
||||||
github.com/anacrolix/go-libutp v1.2.0 // indirect
|
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
||||||
github.com/anacrolix/log v0.13.1 // indirect
|
github.com/anacrolix/log v0.14.0 // indirect
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.0 // indirect
|
|
||||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||||
github.com/anacrolix/multiless v0.2.1-0.20211218050420-533661eef5dc // indirect
|
github.com/anacrolix/multiless v0.3.0 // indirect
|
||||||
github.com/anacrolix/stm v0.3.0 // indirect
|
github.com/anacrolix/stm v0.4.0 // indirect
|
||||||
github.com/anacrolix/sync v0.4.0 // indirect
|
github.com/anacrolix/sync v0.4.0 // indirect
|
||||||
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
|
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
|
||||||
github.com/anacrolix/utp v0.1.0 // indirect
|
github.com/anacrolix/utp v0.1.0 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/benbjohnson/immutable v0.3.0 // indirect
|
github.com/benbjohnson/immutable v0.3.0 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.2.0 // indirect
|
github.com/bits-and-blooms/bitset v1.2.2 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/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/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||||
github.com/google/btree v1.0.1 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
|
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.2.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/google/btree v1.1.2 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.5.0 // 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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/lispad/go-generics-tools v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.2 // indirect
|
github.com/pion/datachannel v1.5.2 // indirect
|
||||||
github.com/pion/dtls/v2 v2.1.2 // indirect
|
github.com/pion/dtls/v2 v2.2.4 // indirect
|
||||||
github.com/pion/ice/v2 v2.1.20 // indirect
|
github.com/pion/ice/v2 v2.2.6 // indirect
|
||||||
github.com/pion/interceptor v0.1.7 // indirect
|
github.com/pion/interceptor v0.1.11 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/mdns v0.0.5 // indirect
|
github.com/pion/mdns v0.0.5 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.9 // indirect
|
github.com/pion/rtcp v1.2.9 // indirect
|
||||||
github.com/pion/rtp v1.7.4 // indirect
|
github.com/pion/rtp v1.7.13 // indirect
|
||||||
github.com/pion/sctp v1.8.2 // indirect
|
github.com/pion/sctp v1.8.2 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.4 // indirect
|
github.com/pion/sdp/v3 v3.0.5 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.5 // indirect
|
github.com/pion/srtp/v2 v2.0.9 // indirect
|
||||||
github.com/pion/stun v0.3.5 // indirect
|
github.com/pion/stun v0.3.5 // indirect
|
||||||
github.com/pion/transport v0.13.0 // indirect
|
github.com/pion/transport v0.13.1 // indirect
|
||||||
github.com/pion/turn/v2 v2.0.6 // indirect
|
github.com/pion/transport/v2 v2.0.0 // indirect
|
||||||
github.com/pion/udp v0.1.1 // indirect
|
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||||
github.com/pion/webrtc/v3 v3.1.24-0.20220208053747-94262c1b2b38 // indirect
|
github.com/pion/udp v0.1.4 // indirect
|
||||||
|
github.com/pion/webrtc/v3 v3.1.42 // indirect
|
||||||
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect
|
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
|
||||||
github.com/tidwall/btree v1.3.1 // indirect
|
github.com/rs/zerolog v1.31.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
|
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
github.com/tidwall/btree v1.6.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
go.opentelemetry.io/otel v1.8.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
go.opentelemetry.io/otel/trace v1.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.11.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
|
||||||
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
|
golang.org/x/net v0.12.0 // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
golang.org/x/text v0.11.0 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||||
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
@@ -34,7 +33,7 @@ func (c *DefaultFileController) Touch(name string, size int64) (file *os.File, e
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, err = os.Create(name)
|
file, err = os.Create(name)
|
||||||
if err != nil && !errors.Is(err, os.ErrExist) {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if size > 0 {
|
if size > 0 {
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ type Fetcher interface {
|
|||||||
// Name return the name of the protocol.
|
// Name return the name of the protocol.
|
||||||
Name() string
|
Name() string
|
||||||
|
|
||||||
Setup(ctl *controller.Controller) error
|
Setup(ctl *controller.Controller)
|
||||||
// Resolve resource info from request
|
// Resolve resource info from request
|
||||||
Resolve(req *base.Request) error
|
Resolve(req *base.Request) error
|
||||||
// Create ready to download, but not started
|
// Create ready to download, but not started
|
||||||
Create(opts *base.Options) error
|
Create(opts *base.Options) error
|
||||||
Start() error
|
Start() error
|
||||||
Pause() error
|
Pause() error
|
||||||
Continue() error
|
|
||||||
Close() error
|
Close() error
|
||||||
|
|
||||||
// Meta returns the meta information of the download.
|
// Meta returns the meta information of the download.
|
||||||
@@ -37,12 +36,25 @@ type FetcherMeta struct {
|
|||||||
Opts *base.Options `json:"opts"`
|
Opts *base.Options `json:"opts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *FetcherMeta) Filepath(file *base.FileInfo) string {
|
// FolderPath return the folder path of the meta info.
|
||||||
finalName := m.Opts.Name
|
func (m *FetcherMeta) FolderPath() string {
|
||||||
if finalName == "" {
|
// check if rename folder
|
||||||
finalName = file.Name
|
folder := m.Res.Name
|
||||||
|
if m.Opts.Name != "" {
|
||||||
|
folder = m.Opts.Name
|
||||||
}
|
}
|
||||||
return path.Join(m.Opts.Path, m.Res.RootDir, finalName)
|
return path.Join(m.Opts.Path, folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SingleFilepath return the single file path of the meta info.
|
||||||
|
func (m *FetcherMeta) SingleFilepath() string {
|
||||||
|
// check if rename file
|
||||||
|
file := m.Res.Files[0]
|
||||||
|
fileName := file.Name
|
||||||
|
if m.Opts.Name != "" {
|
||||||
|
fileName = m.Opts.Name
|
||||||
|
}
|
||||||
|
return path.Join(m.Opts.Path, file.Path, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetcherBuilder defines the interface for a fetcher builder.
|
// FetcherBuilder defines the interface for a fetcher builder.
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
zerolog.Logger
|
||||||
|
logFile *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) CLose() {
|
||||||
|
l.logFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger create a new logger
|
||||||
|
func NewLogger(logFile bool, logPath string) *Logger {
|
||||||
|
var out io.Writer
|
||||||
|
if logFile {
|
||||||
|
// log to file
|
||||||
|
logDir := filepath.Dir(logPath)
|
||||||
|
if err := util.CreateDirIfNotExist(logDir); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
logfile *os.File
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
logfile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
out = logfile
|
||||||
|
} else {
|
||||||
|
out = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := &Logger{}
|
||||||
|
if logFile {
|
||||||
|
logger.logFile = out.(*os.File)
|
||||||
|
}
|
||||||
|
logger.Logger = zerolog.New(zerolog.ConsoleWriter{
|
||||||
|
NoColor: true,
|
||||||
|
Out: out,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
|
}).With().Timestamp().Logger()
|
||||||
|
return logger
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewLogger(t *testing.T) {
|
||||||
|
logger := NewLogger(false, "")
|
||||||
|
logger.Info().Msg("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewLoggerFile(t *testing.T) {
|
||||||
|
logPath := "./testdata/test.log"
|
||||||
|
logger := NewLogger(true, logPath)
|
||||||
|
defer func() {
|
||||||
|
logger.CLose()
|
||||||
|
os.Remove(logPath)
|
||||||
|
}()
|
||||||
|
logger.Info().Msg("test")
|
||||||
|
|
||||||
|
// read log file, if not exist or empty, test fail.
|
||||||
|
file, err := os.Open(logPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stat.Size() == 0 {
|
||||||
|
t.Fatal("log file is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
-75
@@ -1,21 +1,26 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/anacrolix/torrent"
|
"github.com/anacrolix/torrent"
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
"github.com/anacrolix/torrent/storage"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var client *torrent.Client
|
var (
|
||||||
|
client *torrent.Client
|
||||||
|
lock sync.Mutex
|
||||||
|
torrentDirMap = make(map[string]string)
|
||||||
|
ftMap = make(map[string]*fileTorrentImpl)
|
||||||
|
)
|
||||||
|
|
||||||
type Fetcher struct {
|
type Fetcher struct {
|
||||||
ctl *controller.Controller
|
ctl *controller.Controller
|
||||||
@@ -23,100 +28,78 @@ type Fetcher struct {
|
|||||||
torrent *torrent.Torrent
|
torrent *torrent.Torrent
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
|
|
||||||
ready atomic.Bool
|
torrentReady atomic.Bool
|
||||||
|
create atomic.Bool
|
||||||
progress fetcher.Progress
|
progress fetcher.Progress
|
||||||
|
|
||||||
torrentPaths map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Name() string {
|
func (f *Fetcher) Name() string {
|
||||||
return "bt"
|
return "bt"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||||
f.ctl = ctl
|
f.ctl = ctl
|
||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
var once sync.Once
|
|
||||||
once.Do(func() {
|
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f.torrentPaths = make(map[string]string)
|
func (f *Fetcher) initClient() (err error) {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
if client != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.ListenPort = 0
|
||||||
|
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||||
|
ClientBaseDir: cfg.DataDir,
|
||||||
|
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||||
|
if dir, ok := torrentDirMap[infoHash.String()]; ok {
|
||||||
|
ft.setTorrentDir(dir)
|
||||||
|
}
|
||||||
|
ftMap[infoHash.String()] = ft
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
client, err = torrent.NewClient(cfg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
if err := f.addTorrent(req.URL); err != nil {
|
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.torrent.Drop()
|
|
||||||
res := &base.Resource{
|
if err := f.addTorrent(req); err != nil {
|
||||||
Name: f.torrent.Name(),
|
return err
|
||||||
Range: true,
|
|
||||||
RootDir: f.torrent.Name(),
|
|
||||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
|
||||||
Hash: f.torrent.InfoHash().String(),
|
|
||||||
}
|
|
||||||
for i, file := range f.torrent.Files() {
|
|
||||||
res.Files[i] = &base.FileInfo{
|
|
||||||
Name: filepath.Base(file.DisplayPath()),
|
|
||||||
Path: util.Dir(file.Path()),
|
|
||||||
Size: file.Length(),
|
|
||||||
}
|
|
||||||
res.Size += file.Length()
|
|
||||||
}
|
}
|
||||||
|
f.updateRes()
|
||||||
f.meta.Req = req
|
f.meta.Req = req
|
||||||
f.meta.Res = res
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||||
|
f.create.Store(true)
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
if len(opts.SelectFiles) == 0 {
|
if f.meta.Res != nil {
|
||||||
opts.SelectFiles = make([]int, 0)
|
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
||||||
for i := range f.torrent.Files() {
|
|
||||||
opts.SelectFiles = append(opts.SelectFiles, i)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if opts.Path != "" {
|
|
||||||
f.torrentPaths[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||||
f.ready.Store(false)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Start() (err error) {
|
func (f *Fetcher) Start() (err error) {
|
||||||
if !f.ready.Load() {
|
if !f.torrentReady.Load() {
|
||||||
if err = f.addTorrent(f.meta.Req.URL); err != nil {
|
if err = f.addTorrent(f.meta.Req); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
||||||
|
ft.setTorrentDir(f.meta.FolderPath())
|
||||||
|
}
|
||||||
files := f.torrent.Files()
|
files := f.torrent.Files()
|
||||||
if len(f.meta.Opts.SelectFiles) == len(files) {
|
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||||
f.torrent.DownloadAll()
|
f.torrent.DownloadAll()
|
||||||
@@ -130,23 +113,28 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Pause() (err error) {
|
func (f *Fetcher) Pause() (err error) {
|
||||||
f.ready.Store(false)
|
f.torrentReady.Store(false)
|
||||||
f.torrent.Drop()
|
f.safeDrop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Continue() (err error) {
|
func (f *Fetcher) Close() (err error) {
|
||||||
return f.Start()
|
f.safeDrop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Close() (err error) {
|
func (f *Fetcher) safeDrop() {
|
||||||
|
defer func() {
|
||||||
|
// ignore panic
|
||||||
|
_ = recover()
|
||||||
|
}()
|
||||||
|
|
||||||
f.torrent.Drop()
|
f.torrent.Drop()
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Wait() (err error) {
|
func (f *Fetcher) Wait() (err error) {
|
||||||
for {
|
for {
|
||||||
if f.ready.Load() {
|
if f.torrentReady.Load() {
|
||||||
done := true
|
done := true
|
||||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
file := f.torrent.Files()[selectIndex]
|
file := f.torrent.Files()[selectIndex]
|
||||||
@@ -182,7 +170,7 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
if !f.ready.Load() {
|
if !f.torrentReady.Load() {
|
||||||
return f.progress
|
return f.progress
|
||||||
}
|
}
|
||||||
for i := range f.progress {
|
for i := range f.progress {
|
||||||
@@ -193,12 +181,45 @@ func (f *Fetcher) Progress() fetcher.Progress {
|
|||||||
return f.progress
|
return f.progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(url string) (err error) {
|
func (f *Fetcher) updateRes() {
|
||||||
schema := util.ParseSchema(url)
|
res := &base.Resource{
|
||||||
|
Name: f.torrent.Name(),
|
||||||
|
Range: true,
|
||||||
|
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||||
|
Hash: f.torrent.InfoHash().String(),
|
||||||
|
}
|
||||||
|
for i, file := range f.torrent.Files() {
|
||||||
|
res.Files[i] = &base.FileInfo{
|
||||||
|
Name: filepath.Base(file.DisplayPath()),
|
||||||
|
Path: util.Dir(file.Path()),
|
||||||
|
Size: file.Length(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.CalcSize()
|
||||||
|
f.meta.Res = res
|
||||||
|
if f.meta.Opts != nil {
|
||||||
|
f.meta.Opts.InitSelectFiles(len(res.Files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||||
|
if err = f.initClient(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schema := util.ParseSchema(req.URL)
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(url)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
|
} else if schema == "APPLICATION/X-BITTORRENT" {
|
||||||
|
_, data := util.ParseDataUri(req.URL)
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
var metaInfo *metainfo.MetaInfo
|
||||||
|
metaInfo, err = metainfo.Load(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.torrent, err = client.AddTorrent(metaInfo)
|
||||||
} else {
|
} else {
|
||||||
f.torrent, err = client.AddTorrentFromFile(url)
|
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -208,22 +229,38 @@ func (f *Fetcher) addTorrent(url string) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use map to deduplicate
|
||||||
|
trackers := make(map[string]bool)
|
||||||
|
if req.Extra != nil {
|
||||||
|
extra := req.Extra.(*bt.ReqExtra)
|
||||||
|
if len(extra.Trackers) > 0 {
|
||||||
|
for _, tracker := range extra.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if exist && len(cfg.Trackers) > 0 {
|
if exist && len(cfg.Trackers) > 0 {
|
||||||
announceList := make([][]string, 0)
|
|
||||||
for _, tracker := range cfg.Trackers {
|
for _, tracker := range cfg.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(trackers) > 0 {
|
||||||
|
announceList := make([][]string, 0)
|
||||||
|
for tracker := range trackers {
|
||||||
announceList = append(announceList, []string{tracker})
|
announceList = append(announceList, []string{tracker})
|
||||||
}
|
}
|
||||||
f.torrent.AddTrackers(announceList)
|
f.torrent.AddTrackers(announceList)
|
||||||
}
|
}
|
||||||
<-f.torrent.GotInfo()
|
<-f.torrent.GotInfo()
|
||||||
f.ready.Store(true)
|
f.torrentReady.Store(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetcherBuilder struct {
|
type FetcherBuilder struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var schemes = []string{"FILE", "MAGNET"}
|
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Schemes() []string {
|
func (fb *FetcherBuilder) Schemes() []string {
|
||||||
return schemes
|
return schemes
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -14,13 +17,51 @@ func TestFetcher_Resolve_Torrent(t *testing.T) {
|
|||||||
doResolve(t, buildFetcher())
|
doResolve(t, buildFetcher())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_Config(t *testing.T) {
|
func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
||||||
doResolve(t, buildConfigFetcher())
|
fetcher := buildFetcher()
|
||||||
|
buf, err := os.ReadFile("./testdata/ubuntu-22.04-live-server-amd64.iso.torrent")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
// convert to data uri
|
||||||
|
dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf)
|
||||||
|
err = fetcher.Resolve(&base.Request{
|
||||||
|
URL: dataUri,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &base.Resource{
|
||||||
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
Size: 1466714112,
|
||||||
|
Range: true,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
Size: 1466714112,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_Config(t *testing.T) {
|
||||||
|
doResolve(t, buildConfigFetcher())
|
||||||
|
}
|
||||||
|
|
||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
err := fetcher.Resolve(&base.Request{
|
||||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||||
|
Extra: bt.ReqExtra{
|
||||||
|
Trackers: []string{
|
||||||
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -30,7 +71,6 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
Size: 1466714112,
|
Size: 1466714112,
|
||||||
Range: true,
|
Range: true,
|
||||||
RootDir: "ubuntu-22.04-live-server-amd64.iso",
|
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
{
|
{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/common"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/segments"
|
||||||
|
)
|
||||||
|
|
||||||
|
// File-based storage for torrents, that isn't yet bound to a particular torrent.
|
||||||
|
type fileClientImpl struct {
|
||||||
|
opts newFileClientOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
type newFileClientOpts struct {
|
||||||
|
// The base directory for all downloads.
|
||||||
|
ClientBaseDir string
|
||||||
|
HandleFileTorrent func(infoHash metainfo.Hash, ft *fileTorrentImpl)
|
||||||
|
FilePathMaker storage.FilePathMaker
|
||||||
|
PieceCompletion storage.PieceCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
|
||||||
|
if opts.FilePathMaker == nil {
|
||||||
|
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
||||||
|
var parts []string
|
||||||
|
if opts.Info.Name != metainfo.NoName {
|
||||||
|
parts = append(parts, opts.Info.Name)
|
||||||
|
}
|
||||||
|
return filepath.Join(append(parts, opts.File.Path...)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.PieceCompletion == nil {
|
||||||
|
ret, err := storage.NewDefaultPieceCompletionForDir(opts.ClientBaseDir)
|
||||||
|
if err != nil {
|
||||||
|
ret = storage.NewMapPieceCompletion()
|
||||||
|
}
|
||||||
|
opts.PieceCompletion = ret
|
||||||
|
}
|
||||||
|
return fileClientImpl{opts}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs fileClientImpl) Close() error {
|
||||||
|
return fs.opts.PieceCompletion.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
||||||
|
upvertedFiles := info.UpvertedFiles()
|
||||||
|
files := make([]file, 0, len(upvertedFiles))
|
||||||
|
for _, fileInfo := range upvertedFiles {
|
||||||
|
filePath := filepath.Join("", fs.opts.FilePathMaker(storage.FilePathMakerOpts{
|
||||||
|
Info: info,
|
||||||
|
File: &fileInfo,
|
||||||
|
}))
|
||||||
|
f := file{
|
||||||
|
rawPath: filePath,
|
||||||
|
path: filePath,
|
||||||
|
length: fileInfo.Length,
|
||||||
|
}
|
||||||
|
if f.length == 0 {
|
||||||
|
err = CreateNativeZeroLengthFile(f.path)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("creating zero length file: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
t := &fileTorrentImpl{
|
||||||
|
files,
|
||||||
|
segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
|
||||||
|
infoHash,
|
||||||
|
fs.opts.PieceCompletion,
|
||||||
|
}
|
||||||
|
fs.opts.HandleFileTorrent(infoHash, t)
|
||||||
|
return storage.TorrentImpl{
|
||||||
|
Piece: t.Piece,
|
||||||
|
Close: t.Close,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type file struct {
|
||||||
|
// The safe, OS-local file path.
|
||||||
|
rawPath string
|
||||||
|
path string
|
||||||
|
length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileTorrentImpl struct {
|
||||||
|
files []file
|
||||||
|
segmentLocater segments.Index
|
||||||
|
infoHash metainfo.Hash
|
||||||
|
completion storage.PieceCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
|
||||||
|
// Create a view onto the file-based torrent storage.
|
||||||
|
_io := fileTorrentImplIO{fts}
|
||||||
|
// Return the appropriate segments of this.
|
||||||
|
return &filePieceImpl{
|
||||||
|
fts,
|
||||||
|
p,
|
||||||
|
missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
|
||||||
|
io.NewSectionReader(_io, p.Offset(), p.Length()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) setTorrentDir(dir string) {
|
||||||
|
for i, f := range fts.files {
|
||||||
|
fts.files[i].path = filepath.Join(dir, f.rawPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper to create zero-length files which won't appear for file-orientated storage since no
|
||||||
|
// writes will ever occur to them (no torrent data is associated with a zero-length file). The
|
||||||
|
// caller should make sure the file name provided is safe/sanitized.
|
||||||
|
func CreateNativeZeroLengthFile(name string) error {
|
||||||
|
os.MkdirAll(filepath.Dir(name), 0o777)
|
||||||
|
var f io.Closer
|
||||||
|
f, err := os.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exposes file-based storage of a torrent, as one big ReadWriterAt.
|
||||||
|
type fileTorrentImplIO struct {
|
||||||
|
fts *fileTorrentImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns EOF on short or missing file.
|
||||||
|
func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
|
||||||
|
f, err := os.Open(file.path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// File missing is treated the same as a short file.
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
// Limit the read to within the expected bounds of this file.
|
||||||
|
if int64(len(b)) > file.length-off {
|
||||||
|
b = b[:file.length-off]
|
||||||
|
}
|
||||||
|
for off < file.length && len(b) != 0 {
|
||||||
|
n1, err1 := f.ReadAt(b, off)
|
||||||
|
b = b[n1:]
|
||||||
|
n += n1
|
||||||
|
off += int64(n1)
|
||||||
|
if n1 == 0 {
|
||||||
|
err = err1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
|
||||||
|
func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
|
||||||
|
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
|
||||||
|
n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
|
||||||
|
n += n1
|
||||||
|
b = b[n1:]
|
||||||
|
err = err1
|
||||||
|
return err == nil // && int64(n1) == e.Length
|
||||||
|
})
|
||||||
|
if len(b) != 0 && err == nil {
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
|
||||||
|
// log.Printf("write at %v: %v bytes", off, len(p))
|
||||||
|
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
|
||||||
|
name := fst.fts.files[i].path
|
||||||
|
os.MkdirAll(filepath.Dir(name), 0o777)
|
||||||
|
var f *os.File
|
||||||
|
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var n1 int
|
||||||
|
n1, err = f.WriteAt(p[:e.Length], e.Start)
|
||||||
|
// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
|
||||||
|
closeErr := f.Close()
|
||||||
|
n += n1
|
||||||
|
p = p[n1:]
|
||||||
|
if err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
if err == nil && int64(n1) != e.Length {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file_misc.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import "github.com/anacrolix/torrent/metainfo"
|
||||||
|
|
||||||
|
type requiredLength struct {
|
||||||
|
fileIndex int
|
||||||
|
length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, fi := range info.UpvertedFiles() {
|
||||||
|
if off >= fi.Length {
|
||||||
|
off -= fi.Length
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n1 := n
|
||||||
|
if off+n1 > fi.Length {
|
||||||
|
n1 = fi.Length - off
|
||||||
|
}
|
||||||
|
ret = append(ret, requiredLength{
|
||||||
|
fileIndex: i,
|
||||||
|
length: off + n1,
|
||||||
|
})
|
||||||
|
n -= n1
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
off = 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file_piece.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type filePieceImpl struct {
|
||||||
|
*fileTorrentImpl
|
||||||
|
p metainfo.Piece
|
||||||
|
io.WriterAt
|
||||||
|
io.ReaderAt
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.PieceImpl = (*filePieceImpl)(nil)
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) pieceKey() metainfo.PieceKey {
|
||||||
|
return metainfo.PieceKey{InfoHash: fs.infoHash, Index: fs.p.Index()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) Completion() storage.Completion {
|
||||||
|
c, err := fs.completion.Get(fs.pieceKey())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting piece completion: %s", err)
|
||||||
|
c.Ok = false
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
verified := true
|
||||||
|
if c.Complete {
|
||||||
|
// If it's allegedly complete, check that its constituent files have the necessary length.
|
||||||
|
for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
|
||||||
|
s, err := os.Stat(fs.files[fi.fileIndex].path)
|
||||||
|
if err != nil || s.Size() < fi.length {
|
||||||
|
verified = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
// The completion was wrong, fix it.
|
||||||
|
c.Complete = false
|
||||||
|
fs.completion.Set(fs.pieceKey(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) MarkComplete() error {
|
||||||
|
return fs.completion.Set(fs.pieceKey(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) MarkNotComplete() error {
|
||||||
|
return fs.completion.Set(fs.pieceKey(), false)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package http
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
@@ -34,15 +35,15 @@ func (re *RequestError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type chunk struct {
|
type chunk struct {
|
||||||
Status base.Status
|
|
||||||
Begin int64
|
Begin int64
|
||||||
End int64
|
End int64
|
||||||
Downloaded int64
|
Downloaded int64
|
||||||
|
|
||||||
|
retryTimes int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newChunk(begin int64, end int64) *chunk {
|
func newChunk(begin int64, end int64) *chunk {
|
||||||
return &chunk{
|
return &chunk{
|
||||||
Status: base.DownloadStatusReady,
|
|
||||||
Begin: begin,
|
Begin: begin,
|
||||||
End: end,
|
End: end,
|
||||||
}
|
}
|
||||||
@@ -53,23 +54,20 @@ type Fetcher struct {
|
|||||||
doneCh chan error
|
doneCh chan error
|
||||||
|
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
status base.Status
|
|
||||||
chunks []*chunk
|
chunks []*chunk
|
||||||
|
|
||||||
file *os.File
|
file *os.File
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
pauseCh chan interface{}
|
eg *errgroup.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Name() string {
|
func (f *Fetcher) Name() string {
|
||||||
return "http"
|
return "http"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||||
f.ctl = ctl
|
f.ctl = ctl
|
||||||
f.doneCh = make(chan error, 1)
|
f.doneCh = make(chan error, 1)
|
||||||
f.pauseCh = make(chan interface{}, 1)
|
|
||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
@@ -85,13 +83,14 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
client := buildClient()
|
client := buildClient()
|
||||||
// 只访问一个字节,测试资源是否支持Range请求
|
// send Range request to check whether the server supports breakpoint continuation
|
||||||
|
// just test one byte, Range: bytes=0-0
|
||||||
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
|
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
|
||||||
httpResp, err := client.Do(httpReq)
|
httpResp, err := client.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 拿到响应头就关闭,不用加defer
|
// close response body immediately
|
||||||
httpResp.Body.Close()
|
httpResp.Body.Close()
|
||||||
res := &base.Resource{
|
res := &base.Resource{
|
||||||
Range: false,
|
Range: false,
|
||||||
@@ -128,7 +127,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
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["filePath"]
|
filename := params["filename"]
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
file.Name = filename
|
file.Name = filename
|
||||||
}
|
}
|
||||||
@@ -139,10 +138,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
}
|
}
|
||||||
// unknown file filePath
|
// unknown file filePath
|
||||||
if file.Name == "" || file.Name == "/" || file.Name == "." {
|
if file.Name == "" || file.Name == "/" || file.Name == "." {
|
||||||
file.Name = "unknown"
|
file.Name = httpReq.URL.Hostname()
|
||||||
}
|
}
|
||||||
res.Files = append(res.Files, file)
|
res.Files = append(res.Files, file)
|
||||||
res.Name = file.Name
|
|
||||||
f.meta.Req = req
|
f.meta.Req = req
|
||||||
f.meta.Res = res
|
f.meta.Res = res
|
||||||
return nil
|
return nil
|
||||||
@@ -150,7 +148,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
|
|
||||||
func (f *Fetcher) Create(opts *base.Options) error {
|
func (f *Fetcher) Create(opts *base.Options) error {
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
f.status = base.DownloadStatusReady
|
|
||||||
|
|
||||||
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -178,44 +175,38 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Start() (err error) {
|
func (f *Fetcher) Start() (err error) {
|
||||||
// 创建文件
|
name := f.meta.SingleFilepath()
|
||||||
name := f.filepath()
|
// if file not exist, create it, else open it
|
||||||
|
_, err = os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
f.file, err = f.ctl.Touch(name, f.meta.Res.Size)
|
f.file, err = f.ctl.Touch(name, f.meta.Res.Size)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.file, err = os.OpenFile(name, os.O_RDWR, os.ModeAppend)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.status = base.DownloadStatusRunning
|
if f.chunks == nil {
|
||||||
f.chunks = f.splitChunk()
|
f.chunks = f.splitChunk()
|
||||||
|
}
|
||||||
f.fetch()
|
f.fetch()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Pause() (err error) {
|
func (f *Fetcher) Pause() (err error) {
|
||||||
if base.DownloadStatusRunning != f.status {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f.status = base.DownloadStatusPause
|
|
||||||
|
|
||||||
if f.cancel != nil {
|
if f.cancel != nil {
|
||||||
f.cancel()
|
f.cancel()
|
||||||
<-f.pauseCh
|
// wait for pause handle complete
|
||||||
|
f.eg.Wait()
|
||||||
|
f.file.Close()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Continue() (err error) {
|
|
||||||
if base.DownloadStatusRunning == f.status || base.DownloadStatusDone == f.status {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f.status = base.DownloadStatusRunning
|
|
||||||
f.file, err = os.OpenFile(f.filepath(), os.O_RDWR, os.ModeAppend)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.fetch()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Close() (err error) {
|
func (f *Fetcher) Close() (err error) {
|
||||||
if err = f.Pause(); err != nil {
|
if err = f.Pause(); err != nil {
|
||||||
return
|
return
|
||||||
@@ -243,61 +234,67 @@ func (f *Fetcher) Wait() (err error) {
|
|||||||
return <-f.doneCh
|
return <-f.doneCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) filepath() string {
|
|
||||||
return f.meta.Filepath(f.meta.Res.Files[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) fetch() {
|
func (f *Fetcher) fetch() {
|
||||||
f.ctx, f.cancel = context.WithCancel(context.Background())
|
var ctx context.Context
|
||||||
eg, _ := errgroup.WithContext(f.ctx)
|
ctx, f.cancel = context.WithCancel(context.Background())
|
||||||
|
f.eg, _ = errgroup.WithContext(ctx)
|
||||||
for i := 0; i < len(f.chunks); i++ {
|
for i := 0; i < len(f.chunks); i++ {
|
||||||
i := i
|
i := i
|
||||||
eg.Go(func() error {
|
f.eg.Go(func() error {
|
||||||
return f.fetchChunk(i)
|
return f.fetchChunk(i, ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := eg.Wait()
|
err := f.eg.Wait()
|
||||||
// 下载停止,关闭文件句柄
|
// check if canceled
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
f.file.Close()
|
f.file.Close()
|
||||||
if f.status == base.DownloadStatusPause {
|
|
||||||
f.pauseCh <- nil
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
f.status = base.DownloadStatusError
|
|
||||||
} else {
|
|
||||||
f.status = base.DownloadStatusDone
|
|
||||||
}
|
|
||||||
f.doneCh <- err
|
f.doneCh <- err
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) fetchChunk(index int) (err error) {
|
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||||
chunk := f.chunks[index]
|
chunk := f.chunks[index]
|
||||||
|
chunk.retryTimes = 0
|
||||||
|
|
||||||
httpReq, err := buildRequest(f.ctx, f.meta.Req)
|
httpReq, err := buildRequest(ctx, f.meta.Req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
client = buildClient()
|
client = buildClient()
|
||||||
buf = make([]byte, 8192)
|
buf = make([]byte, 8192)
|
||||||
|
maxRetries = 3
|
||||||
)
|
)
|
||||||
// 重试5次
|
// retry until all remain chunks failed
|
||||||
for i := 0; i < 5; i++ {
|
for true {
|
||||||
// 如果下载完成直接返回
|
// if chunk is completed, return
|
||||||
if chunk.Status == base.DownloadStatusDone {
|
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 如果已暂停直接跳出
|
|
||||||
if f.status == base.DownloadStatusPause {
|
if chunk.retryTimes >= maxRetries {
|
||||||
|
if !f.meta.Res.Range {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// check if all failed
|
||||||
|
allFailed := true
|
||||||
|
for _, c := range f.chunks {
|
||||||
|
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
|
||||||
|
allFailed = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if allFailed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
resp *http.Response
|
resp *http.Response
|
||||||
retry bool
|
|
||||||
)
|
)
|
||||||
if f.meta.Res.Range {
|
if f.meta.Res.Range {
|
||||||
httpReq.Header.Set(base.HttpHeaderRange,
|
httpReq.Header.Set(base.HttpHeaderRange,
|
||||||
@@ -317,49 +314,42 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 请求失败重试
|
// retry request after 1 second
|
||||||
|
chunk.retryTimes = chunk.retryTimes + 1
|
||||||
|
time.Sleep(time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// 请求成功就重置错误次数,连续失败5次才终止
|
// Http request success, reset retry times
|
||||||
i = 0
|
chunk.retryTimes = 0
|
||||||
retry, err = func() (bool, error) {
|
err = func() error {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := resp.Body.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
chunk.Downloaded += int64(n)
|
chunk.Downloaded += int64(n)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err == io.EOF {
|
||||||
return true, err
|
return nil
|
||||||
}
|
}
|
||||||
break
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
|
||||||
}()
|
}()
|
||||||
if !retry {
|
// if chunk is completed, reset other not complete chunks retry times
|
||||||
// 下载成功,跳出重试
|
if err == nil {
|
||||||
|
for _, c := range f.chunks {
|
||||||
|
if c != chunk && c.retryTimes > 0 {
|
||||||
|
c.retryTimes = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
chunk.Status = base.DownloadStatusError
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isComplete := f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
|
|
||||||
if f.status == base.DownloadStatusPause && !isComplete {
|
|
||||||
chunk.Status = base.DownloadStatusPause
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk.Status = base.DownloadStatusDone
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +386,6 @@ func buildClient() *http.Client {
|
|||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Jar: jar,
|
Jar: jar,
|
||||||
Timeout: time.Second * 60,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +418,9 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
|
|||||||
body = bytes.NewBufferString(extra.Body)
|
body = bytes.NewBufferString(extra.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||||
|
headers[base.HttpHeaderUserAgent] = []string{base.AgentName}
|
||||||
|
}
|
||||||
|
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
|
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
|
||||||
@@ -472,7 +464,6 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
fb := &FetcherBuilder{}
|
fb := &FetcherBuilder{}
|
||||||
fetcher := fb.Build().(*Fetcher)
|
fetcher := fb.Build().(*Fetcher)
|
||||||
fetcher.meta = meta
|
fetcher.meta = meta
|
||||||
fetcher.status = base.DownloadStatusPause
|
|
||||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||||
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||||
if len(fd.Chunks) == 0 {
|
if len(fd.Chunks) == 0 {
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFetcher_Resolve(t *testing.T) {
|
func TestFetcher_Resolve(t *testing.T) {
|
||||||
testResolve(test.StartTestFileServer, &base.Resource{
|
testResolve(test.StartTestFileServer, test.BuildName, &base.Resource{
|
||||||
Name: test.BuildName,
|
|
||||||
Size: test.BuildSize,
|
Size: test.BuildSize,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -26,8 +25,17 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
testResolve(test.StartTestChunkedServer, &base.Resource{
|
testResolve(test.StartTestCustomServer, "disposition", &base.Resource{
|
||||||
|
Size: test.BuildSize,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
Name: test.BuildName,
|
Name: test.BuildName,
|
||||||
|
Size: test.BuildSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Range: false,
|
Range: false,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -37,14 +45,25 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
|
|
||||||
|
fetcher := buildFetcher()
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "http://github.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if fetcher.Meta().Res.Files[0].Name != "github.com" {
|
||||||
|
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, "github.com")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testResolve(startTestServer func() net.Listener, want *base.Resource, t *testing.T) {
|
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
||||||
listener := startTestServer()
|
listener := startTestServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
fetcher := buildFetcher()
|
fetcher := buildFetcher()
|
||||||
err := fetcher.Resolve(&base.Request{
|
err := fetcher.Resolve(&base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + path,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -57,7 +76,7 @@ func testResolve(startTestServer func() net.Listener, want *base.Resource, t *te
|
|||||||
func TestFetcher_DownloadNormal(t *testing.T) {
|
func TestFetcher_DownloadNormal(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// 正常下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
downloadNormal(listener, 5, t)
|
downloadNormal(listener, 5, t)
|
||||||
downloadNormal(listener, 8, t)
|
downloadNormal(listener, 8, t)
|
||||||
@@ -67,7 +86,7 @@ func TestFetcher_DownloadNormal(t *testing.T) {
|
|||||||
func TestFetcher_DownloadContinue(t *testing.T) {
|
func TestFetcher_DownloadContinue(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// 暂停继续
|
|
||||||
downloadContinue(listener, 1, t)
|
downloadContinue(listener, 1, t)
|
||||||
downloadContinue(listener, 5, t)
|
downloadContinue(listener, 5, t)
|
||||||
downloadContinue(listener, 8, t)
|
downloadContinue(listener, 8, t)
|
||||||
@@ -75,9 +94,9 @@ func TestFetcher_DownloadContinue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadChunked(t *testing.T) {
|
func TestFetcher_DownloadChunked(t *testing.T) {
|
||||||
listener := test.StartTestChunkedServer()
|
listener := test.StartTestCustomServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
downloadNormal(listener, 2, t)
|
downloadNormal(listener, 2, t)
|
||||||
}
|
}
|
||||||
@@ -85,24 +104,33 @@ func TestFetcher_DownloadChunked(t *testing.T) {
|
|||||||
func TestFetcher_DownloadPost(t *testing.T) {
|
func TestFetcher_DownloadPost(t *testing.T) {
|
||||||
listener := test.StartTestPostServer()
|
listener := test.StartTestPostServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// post下载
|
|
||||||
downloadPost(listener, 1, t)
|
downloadPost(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadRetry(t *testing.T) {
|
func TestFetcher_DownloadRetry(t *testing.T) {
|
||||||
listener := test.StartTestRetryServer()
|
listener := test.StartTestRetryServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadError(t *testing.T) {
|
func TestFetcher_DownloadError(t *testing.T) {
|
||||||
listener := test.StartTestErrorServer()
|
listener := test.StartTestErrorServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadError(listener, 1, t)
|
downloadError(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadLimit(t *testing.T) {
|
||||||
|
listener := test.StartTestLimitServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadNormal(listener, 1, t)
|
||||||
|
downloadNormal(listener, 2, t)
|
||||||
|
downloadNormal(listener, 8, t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadResume(t *testing.T) {
|
func TestFetcher_DownloadResume(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -114,7 +142,9 @@ func TestFetcher_DownloadResume(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_Config(t *testing.T) {
|
func TestFetcher_Config(t *testing.T) {
|
||||||
fetcher := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t)
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
fetcher := doDownloadReady(buildConfigFetcher(), listener, 0, t)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -211,7 +241,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 50)
|
time.Sleep(time.Millisecond * 50)
|
||||||
if err := fetcher.Continue(); err != nil {
|
if err := fetcher.Start(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = fetcher.Wait()
|
err = fetcher.Wait()
|
||||||
@@ -259,7 +289,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
fetcher.Setup(controller.NewController())
|
fetcher.Setup(controller.NewController())
|
||||||
fetcher.Continue()
|
fetcher.Start()
|
||||||
|
|
||||||
err = fetcher.Wait()
|
err = fetcher.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+104
-12
@@ -1,6 +1,7 @@
|
|||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -8,6 +9,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +22,9 @@ const (
|
|||||||
BuildFile = Dir + BuildName
|
BuildFile = Dir + BuildName
|
||||||
|
|
||||||
DownloadName = "download.data"
|
DownloadName = "download.data"
|
||||||
|
DownloadRename = "download (1).data"
|
||||||
DownloadFile = Dir + DownloadName
|
DownloadFile = Dir + DownloadName
|
||||||
|
DownloadRenameFile = Dir + DownloadRename
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartTestFileServer() net.Listener {
|
func StartTestFileServer() net.Listener {
|
||||||
@@ -46,7 +52,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartTestChunkedServer() net.Listener {
|
func StartTestCustomServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
@@ -57,6 +63,17 @@ func StartTestChunkedServer() net.Listener {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
io.Copy(writer, file)
|
io.Copy(writer, file)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/disposition", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Disposition", "attachment; filename=\""+BuildName+"\"")
|
||||||
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -67,7 +84,7 @@ func StartTestRetryServer() net.Listener {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
counter++
|
counter++
|
||||||
if counter != 1 && counter < 5 {
|
if counter != 1 && counter < 2 {
|
||||||
writer.WriteHeader(500)
|
writer.WriteHeader(500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -120,6 +137,79 @@ func StartTestErrorServer() net.Listener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartTestLimitServer connections limit server
|
||||||
|
func StartTestLimitServer() net.Listener {
|
||||||
|
var connections atomic.Int32
|
||||||
|
|
||||||
|
return startTestServer(func() http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
connections.Add(-1)
|
||||||
|
}()
|
||||||
|
connections.Add(1)
|
||||||
|
if connections.Load() > 4 {
|
||||||
|
writer.WriteHeader(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r := request.Header.Get("Range")
|
||||||
|
if r == "" {
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
writer.WriteHeader(200)
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
} else {
|
||||||
|
// split range
|
||||||
|
s := strings.Split(r, "=")
|
||||||
|
if len(s) != 2 {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = strings.Split(s[1], "-")
|
||||||
|
if len(s) != 2 {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start, err := strconv.ParseInt(s[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseInt(s[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if start < 0 || end < 0 || start > end {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end >= BuildSize {
|
||||||
|
end = BuildSize - 1
|
||||||
|
}
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
|
||||||
|
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
||||||
|
writer.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
writer.WriteHeader(206)
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
file.Seek(start, 0)
|
||||||
|
io.CopyN(writer, file, end-start+1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return mux
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -129,6 +219,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
// 随机生成一个文件
|
// 随机生成一个文件
|
||||||
l := int64(8192)
|
l := int64(8192)
|
||||||
buf := make([]byte, l)
|
buf := make([]byte, l)
|
||||||
@@ -149,28 +240,29 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
server.Handler = serverHandle()
|
server.Handler = serverHandle()
|
||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
|
|
||||||
return &delFileListener{
|
return &shutdownListener{
|
||||||
File: file,
|
server: server,
|
||||||
Listener: listener,
|
Listener: listener,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type delFileListener struct {
|
type shutdownListener struct {
|
||||||
*os.File
|
server *http.Server
|
||||||
net.Listener
|
net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *delFileListener) Close() error {
|
func (c *shutdownListener) Close() error {
|
||||||
defer func() {
|
closeErr := c.server.Shutdown(context.Background())
|
||||||
c.File.Close()
|
if err := ifExistAndRemove(BuildFile); err != nil {
|
||||||
if err := ifExistAndRemove(c.File.Name()); err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
if err := ifExistAndRemove(DownloadFile); err != nil {
|
if err := ifExistAndRemove(DownloadFile); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
}()
|
if err := ifExistAndRemove(DownloadRenameFile); err != nil {
|
||||||
return c.Listener.Close()
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return closeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifExistAndRemove(name string) error {
|
func ifExistAndRemove(name string) error {
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ const (
|
|||||||
HttpHeaderContentLength = "Content-Length"
|
HttpHeaderContentLength = "Content-Length"
|
||||||
HttpHeaderContentRange = "Content-Range"
|
HttpHeaderContentRange = "Content-Range"
|
||||||
HttpHeaderContentDisposition = "Content-Disposition"
|
HttpHeaderContentDisposition = "Content-Disposition"
|
||||||
|
HttpHeaderUserAgent = "User-Agent"
|
||||||
|
|
||||||
HttpHeaderRangeFormat = "bytes=%d-%d"
|
HttpHeaderRangeFormat = "bytes=%d-%d"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AgentName = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
+55
-1
@@ -1,6 +1,7 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -8,24 +9,58 @@ import (
|
|||||||
type Request struct {
|
type Request struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Extra any `json:"extra"`
|
Extra any `json:"extra"`
|
||||||
|
// Labels is used to mark the download task
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) Validate() error {
|
||||||
|
if r.URL == "" {
|
||||||
|
return fmt.Errorf("invalid request url")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resource download resource
|
// Resource download resource
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
|
// if name is not empty, the resource is a folder and the name is the folder name
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
// is support range download
|
// is support range download
|
||||||
Range bool `json:"range"`
|
Range bool `json:"range"`
|
||||||
RootDir string `json:"rootDir"`
|
|
||||||
// file list
|
// file list
|
||||||
Files []*FileInfo `json:"files"`
|
Files []*FileInfo `json:"files"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resource) Validate() error {
|
||||||
|
if r.Name == "" {
|
||||||
|
return fmt.Errorf("invalid resource name")
|
||||||
|
}
|
||||||
|
if r.Files == nil || len(r.Files) == 0 {
|
||||||
|
return fmt.Errorf("invalid resource files")
|
||||||
|
}
|
||||||
|
for _, file := range r.Files {
|
||||||
|
if file.Name == "" {
|
||||||
|
return fmt.Errorf("invalid resource file name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) CalcSize() {
|
||||||
|
var size int64
|
||||||
|
for _, file := range r.Files {
|
||||||
|
size += file.Size
|
||||||
|
}
|
||||||
|
r.Size = size
|
||||||
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
|
||||||
|
Req *Request `json:"req"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options for download
|
// Options for download
|
||||||
@@ -40,6 +75,25 @@ type Options struct {
|
|||||||
Extra any `json:"extra"`
|
Extra any `json:"extra"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Options) InitSelectFiles(fileSize int) {
|
||||||
|
// if selectFiles is empty, select all files
|
||||||
|
if len(o.SelectFiles) == 0 {
|
||||||
|
o.SelectFiles = make([]int, fileSize)
|
||||||
|
for i := 0; i < fileSize; i++ {
|
||||||
|
o.SelectFiles[i] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) Clone() *Options {
|
||||||
|
return &Options{
|
||||||
|
Name: o.Name,
|
||||||
|
Path: o.Path,
|
||||||
|
SelectFiles: o.SelectFiles,
|
||||||
|
Extra: o.Extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ParseReqExtra[E any](req *Request) error {
|
func ParseReqExtra[E any](req *Request) error {
|
||||||
if req.Extra == nil {
|
if req.Extra == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+257
-152
File diff suppressed because it is too large.
Load diff
@@ -27,7 +27,6 @@ func TestDownloader_Resolve(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: test.BuildName,
|
|
||||||
Size: test.BuildSize,
|
Size: test.BuildSize,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -85,6 +84,51 @@ func TestDownloader_Create(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_CreateRename(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(nil)
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer downloader.Clear()
|
||||||
|
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
_, err := downloader.CreateDirect(req, &base.Options{
|
||||||
|
Path: test.Dir,
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
want := test.FileMd5(test.BuildFile)
|
||||||
|
got := test.FileMd5(test.DownloadFile)
|
||||||
|
if want != got {
|
||||||
|
t.Errorf("Downloader_CreateRename() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
got = test.FileMd5(test.DownloadRenameFile)
|
||||||
|
if want != got {
|
||||||
|
t.Errorf("Downloader_CreateRename() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDownloader_StoreAndRestore(t *testing.T) {
|
func TestDownloader_StoreAndRestore(t *testing.T) {
|
||||||
listener := test.StartTestSlowFileServer(time.Millisecond * 2000)
|
listener := test.StartTestSlowFileServer(time.Millisecond * 2000)
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/dop251/goja_nodejs/eventloop"
|
||||||
|
"github.com/dop251/goja_nodejs/process"
|
||||||
|
"github.com/dop251/goja_nodejs/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed polyfill/out/index.js
|
||||||
|
var polyfillScript string
|
||||||
|
|
||||||
|
type Engine struct {
|
||||||
|
loop *eventloop.EventLoop
|
||||||
|
|
||||||
|
Runtime *goja.Runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunString executes the script and returns the go type value
|
||||||
|
// if script result is promise, it will be resolved
|
||||||
|
func (e *Engine) RunString(script string) (value any, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("%v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var result goja.Value
|
||||||
|
e.loop.Run(func(runtime *goja.Runtime) {
|
||||||
|
result, err = runtime.RunString(script)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return resolveResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallFunction calls the function and returns the go type value
|
||||||
|
// if function result is promise, it will be resolved
|
||||||
|
func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("%v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var result goja.Value
|
||||||
|
e.loop.Run(func(runtime *goja.Runtime) {
|
||||||
|
if args == nil {
|
||||||
|
result, err = fn(nil)
|
||||||
|
} else {
|
||||||
|
var jsArgs []goja.Value
|
||||||
|
for _, arg := range args {
|
||||||
|
jsArgs = append(jsArgs, runtime.ToValue(arg))
|
||||||
|
}
|
||||||
|
result, err = fn(nil, jsArgs...)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resolveResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) Close() {
|
||||||
|
e.loop.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEngine() *Engine {
|
||||||
|
loop := eventloop.NewEventLoop()
|
||||||
|
engine := &Engine{
|
||||||
|
loop: loop,
|
||||||
|
}
|
||||||
|
loop.Run(func(runtime *goja.Runtime) {
|
||||||
|
engine.Runtime = runtime
|
||||||
|
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||||
|
url.Enable(runtime)
|
||||||
|
process.Enable(runtime)
|
||||||
|
if err := file.Enable(runtime); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := formdata.Enable(runtime); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := xhr.Enable(runtime); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := runtime.RunString(polyfillScript); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// polyfill global
|
||||||
|
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(script string) (value any, err error) {
|
||||||
|
engine := NewEngine()
|
||||||
|
return engine.RunString(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the value is Promise, it will be resolved and return the result.
|
||||||
|
func resolveResult(value goja.Value) (any, error) {
|
||||||
|
export := value.Export()
|
||||||
|
switch export.(type) {
|
||||||
|
case *goja.Promise:
|
||||||
|
p := export.(*goja.Promise)
|
||||||
|
switch p.State() {
|
||||||
|
case goja.PromiseStatePending:
|
||||||
|
return nil, nil
|
||||||
|
case goja.PromiseStateFulfilled:
|
||||||
|
return p.Result().Export(), nil
|
||||||
|
case goja.PromiseStateRejected:
|
||||||
|
return nil, errors.New(p.Result().String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return export, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPolyfill(t *testing.T) {
|
||||||
|
doTestPolyfill(t, "XMLHttpRequest")
|
||||||
|
doTestPolyfill(t, "Blob")
|
||||||
|
doTestPolyfill(t, "FormData")
|
||||||
|
doTestPolyfill(t, "fetch")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetch(t *testing.T) {
|
||||||
|
server := startServer()
|
||||||
|
defer server.Close()
|
||||||
|
engine := NewEngine()
|
||||||
|
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", server.Addr().String())); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err := engine.RunString(`
|
||||||
|
async function testGet(){
|
||||||
|
const resp = await fetch(host+'/get');
|
||||||
|
return resp.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testText(){
|
||||||
|
const resp = await fetch(host+'/text',{
|
||||||
|
method: 'POST',
|
||||||
|
body: 'test'
|
||||||
|
});
|
||||||
|
return await resp.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testOctetStream(file){
|
||||||
|
const resp = await fetch(host+'/octetStream',{
|
||||||
|
method: 'POST',
|
||||||
|
body: file
|
||||||
|
});
|
||||||
|
return await resp.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFormData(file){
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', 'test');
|
||||||
|
formData.append('f', file);
|
||||||
|
const resp = await fetch(host+'/formData',{
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testProgress(){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', host+'/get');
|
||||||
|
const xhrUploadPromise = new Promise((resolve, reject) => {
|
||||||
|
xhr.upload.onprogress = function(e){
|
||||||
|
if(e.loaded === e.total){
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const xhrPromise = new Promise((resolve, reject) => {
|
||||||
|
xhr.onprogress = function(e){
|
||||||
|
if(e.loaded === e.total){
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Promise.all([xhrUploadPromise, xhrPromise]).then(() => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
xhr.send();
|
||||||
|
setTimeout(() => {
|
||||||
|
reject('timeout');
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testAbort(){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', host+'/timeout?duration=500');
|
||||||
|
xhr.onabort = function() {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
setTimeout(() => {
|
||||||
|
xhr.abort();
|
||||||
|
}, 200);
|
||||||
|
setTimeout(() => {
|
||||||
|
reject('timeout');
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testTimeout(){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const t = 500;
|
||||||
|
xhr.open('GET', host+'/timeout?duration='+t);
|
||||||
|
xhr.timeout = t - 200;
|
||||||
|
xhr.onload = function() {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
xhr.ontimeout = function() {
|
||||||
|
reject('timeout');
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := callTestFun(engine, "testGet")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result != int64(200) {
|
||||||
|
t.Fatalf("testGet failed, want %d, got %d", 200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = callTestFun(engine, "testText")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result != "test" {
|
||||||
|
t.Fatalf("testText failed, want %s, got %s", "test", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
jsFile, _, md5 := buildFile(t, engine.Runtime)
|
||||||
|
result, err = callTestFun(engine, "testOctetStream", jsFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result != md5 {
|
||||||
|
t.Fatalf("testOctetStream failed, want %s, got %s", md5, result)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
func() {
|
||||||
|
jsFile, goFile, md5 := buildFile(t, engine.Runtime)
|
||||||
|
result, err = callTestFun(engine, "testFormData", jsFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"f": map[string]string{
|
||||||
|
"filename": goFile.Name,
|
||||||
|
"md5": md5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !test.JsonEqual(result, want) {
|
||||||
|
t.Fatalf("testFormData failed, want %v, got %v", want, result)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = callTestFun(engine, "testProgress")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("progress test failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = callTestFun(engine, "testAbort")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("abort test failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = callTestFun(engine, "testTimeout")
|
||||||
|
if err == nil || err.Error() != "timeout" {
|
||||||
|
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTestPolyfill(t *testing.T, module string) {
|
||||||
|
value, err := Run(fmt.Sprintf(`
|
||||||
|
!!globalThis['%s']
|
||||||
|
`, module))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !value.(bool) {
|
||||||
|
t.Fatalf("module %s not polyfilled", module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer() net.Listener {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
server := &http.Server{}
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
buf, _ := io.ReadAll(r.Body)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(buf)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/octetStream", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
md5 := calcMd5(r.Body)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(md5))
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/formData", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseMultipartForm(1024 * 1024 * 30)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result := make(map[string]any)
|
||||||
|
for k, v := range r.MultipartForm.Value {
|
||||||
|
result[k] = v[0]
|
||||||
|
}
|
||||||
|
for k, v := range r.MultipartForm.File {
|
||||||
|
f, _ := v[0].Open()
|
||||||
|
result[k] = map[string]string{
|
||||||
|
"filename": v[0].Filename,
|
||||||
|
"md5": calcMd5(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
buf, _ := json.Marshal(result)
|
||||||
|
w.Write(buf)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/timeout", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
duration := r.URL.Query().Get("duration")
|
||||||
|
t, _ := strconv.Atoi(duration)
|
||||||
|
time.Sleep(time.Duration(t) * time.Millisecond)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
server.Handler = mux
|
||||||
|
go server.Serve(listener)
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFile(t *testing.T, runtime *goja.Runtime) (goja.Value, *file.File, string) {
|
||||||
|
jsFile, err := file.NewJsFile(runtime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f := jsFile.Export().(*file.File)
|
||||||
|
data := "test"
|
||||||
|
f.Reader = strings.NewReader(data)
|
||||||
|
f.Name = "test.txt"
|
||||||
|
f.Size = int64(len(data))
|
||||||
|
return jsFile, f, calcMd5(strings.NewReader(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func callTestFun(engine *Engine, fun string, args ...any) (any, error) {
|
||||||
|
test, ok := goja.AssertFunction(engine.Runtime.Get(fun))
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("function not found:" + fun)
|
||||||
|
}
|
||||||
|
return engine.CallFunction(test, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcMd5(reader io.Reader) string {
|
||||||
|
// Open a new hash interface to write to
|
||||||
|
hash := md5.New()
|
||||||
|
|
||||||
|
// Copy the file in the hash interface and check for any error
|
||||||
|
if _, err := io.Copy(hash, reader); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
io.Reader `json:""`
|
||||||
|
io.Closer `json:""`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJsFile(runtime *goja.Runtime) (goja.Value, error) {
|
||||||
|
fileCtor, ok := goja.AssertConstructor(runtime.Get("File"))
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("file is not defined")
|
||||||
|
}
|
||||||
|
return fileCtor(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Enable(runtime *goja.Runtime) error {
|
||||||
|
file := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
instance := &File{}
|
||||||
|
instanceValue := runtime.ToValue(instance).(*goja.Object)
|
||||||
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
return instanceValue
|
||||||
|
})
|
||||||
|
return runtime.Set("File", file)
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package formdata
|
||||||
|
|
||||||
|
import "github.com/dop251/goja"
|
||||||
|
|
||||||
|
type FormData struct {
|
||||||
|
data map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) Append(name string, value any) {
|
||||||
|
fd.data[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) Delete(name string) {
|
||||||
|
delete(fd.data, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) Entries() []any {
|
||||||
|
var entries []any
|
||||||
|
for k, v := range fd.data {
|
||||||
|
entries = append(entries, []any{k, v})
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) Get(name string) any {
|
||||||
|
return fd.data[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) GetAll(name string) []any {
|
||||||
|
return []any{fd.data[name]}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) Has(name string) bool {
|
||||||
|
_, ok := fd.data[name]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) Keys() []string {
|
||||||
|
var keys []string
|
||||||
|
for k := range fd.data {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) Set(name string, value any) {
|
||||||
|
fd.data[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd *FormData) Values() []any {
|
||||||
|
var values []any
|
||||||
|
for _, v := range fd.data {
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func Enable(runtime *goja.Runtime) error {
|
||||||
|
file := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
instance := &FormData{
|
||||||
|
data: make(map[string]any),
|
||||||
|
}
|
||||||
|
instanceValue := runtime.ToValue(instance).(*goja.Object)
|
||||||
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
return instanceValue
|
||||||
|
})
|
||||||
|
return runtime.Set("FormData", file)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package inject
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ThrowTypeError(vm *goja.Runtime, msg string) {
|
||||||
|
panic(vm.NewTypeError(msg))
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
package xhr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventLoad = "load"
|
||||||
|
eventReadystatechange = "readystatechange"
|
||||||
|
eventProgress = "progress"
|
||||||
|
eventAbort = "abort"
|
||||||
|
eventError = "error"
|
||||||
|
eventTimeout = "timeout"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProgressEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
LengthComputable bool `json:"lengthComputable"`
|
||||||
|
Loaded int64 `json:"loaded"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventProp struct {
|
||||||
|
eventListeners map[string]func(event *ProgressEvent)
|
||||||
|
Onload func(event *ProgressEvent) `json:"onload"`
|
||||||
|
Onprogress func(event *ProgressEvent) `json:"onprogress"`
|
||||||
|
Onabort func(event *ProgressEvent) `json:"onabort"`
|
||||||
|
Onerror func(event *ProgressEvent) `json:"onerror"`
|
||||||
|
Ontimeout func(event *ProgressEvent) `json:"ontimeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *EventProp) AddEventListener(event string, cb func(event *ProgressEvent)) {
|
||||||
|
ep.eventListeners[event] = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *EventProp) RemoveEventListener(event string) {
|
||||||
|
delete(ep.eventListeners, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *EventProp) callOnload() {
|
||||||
|
event := &ProgressEvent{
|
||||||
|
Type: eventLoad,
|
||||||
|
LengthComputable: false,
|
||||||
|
}
|
||||||
|
if ep.Onload != nil {
|
||||||
|
ep.Onload(event)
|
||||||
|
}
|
||||||
|
ep.callEventListener(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *EventProp) callOnprogress(loaded, total int64) {
|
||||||
|
event := &ProgressEvent{
|
||||||
|
Type: eventProgress,
|
||||||
|
LengthComputable: true,
|
||||||
|
Loaded: loaded,
|
||||||
|
Total: total,
|
||||||
|
}
|
||||||
|
if ep.Onprogress != nil {
|
||||||
|
ep.Onprogress(event)
|
||||||
|
}
|
||||||
|
ep.callEventListener(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *EventProp) callOnabort() {
|
||||||
|
event := &ProgressEvent{
|
||||||
|
Type: eventAbort,
|
||||||
|
LengthComputable: false,
|
||||||
|
}
|
||||||
|
if ep.Onabort != nil {
|
||||||
|
ep.Onabort(event)
|
||||||
|
}
|
||||||
|
ep.callEventListener(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *EventProp) callOnerror() {
|
||||||
|
event := &ProgressEvent{
|
||||||
|
Type: eventError,
|
||||||
|
LengthComputable: false,
|
||||||
|
}
|
||||||
|
if ep.Onerror != nil {
|
||||||
|
ep.Onerror(event)
|
||||||
|
}
|
||||||
|
ep.callEventListener(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *EventProp) callOntimeout() {
|
||||||
|
event := &ProgressEvent{
|
||||||
|
Type: eventTimeout,
|
||||||
|
LengthComputable: false,
|
||||||
|
}
|
||||||
|
if ep.Ontimeout != nil {
|
||||||
|
ep.Ontimeout(event)
|
||||||
|
}
|
||||||
|
ep.callEventListener(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *EventProp) callEventListener(event *ProgressEvent) {
|
||||||
|
if cb, ok := ep.eventListeners[event.Type]; ok {
|
||||||
|
cb(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type XMLHttpRequestUpload struct {
|
||||||
|
*EventProp
|
||||||
|
}
|
||||||
|
|
||||||
|
type XMLHttpRequest struct {
|
||||||
|
method string
|
||||||
|
url string
|
||||||
|
requestHeaders map[string]string
|
||||||
|
responseHeaders map[string]string
|
||||||
|
aborted bool
|
||||||
|
|
||||||
|
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||||
|
Timeout int `json:"timeout"`
|
||||||
|
ReadyState int `json:"readyState"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
StatusText string `json:"statusText"`
|
||||||
|
Response string `json:"response"`
|
||||||
|
ResponseText string `json:"responseText"`
|
||||||
|
*EventProp
|
||||||
|
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xhr *XMLHttpRequest) Open(method, url string) {
|
||||||
|
xhr.method = method
|
||||||
|
xhr.url = url
|
||||||
|
xhr.requestHeaders = make(map[string]string)
|
||||||
|
xhr.responseHeaders = make(map[string]string)
|
||||||
|
xhr.doReadystatechange(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
||||||
|
xhr.requestHeaders[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
d := xhr.parseData(data)
|
||||||
|
var (
|
||||||
|
contentType string
|
||||||
|
contentLength int64
|
||||||
|
)
|
||||||
|
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
||||||
|
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
||||||
|
} else {
|
||||||
|
switch d.(type) {
|
||||||
|
case string:
|
||||||
|
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(d.(string)))
|
||||||
|
contentType = "text/plain;charset=UTF-8"
|
||||||
|
contentLength = int64(len(d.(string)))
|
||||||
|
case *file.File:
|
||||||
|
req, err = http.NewRequest(xhr.method, xhr.url, d.(*file.File).Reader)
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
contentLength = d.(*file.File).Size
|
||||||
|
case *formdata.FormData:
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
mw := NewMultipart(pw)
|
||||||
|
for _, e := range d.(*formdata.FormData).Entries() {
|
||||||
|
arr := e.([]any)
|
||||||
|
k := arr[0].(string)
|
||||||
|
v := arr[1]
|
||||||
|
switch v.(type) {
|
||||||
|
case string:
|
||||||
|
mw.WriteField(k, v.(string))
|
||||||
|
case *file.File:
|
||||||
|
mw.WriteFile(k, v.(*file.File))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer pw.Close()
|
||||||
|
defer mw.Close()
|
||||||
|
mw.Send()
|
||||||
|
}()
|
||||||
|
req, err = http.NewRequest(xhr.method, xhr.url, pr)
|
||||||
|
contentType = mw.FormDataContentType()
|
||||||
|
contentLength = mw.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
xhr.callOnerror()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if contentType != "" {
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
if contentLength > 0 {
|
||||||
|
req.ContentLength = contentLength
|
||||||
|
}
|
||||||
|
for k, v := range xhr.requestHeaders {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
// handle timeout error
|
||||||
|
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||||
|
if xhr.Timeout > 0 {
|
||||||
|
xhr.Upload.callOntimeout()
|
||||||
|
xhr.callOntimeout()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xhr.Upload.callOnerror()
|
||||||
|
xhr.callOnerror()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
xhr.Upload.callOnprogress(contentLength, contentLength)
|
||||||
|
if !xhr.aborted {
|
||||||
|
xhr.Upload.callOnload()
|
||||||
|
}
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
xhr.responseHeaders[k] = v[0]
|
||||||
|
}
|
||||||
|
xhr.Status = resp.StatusCode
|
||||||
|
xhr.StatusText = resp.Status
|
||||||
|
xhr.doReadystatechange(2)
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
xhr.callOnerror()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xhr.doReadystatechange(3)
|
||||||
|
xhr.Response = string(buf)
|
||||||
|
xhr.ResponseText = xhr.Response
|
||||||
|
xhr.doReadystatechange(4)
|
||||||
|
respBodyLen := int64(len(buf))
|
||||||
|
xhr.callOnprogress(respBodyLen, respBodyLen)
|
||||||
|
if !xhr.aborted {
|
||||||
|
xhr.callOnload()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xhr *XMLHttpRequest) Abort() {
|
||||||
|
xhr.doReadystatechange(0)
|
||||||
|
xhr.aborted = true
|
||||||
|
xhr.Upload.callOnabort()
|
||||||
|
xhr.callOnabort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
||||||
|
return xhr.responseHeaders[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for k, v := range xhr.responseHeaders {
|
||||||
|
buf.WriteString(k)
|
||||||
|
buf.WriteString(": ")
|
||||||
|
buf.WriteString(v)
|
||||||
|
buf.WriteString("\r\n")
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xhr *XMLHttpRequest) callOnreadystatechange() {
|
||||||
|
event := &ProgressEvent{
|
||||||
|
Type: eventReadystatechange,
|
||||||
|
LengthComputable: false,
|
||||||
|
}
|
||||||
|
if xhr.Onreadystatechange != nil {
|
||||||
|
xhr.Onreadystatechange(event)
|
||||||
|
}
|
||||||
|
xhr.callEventListener(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xhr *XMLHttpRequest) doReadystatechange(state int) {
|
||||||
|
if xhr.aborted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xhr.ReadyState = state
|
||||||
|
xhr.callOnreadystatechange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse js data to go struct
|
||||||
|
func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
|
||||||
|
// check if data is null or undefined
|
||||||
|
if data == nil || goja.IsNull(data) || goja.IsUndefined(data) || goja.IsNaN(data) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// check if data is File
|
||||||
|
f, ok := data.Export().(*file.File)
|
||||||
|
if ok {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
// check if data is FormData
|
||||||
|
fd, ok := data.Export().(*formdata.FormData)
|
||||||
|
if ok {
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
// otherwise, return data as string
|
||||||
|
return data.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Enable(runtime *goja.Runtime) error {
|
||||||
|
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
if len(call.Arguments) < 1 {
|
||||||
|
inject.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
|
||||||
|
}
|
||||||
|
instance := &ProgressEvent{
|
||||||
|
Type: call.Argument(0).String(),
|
||||||
|
}
|
||||||
|
instanceValue := runtime.ToValue(instance).(*goja.Object)
|
||||||
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
return instanceValue
|
||||||
|
})
|
||||||
|
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
instance := &XMLHttpRequest{
|
||||||
|
Upload: &XMLHttpRequestUpload{
|
||||||
|
EventProp: &EventProp{
|
||||||
|
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EventProp: &EventProp{
|
||||||
|
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
instanceValue := runtime.ToValue(instance).(*goja.Object)
|
||||||
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
return instanceValue
|
||||||
|
})
|
||||||
|
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runtime.Set("XMLHttpRequest", xhr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap multipart.Writer and stat content length
|
||||||
|
type multipartWrapper struct {
|
||||||
|
statBuffer *bytes.Buffer
|
||||||
|
statWriter *multipart.Writer
|
||||||
|
writer *multipart.Writer
|
||||||
|
fields map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMultipart(w io.Writer) *multipartWrapper {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
return &multipartWrapper{
|
||||||
|
statBuffer: &buf,
|
||||||
|
statWriter: multipart.NewWriter(&buf),
|
||||||
|
writer: multipart.NewWriter(w),
|
||||||
|
fields: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multipartWrapper) WriteField(fieldname string, value string) error {
|
||||||
|
w.fields[fieldname] = value
|
||||||
|
return w.statWriter.WriteField(fieldname, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multipartWrapper) WriteFile(fieldname string, file *file.File) error {
|
||||||
|
w.fields[fieldname] = file
|
||||||
|
_, err := w.statWriter.CreateFormFile(fieldname, file.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multipartWrapper) Size() int64 {
|
||||||
|
w.statWriter.Close()
|
||||||
|
size := int64(w.statBuffer.Len())
|
||||||
|
for _, v := range w.fields {
|
||||||
|
switch v.(type) {
|
||||||
|
case *file.File:
|
||||||
|
f := v.(*file.File)
|
||||||
|
size += f.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multipartWrapper) Send() error {
|
||||||
|
for k, v := range w.fields {
|
||||||
|
switch v.(type) {
|
||||||
|
case string:
|
||||||
|
if err := w.writer.WriteField(k, v.(string)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *file.File:
|
||||||
|
f := v.(*file.File)
|
||||||
|
fw, err := w.writer.CreateFormFile(k, f.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = io.Copy(fw, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multipartWrapper) FormDataContentType() string {
|
||||||
|
return w.writer.FormDataContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multipartWrapper) Close() error {
|
||||||
|
return w.writer.Close()
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
(()=>{var t={725:function(t,e,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(t){"use strict";var e=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(t,e){return(e=document.createElement("a")).href=t,e},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=e&&e.prototype.append&&e.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(t){}function c(t){return t.map((function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var r=new Uint8Array(t.byteLength);r.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=r.buffer}return e}return t}))}function h(t,r){r=r||{};var o=new e;return c(t).forEach((function(t){o.append(t)})),r.type?o.getBlob(r.type):o.getBlob()}function p(t,e){return new o(c(t),e||{})}i.Blob&&(h.prototype=Blob.prototype,p.prototype=Blob.prototype);var y="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(t){for(var e=0,r=t.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);e<r;){var u=t.charCodeAt(e++);if(u>=55296&&u<=56319){if(e<r){var f=t.charCodeAt(e);56320==(64512&f)&&(++e,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+e/t.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},d="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(t){for(var e=t.length,r=[],o=0;o<e;){var n,i,a,s,u=t[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=e)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=t[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=t[o+1],i=t[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=t[o+1],i=t[o+2],a=t[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",p=0;p<c;)h+=String.fromCharCode.apply(String,r.slice(p,p+=4096));return h};function b(){var e=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;e&&r&&(XMLHttpRequest.prototype.send=function(t){t instanceof Blob?(this.setRequestHeader("Content-Type",t.type),r.call(this,t)):r.call(this,t)});try{new File([],""),t.File=i.File,t.FileReader=i.FileReader}catch(e){try{t.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(e){t.File=function(t,e,r){var o=new Blob(t,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=e.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),t.Blob=f?i.Blob:p):l?(b(),t.Blob=h):function(){function e(t){for(var e=new Array(t.byteLength),r=new Uint8Array(t),o=e.length;o--;)e[o]=r[o];return e}function o(t){for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<t.length;o+=3){var n=t[o],i=o+1<t.length,a=i?t[o+1]:0,s=o+2<t.length,u=s?t[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(e[f],e[l],e[c],e[h])}return r.join("")}var s=Object.create||function(t){function e(){}return e.prototype=t,new e};function u(t){return Object.prototype.toString.call(t).slice(8,-1)}function f(t,e){return"object"==typeof t&&Object.prototype.isPrototypeOf.call(t.prototype,e)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(t){return e=l,r=u(t),-1!==e.indexOf(r)||f(i.ArrayBuffer,t);var e,r}function h(t,r){r=null==r?{}:r;for(var o=0,n=(t=t?t.slice():[]).length;o<n;o++){var a=t[o];a instanceof h?t[o]=a._buffer:"string"==typeof a?t[o]=y(a):"DataView"===u(s=a)||f(i.DataView,s)?t[o]=e(a.buffer):c(a)?t[o]=e(a):t[o]=y(String(a))}var s;this._buffer=i.Uint8Array?function(t){for(var e=0,r=t.length;r--;)e+=t[r].length;for(var o=new Uint8Array(e),n=0,i=0;i<t.length;i++){var a=t[i];o.set(a,n),n+=a.byteLength||a.length}return o}(t):[].concat.apply([],t),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
||||||
+2236
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "polyfill",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "webpack --mode production --watch",
|
||||||
|
"build": "webpack --mode production"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^5.75.0",
|
||||||
|
"webpack-cli": "^5.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"blob-polyfill": "^7.0.20220408",
|
||||||
|
"whatwg-fetch": "^3.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { Blob } from "blob-polyfill";
|
||||||
|
|
||||||
|
globalThis.Blob = Blob;
|
||||||
Loaded 50 of 193 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user