fscan/plugins/local/downloader.go
ZacharyZcR 95497da8ca refactor: 优化插件系统设计,消除代码重复
主要改进:
1. 修复Services插件端口数据重复问题
   - 删除插件结构体中的ports字段和GetPorts()方法
   - 系统统一使用注册时的端口信息

2. 引入BasePlugin基础结构体
   - 消除51个插件中重复的name字段和Name()方法
   - 统一插件基础功能,简化代码维护

3. 统一插件接口设计
   - 保持向后兼容,功能完全不变
   - 代码更简洁,符合工程最佳实践

影响范围:
- services插件:29个文件简化
- web插件:2个文件简化
- local插件:21个文件简化
- 总计删除约150行重复代码
2025-09-02 05:36:12 +08:00

251 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"
"github.com/shadow1ng/fscan/plugins"
)
// DownloaderPlugin 文件下载插件 - Linus式简化版本
//
// 设计哲学:直接实现,删除过度设计
// - 删除复杂的继承体系
// - 直接实现文件下载功能
// - 保持原有功能逻辑
type DownloaderPlugin struct {
plugins.BasePlugin
downloadURL string
savePath string
downloadTimeout time.Duration
maxFileSize int64
}
// NewDownloaderPlugin 创建文件下载插件
func NewDownloaderPlugin() *DownloaderPlugin {
return &DownloaderPlugin{
BasePlugin: plugins.NewBasePlugin("downloader"),
downloadURL: common.DownloadURL,
savePath: common.DownloadSavePath,
downloadTimeout: 30 * time.Second,
maxFileSize: 100 * 1024 * 1024, // 100MB
}
}
// 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()
})
}