A:更新工具类
This commit is contained in:
1 parent
39823db70a
commit
bae2721935
14 files changed
+598
-26
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package consts
|
||||
|
||||
type ImageFormat string
|
||||
|
||||
const (
|
||||
FormatJPG ImageFormat = "jpg"
|
||||
FormatPNG ImageFormat = "png"
|
||||
FormatWEBP ImageFormat = "webp"
|
||||
)
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
File renamed without changes.
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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,让它知道启动失败
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user