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: