Files
kenaito-kopsc/internal/executor/netspeed.go
T

169 lines
3.9 KiB
Go

package executor
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// runNetSpeed implements `kopsc net speed`: simple network speed test.
func runNetSpeed(ctx context.Context, req BuiltinRequest) error {
opts, err := parseNetSpeedArgs(req.Args)
if err != nil {
return err
}
fmt.Fprint(req.Stdout, "=== 网络速度测试 ===\n")
// Download test
if !opts.uploadOnly {
fmt.Fprintln(req.Stdout, "测试下载速度...")
downloadSpeed, err := testDownload(ctx, opts.size)
if err != nil {
fmt.Fprintf(req.Stderr, "下载测试失败: %v\n", err)
} else {
fmt.Fprintf(req.Stdout, "下载速度: %s/s\n", humanSize(downloadSpeed))
}
fmt.Fprintln(req.Stdout)
}
// Upload test
if !opts.downloadOnly {
fmt.Fprintln(req.Stdout, "测试上传速度...")
uploadSpeed, err := testUpload(ctx, opts.size)
if err != nil {
fmt.Fprintf(req.Stderr, "上传测试失败: %v\n", err)
} else {
fmt.Fprintf(req.Stdout, "上传速度: %s/s\n", humanSize(uploadSpeed))
}
}
return nil
}
type netSpeedOptions struct {
size int64
downloadOnly bool
uploadOnly bool
}
func parseNetSpeedArgs(args []string) (netSpeedOptions, error) {
opts := netSpeedOptions{size: 10 * 1024 * 1024} // 10MB default
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--":
remaining := args[i+1:]
if len(remaining) > 0 {
n, err := parseSizeArg(remaining[0])
if err != nil {
return opts, fmt.Errorf("大小参数无效: %w", err)
}
opts.size = n
}
i = len(args)
case arg == "--size" || arg == "-s":
if i+1 >= len(args) {
return opts, fmt.Errorf("%s 缺少大小参数", arg)
}
i++
n, err := parseSizeArg(args[i])
if err != nil {
return opts, fmt.Errorf("--size: %w", err)
}
opts.size = n
case strings.HasPrefix(arg, "--size="):
n, err := parseSizeArg(strings.TrimPrefix(arg, "--size="))
if err != nil {
return opts, fmt.Errorf("--size: %w", err)
}
opts.size = n
case arg == "--download" || arg == "-d":
opts.downloadOnly = true
case arg == "--upload" || arg == "-u":
opts.uploadOnly = true
default:
return opts, fmt.Errorf("未知选项 %q", arg)
}
}
if opts.downloadOnly && opts.uploadOnly {
return opts, fmt.Errorf("--download 和 --upload 不能同时使用")
}
return opts, nil
}
func testDownload(ctx context.Context, size int64) (int64, error) {
// Use Cloudflare's speed test endpoint
url := fmt.Sprintf("https://speed.cloudflare.com/__down?bytes=%d", size)
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return 0, err
}
start := time.Now()
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("HTTP %d", resp.StatusCode)
}
n, err := io.Copy(io.Discard, resp.Body)
if err != nil {
return 0, err
}
elapsed := time.Since(start).Seconds()
if elapsed == 0 {
elapsed = 0.001
}
return int64(float64(n) / elapsed), nil
}
func testUpload(ctx context.Context, size int64) (int64, error) {
// Use httpbin.org for upload test
url := "https://httpbin.org/post"
// Create a reader that generates data on the fly
data := make([]byte, size)
for i := range data {
data[i] = 'A'
}
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(data)))
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/octet-stream")
start := time.Now()
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("HTTP %d", resp.StatusCode)
}
elapsed := time.Since(start).Seconds()
if elapsed == 0 {
elapsed = 0.001
}
return int64(float64(size) / elapsed), nil
}