fscan/plugins/local/downloader.go
ZacharyZcR 8f54702c02 refactor: 精准修复插件系统三个设计问题
经Linus式架构审计,发现并修复插件系统中的具体问题:

## 核心修复

### 1. 消除local插件GetPorts()方法冗余
- 删除21个local插件中无意义的GetPorts()方法
- 简化local.Plugin接口:移除端口概念
- 理由:本地插件不涉及网络,端口概念完全多余

### 2. 消除web插件GetPorts()方法冗余
- 删除2个web插件中无用的GetPorts()方法
- 简化web.WebPlugin接口:专注智能HTTP检测
- 理由:Web插件使用动态HTTP检测,预定义端口无价值

### 3. 统一插件命名规范
- 统一所有插件接口使用Name()方法(符合Go惯例)
- 消除GetName()与Name()不一致问题
- 简化适配器:不再需要方法名转换

## 技术改进

接口精简:
- local插件:GetName() + GetPorts() → Name()
- web插件:GetName() + GetPorts() → Name()
- services插件:GetName() → Name()(保留GetPorts(),业务必需)

代码减少:
- 删除23个无用GetPorts()方法
- 重命名52个Name()方法
- 简化3个插件接口定义

## 影响范围

修改文件:55个插件文件
代码变更:-155行 +61行(净减少94行)
功能影响:零破坏性,保持所有业务逻辑不变

这是基于业务需求分析的精准重构,消除真正多余的部分,
保持系统架构合理性和向后兼容性。
2025-08-26 20:38:39 +08:00

254 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package local
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
)
// DownloaderPlugin 文件下载插件 - Linus式简化版本
//
// 设计哲学:直接实现,删除过度设计
// - 删除复杂的继承体系
// - 直接实现文件下载功能
// - 保持原有功能逻辑
type DownloaderPlugin struct {
name string
downloadURL string
savePath string
downloadTimeout time.Duration
maxFileSize int64
}
// NewDownloaderPlugin 创建文件下载插件
func NewDownloaderPlugin() *DownloaderPlugin {
return &DownloaderPlugin{
name: "downloader",
downloadURL: common.DownloadURL,
savePath: common.DownloadSavePath,
downloadTimeout: 30 * time.Second,
maxFileSize: 100 * 1024 * 1024, // 100MB
}
}
// GetName 实现Plugin接口
func (p *DownloaderPlugin) Name() string {
return p.name
}
// Scan 执行文件下载任务 - 直接实现
func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
var output strings.Builder
output.WriteString("=== 文件下载 ===\n")
// 验证参数
if err := p.validateParameters(); err != nil {
output.WriteString(fmt.Sprintf("参数验证失败: %v\n", err))
return &ScanResult{
Success: false,
Output: output.String(),
Error: err,
}
}
output.WriteString(fmt.Sprintf("下载URL: %s\n", p.downloadURL))
output.WriteString(fmt.Sprintf("保存路径: %s\n", p.savePath))
output.WriteString(fmt.Sprintf("平台: %s\n\n", runtime.GOOS))
// 检查保存路径权限
if err := p.checkSavePathPermissions(); err != nil {
output.WriteString(fmt.Sprintf("保存路径检查失败: %v\n", err))
return &ScanResult{
Success: false,
Output: output.String(),
Error: err,
}
}
// 执行下载
downloadInfo, err := p.downloadFile(ctx)
if err != nil {
output.WriteString(fmt.Sprintf("下载失败: %v\n", err))
return &ScanResult{
Success: false,
Output: output.String(),
Error: err,
}
}
// 输出下载结果
output.WriteString("✓ 文件下载成功!\n")
output.WriteString(fmt.Sprintf("文件大小: %v bytes\n", downloadInfo["file_size"]))
if contentType, ok := downloadInfo["content_type"]; ok && contentType != "" {
output.WriteString(fmt.Sprintf("文件类型: %v\n", contentType))
}
output.WriteString(fmt.Sprintf("下载用时: %v\n", downloadInfo["download_time"]))
common.LogSuccess(fmt.Sprintf("文件下载完成: %s -> %s (大小: %v bytes)",
p.downloadURL, p.savePath, downloadInfo["file_size"]))
return &ScanResult{
Success: true,
Output: output.String(),
Error: nil,
}
}
// validateParameters 验证输入参数
func (p *DownloaderPlugin) validateParameters() error {
if p.downloadURL == "" {
return fmt.Errorf("下载URL不能为空请使用 -download-url 参数指定")
}
// 验证URL格式
if !strings.HasPrefix(strings.ToLower(p.downloadURL), "http://") &&
!strings.HasPrefix(strings.ToLower(p.downloadURL), "https://") {
return fmt.Errorf("无效的URL格式必须以 http:// 或 https:// 开头")
}
// 如果没有指定保存路径使用URL中的文件名
if p.savePath == "" {
filename := p.extractFilenameFromURL(p.downloadURL)
if filename == "" {
filename = "downloaded_file"
}
p.savePath = filename
}
return nil
}
// extractFilenameFromURL 从URL中提取文件名
func (p *DownloaderPlugin) extractFilenameFromURL(url string) string {
// 移除查询参数
if idx := strings.Index(url, "?"); idx != -1 {
url = url[:idx]
}
// 获取路径的最后一部分
parts := strings.Split(url, "/")
if len(parts) > 0 {
filename := parts[len(parts)-1]
if filename != "" && !strings.Contains(filename, "=") {
return filename
}
}
return ""
}
// checkSavePathPermissions 检查保存路径权限
func (p *DownloaderPlugin) checkSavePathPermissions() error {
// 获取保存目录
saveDir := filepath.Dir(p.savePath)
if saveDir == "." || saveDir == "" {
// 使用当前目录
var err error
saveDir, err = os.Getwd()
if err != nil {
return fmt.Errorf("获取当前目录失败: %v", err)
}
p.savePath = filepath.Join(saveDir, filepath.Base(p.savePath))
}
// 确保目录存在
if err := os.MkdirAll(saveDir, 0755); err != nil {
return fmt.Errorf("创建保存目录失败: %v", err)
}
// 检查写入权限
testFile := filepath.Join(saveDir, ".fscan_write_test")
if file, err := os.Create(testFile); err != nil {
return fmt.Errorf("保存目录无写入权限: %v", err)
} else {
file.Close()
os.Remove(testFile)
}
return nil
}
// downloadFile 执行文件下载
func (p *DownloaderPlugin) downloadFile(ctx context.Context) (map[string]interface{}, error) {
startTime := time.Now()
// 创建带超时的HTTP客户端
client := &http.Client{
Timeout: p.downloadTimeout,
}
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", p.downloadURL, nil)
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
}
// 设置User-Agent
req.Header.Set("User-Agent", "fscan-downloader/1.0")
// 发送请求
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP请求失败状态码: %d %s", resp.StatusCode, resp.Status)
}
// 检查文件大小
contentLength := resp.ContentLength
if contentLength > p.maxFileSize {
return nil, fmt.Errorf("文件过大 (%d bytes),超过最大限制 (%d bytes)",
contentLength, p.maxFileSize)
}
// 创建保存文件
outFile, err := os.Create(p.savePath)
if err != nil {
return nil, fmt.Errorf("创建保存文件失败: %v", err)
}
defer outFile.Close()
// 使用带限制的Reader防止过大文件
limitedReader := io.LimitReader(resp.Body, p.maxFileSize)
// 复制数据
written, err := io.Copy(outFile, limitedReader)
if err != nil {
// 清理部分下载的文件
os.Remove(p.savePath)
return nil, fmt.Errorf("文件下载失败: %v", err)
}
downloadTime := time.Since(startTime)
// 返回下载信息
downloadInfo := map[string]interface{}{
"save_path": p.savePath,
"file_size": written,
"content_type": resp.Header.Get("Content-Type"),
"download_time": downloadTime,
}
return downloadInfo, nil
}
// 注册插件
func init() {
RegisterLocalPlugin("downloader", func() Plugin {
return NewDownloaderPlugin()
})
}