mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 01:20:27 +00:00
feat: support RESTful api (#33)
This commit is contained in:
1 parent
e292926697
commit
c6aa83194f
22 files changed
+603
-125
No files matched your search
@@ -38,9 +38,9 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run Unit tests.
|
||||
run: go test ./... -v -covermode=count -coverprofile=coverage.txt
|
||||
- uses: codecov/codecov-action@v1
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
files: ./coverage.txt
|
||||
fail_ci_if_error: true
|
||||
build:
|
||||
name: Build
|
||||
|
||||
+1
-1
@@ -21,4 +21,4 @@ bin/
|
||||
.torrent.db-shm
|
||||
.torrent.db-wal
|
||||
|
||||
.data
|
||||
*.data
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func main() {
|
||||
finallyCh := make(chan error)
|
||||
err := download.Boot().
|
||||
_, err := download.Boot().
|
||||
URL("https://www.baidu.com/index.html").
|
||||
Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
|
||||
@@ -15,7 +15,7 @@ func main() {
|
||||
args := parse()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
err := download.Boot().
|
||||
_, err := download.Boot().
|
||||
URL(args.url).
|
||||
Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyProgress {
|
||||
@@ -55,10 +55,10 @@ var (
|
||||
)
|
||||
|
||||
func printProgress(task *download.Task, title string) {
|
||||
rate := float64(task.Progress.Downloaded) / float64(task.Res.Length)
|
||||
rate := float64(task.Progress.Downloaded) / float64(task.Res.Size)
|
||||
completeWidth := int(progressWidth * rate)
|
||||
speed := util.ByteFmt(task.Progress.Speed)
|
||||
totalSize := util.ByteFmt(task.Res.Length)
|
||||
totalSize := util.ByteFmt(task.Res.Size)
|
||||
sb.WriteString(fmt.Sprintf("\r%s [", title))
|
||||
for i := 0; i < progressWidth; i++ {
|
||||
if i < completeWidth {
|
||||
|
||||
@@ -5,6 +5,7 @@ go 1.18
|
||||
require (
|
||||
github.com/anacrolix/torrent v1.46.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
)
|
||||
|
||||
@@ -159,6 +159,8 @@ github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORR
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
|
||||
@@ -45,10 +45,10 @@ func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
|
||||
}
|
||||
<-f.torrent.GotInfo()
|
||||
res = &base.Resource{
|
||||
Req: req,
|
||||
Length: f.torrent.Length(),
|
||||
Range: true,
|
||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||
Req: req,
|
||||
Size: f.torrent.Length(),
|
||||
Range: true,
|
||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||
}
|
||||
for i, file := range f.torrent.Files() {
|
||||
res.Files[i] = &base.FileInfo{
|
||||
@@ -62,12 +62,6 @@ func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
|
||||
|
||||
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) (err error) {
|
||||
f.opts = opts
|
||||
if len(f.opts.SelectFiles) == 0 {
|
||||
f.opts.SelectFiles = make([]int, len(res.Files))
|
||||
for i := range f.opts.SelectFiles {
|
||||
f.opts.SelectFiles[i] = i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||
}
|
||||
|
||||
want := &base.Resource{
|
||||
Req: res.Req,
|
||||
Length: 1466714112,
|
||||
Range: true,
|
||||
Req: res.Req,
|
||||
Size: 1466714112,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
@@ -30,7 +30,7 @@ func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(want, res) {
|
||||
t.Errorf("Resolve error = %v, want %v", res, want)
|
||||
t.Errorf("Resolve() got = %v, want %v", res, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/monkeyWie/gopeed-core/pkg/base"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
@@ -75,7 +74,7 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Length = parse
|
||||
res.Size = parse
|
||||
}
|
||||
} else if base.HttpCodeOK == httpResp.StatusCode {
|
||||
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
|
||||
@@ -85,13 +84,13 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Length = parse
|
||||
res.Size = parse
|
||||
}
|
||||
} else {
|
||||
return nil, NewRequestError(httpResp.StatusCode, httpResp.Status)
|
||||
}
|
||||
file := &base.FileInfo{
|
||||
Size: res.Length,
|
||||
Size: res.Size,
|
||||
}
|
||||
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
||||
if contentDisposition != "" {
|
||||
@@ -123,14 +122,14 @@ func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
|
||||
func (f *Fetcher) Start() (err error) {
|
||||
// 创建文件
|
||||
name := f.filename()
|
||||
_, err = f.Ctl.Touch(name, f.res.Length)
|
||||
_, err = f.Ctl.Touch(name, f.res.Size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.status = base.DownloadStatusStart
|
||||
f.status = base.DownloadStatusRunning
|
||||
if f.res.Range {
|
||||
// 每个连接平均需要下载的分块大小
|
||||
chunkSize := f.res.Length / int64(f.opts.Connections)
|
||||
chunkSize := f.res.Size / int64(f.opts.Connections)
|
||||
f.chunks = make([]*Chunk, f.opts.Connections)
|
||||
for i := 0; i < f.opts.Connections; i++ {
|
||||
var (
|
||||
@@ -139,7 +138,7 @@ func (f *Fetcher) Start() (err error) {
|
||||
)
|
||||
if i == f.opts.Connections-1 {
|
||||
// 最后一个分块需要保证把文件下载完
|
||||
end = f.res.Length - 1
|
||||
end = f.res.Size - 1
|
||||
} else {
|
||||
end = begin + chunkSize - 1
|
||||
}
|
||||
@@ -156,7 +155,7 @@ func (f *Fetcher) Start() (err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) Pause() (err error) {
|
||||
if base.DownloadStatusStart != f.status {
|
||||
if base.DownloadStatusRunning != f.status {
|
||||
return
|
||||
}
|
||||
f.status = base.DownloadStatusPause
|
||||
@@ -166,10 +165,10 @@ func (f *Fetcher) Pause() (err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) Continue() (err error) {
|
||||
if base.DownloadStatusStart == f.status || base.DownloadStatusDone == f.status {
|
||||
if base.DownloadStatusRunning == f.status || base.DownloadStatusDone == f.status {
|
||||
return
|
||||
}
|
||||
f.status = base.DownloadStatusStart
|
||||
f.status = base.DownloadStatusRunning
|
||||
var name = f.filename()
|
||||
_, err = f.Ctl.Open(name)
|
||||
if err != nil {
|
||||
@@ -352,7 +351,7 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
|
||||
}
|
||||
}
|
||||
if extra.Body != "" {
|
||||
body = ioutil.NopCloser(bytes.NewBufferString(extra.Body))
|
||||
body = bytes.NewBufferString(extra.Body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/monkeyWie/gopeed-core/internal/controller"
|
||||
"github.com/monkeyWie/gopeed-core/internal/fetcher"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/base"
|
||||
@@ -13,8 +14,8 @@ import (
|
||||
|
||||
func TestFetcher_Resolve(t *testing.T) {
|
||||
testResolve(test.StartTestFileServer, &base.Resource{
|
||||
Length: test.BuildSize,
|
||||
Range: true,
|
||||
Size: test.BuildSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
@@ -23,8 +24,8 @@ func TestFetcher_Resolve(t *testing.T) {
|
||||
},
|
||||
}, t)
|
||||
testResolve(test.StartTestChunkedServer, &base.Resource{
|
||||
Length: 0,
|
||||
Range: false,
|
||||
Size: 0,
|
||||
Range: false,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
@@ -46,7 +47,7 @@ func testResolve(startTestServer func() net.Listener, want *base.Resource, t *te
|
||||
}
|
||||
res.Req = nil
|
||||
if !reflect.DeepEqual(want, res) {
|
||||
t.Errorf("Resolve error = %v, want %v", res, want)
|
||||
t.Errorf("Resolve() got = %v, want %v", res, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +76,13 @@ func TestFetcher_DownloadChunked(t *testing.T) {
|
||||
defer listener.Close()
|
||||
// chunked编码下载
|
||||
downloadNormal(listener, 1, t)
|
||||
//downloadContinue(listener, 1, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadPost(t *testing.T) {
|
||||
listener := test.StartTestPostServer()
|
||||
defer listener.Close()
|
||||
// post下载
|
||||
downloadPost(listener, 1, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadRetry(t *testing.T) {
|
||||
@@ -135,7 +142,31 @@ func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("Download error = %v, want %v", got, want)
|
||||
t.Errorf("Download() got = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadPost(listener net.Listener, connections int, t *testing.T) {
|
||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
||||
fetcher.(*Fetcher).res.Req.Extra = Extra{
|
||||
Method: "POST",
|
||||
Header: map[string]string{
|
||||
"Authorization": "Bearer 123456",
|
||||
},
|
||||
Body: fmt.Sprintf(`{"name":"%s"}`, test.BuildName),
|
||||
}
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = fetcher.Wait()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("Download() got = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +191,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("Download error = %v, want %v", got, want)
|
||||
t.Errorf("Download() got = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +203,7 @@ func downloadError(listener net.Listener, connections int, t *testing.T) {
|
||||
}
|
||||
err = fetcher.Wait()
|
||||
if err == nil {
|
||||
t.Errorf("Download error = %v, want %v", err, nil)
|
||||
t.Errorf("Download() got = %v, want %v", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +231,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("Download error = %v, want %v", got, want)
|
||||
t.Errorf("Download() got = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-13
@@ -1,16 +1,13 @@
|
||||
package base
|
||||
|
||||
import "errors"
|
||||
|
||||
type Status int
|
||||
type Status string
|
||||
|
||||
const (
|
||||
DownloadStatusPrepare Status = iota
|
||||
DownloadStatusReady
|
||||
DownloadStatusStart
|
||||
DownloadStatusPause
|
||||
DownloadStatusError
|
||||
DownloadStatusDone
|
||||
DownloadStatusReady Status = "ready"
|
||||
DownloadStatusRunning Status = "running"
|
||||
DownloadStatusPause Status = "pause"
|
||||
DownloadStatusError Status = "error"
|
||||
DownloadStatusDone Status = "done"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,7 +21,3 @@ const (
|
||||
|
||||
HttpHeaderRangeFormat = "bytes=%d-%d"
|
||||
)
|
||||
|
||||
var (
|
||||
DeleteErr = errors.New("delete")
|
||||
)
|
||||
+14
-16
@@ -3,38 +3,36 @@ package base
|
||||
// 下载请求
|
||||
type Request struct {
|
||||
// 下载链接
|
||||
Schema string
|
||||
// 下载链接
|
||||
URL string
|
||||
URL string `json:"url"`
|
||||
// 附加信息
|
||||
Extra interface{}
|
||||
Extra interface{} `json:"extra"`
|
||||
}
|
||||
|
||||
// 资源信息
|
||||
type Resource struct {
|
||||
Req *Request
|
||||
Req *Request `json:"req"`
|
||||
// 资源总大小
|
||||
Length int64
|
||||
Size int64 `json:"size"`
|
||||
// 是否支持断点下载
|
||||
Range bool
|
||||
Range bool `json:"range"`
|
||||
// 资源所包含的文件列表
|
||||
Files []*FileInfo
|
||||
Files []*FileInfo `json:"files"`
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Path string
|
||||
Size int64
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// 下载选项
|
||||
type Options struct {
|
||||
// 保存文件名
|
||||
Name string
|
||||
Name string `json:"name"`
|
||||
// 保存目录
|
||||
Path string
|
||||
Path string `json:"path"`
|
||||
// 并发连接数
|
||||
Connections int
|
||||
// 下载的文件下标列表
|
||||
SelectFiles []int
|
||||
Connections int `json:"connections"`
|
||||
// 选择下载的文件下标列表
|
||||
SelectFiles []int `json:"select_files"`
|
||||
}
|
||||
+75
-36
@@ -17,17 +17,17 @@ type Listener func(event *Event)
|
||||
|
||||
type Progress struct {
|
||||
// 下载耗时(纳秒)
|
||||
Used int64
|
||||
Used int64 `json:"used"`
|
||||
// 每秒下载字节数
|
||||
Speed int64
|
||||
Speed int64 `json:"speed"`
|
||||
// 已下载的字节数
|
||||
Downloaded int64
|
||||
Downloaded int64 `json:"downloaded"`
|
||||
}
|
||||
|
||||
type Downloader struct {
|
||||
*controller.DefaultController
|
||||
fetchBuilders map[string]fetcher.FetcherBuilder
|
||||
tasks map[string]*Task
|
||||
tasks []*Task
|
||||
listener Listener
|
||||
}
|
||||
|
||||
@@ -39,15 +39,14 @@ func NewDownloader(fbs ...fetcher.FetcherBuilder) *Downloader {
|
||||
d.fetchBuilders[strings.ToUpper(p)] = f
|
||||
}
|
||||
}
|
||||
d.tasks = make(map[string]*Task)
|
||||
d.tasks = make([]*Task, 0)
|
||||
|
||||
// 每秒统计一次下载速度
|
||||
go func() {
|
||||
for {
|
||||
if len(d.tasks) > 0 {
|
||||
for _, task := range d.tasks {
|
||||
if task.Status == base.DownloadStatusPrepare ||
|
||||
task.Status == base.DownloadStatusDone ||
|
||||
if task.Status == base.DownloadStatusDone ||
|
||||
task.Status == base.DownloadStatusError ||
|
||||
task.Status == base.DownloadStatusPause {
|
||||
continue
|
||||
@@ -79,42 +78,55 @@ func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||
return nil, errors.New("unsupported protocol")
|
||||
}
|
||||
|
||||
func (d *Downloader) Resolve(req *base.Request) (string, *base.Resource, error) {
|
||||
func (d *Downloader) Resolve(req *base.Request) (*base.Resource, error) {
|
||||
fetcher, err := d.buildFetcher(req.URL)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
res, err := fetcher.Resolve(req)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
task := NewTask()
|
||||
task.fetcher = fetcher
|
||||
task.Res = res
|
||||
d.tasks[task.ID] = task
|
||||
return task.ID, res, nil
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *Downloader) Create(taskID string, res *base.Resource, opts *base.Options) (err error) {
|
||||
task := d.tasks[taskID]
|
||||
fetcher := d.tasks[taskID].fetcher
|
||||
func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId string, err error) {
|
||||
if opts == nil {
|
||||
opts = &base.Options{}
|
||||
}
|
||||
if !res.Range || opts.Connections < 1 {
|
||||
opts.Connections = 1
|
||||
}
|
||||
if len(opts.SelectFiles) == 0 {
|
||||
opts.SelectFiles = make([]int, len(res.Files))
|
||||
for i := range opts.SelectFiles {
|
||||
opts.SelectFiles[i] = i
|
||||
}
|
||||
}
|
||||
|
||||
fetcher, err := d.buildFetcher(res.Req.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = fetcher.Create(res, opts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
task := NewTask()
|
||||
task.fetcher = fetcher
|
||||
task.Res = res
|
||||
task.Opts = opts
|
||||
task.Status = base.DownloadStatusReady
|
||||
task.Progress = &Progress{}
|
||||
task.timer = &util.Timer{}
|
||||
task.locker = new(sync.Mutex)
|
||||
task.timer.Start()
|
||||
task.Status = base.DownloadStatusRunning
|
||||
d.tasks = append(d.tasks, task)
|
||||
d.emit(EventKeyStart, task)
|
||||
taskId = task.ID
|
||||
|
||||
err = fetcher.Start()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -122,18 +134,20 @@ func (d *Downloader) Create(taskID string, res *base.Resource, opts *base.Option
|
||||
go func() {
|
||||
err = fetcher.Wait()
|
||||
if err != nil {
|
||||
task.Status = base.DownloadStatusError
|
||||
d.emit(EventKeyError, task, err)
|
||||
} else {
|
||||
task.Progress.Used = task.timer.Used()
|
||||
if task.Res.Length == 0 {
|
||||
task.Res.Length = task.fetcher.Progress().TotalDownloaded()
|
||||
if task.Res.Size == 0 {
|
||||
task.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
||||
}
|
||||
used := task.Progress.Used / int64(time.Second)
|
||||
if used == 0 {
|
||||
used = 1
|
||||
}
|
||||
task.Progress.Speed = task.Res.Length / used
|
||||
task.Progress.Downloaded = task.Res.Length
|
||||
task.Progress.Speed = task.Res.Size / used
|
||||
task.Progress.Downloaded = task.Res.Size
|
||||
task.Status = base.DownloadStatusDone
|
||||
d.emit(EventKeyDone, task)
|
||||
}
|
||||
d.emit(EventKeyFinally, task, err)
|
||||
@@ -142,21 +156,33 @@ func (d *Downloader) Create(taskID string, res *base.Resource, opts *base.Option
|
||||
}
|
||||
|
||||
func (d *Downloader) Pause(id string) {
|
||||
task := d.tasks[id]
|
||||
task := d.GetTask(id)
|
||||
if task == nil {
|
||||
return
|
||||
}
|
||||
task.locker.Lock()
|
||||
defer task.locker.Unlock()
|
||||
task.timer.Pause()
|
||||
task.fetcher.Pause()
|
||||
d.emit(EventKeyPause, task)
|
||||
if task.Status == base.DownloadStatusRunning {
|
||||
task.Status = base.DownloadStatusPause
|
||||
task.timer.Pause()
|
||||
task.fetcher.Pause()
|
||||
d.emit(EventKeyPause, task)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Downloader) Continue(id string) {
|
||||
task := d.tasks[id]
|
||||
task := d.GetTask(id)
|
||||
if task == nil {
|
||||
return
|
||||
}
|
||||
task.locker.Lock()
|
||||
defer task.locker.Unlock()
|
||||
task.timer.Continue()
|
||||
task.fetcher.Continue()
|
||||
d.emit(EventKeyContinue, task)
|
||||
if task.Status == base.DownloadStatusPause || task.Status == base.DownloadStatusError {
|
||||
task.Status = base.DownloadStatusRunning
|
||||
task.timer.Continue()
|
||||
task.fetcher.Continue()
|
||||
d.emit(EventKeyContinue, task)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Downloader) Listener(fn Listener) {
|
||||
@@ -177,6 +203,19 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Downloader) GetTask(id string) *Task {
|
||||
for _, task := range d.tasks {
|
||||
if task.ID == id {
|
||||
return task
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Downloader) GetTasks() []*Task {
|
||||
return d.tasks
|
||||
}
|
||||
|
||||
var defaultDownloader = NewDownloader(new(http.FetcherBuilder), new(bt.FetcherBuilder))
|
||||
|
||||
type boot struct {
|
||||
@@ -195,7 +234,7 @@ func (b *boot) Extra(extra interface{}) *boot {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *boot) Resolve() (string, *base.Resource, error) {
|
||||
func (b *boot) Resolve() (*base.Resource, error) {
|
||||
return defaultDownloader.Resolve(&base.Request{
|
||||
URL: b.url,
|
||||
Extra: b.extra,
|
||||
@@ -207,13 +246,13 @@ func (b *boot) Listener(listener Listener) *boot {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *boot) Create(opts *base.Options) error {
|
||||
taskID, res, err := b.Resolve()
|
||||
func (b *boot) Create(opts *base.Options) (string, error) {
|
||||
res, err := b.Resolve()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
defaultDownloader.Listener(b.listener)
|
||||
return defaultDownloader.Create(taskID, res, opts)
|
||||
return defaultDownloader.Create(res, opts)
|
||||
}
|
||||
|
||||
func Boot() *boot {
|
||||
|
||||
@@ -17,14 +17,14 @@ func TestDownloader_Resolve(t *testing.T) {
|
||||
req := &base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
}
|
||||
_, res, err := downloader.Resolve(req)
|
||||
res, err := downloader.Resolve(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := &base.Resource{
|
||||
Req: req,
|
||||
Length: test.BuildSize,
|
||||
Range: true,
|
||||
Req: req,
|
||||
Size: test.BuildSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
@@ -33,8 +33,8 @@ func TestDownloader_Resolve(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(res, want) {
|
||||
t.Errorf("Resolve error = %s, want %s", test.ToJson(res), test.ToJson(want))
|
||||
if !reflect.DeepEqual(want, res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", res, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestDownloader_Create(t *testing.T) {
|
||||
req := &base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
}
|
||||
taskID, res, err := downloader.Resolve(req)
|
||||
res, err := downloader.Resolve(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func TestDownloader_Create(t *testing.T) {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
err = downloader.Create(taskID, res, &base.Options{
|
||||
_, err = downloader.Create(res, &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
Connections: 4,
|
||||
@@ -70,6 +70,6 @@ func TestDownloader_Create(t *testing.T) {
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("Download error = %v, want %v", got, want)
|
||||
t.Errorf("Download() got = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
+11
-8
@@ -6,15 +6,17 @@ import (
|
||||
"github.com/monkeyWie/gopeed-core/pkg/base"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/util"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
ID string
|
||||
Res *base.Resource
|
||||
Opts *base.Options
|
||||
Status base.Status
|
||||
Files map[string]*base.FileInfo
|
||||
Progress *Progress
|
||||
ID string `json:"id"`
|
||||
Res *base.Resource `json:"res"`
|
||||
Opts *base.Options `json:"opts"`
|
||||
Status base.Status `json:"status"`
|
||||
Files map[string]*base.FileInfo `json:"files"`
|
||||
Progress *Progress `json:"progress"`
|
||||
CreateTime time.Time `json:"create_time"`
|
||||
|
||||
fetcher fetcher.Fetcher
|
||||
timer *util.Timer
|
||||
@@ -23,7 +25,8 @@ type Task struct {
|
||||
|
||||
func NewTask() *Task {
|
||||
return &Task{
|
||||
ID: uuid.New().String(),
|
||||
Status: base.DownloadStatusPrepare,
|
||||
ID: uuid.New().String(),
|
||||
Status: base.DownloadStatusReady,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/base"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/download"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/rest/model"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/rest/util"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Resolve(w http.ResponseWriter, r *http.Request) {
|
||||
var req base.Request
|
||||
if util.ReadJson(w, r, &req) {
|
||||
resource, err := Downloader.Resolve(&req)
|
||||
if err != nil {
|
||||
util.WriteJson(w, 500, model.NewResultWithMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
util.WriteJsonOk(w, model.NewResultWithData(resource))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTaskReq
|
||||
if util.ReadJson(w, r, &req) {
|
||||
taskId, err := Downloader.Create(req.Resource, req.Options)
|
||||
if err != nil {
|
||||
util.WriteJson(w, 500, model.NewResultWithMsg(err.Error()))
|
||||
return
|
||||
}
|
||||
util.WriteJsonOk(w, model.NewResultWithData(taskId))
|
||||
}
|
||||
}
|
||||
|
||||
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
Downloader.Pause(taskId)
|
||||
util.WriteJsonOk(w, nil)
|
||||
}
|
||||
|
||||
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
Downloader.Continue(taskId)
|
||||
util.WriteJsonOk(w, nil)
|
||||
}
|
||||
|
||||
func GetTask(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
task := Downloader.GetTask(taskId)
|
||||
if task == nil {
|
||||
util.WriteJson(w, 404, model.NewResultWithMsg("task not found"))
|
||||
return
|
||||
}
|
||||
util.WriteJsonOk(w, model.NewResultWithData(task))
|
||||
}
|
||||
|
||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||
status := r.FormValue("status")
|
||||
if status == "" {
|
||||
util.WriteJson(w, 400, model.NewResultWithMsg("param is required: status"))
|
||||
return
|
||||
}
|
||||
statusArr := strings.Split(status, ",")
|
||||
tasks := Downloader.GetTasks()
|
||||
var result []*download.Task
|
||||
for _, task := range tasks {
|
||||
for _, s := range statusArr {
|
||||
if task.Status == base.Status(s) {
|
||||
result = append(result, task)
|
||||
}
|
||||
}
|
||||
}
|
||||
util.WriteJsonOk(w, model.NewResultWithData(tasks))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
type Result[T any] struct {
|
||||
Msg string `json:"msg"`
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
func NewResult[T any](msg string, data T) *Result[T] {
|
||||
return &Result[T]{
|
||||
Msg: msg,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func NewResultWithMsg(msg string) *Result[any] {
|
||||
return NewResult[any](msg, nil)
|
||||
}
|
||||
|
||||
func NewResultWithData[T any](data T) *Result[T] {
|
||||
return NewResult[T]("", data)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package model
|
||||
|
||||
import "github.com/monkeyWie/gopeed-core/pkg/base"
|
||||
|
||||
type CreateTaskReq struct {
|
||||
Resource *base.Resource `json:"resource"`
|
||||
Options *base.Options `json:"options"`
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/monkeyWie/gopeed-core/internal/protocol/bt"
|
||||
fhttp "github.com/monkeyWie/gopeed-core/internal/protocol/http"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/download"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
srv *http.Server
|
||||
|
||||
Downloader = download.NewDownloader(new(fhttp.FetcherBuilder), new(bt.FetcherBuilder))
|
||||
)
|
||||
|
||||
func Start(addr string, port int) (int, error) {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addr, port))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var r = mux.NewRouter()
|
||||
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
|
||||
|
||||
srv = &http.Server{Handler: r}
|
||||
go func() {
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
// TODO log
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
return listener.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
func Stop() error {
|
||||
if srv != nil {
|
||||
return srv.Shutdown(context.TODO())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/base"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/download"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/rest/model"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/test"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
restPort int
|
||||
|
||||
taskReq = &base.Request{}
|
||||
taskRes = &base.Resource{
|
||||
Req: taskReq,
|
||||
Size: test.BuildSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
Path: "",
|
||||
Size: test.BuildSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
createReq = &model.CreateTaskReq{
|
||||
Resource: taskRes,
|
||||
Options: &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
Connections: 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[*base.Resource](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||
if !reflect.DeepEqual(taskRes, resp) {
|
||||
t.Errorf("Resolve() got = %v, want %v", resp, taskRes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
if taskId == "" {
|
||||
t.Fatal("create task failed")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("CreateTask() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPauseAndContinueTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
type result struct {
|
||||
pauseCount int
|
||||
continueCount int
|
||||
md5 string
|
||||
}
|
||||
|
||||
var got = result{
|
||||
pauseCount: 0,
|
||||
continueCount: 0,
|
||||
md5: "",
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
switch event.Key {
|
||||
case download.EventKeyPause:
|
||||
got.pauseCount++
|
||||
case download.EventKeyContinue:
|
||||
got.continueCount++
|
||||
case download.EventKeyFinally:
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
t1 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t1.Status != base.DownloadStatusRunning {
|
||||
t.Errorf("CreateTask() got = %v, want %v", t1.Status, base.DownloadStatusRunning)
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/"+taskId+"/pause", nil)
|
||||
t2 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t2.Status != base.DownloadStatusPause {
|
||||
t.Errorf("PauseTask() got = %v, want %v", t2.Status, base.DownloadStatusPause)
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/"+taskId+"/continue", nil)
|
||||
t3 := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||
if t3.Status != base.DownloadStatusRunning {
|
||||
t.Errorf("ContinueTask() got = %v, want %v", t3.Status, base.DownloadStatusRunning)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
want := result{
|
||||
pauseCount: 1,
|
||||
continueCount: 1,
|
||||
md5: test.FileMd5(test.BuildFile),
|
||||
}
|
||||
got.md5 = test.FileMd5(test.DownloadFile)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("PauseAndContinueTask() got = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
|
||||
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
code, _ := httpRequest[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
|
||||
checkCode(code, 400)
|
||||
|
||||
wg.Wait()
|
||||
r := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks?status=done", nil)
|
||||
if r[0].Status != base.DownloadStatusDone {
|
||||
t.Errorf("GetTasks() got = %v, want %v", r[0].Status, base.DownloadStatusDone)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func doTest(handler func()) {
|
||||
fileListener := doStart()
|
||||
defer func() {
|
||||
fileListener.Close()
|
||||
Stop()
|
||||
}()
|
||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||
handler()
|
||||
}
|
||||
|
||||
func doStart() net.Listener {
|
||||
port, err := Start("127.0.0.1", 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
restPort = port
|
||||
return test.StartTestFileServer()
|
||||
}
|
||||
|
||||
func httpRequest[T any](method string, path string, req any) (int, *model.Result[T]) {
|
||||
var body io.Reader
|
||||
if req != nil {
|
||||
buf, _ := json.Marshal(req)
|
||||
body = bytes.NewBuffer(buf)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(method, fmt.Sprintf("http://127.0.0.1:%d%s", restPort, path), body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var r model.Result[T]
|
||||
if err := json.NewDecoder(response.Body).Decode(&r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return response.StatusCode, &r
|
||||
}
|
||||
|
||||
func httpRequestCheckOk[T any](method string, path string, req any) T {
|
||||
code, result := httpRequest[T](method, path, req)
|
||||
checkOk(code)
|
||||
return result.Data
|
||||
}
|
||||
|
||||
func checkOk(code int) {
|
||||
checkCode(code, 200)
|
||||
}
|
||||
|
||||
func checkCode(code int, exceptCode int) {
|
||||
if code != exceptCode {
|
||||
panic(fmt.Sprintf("code got = %d, want %d", code, exceptCode))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/rest/model"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ReadJson(w http.ResponseWriter, r *http.Request, v any) bool {
|
||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||
WriteJson(w, 500, model.NewResultWithMsg(err.Error()))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func WriteJsonOk(w http.ResponseWriter, v any) {
|
||||
WriteJson(w, 200, v)
|
||||
}
|
||||
|
||||
func WriteJson(w http.ResponseWriter, code int, v any) {
|
||||
w.WriteHeader(code)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
@@ -61,6 +62,29 @@ func StartTestRetryServer() net.Listener {
|
||||
})
|
||||
}
|
||||
|
||||
func StartTestPostServer() net.Listener {
|
||||
return startTestServer(func() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
||||
var data map[string]interface{}
|
||||
if err := json.NewDecoder(request.Body).Decode(&data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if data["name"] == BuildName {
|
||||
file, err := os.Open(BuildFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
io.Copy(writer, file)
|
||||
}
|
||||
}
|
||||
})
|
||||
return mux
|
||||
})
|
||||
}
|
||||
|
||||
func StartTestErrorServer() net.Listener {
|
||||
counter := 0
|
||||
return startTestServer(func() http.Handler {
|
||||
|
||||
Reference in new issue
Block a user