mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 09:30:27 +00:00
feat: implement Model Context Protocol (MCP) server and download manager capabilities
This commit is contained in:
1 parent
88d150b933
commit
e93437c95f
14 files changed
+976
-23
No files matched your search
@@ -2,8 +2,13 @@ package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -951,8 +956,14 @@ func (f *Fetcher) expandConnections() {
|
||||
}
|
||||
f.fileMu.Unlock()
|
||||
|
||||
f.setState(stateDone)
|
||||
f.doneCh <- nil
|
||||
var err error
|
||||
if chkErr := f.verifyChecksum(); chkErr != nil {
|
||||
err = chkErr
|
||||
f.setState(stateError)
|
||||
} else {
|
||||
f.setState(stateDone)
|
||||
}
|
||||
f.doneCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2209,10 +2220,15 @@ func (f *Fetcher) onDownloadComplete() {
|
||||
}
|
||||
f.fileMu.Unlock()
|
||||
|
||||
if finalErr != nil {
|
||||
f.setState(stateError)
|
||||
if finalErr == nil {
|
||||
if chkErr := f.verifyChecksum(); chkErr != nil {
|
||||
finalErr = chkErr
|
||||
f.setState(stateError)
|
||||
} else {
|
||||
f.setState(stateDone)
|
||||
}
|
||||
} else {
|
||||
f.setState(stateDone)
|
||||
f.setState(stateError)
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -2221,6 +2237,46 @@ func (f *Fetcher) onDownloadComplete() {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fetcher) verifyChecksum() error {
|
||||
if f.meta == nil || f.meta.Opts == nil || f.meta.Opts.Checksum == nil {
|
||||
return nil
|
||||
}
|
||||
chk := f.meta.Opts.Checksum
|
||||
if chk.Algorithm == "" && chk.Expected == "" {
|
||||
return nil
|
||||
}
|
||||
var h hash.Hash
|
||||
switch strings.ToLower(strings.TrimSpace(chk.Algorithm)) {
|
||||
case "md5":
|
||||
h = md5.New()
|
||||
case "sha1", "sha-1":
|
||||
h = sha1.New()
|
||||
case "sha256", "sha-256":
|
||||
h = sha256.New()
|
||||
default:
|
||||
return fmt.Errorf("unsupported checksum algorithm: %s", chk.Algorithm)
|
||||
}
|
||||
|
||||
filePath := f.meta.SingleFilepath()
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file for checksum verification failed: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := make([]byte, 64*1024)
|
||||
if _, err := io.CopyBuffer(h, file, buf); err != nil {
|
||||
return fmt.Errorf("calculate checksum failed: %w", err)
|
||||
}
|
||||
|
||||
actual := hex.EncodeToString(h.Sum(nil))
|
||||
expected := strings.ToLower(strings.TrimSpace(chk.Expected))
|
||||
if !strings.EqualFold(actual, expected) {
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", expected, actual)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) checkCompletion() bool {
|
||||
// Check if all data has been downloaded
|
||||
f.connMu.Lock()
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
gohttp "net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
)
|
||||
|
||||
func TestFetcher_ChecksumVerification(t *testing.T) {
|
||||
content := []byte("Hello, Gopeed Checksum Verification Test Data!")
|
||||
md5Hasher := md5.New()
|
||||
md5Hasher.Write(content)
|
||||
correctMd5 := hex.EncodeToString(md5Hasher.Sum(nil))
|
||||
|
||||
sha256Hasher := sha256.New()
|
||||
sha256Hasher.Write(content)
|
||||
correctSha256 := hex.EncodeToString(sha256Hasher.Sum(nil))
|
||||
|
||||
wrongHash := "0123456789abcdef0123456789abcdef"
|
||||
|
||||
server := httptest.NewServer(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
|
||||
w.Header().Set(base.HttpHeaderContentLength, fmt.Sprintf("%d", len(content)))
|
||||
w.Header().Set(base.HttpHeaderAcceptRanges, base.HttpHeaderBytes)
|
||||
w.WriteHeader(gohttp.StatusOK)
|
||||
_, _ = w.Write(content)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
t.Run("Valid MD5", func(t *testing.T) {
|
||||
f := buildFetcher()
|
||||
defer f.Close()
|
||||
|
||||
opts := &base.Options{
|
||||
Path: t.TempDir(),
|
||||
Name: "test_valid_md5.txt",
|
||||
Extra: &http.OptsExtra{
|
||||
Connections: 1,
|
||||
},
|
||||
Checksum: &base.ChecksumOption{
|
||||
Algorithm: "md5",
|
||||
Expected: correctMd5,
|
||||
},
|
||||
}
|
||||
|
||||
if err := f.Resolve(&base.Request{URL: server.URL + "/test.txt"}, opts); err != nil {
|
||||
t.Fatalf("Resolve error: %v", err)
|
||||
}
|
||||
if err := f.Start(); err != nil {
|
||||
t.Fatalf("Start error: %v", err)
|
||||
}
|
||||
if err := f.Wait(); err != nil {
|
||||
t.Fatalf("Wait error (expected success): %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Valid SHA256", func(t *testing.T) {
|
||||
f := buildFetcher()
|
||||
defer f.Close()
|
||||
|
||||
opts := &base.Options{
|
||||
Path: t.TempDir(),
|
||||
Name: "test_valid_sha256.txt",
|
||||
Extra: &http.OptsExtra{
|
||||
Connections: 1,
|
||||
},
|
||||
Checksum: &base.ChecksumOption{
|
||||
Algorithm: "sha256",
|
||||
Expected: correctSha256,
|
||||
},
|
||||
}
|
||||
|
||||
if err := f.Resolve(&base.Request{URL: server.URL + "/test.txt"}, opts); err != nil {
|
||||
t.Fatalf("Resolve error: %v", err)
|
||||
}
|
||||
if err := f.Start(); err != nil {
|
||||
t.Fatalf("Start error: %v", err)
|
||||
}
|
||||
if err := f.Wait(); err != nil {
|
||||
t.Fatalf("Wait error (expected success): %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Wrong MD5 mismatch", func(t *testing.T) {
|
||||
f := buildFetcher()
|
||||
defer f.Close()
|
||||
|
||||
opts := &base.Options{
|
||||
Path: t.TempDir(),
|
||||
Name: "test_wrong_md5.txt",
|
||||
Extra: &http.OptsExtra{
|
||||
Connections: 1,
|
||||
},
|
||||
Checksum: &base.ChecksumOption{
|
||||
Algorithm: "md5",
|
||||
Expected: wrongHash,
|
||||
},
|
||||
}
|
||||
|
||||
if err := f.Resolve(&base.Request{URL: server.URL + "/test.txt"}, opts); err != nil {
|
||||
t.Fatalf("Resolve error: %v", err)
|
||||
}
|
||||
if err := f.Start(); err != nil {
|
||||
t.Fatalf("Start error: %v", err)
|
||||
}
|
||||
err := f.Wait()
|
||||
if err == nil {
|
||||
t.Fatal("Wait expected checksum mismatch error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "checksum mismatch") {
|
||||
t.Fatalf("expected 'checksum mismatch' in error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Unsupported Algorithm", func(t *testing.T) {
|
||||
f := buildFetcher()
|
||||
defer f.Close()
|
||||
|
||||
opts := &base.Options{
|
||||
Path: t.TempDir(),
|
||||
Name: "test_unsupported.txt",
|
||||
Extra: &http.OptsExtra{
|
||||
Connections: 1,
|
||||
},
|
||||
Checksum: &base.ChecksumOption{
|
||||
Algorithm: "invalid-algo",
|
||||
Expected: wrongHash,
|
||||
},
|
||||
}
|
||||
|
||||
if err := f.Resolve(&base.Request{URL: server.URL + "/test.txt"}, opts); err != nil {
|
||||
t.Fatalf("Resolve error: %v", err)
|
||||
}
|
||||
if err := f.Start(); err != nil {
|
||||
t.Fatalf("Start error: %v", err)
|
||||
}
|
||||
err := f.Wait()
|
||||
if err == nil {
|
||||
t.Fatal("Wait expected error for unsupported algorithm, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupported checksum algorithm") {
|
||||
t.Fatalf("expected 'unsupported checksum algorithm' in error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("No Checksum (Opt-in backward compatibility)", func(t *testing.T) {
|
||||
f := buildFetcher()
|
||||
defer f.Close()
|
||||
|
||||
opts := &base.Options{
|
||||
Path: t.TempDir(),
|
||||
Name: test.DownloadName,
|
||||
Extra: &http.OptsExtra{
|
||||
Connections: 1,
|
||||
},
|
||||
}
|
||||
|
||||
if err := f.Resolve(&base.Request{URL: server.URL + "/test.txt"}, opts); err != nil {
|
||||
t.Fatalf("Resolve error: %v", err)
|
||||
}
|
||||
if err := f.Start(); err != nil {
|
||||
t.Fatalf("Start error: %v", err)
|
||||
}
|
||||
if err := f.Wait(); err != nil {
|
||||
t.Fatalf("Wait error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user