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) GetName() string { return p.name } // GetPorts 实现Plugin接口 - local插件不需要端口 func (p *DownloaderPlugin) GetPorts() []int { return []int{} } // 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() }) }