🎨 大部分的 make 指令

This commit is contained in:
zhaoyupeng
2025-11-24 18:37:44 +08:00
commit 27fa38aef0
38 changed files with 4356 additions and 0 deletions

225
pkg/archiver/archiver.go Normal file
View File

@@ -0,0 +1,225 @@
package archiver
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"gitea.loveuer.com/yizhisec/pkg3/logger"
)
// Options defines options for downloading and extracting archives
type Options struct {
// InsecureSkipVerify skips TLS certificate verification (equivalent to wget --no-check-certificate)
InsecureSkipVerify bool
// HTTPClient allows providing a custom HTTP client
HTTPClient *http.Client
// OnProgress is called during extraction with the current file being extracted
OnProgress func(filename string, index int, total int)
// IsGzipped explicitly specifies whether the archive is gzip compressed
// If nil, auto-detect based on file extension (.tar.gz, .tgz)
IsGzipped *bool
}
// Option is a functional option for configuring the archiver
type Option func(*Options)
// WithInsecureSkipVerify skips TLS certificate verification
func WithInsecureSkipVerify() Option {
return func(o *Options) {
o.InsecureSkipVerify = true
}
}
// WithHTTPClient sets a custom HTTP client
func WithHTTPClient(client *http.Client) Option {
return func(o *Options) {
o.HTTPClient = client
}
}
// WithProgress sets a progress callback
func WithProgress(callback func(filename string, index int, total int)) Option {
return func(o *Options) {
o.OnProgress = callback
}
}
// WithGzipCompression explicitly sets whether the archive is gzip compressed
func WithGzipCompression(isGzipped bool) Option {
return func(o *Options) {
o.IsGzipped = &isGzipped
}
}
// DownloadAndExtract downloads a tar or tar.gz file from URL and extracts it to destDir
// Supports both .tar and .tar.gz formats
func DownloadAndExtract(ctx context.Context, url, destDir string, opts ...Option) error {
options := &Options{
InsecureSkipVerify: false,
}
for _, opt := range opts {
opt(options)
}
logger.Debug("开始下载和解压: %s -> %s", url, destDir)
// Create HTTP client
client := options.HTTPClient
if client == nil {
client = &http.Client{}
if options.InsecureSkipVerify {
logger.Debug("TLS 证书验证已禁用")
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
}
// Download the file
logger.Debug("发起 HTTP 请求: %s", url)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.Debug("创建请求失败: %v", err)
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
logger.Debug("下载失败: %v", err)
return fmt.Errorf("failed to download: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Debug("HTTP 状态码异常: %d", resp.StatusCode)
return fmt.Errorf("bad status: %s", resp.Status)
}
logger.Debug("下载成功,准备解压")
// Determine if the file is gzipped
var reader io.Reader = resp.Body
var isGzipped bool
if options.IsGzipped != nil {
// Explicitly specified by option
isGzipped = *options.IsGzipped
logger.Debug("压缩格式由选项指定: isGzipped=%v", isGzipped)
} else {
// Auto-detect based on file extension
isGzipped = strings.HasSuffix(url, ".tar.gz") || strings.HasSuffix(url, ".tgz")
logger.Debug("根据文件扩展名自动检测压缩格式: isGzipped=%v", isGzipped)
}
if isGzipped {
logger.Debug("使用 gzip 解压")
gzReader, err := gzip.NewReader(resp.Body)
if err != nil {
logger.Debug("创建 gzip reader 失败: %v", err)
return fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzReader.Close()
reader = gzReader
} else {
logger.Debug("tar 格式(未压缩)")
}
// Extract tar archive
if err := extractTar(reader, destDir, options); err != nil {
return fmt.Errorf("failed to extract tar: %w", err)
}
logger.Debug("解压完成")
return nil
}
// extractTar extracts a tar archive to the destination directory
func extractTar(r io.Reader, destDir string, options *Options) error {
tarReader := tar.NewReader(r)
fileCount := 0
totalFiles := 0
// First pass: count total files (optional, for progress reporting)
// For now, we'll just extract directly
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
logger.Debug("读取 tar 条目失败: %v", err)
return fmt.Errorf("failed to read tar header: %w", err)
}
target := filepath.Join(destDir, header.Name)
logger.Debug("解压文件: %s", header.Name)
// Call progress callback if provided
if options.OnProgress != nil {
options.OnProgress(header.Name, fileCount, totalFiles)
}
switch header.Typeflag {
case tar.TypeDir:
// Create directory
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
logger.Debug("创建目录失败 %s: %v", target, err)
return fmt.Errorf("failed to create directory %s: %w", target, err)
}
case tar.TypeReg:
// Create regular file
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
logger.Debug("创建父目录失败 %s: %v", filepath.Dir(target), err)
return fmt.Errorf("failed to create parent directory: %w", err)
}
outFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
logger.Debug("创建文件失败 %s: %v", target, err)
return fmt.Errorf("failed to create file %s: %w", target, err)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
logger.Debug("写入文件失败 %s: %v", target, err)
return fmt.Errorf("failed to write file %s: %w", target, err)
}
outFile.Close()
fileCount++
case tar.TypeSymlink:
// Create symbolic link
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
logger.Debug("创建符号链接父目录失败 %s: %v", filepath.Dir(target), err)
return fmt.Errorf("failed to create parent directory for symlink: %w", err)
}
// Remove existing file/link if exists
os.Remove(target)
if err := os.Symlink(header.Linkname, target); err != nil {
logger.Debug("创建符号链接失败 %s -> %s: %v", target, header.Linkname, err)
return fmt.Errorf("failed to create symlink %s -> %s: %w", target, header.Linkname, err)
}
fileCount++
default:
logger.Debug("跳过不支持的文件类型: %s (type: %v)", header.Name, header.Typeflag)
}
}
logger.Debug("解压完成,共 %d 个文件", fileCount)
return nil
}