A:更新工具类

This commit is contained in:
jimmy committed 2025-06-29 20:33:11 +08:00
1 parent 39823db70a
commit bae2721935
14 files changed
+598 -26

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ require (
github.com/gin-contrib/pprof v1.5.3
github.com/gin-gonic/gin v1.10.1
github.com/google/uuid v1.6.0
github.com/h2non/filetype v1.1.3
go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.6.0
+2
View File
@@ -38,6 +38,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+64 -1
View File
@@ -1,10 +1,39 @@
package config
import (
"go.uber.org/zap"
"os"
"path/filepath"
"rear/internal/consts"
"rear/internal/utils"
"rear/pkg/logger"
"time"
)
// ImageCompressionOptions 压缩图像配置
type ImageCompressionOptions struct {
// 缩略图格式
ThumbnailFormat consts.ImageFormat
// 缩略图大小
ThumbnailSize []int
// 缩略图质量
ThumbnailQuality int
}
// PathConfig 路径相关配置
type PathConfig struct {
// 缓存内容存放
CachePath string
// 缩略图存放路径
ThumbnailPath string
// 日志路径
LogPath string
// 临时文件路径
TempPath string
// png 临时文件
PngTempPath string
}
// Config 配置结构
type Config struct {
Port string
@@ -12,16 +41,45 @@ type Config struct {
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
// 基础支持的格式
// 基础支持的格式【ImageMagick, libvips】
BaseSupportedFileTypes []string
// 特殊支持的格式
SpecialSupportedFileTypes []string
// 支持的缩略图格式
SupportedThumbnailFormat []string
ImageCompressionOption ImageCompressionOptions
// Raw 单独支持【rawtherapee-cli, dcraw】
// heic 单独支持【heif-convert, magick】
PathConfig PathConfig
// 软件运行目录
AppPath string
AppDir string
}
var CONFIG Config
// InitConfig 初始化配置
func InitConfig() *Config {
i := ImageCompressionOptions{
ThumbnailFormat: consts.FormatJPG,
ThumbnailSize: []int{256, 512, 720},
ThumbnailQuality: 80,
}
pathConfig := PathConfig{
CachePath: "cache",
ThumbnailPath: "thumbnail",
LogPath: "app-logs",
TempPath: "app-tmp",
PngTempPath: "png-tmp",
}
execPath, err := os.Executable()
if err != nil {
logger.Fatal("无法获取程序路径: %v", zap.Error(err))
}
CONFIG = Config{
Port: utils.GetEnv("PORT", "8080"),
Mode: utils.GetEnv("GIN_MODE", "debug"),
@@ -30,6 +88,11 @@ func InitConfig() *Config {
IdleTimeout: 60 * time.Second,
BaseSupportedFileTypes: []string{".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp"},
SpecialSupportedFileTypes: []string{".gif", ".heic", ".heif", ".webp", ".avif", ".jxl"},
SupportedThumbnailFormat: []string{".jpg", ".webp"},
ImageCompressionOption: i,
PathConfig: pathConfig,
AppPath: execPath,
AppDir: filepath.Dir(execPath),
}
return &CONFIG
}
+9
View File
@@ -0,0 +1,9 @@
package consts
type ImageFormat string
const (
FormatJPG ImageFormat = "jpg"
FormatPNG ImageFormat = "png"
FormatWEBP ImageFormat = "webp"
)
+3 -3
View File
@@ -2,14 +2,14 @@ package container
import "rear/internal/repositories"
type Container struct {
type DbContainer struct {
LibraryRepo *repositories.LibraryRepository
UserRepo *repositories.UserService
// 其他服务...
}
func NewContainer() *Container {
return &Container{
func NewContainer() *DbContainer {
return &DbContainer{
LibraryRepo: repositories.NewLibraryRepository(),
UserRepo: repositories.NewUserService(),
}
+6 -2
View File
@@ -8,10 +8,14 @@ type TaskContainer struct {
// 照片任务处理管理
ImgTaskManager *workflow.ImgTaskManager
// 其他服务...
// 数据库服务
DbContainer *DbContainer
}
func NewTaskContainer() *TaskContainer {
func NewTaskContainer(con *DbContainer) *TaskContainer {
return &TaskContainer{
ImgTaskManager: workflow.NewImgTaskManager(10),
DbContainer: con,
ImgTaskManager: workflow.NewImgTaskManager(5),
}
}
+15 -2
View File
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"go.uber.org/zap"
"os"
"os/exec"
"path/filepath"
@@ -17,6 +18,7 @@ import (
// 全局变量存储工具路径
var (
ImageMagickPath string
VipsPath string
ExifToolPath string
toolsInitOnce sync.Once
toolsInitErr error
@@ -27,6 +29,7 @@ type Config struct {
// 工具路径(如果为空,会自动检测)
ImageMagickPath string
ExifToolPath string
VipsPath string
}
// Initialize 初始化工具路径(可选调用,如果不调用会在第一次使用时自动初始化)
@@ -38,6 +41,7 @@ func Initialize(config *Config) error {
toolsInitOnce.Do(func() {
ImageMagickPath = config.ImageMagickPath
ExifToolPath = config.ExifToolPath
VipsPath = config.VipsPath
toolsInitErr = detectTools()
})
@@ -72,6 +76,14 @@ func detectTools() error {
return fmt.Errorf("ExifTool not found")
}
// 检测 libvips
if VipsPath == "" {
VipsPath = findTool("vips", execDir)
}
if VipsPath == "" {
return fmt.Errorf("libvips not found")
}
return nil
}
@@ -94,10 +106,11 @@ func findTool(name string, execDir string) string {
filepath.Join(execDir, "bin", exeName),
filepath.Join(execDir, "tools", exeName),
filepath.Join(execDir, "tools", name, exeName),
filepath.Join(execDir, "tools", name, "bin", exeName),
}
jsonBytes, _ := json.Marshal(searchPaths)
msg := string(jsonBytes)
logger.Warn(msg)
logger.Warn("查找工具路径", zap.String("paths", msg))
for _, path := range searchPaths {
if _, err := os.Stat(path); err == nil {
@@ -115,7 +128,7 @@ func findTool(name string, execDir string) string {
// EnsureInitialized 确保工具已初始化
func EnsureInitialized() error {
if toolsInitErr == nil && ImageMagickPath == "" && ExifToolPath == "" {
if toolsInitErr == nil && ImageMagickPath == "" && ExifToolPath == "" && VipsPath == "" {
return Initialize(nil)
}
return toolsInitErr
+337
View File
@@ -0,0 +1,337 @@
package tools
import (
"context"
"fmt"
"os"
"path/filepath"
"rear/internal/utils"
"strconv"
"strings"
)
// ProcessOptions 图像处理选项
type ProcessOptions struct {
// 基础选项
MaxSize int // 最长边的最大尺寸(保持比例)
Quality int // 压缩质量 (1-100)
Format string // 输出格式 (jpeg, png, webp, avif等)
// 高级选项
Strip bool // 是否移除EXIF等元数据
Interlace bool // 是否启用渐进式JPEG
Optimize bool // 是否启用优化
Background string // 背景色(处理透明图像时)
Sharpen bool // 是否启用锐化
AutoRotate bool // 是否根据EXIF自动旋转
// WebP特定选项
WebPLossless bool // WebP无损压缩
WebPEffort int // WebP压缩努力程度 (0-6)
// 尺寸控制
Width int // 固定宽度(如果设置则忽略MaxSize)
Height int // 固定高度(如果设置则忽略MaxSize)
Crop bool // 是否裁剪以填充指定尺寸
}
// VipsImageInfo vips图片信息
type VipsImageInfo struct {
Format string
Width int
Height int
Channels int
ColorSpace string
FileSize string
}
// DefaultOptions 返回默认处理选项
func DefaultOptions() *ProcessOptions {
return &ProcessOptions{
MaxSize: 512,
Quality: 85,
Format: "jpeg",
Strip: true,
Interlace: false,
Optimize: true,
Background: "white",
Sharpen: false,
AutoRotate: true,
WebPLossless: false,
WebPEffort: 4,
Width: 0,
Height: 0,
Crop: false,
}
}
// ProcessImageWithVips 使用 libvips 处理图像
// 示例: ProcessImageWithVips(ctx, "input.jpg", "output.webp", DefaultOptions())
func ProcessImageWithVips(ctx context.Context, inputPath, outputPath string, options *ProcessOptions) error {
if err := utils.EnsureInitialized(); err != nil {
return err
}
if options == nil {
options = DefaultOptions()
}
// 检查输入文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
return fmt.Errorf("input file not found: %s", inputPath)
}
// 确保输出目录存在
outputDir := filepath.Dir(outputPath)
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// 构建vips命令参数
args := buildVipsArgs(inputPath, outputPath, options)
// 执行命令
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, args...)
if err != nil {
return fmt.Errorf("vips command failed: %w, stderr: %s", err, string(result.Stderr))
}
return nil
}
// ThumbnailImage 生成缩略图(使用vips thumbnail命令)
func ThumbnailImage(ctx context.Context, input, output string, size int) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
MaxSize: size,
Quality: 85,
Strip: true,
})
}
// ResizeImageWithVips 使用vips调整图片大小
func ResizeImageWithVips(ctx context.Context, input, output string, width, height int) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
Width: width,
Height: height,
Quality: 85,
Strip: true,
})
}
// ResizeImageKeepAspectVips 使用vips按比例调整图片大小(保持宽高比)
func ResizeImageKeepAspectVips(ctx context.Context, input, output string, maxSize int) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
MaxSize: maxSize,
Quality: 85,
Strip: true,
})
}
// ConvertImageFormat 转换图像格式
func ConvertImageFormat(ctx context.Context, input, output, format string) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
Format: format,
Quality: 85,
Strip: true,
})
}
// CompressImage 压缩图像
func CompressImage(ctx context.Context, input, output string, quality int) error {
return ProcessImageWithVips(ctx, input, output, &ProcessOptions{
Quality: quality,
Strip: true,
Optimize: true,
})
}
// GetVipsImageInfo 获取图像信息
func GetVipsImageInfo(ctx context.Context, imagePath string) (*VipsImageInfo, error) {
if err := utils.EnsureInitialized(); err != nil {
return nil, err
}
result, err := utils.ExecuteCommand(ctx, utils.VipsPath, "identify", imagePath)
if err != nil {
return nil, fmt.Errorf("failed to get image info: %w", err)
}
// 解析vips identify输出
info := &VipsImageInfo{}
lines := strings.Split(string(result.Stdout), "\n")
for _, line := range lines {
if strings.Contains(line, ":") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch key {
case "format":
info.Format = value
case "width":
if w, err := strconv.Atoi(value); err == nil {
info.Width = w
}
case "height":
if h, err := strconv.Atoi(value); err == nil {
info.Height = h
}
case "channels":
if c, err := strconv.Atoi(value); err == nil {
info.Channels = c
}
case "interpretation":
info.ColorSpace = value
}
}
}
}
// 获取文件大小
if stat, err := os.Stat(imagePath); err == nil {
info.FileSize = fmt.Sprintf("%d bytes", stat.Size())
}
return info, nil
}
// BatchProcessImages 批量处理图像
func BatchProcessImages(ctx context.Context, inputDir, outputDir string, options *ProcessOptions) error {
if options == nil {
options = DefaultOptions()
}
// 支持的图像格式
supportedExts := map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".webp": true,
".tiff": true,
".bmp": true,
".tif": true,
}
return filepath.Walk(inputDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(path))
if !supportedExts[ext] {
return nil
}
// 计算相对路径
relPath, err := filepath.Rel(inputDir, path)
if err != nil {
return err
}
// 构建输出路径
outputPath := filepath.Join(outputDir, relPath)
// 根据输出格式修改扩展名
if options.Format != "" {
outputPath = strings.TrimSuffix(outputPath, filepath.Ext(outputPath)) + "." + options.Format
}
return ProcessImageWithVips(ctx, path, outputPath, options)
})
}
// IsVipsAvailable 检查 libvips 是否可用
func IsVipsAvailable() bool {
if err := utils.EnsureInitialized(); err != nil {
return false
}
return utils.VipsPath != ""
}
// buildVipsArgs 构建vips命令参数
func buildVipsArgs(inputPath, outputPath string, options *ProcessOptions) []string {
args := []string{"thumbnail"}
// 输入文件
args = append(args, inputPath)
// 输出文件
args = append(args, outputPath)
// 尺寸参数
if options.Width > 0 && options.Height > 0 {
if options.Crop {
// 裁剪模式:填充指定尺寸
args = append(args, fmt.Sprintf("%dx%d^", options.Width, options.Height))
args = append(args, "--crop", "centre")
} else {
// 适应模式:保持比例
args = append(args, fmt.Sprintf("%dx%d", options.Width, options.Height))
}
} else if options.Width > 0 {
args = append(args, strconv.Itoa(options.Width))
} else if options.Height > 0 {
args = append(args, fmt.Sprintf("x%d", options.Height))
} else {
// 使用MaxSize(最长边)
args = append(args, strconv.Itoa(options.MaxSize))
}
// 质量设置
if options.Quality > 0 && options.Quality <= 100 {
args = append(args, "--quality", strconv.Itoa(options.Quality))
}
// 格式转换
if options.Format != "" {
args = append(args, "--format", options.Format)
}
// 移除元数据
if options.Strip {
args = append(args, "--strip")
}
// 渐进式JPEG
if options.Interlace {
args = append(args, "--interlace")
}
// 优化
if options.Optimize {
args = append(args, "--optimize")
}
// 背景色
if options.Background != "" {
args = append(args, "--background", options.Background)
}
// 锐化
if options.Sharpen {
args = append(args, "--sharpen", "1")
}
// 自动旋转
if options.AutoRotate {
args = append(args, "--auto-rotate")
}
// WebP特定选项
if strings.ToLower(options.Format) == "webp" {
if options.WebPLossless {
args = append(args, "--webp-lossless")
}
if options.WebPEffort >= 0 && options.WebPEffort <= 6 {
args = append(args, "--webp-effort", strconv.Itoa(options.WebPEffort))
}
}
return args
}
+95 -4
View File
@@ -5,11 +5,16 @@ import (
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/h2non/filetype"
"go.uber.org/zap"
"os"
"os/exec"
"rear/internal/consts"
"rear/internal/utils/tools"
"rear/pkg/logger"
"rear/pkg/utils"
"runtime"
"sync"
"time"
@@ -85,17 +90,100 @@ func (pt *PictureTask) Resume() {
func (pt *PictureTask) Run() {
pt.setStatus(StatusRunning)
// 文件是否存在
if !fileExists(pt.Path) {
pt.setError(errors.New("file does not exist"))
logger.Error(
"指定文件不存在!",
zap.String("path", pt.Path),
)
return
}
pt.waitIfPaused()
// 读取文件
buf, err := os.ReadFile(pt.Path)
if err != nil {
logger.Error(
"文件读取失败!",
zap.String("path", pt.Path),
)
return
}
pt.waitIfPaused()
// 检测照片格式
kind, err := filetype.Match(buf)
if err != nil {
logger.Error(
"文件类型匹配失败!",
zap.String("path", pt.Path),
)
return
}
pt.waitIfPaused()
// 不同图像类型不同的处理方式
fileType := kind.Extension
logger.Info("探测到的文件类型.", zap.String("fileType", fileType))
// 读取 hash
hash, err := utils.HashUtils.HashFile(pt.Path, utils.SHA256)
if err != nil {
logger.Error(
"Hash获取失败!",
zap.String("path", pt.Path),
)
return
}
logger.Info("获取到 Hash", zap.String("hash", hash))
// 获取基本信息,如果图像的很小则不进行压缩
ctx := context.Background()
exifData, err := tools.GetExifData(ctx, pt.Path)
if err != nil {
logger.Error(
"EXIF数据获取失败!",
zap.String("path", pt.Path),
zap.Error(err),
)
return
}
logger.Info("获取到 EXIF 数据", zap.Any("exifData", exifData))
return
// 如果是非常规格式或 raw 则转换为 png ;如果是 PNG 则无损转换为 webp 或 jpg;如果是 webp 或 jpg 则进行压缩和其他处理
if fileType == string(consts.FormatJPG) {
//options := tools.DefaultOptions()
//options.MaxSize = 800
//options.Quality = 90
//options.Format = "jpeg"
//tools.LibVipsUtil.ProcessImage(options)
} else if fileType == string(consts.FormatWEBP) {
} else if fileType == string(consts.FormatPNG) {
} else {
}
// 分割 Hash 路径
//utils.HashUtils.HashThumbPath(config.CONFIG.AppDir, hash)
// 压缩图像
// 获取 exif
// 保存到数据库
pt.waitIfPaused()
data, err := os.ReadFile(pt.Path)
if err != nil {
pt.setError(err)
logger.Error(
"文件读取失败!",
zap.String("filename", pt.Path),
zap.Error(err),
)
return
}
pt.ImageBuf = data
pt.Hash = calcHash(data)
@@ -288,21 +376,23 @@ func getCurrentCPUUsage() int {
// --- 工具函数 ---
func fileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
return utils.FileUtils.Exists(path)
}
func calcHash(data []byte) string {
logger.Info("calcHash")
h := sha1.Sum(data)
return hex.EncodeToString(h[:])
}
func compressImage(data []byte) ([]byte, error) {
logger.Info("compressImage")
time.Sleep(100 * time.Millisecond) // 模拟耗时
return data, nil
}
func readExifViaCmd(filePath string) (map[string]string, error) {
logger.Info("readExifViaCmd")
cmd := exec.Command("exiftool", filePath)
out, err := cmd.Output()
if err != nil {
@@ -322,6 +412,7 @@ func readExifViaCmd(filePath string) (map[string]string, error) {
}
func saveToDB(hash string, exif map[string]string) error {
logger.Info("saveToDB")
fmt.Printf("Saving %s to DB with %d exif fields\n", hash, len(exif))
return nil
}
+39 -13
View File
@@ -5,6 +5,7 @@ import (
"embed"
"errors"
"fmt"
"go.uber.org/zap"
"log"
"net/http"
"os"
@@ -30,6 +31,9 @@ import (
var exiftoolFS embed.FS
func main() {
// 基础配置加载
config.InitConfig()
// 日志初始化
err := logger.InitDefaultLogger()
if err != nil {
@@ -55,12 +59,11 @@ func main() {
// 初始化照片管理任务
newTaskContainer := container.NewTaskContainer(newContainer)
// 创建运行目录
execPath, err := os.Executable()
execDir := filepath.Dir(execPath)
// 创建软件所需的缓存目录等内容
createCachePath(config.CONFIG.AppDir)
// 将 CLI 放到到运行目录
join := filepath.Join(execDir, "tools", "exiftool")
join := filepath.Join(config.CONFIG.AppDir, "tools", "exiftool")
srcDir := `.\tools\windows_amd64\exiftool\exiftool` // 源目录
// 复制整个目录
@@ -69,6 +72,13 @@ func main() {
return
}
join1 := filepath.Join(config.CONFIG.AppDir, "tools", "vips")
srcDir1 := `.\tools\windows_amd64\libvips\vips` // 源目录
if err := utils.FileUtils.CopyDir(srcDir1, join1); err != nil {
fmt.Printf("复制失败: %v\n", err)
return
}
//if err := initDatabase(); err != nil {
// logger.Fatalf("Failed to initialize database: %v", err)
//}
@@ -78,11 +88,27 @@ func main() {
startHttp(newContainer, newTaskContainer)
}
// 创建软件所需的缓存目录等内容
func createCachePath(dir string) {
// 缩略图目录
thumbnailPath := filepath.Join(dir, config.CONFIG.PathConfig.CachePath, config.CONFIG.PathConfig.ThumbnailPath)
err := utils.FileUtils.CreateDir(thumbnailPath)
if err != nil {
logger.Error("缩略图路径创建失败!", zap.String("path", dir), zap.Error(err))
return
}
// 临时文件路径
tempPath := filepath.Join(dir, config.CONFIG.PathConfig.TempPath, config.CONFIG.PathConfig.PngTempPath)
err = utils.FileUtils.CreateDir(tempPath)
if err != nil {
logger.Error("临时文件夹创建失败!", zap.String("path", dir), zap.Error(err))
return
}
}
func startHttp(con *container.DbContainer, imgContain *container.TaskContainer) {
// 配置加载
netConfig := config.InitConfig()
// 设置Gin模式
gin.SetMode(netConfig.Mode)
gin.SetMode(config.CONFIG.Mode)
// 创建Gin引擎
r := gin.New()
@@ -101,7 +127,7 @@ func startHttp(con *container.DbContainer, imgContain *container.TaskContainer)
r.Use(service.ErrorHandlerMiddleware()) // 最后处理错误
// 性能分析 (仅在debug模式下)
if netConfig.Mode == "debug" {
if config.CONFIG.Mode == "debug" {
pprof.Register(r)
}
@@ -110,11 +136,11 @@ func startHttp(con *container.DbContainer, imgContain *container.TaskContainer)
// 创建HTTP服务器
srv := &http.Server{
Addr: ":" + netConfig.Port,
Addr: ":" + config.CONFIG.Port,
Handler: r,
ReadTimeout: netConfig.ReadTimeout,
WriteTimeout: netConfig.WriteTimeout,
IdleTimeout: netConfig.IdleTimeout,
ReadTimeout: config.CONFIG.ReadTimeout,
WriteTimeout: config.CONFIG.WriteTimeout,
IdleTimeout: config.CONFIG.IdleTimeout,
}
// 优雅关闭
@@ -123,7 +149,7 @@ func startHttp(con *container.DbContainer, imgContain *container.TaskContainer)
// 启动服务器
go func() {
logger.Infof("Server starting on port 127.0.0.1:%s", netConfig.Port)
logger.Infof("Server starting on port 127.0.0.1:%s", config.CONFIG.Port)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Fatalf("Failed to start server: %v", err)
// 发送信号给主goroutine,让它知道启动失败
+1 -1
View File
@@ -34,7 +34,7 @@ type fileUtilsStruct struct{}
// FileUtils 是对外暴露的文件工具对象
var FileUtils = fileUtilsStruct{}
// 1. 检查文件或目录是否存在
// Exists 1. 检查文件或目录是否存在
func (fileUtilsStruct) Exists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
+26
View File
@@ -9,6 +9,7 @@ import (
"hash"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
@@ -327,6 +328,31 @@ func (h hashUtilsStruct) HashFileMultipleAlgorithms(filename string, hashTypes [
return results, nil
}
// HashThumbPath 将 Hash 转换为具体路径
func (h hashUtilsStruct) HashThumbPath(
basePath string,
hash string,
filename string, // 文件名,不带扩展名
ext string, // 扩展名,不带点,如 "jpg", "webp"
) string {
var parts []string
start := 0
segments := []int{2, 2, 2}
for _, length := range segments {
end := start + length
if end > len(hash) {
break
}
parts = append(parts, hash[start:end])
start = end
}
parts = append(parts, hash) // 最后一级目录为完整 hash
dir := filepath.Join(append([]string{basePath}, parts...)...)
fullName := fmt.Sprintf("%s.%s", filename, ext)
return filepath.Join(dir, fullName)
}
// 便捷函数
func (h hashUtilsStruct) MD5File(filename string) (string, error) {
return h.HashFile(filename, MD5)