package utils import ( "bytes" "fmt" "go-site-clone/config" "go-site-clone/types" "io" "log" "net/http" "net/url" "os" "path" "path/filepath" "regexp" "strings" "time" ) type File struct{} type FileDir struct { Name string `json:"name"` Size int64 `json:"size"` Mode string `json:"mode"` ModTime time.Time `json:"modTime"` } // 下载文件到本地 func (*File) Download(uri string) string { return downloadWithRetry(uri, false, 3, nil) } // DownloadWithOptions 带配置选项的下载 func (*File) DownloadWithOptions(uri string, options *types.DownloadOptions) string { return downloadWithRetry(uri, false, 3, options) } // downloadWithRetry 带重试的下载函数 func downloadWithRetry(uri string, isHTML bool, maxRetries int, options *types.DownloadOptions) string { // 如果没有提供配置,使用默认配置 if options == nil { defaultOpts := types.DefaultDownloadOptions() options = &defaultOpts } // 解析文件链接及路径 u, err := url.Parse(uri) if err != nil { log.Printf("URL解析失败 %s: %v", uri, err) return "" } host := u.Hostname() if u.Port() != "" { host += ":" + u.Port() } // 获取路径部分 filePath := u.Path if isHTML && (filePath == "" || filePath == "/") { filePath = "/index.html" } else if isHTML && path.Base(filePath) == path.Dir(filePath) { filePath = filePath + "/index.html" } appConfig, _ := config.LoadConfig() fp := filepath.Join(appConfig.SiteFileDir, host, filePath) // 检查文件是否已存在 if _, err := os.Stat(fp); err == nil { log.Printf("文件已存在,跳过: %s", fp) return fp } // 重试逻辑 var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { log.Printf("重试下载 (%d/%d): %s", attempt+1, maxRetries, uri) time.Sleep(time.Second * time.Duration(attempt)) // 递增延迟 } // 创建带超时的HTTP客户端 client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Get(uri) if err != nil { lastErr = fmt.Errorf("HTTP请求失败: %w", err) continue } if resp.StatusCode != http.StatusOK { resp.Body.Close() lastErr = fmt.Errorf("HTTP状态码错误: %d", resp.StatusCode) continue } // 创建本地文件 outFile, err := CreateFileWithDirs(fp) if err != nil { resp.Body.Close() lastErr = fmt.Errorf("文件创建失败: %w", err) continue } // 如果是HTML文件,需要替换资源路径 if isHTML { // 读取HTML内容 htmlContent, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { outFile.Close() os.Remove(fp) lastErr = fmt.Errorf("读取HTML内容失败: %w", err) continue } // 替换资源路径 modifiedHTML := replaceHTMLResourcePaths(string(htmlContent), uri, options) // 写入修改后的HTML _, err = outFile.WriteString(modifiedHTML) outFile.Close() if err != nil { os.Remove(fp) lastErr = fmt.Errorf("写入HTML文件失败: %w", err) continue } } else { // 非 HTML 文件,直接写入 _, err = io.Copy(outFile, resp.Body) resp.Body.Close() outFile.Close() if err != nil { os.Remove(fp) // 删除不完整的文件 lastErr = fmt.Errorf("写入文件失败: %w", err) continue } } log.Printf("下载完成: %s", fp) return fp } log.Printf("下载失败(已重试%d次)%s: %v", maxRetries, uri, lastErr) return "" } // 创建目录 func CreateFileWithDirs(filePath string) (*os.File, error) { // 取出目录部分 dir := filepath.Dir(filePath) // 确保目录存在(递归创建) if err := os.MkdirAll(dir, os.ModePerm); err != nil { return nil, fmt.Errorf("创建目录失败: %w", err) } // 创建文件(如果已存在会清空) f, err := os.Create(filePath) if err != nil { return nil, fmt.Errorf("创建文件失败: %w", err) } return f, nil } // 下载html文件到本地 func (*File) HTMLDownload(uri string) string { return downloadWithRetry(uri, true, 3, nil) } // HTMLDownloadWithOptions 带配置选项的HTML下载 func (*File) HTMLDownloadWithOptions(uri string, options *types.DownloadOptions) string { return downloadWithRetry(uri, true, 3, options) } // replaceHTMLResourcePaths 替换HTML中的资源路径为相对路径 func replaceHTMLResourcePaths(htmlContent string, baseURL string, options *types.DownloadOptions) string { if options == nil { defaultOpts := types.DefaultDownloadOptions() options = &defaultOpts } parsedBase, err := url.Parse(baseURL) if err != nil { return htmlContent } baseHost := parsedBase.Hostname() if parsedBase.Port() != "" { baseHost += ":" + parsedBase.Port() } baseScheme := parsedBase.Scheme basePath := parsedBase.Path // 如果基础路径不是以/结尾,去掉文件名部分 if basePath != "" && basePath != "/" { basePath = path.Dir(basePath) } if basePath == "." { basePath = "/" } modified := htmlContent // 1. 替换 CSS 链接: cssRegex := regexp.MustCompile(`(]*?href=["'])([^"']+)(["'][^>]*?>)`) modified = cssRegex.ReplaceAllStringFunc(modified, func(match string) string { parts := cssRegex.FindStringSubmatch(match) if len(parts) == 4 { originalURL := parts[2] if newPath := convertToRelativePath(originalURL, baseURL, baseHost, baseScheme, basePath, "css", options); newPath != "" { return parts[1] + newPath + parts[3] } } return match }) // 2. 替换 JavaScript: