Достижение: Добавлены скрипты и документация для релиза PDF Compressor.
- Добавлен release-body.md для подробных заметок о релизе на русском языке. - Реализован release-gitea.ps1 для автоматизированного релиза Gitea с помощью PowerShell. - Создан release-gitea.sh для автоматизированного релиза Gitea с помощью Bash. - Добавлен release.sh для сборки и маркировки релизов с поддержкой нескольких платформ. - Улучшен пользовательский интерфейс благодаря информативному логированию и обработке ошибок. - Добавлена поддержка переменных окружения и управления конфигурацией. - Добавлена функция создания архивов и загрузки ресурсов в Gitea.
This commit is contained in:
109
internal/usecase/compress_directory.go
Normal file
109
internal/usecase/compress_directory.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"compressor/internal/domain/entities"
|
||||
"compressor/internal/domain/repositories"
|
||||
)
|
||||
|
||||
// CompressDirectoryUseCase сценарий сжатия всех PDF файлов в директории
|
||||
type CompressDirectoryUseCase struct {
|
||||
compressor repositories.PDFCompressor
|
||||
fileRepo repositories.FileRepository
|
||||
configRepo repositories.ConfigRepository
|
||||
}
|
||||
|
||||
// NewCompressDirectoryUseCase создает новый сценарий сжатия директории
|
||||
func NewCompressDirectoryUseCase(
|
||||
compressor repositories.PDFCompressor,
|
||||
fileRepo repositories.FileRepository,
|
||||
configRepo repositories.ConfigRepository,
|
||||
) *CompressDirectoryUseCase {
|
||||
return &CompressDirectoryUseCase{
|
||||
compressor: compressor,
|
||||
fileRepo: fileRepo,
|
||||
configRepo: configRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// DirectoryCompressionResult результат сжатия директории
|
||||
type DirectoryCompressionResult struct {
|
||||
TotalFiles int
|
||||
SuccessCount int
|
||||
FailedCount int
|
||||
Results []*entities.CompressionResult
|
||||
Errors []error
|
||||
}
|
||||
|
||||
// Execute выполняет сжатие всех PDF файлов в директории
|
||||
func (uc *CompressDirectoryUseCase) Execute(inputDir, outputDir string, compressionLevel int) (*DirectoryCompressionResult, error) {
|
||||
// Проверяем существование входной директории
|
||||
if !uc.fileRepo.FileExists(inputDir) {
|
||||
return nil, entities.ErrDirectoryNotFound
|
||||
}
|
||||
|
||||
// Создаем выходную директорию
|
||||
if err := uc.fileRepo.CreateDirectory(outputDir); err != nil {
|
||||
return nil, fmt.Errorf("ошибка создания выходной директории: %w", err)
|
||||
}
|
||||
|
||||
// Получаем список PDF файлов
|
||||
files, err := uc.fileRepo.ListPDFFiles(inputDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка получения списка файлов: %w", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil, entities.ErrNoFilesFound
|
||||
}
|
||||
|
||||
// Создаем конфигурацию сжатия
|
||||
config, err := uc.configRepo.GetCompressionConfig(compressionLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка создания конфигурации: %w", err)
|
||||
}
|
||||
|
||||
// Валидируем конфигурацию
|
||||
if err := uc.configRepo.ValidateConfig(config); err != nil {
|
||||
return nil, fmt.Errorf("ошибка валидации конфигурации: %w", err)
|
||||
}
|
||||
|
||||
result := &DirectoryCompressionResult{
|
||||
TotalFiles: len(files),
|
||||
Results: make([]*entities.CompressionResult, 0, len(files)),
|
||||
Errors: make([]error, 0),
|
||||
}
|
||||
|
||||
// Обрабатываем каждый файл
|
||||
for _, inputFile := range files {
|
||||
fileName := filepath.Base(inputFile)
|
||||
outputFile := filepath.Join(outputDir, fmt.Sprintf("compressed_%s", fileName))
|
||||
|
||||
// Получаем информацию о файле
|
||||
fileInfo, err := uc.fileRepo.GetFileInfo(inputFile)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("ошибка получения информации о файле %s: %w", fileName, err))
|
||||
result.FailedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Выполняем сжатие
|
||||
compressionResult, err := uc.compressor.Compress(inputFile, outputFile, config)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Errorf("ошибка сжатия файла %s: %w", fileName, err))
|
||||
result.FailedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Устанавливаем исходный размер и вычисляем коэффициент сжатия
|
||||
compressionResult.OriginalSize = fileInfo.Size
|
||||
compressionResult.CalculateCompressionRatio()
|
||||
|
||||
result.Results = append(result.Results, compressionResult)
|
||||
result.SuccessCount++
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
175
internal/usecase/compress_images.go
Normal file
175
internal/usecase/compress_images.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"compressor/internal/domain/entities"
|
||||
"compressor/internal/domain/repositories"
|
||||
"compressor/internal/infrastructure/compressors"
|
||||
)
|
||||
|
||||
// CompressImageUseCase обрабатывает сжатие изображений
|
||||
type CompressImageUseCase struct {
|
||||
logger repositories.Logger
|
||||
compressor compressors.ImageCompressor
|
||||
}
|
||||
|
||||
// NewCompressImageUseCase создает новый UseCase для сжатия изображений
|
||||
func NewCompressImageUseCase(logger repositories.Logger, compressor compressors.ImageCompressor) *CompressImageUseCase {
|
||||
return &CompressImageUseCase{
|
||||
logger: logger,
|
||||
compressor: compressor,
|
||||
}
|
||||
}
|
||||
|
||||
// CompressImage сжимает одно изображение
|
||||
func (uc *CompressImageUseCase) CompressImage(inputPath, outputPath string, config *entities.AppCompressionConfig) error {
|
||||
format := compressors.GetImageFormat(inputPath)
|
||||
if format == "" {
|
||||
return fmt.Errorf("неподдерживаемый формат изображения: %s", inputPath)
|
||||
}
|
||||
|
||||
// Проверяем, включено ли сжатие для данного формата
|
||||
switch format {
|
||||
case "jpeg":
|
||||
if !config.EnableJPEG {
|
||||
uc.logger.Info(fmt.Sprintf("Пропуск JPEG файла (сжатие отключено): %s", inputPath))
|
||||
return nil
|
||||
}
|
||||
return uc.compressor.CompressJPEG(inputPath, outputPath, config.JPEGQuality)
|
||||
case "png":
|
||||
if !config.EnablePNG {
|
||||
uc.logger.Info(fmt.Sprintf("Пропуск PNG файла (сжатие отключено): %s", inputPath))
|
||||
return nil
|
||||
}
|
||||
return uc.compressor.CompressPNG(inputPath, outputPath, config.PNGQuality)
|
||||
default:
|
||||
return fmt.Errorf("неподдерживаемый формат изображения: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessImagesInDirectory обрабатывает все изображения в директории
|
||||
func (uc *CompressImageUseCase) ProcessImagesInDirectory(sourceDir, targetDir string, config *entities.AppCompressionConfig, replaceOriginal bool) (*ProcessingResult, error) {
|
||||
result := &ProcessingResult{
|
||||
ProcessedFiles: make([]string, 0),
|
||||
FailedFiles: make([]ProcessingError, 0),
|
||||
SuccessfulFiles: 0,
|
||||
TotalFiles: 0,
|
||||
}
|
||||
|
||||
// Если включены изображения, проверяем настройки
|
||||
if !config.EnableJPEG && !config.EnablePNG {
|
||||
uc.logger.Info("Сжатие изображений отключено в конфигурации")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Рекурсивно обходим директорию
|
||||
err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
uc.logger.Error(fmt.Sprintf("Ошибка доступа к файлу %s: %v", path, err))
|
||||
return nil // Продолжаем обработку других файлов
|
||||
}
|
||||
|
||||
// Пропускаем директории
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Проверяем, является ли файл изображением
|
||||
if !compressors.IsImageFile(path) {
|
||||
return nil // Не изображение, пропускаем
|
||||
}
|
||||
|
||||
result.TotalFiles++
|
||||
|
||||
// Определяем путь выходного файла
|
||||
var outputPath string
|
||||
if replaceOriginal {
|
||||
outputPath = path
|
||||
} else {
|
||||
relPath, err := filepath.Rel(sourceDir, path)
|
||||
if err != nil {
|
||||
uc.logger.Error(fmt.Sprintf("Не удалось получить относительный путь для %s: %v", path, err))
|
||||
result.FailedFiles = append(result.FailedFiles, ProcessingError{
|
||||
FilePath: path,
|
||||
Error: err,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
outputPath = filepath.Join(targetDir, relPath)
|
||||
|
||||
// Создаем директорию для выходного файла
|
||||
outputDir := filepath.Dir(outputPath)
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
uc.logger.Error(fmt.Sprintf("Не удалось создать директорию %s: %v", outputDir, err))
|
||||
result.FailedFiles = append(result.FailedFiles, ProcessingError{
|
||||
FilePath: path,
|
||||
Error: err,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Сжимаем изображение
|
||||
uc.logger.Info(fmt.Sprintf("Сжатие изображения: %s", path))
|
||||
err = uc.CompressImage(path, outputPath, config)
|
||||
if err != nil {
|
||||
uc.logger.Error(fmt.Sprintf("Ошибка сжатия изображения %s: %v", path, err))
|
||||
result.FailedFiles = append(result.FailedFiles, ProcessingError{
|
||||
FilePath: path,
|
||||
Error: err,
|
||||
})
|
||||
} else {
|
||||
result.ProcessedFiles = append(result.ProcessedFiles, path)
|
||||
result.SuccessfulFiles++
|
||||
uc.logger.Info(fmt.Sprintf("Изображение успешно сжато: %s", path))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("ошибка обхода директории %s: %w", sourceDir, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ProcessingResult результат обработки изображений
|
||||
type ProcessingResult struct {
|
||||
ProcessedFiles []string
|
||||
FailedFiles []ProcessingError
|
||||
SuccessfulFiles int
|
||||
TotalFiles int
|
||||
}
|
||||
|
||||
// ProcessingError ошибка обработки файла
|
||||
type ProcessingError struct {
|
||||
FilePath string
|
||||
Error error
|
||||
}
|
||||
|
||||
// GetSupportedImageExtensions возвращает список поддерживаемых расширений изображений
|
||||
func GetSupportedImageExtensions() []string {
|
||||
return []string{".jpg", ".jpeg", ".png"}
|
||||
}
|
||||
|
||||
// CountImageFiles подсчитывает количество изображений в директории
|
||||
func CountImageFiles(dir string) (int, error) {
|
||||
count := 0
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Игнорируем ошибки доступа к файлам
|
||||
}
|
||||
|
||||
if !info.IsDir() && compressors.IsImageFile(path) {
|
||||
count++
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return count, err
|
||||
}
|
||||
73
internal/usecase/compress_pdf.go
Normal file
73
internal/usecase/compress_pdf.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"compressor/internal/domain/entities"
|
||||
"compressor/internal/domain/repositories"
|
||||
)
|
||||
|
||||
// CompressPDFUseCase сценарий сжатия одного PDF файла
|
||||
type CompressPDFUseCase struct {
|
||||
compressor repositories.PDFCompressor
|
||||
fileRepo repositories.FileRepository
|
||||
configRepo repositories.ConfigRepository
|
||||
}
|
||||
|
||||
// NewCompressPDFUseCase создает новый сценарий сжатия PDF
|
||||
func NewCompressPDFUseCase(
|
||||
compressor repositories.PDFCompressor,
|
||||
fileRepo repositories.FileRepository,
|
||||
configRepo repositories.ConfigRepository,
|
||||
) *CompressPDFUseCase {
|
||||
return &CompressPDFUseCase{
|
||||
compressor: compressor,
|
||||
fileRepo: fileRepo,
|
||||
configRepo: configRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute выполняет сжатие PDF файла
|
||||
func (uc *CompressPDFUseCase) Execute(inputPath string, outputPath string, compressionLevel int) (*entities.CompressionResult, error) {
|
||||
// Проверяем существование входного файла
|
||||
if !uc.fileRepo.FileExists(inputPath) {
|
||||
return nil, entities.ErrFileNotFound
|
||||
}
|
||||
|
||||
// Получаем информацию о файле
|
||||
fileInfo, err := uc.fileRepo.GetFileInfo(inputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка получения информации о файле: %w", err)
|
||||
}
|
||||
|
||||
// Создаем конфигурацию сжатия
|
||||
config, err := uc.configRepo.GetCompressionConfig(compressionLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка создания конфигурации: %w", err)
|
||||
}
|
||||
|
||||
// Валидируем конфигурацию
|
||||
if err := uc.configRepo.ValidateConfig(config); err != nil {
|
||||
return nil, fmt.Errorf("ошибка валидации конфигурации: %w", err)
|
||||
}
|
||||
|
||||
// Генерируем имя выходного файла, если не указано
|
||||
if outputPath == "" {
|
||||
ext := filepath.Ext(inputPath)
|
||||
base := inputPath[:len(inputPath)-len(ext)]
|
||||
outputPath = base + "_compressed" + ext
|
||||
}
|
||||
|
||||
// Выполняем сжатие
|
||||
result, err := uc.compressor.Compress(inputPath, outputPath, config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка сжатия файла: %w", err)
|
||||
}
|
||||
|
||||
// Устанавливаем исходный размер
|
||||
result.OriginalSize = fileInfo.Size
|
||||
result.CalculateCompressionRatio()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
137
internal/usecase/process_all_files.go
Normal file
137
internal/usecase/process_all_files.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"compressor/internal/domain/entities"
|
||||
"compressor/internal/domain/repositories"
|
||||
"compressor/internal/infrastructure/compressors"
|
||||
)
|
||||
|
||||
// ProcessAllFilesUseCase сценарий для обработки всех поддерживаемых типов файлов
|
||||
type ProcessAllFilesUseCase struct {
|
||||
pdfProcessor *ProcessPDFsUseCase
|
||||
imageProcessor *CompressImageUseCase
|
||||
logger repositories.Logger
|
||||
}
|
||||
|
||||
// NewProcessAllFilesUseCase создает новый сценарий обработки всех файлов
|
||||
func NewProcessAllFilesUseCase(
|
||||
pdfProcessor *ProcessPDFsUseCase,
|
||||
imageProcessor *CompressImageUseCase,
|
||||
logger repositories.Logger,
|
||||
) *ProcessAllFilesUseCase {
|
||||
return &ProcessAllFilesUseCase{
|
||||
pdfProcessor: pdfProcessor,
|
||||
imageProcessor: imageProcessor,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute выполняет обработку всех поддерживаемых файлов
|
||||
func (uc *ProcessAllFilesUseCase) Execute(config *entities.Config) error {
|
||||
uc.logger.Info("Начинаем обработку файлов")
|
||||
uc.logger.Info("Исходная директория: %s", config.Scanner.SourceDirectory)
|
||||
|
||||
var processedPDFs, processedImages bool
|
||||
|
||||
// Обрабатываем PDF файлы
|
||||
if uc.shouldProcessPDFs(config) {
|
||||
uc.logger.Info("Обработка PDF файлов...")
|
||||
err := uc.pdfProcessor.Execute(config)
|
||||
if err != nil {
|
||||
uc.logger.Error("Ошибка обработки PDF файлов: %v", err)
|
||||
return fmt.Errorf("ошибка обработки PDF файлов: %w", err)
|
||||
}
|
||||
processedPDFs = true
|
||||
uc.logger.Info("Обработка PDF файлов завершена")
|
||||
}
|
||||
|
||||
// Обрабатываем изображения
|
||||
if uc.shouldProcessImages(config) {
|
||||
uc.logger.Info("Обработка изображений...")
|
||||
result, err := uc.imageProcessor.ProcessImagesInDirectory(
|
||||
config.Scanner.SourceDirectory,
|
||||
config.Scanner.TargetDirectory,
|
||||
&config.Compression,
|
||||
config.Scanner.ReplaceOriginal,
|
||||
)
|
||||
if err != nil {
|
||||
uc.logger.Error("Ошибка обработки изображений: %v", err)
|
||||
return fmt.Errorf("ошибка обработки изображений: %w", err)
|
||||
}
|
||||
|
||||
// Логируем результаты обработки изображений
|
||||
uc.logger.Info("Обработка изображений завершена. Всего файлов: %d, Успешно: %d, Ошибок: %d",
|
||||
result.TotalFiles, result.SuccessfulFiles, len(result.FailedFiles))
|
||||
|
||||
for _, failed := range result.FailedFiles {
|
||||
uc.logger.Error("Не удалось обработать изображение %s: %v", failed.FilePath, failed.Error)
|
||||
}
|
||||
|
||||
processedImages = true
|
||||
}
|
||||
|
||||
if !processedPDFs && !processedImages {
|
||||
uc.logger.Warning("Не выбрано ни одного типа файлов для обработки")
|
||||
return fmt.Errorf("не выбрано ни одного типа файлов для обработки")
|
||||
}
|
||||
|
||||
uc.logger.Info("Обработка всех файлов завершена успешно")
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldProcessPDFs проверяет, нужно ли обрабатывать PDF файлы
|
||||
func (uc *ProcessAllFilesUseCase) shouldProcessPDFs(config *entities.Config) bool {
|
||||
// PDF файлы обрабатываются всегда, если есть алгоритм сжатия
|
||||
return config.Compression.Algorithm != ""
|
||||
}
|
||||
|
||||
// shouldProcessImages проверяет, нужно ли обрабатывать изображения
|
||||
func (uc *ProcessAllFilesUseCase) shouldProcessImages(config *entities.Config) bool {
|
||||
return config.Compression.EnableJPEG || config.Compression.EnablePNG
|
||||
}
|
||||
|
||||
// GetSupportedFileTypes возвращает список поддерживаемых типов файлов
|
||||
func (uc *ProcessAllFilesUseCase) GetSupportedFileTypes(config *entities.Config) []string {
|
||||
var types []string
|
||||
|
||||
if uc.shouldProcessPDFs(config) {
|
||||
types = append(types, "PDF")
|
||||
}
|
||||
|
||||
if config.Compression.EnableJPEG {
|
||||
types = append(types, "JPEG")
|
||||
}
|
||||
|
||||
if config.Compression.EnablePNG {
|
||||
types = append(types, "PNG")
|
||||
}
|
||||
|
||||
return types
|
||||
}
|
||||
|
||||
// IsFileSupported проверяет, поддерживается ли данный файл для обработки
|
||||
func (uc *ProcessAllFilesUseCase) IsFileSupported(filename string, config *entities.Config) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
// Проверяем PDF
|
||||
if ext == ".pdf" && uc.shouldProcessPDFs(config) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Проверяем изображения
|
||||
if compressors.IsImageFile(filename) {
|
||||
format := compressors.GetImageFormat(filename)
|
||||
switch format {
|
||||
case "jpeg":
|
||||
return config.Compression.EnableJPEG
|
||||
case "png":
|
||||
return config.Compression.EnablePNG
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
401
internal/usecase/process_pdfs.go
Normal file
401
internal/usecase/process_pdfs.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"compressor/internal/domain/entities"
|
||||
"compressor/internal/domain/repositories"
|
||||
)
|
||||
|
||||
// ProcessPDFsUseCase сценарий автоматической обработки PDF файлов
|
||||
type ProcessPDFsUseCase struct {
|
||||
compressor repositories.PDFCompressor
|
||||
fileRepo repositories.FileRepository
|
||||
configRepo repositories.ConfigRepository
|
||||
logger repositories.Logger
|
||||
progressReporter func(entities.ProcessingStatus)
|
||||
}
|
||||
|
||||
// NewProcessPDFsUseCase создает новый сценарий обработки PDF
|
||||
func NewProcessPDFsUseCase(
|
||||
compressor repositories.PDFCompressor,
|
||||
fileRepo repositories.FileRepository,
|
||||
configRepo repositories.ConfigRepository,
|
||||
logger repositories.Logger,
|
||||
) *ProcessPDFsUseCase {
|
||||
return &ProcessPDFsUseCase{
|
||||
compressor: compressor,
|
||||
fileRepo: fileRepo,
|
||||
configRepo: configRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SetProgressReporter устанавливает функцию для отчета о прогрессе
|
||||
func (uc *ProcessPDFsUseCase) SetProgressReporter(reporter func(entities.ProcessingStatus)) {
|
||||
uc.progressReporter = reporter
|
||||
}
|
||||
|
||||
// reportProgress отправляет обновление прогресса
|
||||
func (uc *ProcessPDFsUseCase) reportProgress(status *entities.ProcessingStatus) {
|
||||
if uc.progressReporter != nil {
|
||||
uc.progressReporter(*status)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute выполняет автоматическую обработку PDF файлов согласно конфигурации
|
||||
func (uc *ProcessPDFsUseCase) Execute(config *entities.Config) error {
|
||||
// Фаза 1: Инициализация
|
||||
status := entities.NewProcessingStatus(0)
|
||||
status.SetPhase(entities.PhaseInitializing, "Инициализация обработки...")
|
||||
uc.reportProgress(status)
|
||||
|
||||
uc.logInfo("╔════════════════════════════════════════════════════════════")
|
||||
uc.logInfo("║ Начало обработки PDF файлов")
|
||||
uc.logInfo("╠════════════════════════════════════════════════════════════")
|
||||
uc.logInfo("║ Исходная директория: %s", config.Scanner.SourceDirectory)
|
||||
|
||||
if config.Scanner.ReplaceOriginal {
|
||||
uc.logInfo("║ Режим: Замена оригинальных файлов")
|
||||
} else {
|
||||
uc.logInfo("║ Целевая директория: %s", config.Scanner.TargetDirectory)
|
||||
}
|
||||
|
||||
uc.logInfo("║ Алгоритм: %s", config.Compression.Algorithm)
|
||||
uc.logInfo("║ Уровень сжатия: %d%%", config.Compression.Level)
|
||||
uc.logInfo("║ Параллельных воркеров: %d", config.Processing.ParallelWorkers)
|
||||
uc.logInfo("╚════════════════════════════════════════════════════════════")
|
||||
|
||||
// Проверяем существование исходной директории
|
||||
if !uc.fileRepo.FileExists(config.Scanner.SourceDirectory) {
|
||||
err := fmt.Errorf("исходная директория не существует: %s", config.Scanner.SourceDirectory)
|
||||
status.Fail(err)
|
||||
uc.reportProgress(status)
|
||||
return err
|
||||
}
|
||||
|
||||
// Создаем целевую директорию, если нужно
|
||||
if !config.Scanner.ReplaceOriginal {
|
||||
if err := uc.fileRepo.CreateDirectory(config.Scanner.TargetDirectory); err != nil {
|
||||
err = fmt.Errorf("ошибка создания целевой директории: %w", err)
|
||||
status.Fail(err)
|
||||
uc.reportProgress(status)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Фаза 2: Сканирование файлов
|
||||
status.SetPhase(entities.PhaseScanning, "Сканирование PDF файлов...")
|
||||
uc.reportProgress(status)
|
||||
uc.logInfo("🔍 Сканирование директории...")
|
||||
|
||||
files, err := uc.fileRepo.ListPDFFiles(config.Scanner.SourceDirectory)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("ошибка получения списка файлов: %w", err)
|
||||
status.Fail(err)
|
||||
uc.reportProgress(status)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
uc.logWarning("⚠️ PDF файлы не найдены в директории: %s", config.Scanner.SourceDirectory)
|
||||
status.Complete()
|
||||
uc.reportProgress(status)
|
||||
return nil
|
||||
}
|
||||
|
||||
status.TotalFiles = len(files)
|
||||
uc.logSuccess("✓ Найдено файлов для обработки: %d", len(files))
|
||||
|
||||
// Создаем конфигурацию сжатия
|
||||
compressionConfig := entities.NewCompressionConfigWithLicense(config.Compression.Level, config.Compression.UniPDFLicenseKey)
|
||||
|
||||
if err := uc.configRepo.ValidateConfig(compressionConfig); err != nil {
|
||||
err = fmt.Errorf("ошибка валидации конфигурации сжатия: %w", err)
|
||||
status.Fail(err)
|
||||
uc.reportProgress(status)
|
||||
return err
|
||||
}
|
||||
|
||||
// Фаза 3: Сжатие файлов
|
||||
status.SetPhase(entities.PhaseCompressing, "Сжатие PDF файлов...")
|
||||
uc.reportProgress(status)
|
||||
uc.logInfo("")
|
||||
uc.logInfo("🔄 Начало сжатия файлов...")
|
||||
uc.logInfo("─────────────────────────────────────────────────────────────")
|
||||
|
||||
// Создаем воркеры для параллельной обработки
|
||||
workers := config.Processing.ParallelWorkers
|
||||
if workers <= 0 {
|
||||
workers = 1
|
||||
}
|
||||
|
||||
// Каналы для координации работы
|
||||
jobs := make(chan string, len(files))
|
||||
results := make(chan *entities.CompressionResult, len(files))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Запускаем воркеров
|
||||
for w := 0; w < workers; w++ {
|
||||
wg.Add(1)
|
||||
go uc.worker(w, jobs, results, &wg, config, compressionConfig, status)
|
||||
}
|
||||
|
||||
// Отправляем задачи воркерам
|
||||
for _, file := range files {
|
||||
jobs <- file
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
// Горутина для сбора результатов
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
// Обрабатываем результаты
|
||||
fileCounter := 0
|
||||
for result := range results {
|
||||
fileCounter++
|
||||
status.AddResult(result)
|
||||
|
||||
// Обновляем текущий файл
|
||||
status.SetCurrentFile(result.CurrentFile, result.OriginalSize)
|
||||
|
||||
// Отправляем обновление прогресса
|
||||
uc.reportProgress(status)
|
||||
|
||||
// Логируем результат обработки файла
|
||||
fileName := filepath.Base(result.CurrentFile)
|
||||
if result.Success && result.Error == nil {
|
||||
uc.logSuccess("[%d/%d] ✓ %s", fileCounter, status.TotalFiles, fileName)
|
||||
uc.logInfo(" └─ Размер: %.2f MB → %.2f MB",
|
||||
float64(result.OriginalSize)/1024/1024,
|
||||
float64(result.CompressedSize)/1024/1024)
|
||||
uc.logInfo(" └─ Сжатие: %.1f%% | Сэкономлено: %.2f MB",
|
||||
result.CompressionRatio,
|
||||
float64(result.SavedSpace)/1024/1024)
|
||||
} else {
|
||||
uc.logError("[%d/%d] ✗ %s", fileCounter, status.TotalFiles, fileName)
|
||||
uc.logError(" └─ Ошибка: %v", result.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// Финальная фаза
|
||||
status.Complete()
|
||||
uc.reportProgress(status)
|
||||
|
||||
// Логируем итоговую статистику
|
||||
uc.logInfo("")
|
||||
uc.logInfo("╔════════════════════════════════════════════════════════════")
|
||||
uc.logInfo("║ Обработка завершена")
|
||||
uc.logInfo("╠════════════════════════════════════════════════════════════")
|
||||
uc.logInfo("║ Время выполнения: %s", status.FormatElapsedTime())
|
||||
uc.logInfo("╠════════════════════════════════════════════════════════════")
|
||||
uc.logInfo("║ Статистика файлов:")
|
||||
uc.logInfo("║ • Всего: %d", status.TotalFiles)
|
||||
uc.logSuccess("║ • Успешно: %d", status.SuccessfulFiles)
|
||||
|
||||
if status.FailedFiles > 0 {
|
||||
uc.logError("║ • Ошибок: %d", status.FailedFiles)
|
||||
}
|
||||
|
||||
if status.SkippedFiles > 0 {
|
||||
uc.logWarning("║ • Пропущено: %d", status.SkippedFiles)
|
||||
}
|
||||
|
||||
if status.TotalOriginalSize > 0 {
|
||||
uc.logInfo("╠════════════════════════════════════════════════════════════")
|
||||
uc.logInfo("║ Статистика сжатия:")
|
||||
uc.logInfo("║ • Исходный размер: %.2f MB", float64(status.TotalOriginalSize)/1024/1024)
|
||||
uc.logInfo("║ • Сжатый размер: %.2f MB", float64(status.TotalCompressedSize)/1024/1024)
|
||||
uc.logSuccess("║ • Среднее сжатие: %.1f%%", status.AverageCompression)
|
||||
uc.logSuccess("║ • Сэкономлено: %.2f MB", float64(status.TotalSavedSpace)/1024/1024)
|
||||
}
|
||||
|
||||
uc.logInfo("╚════════════════════════════════════════════════════════════")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// worker обрабатывает файлы в отдельной горутине
|
||||
func (uc *ProcessPDFsUseCase) worker(
|
||||
id int,
|
||||
jobs <-chan string,
|
||||
results chan<- *entities.CompressionResult,
|
||||
wg *sync.WaitGroup,
|
||||
config *entities.Config,
|
||||
compressionConfig *entities.CompressionConfig,
|
||||
status *entities.ProcessingStatus,
|
||||
) {
|
||||
defer wg.Done()
|
||||
|
||||
for inputFile := range jobs {
|
||||
fileName := filepath.Base(inputFile)
|
||||
|
||||
// Определяем путь выходного файла
|
||||
var outputFile string
|
||||
if config.Scanner.ReplaceOriginal {
|
||||
outputFile = inputFile + ".tmp"
|
||||
} else {
|
||||
// Получаем относительный путь от исходной директории
|
||||
relPath, err := filepath.Rel(config.Scanner.SourceDirectory, inputFile)
|
||||
if err != nil {
|
||||
// Если не удалось получить относительный путь, используем просто имя файла
|
||||
outputFile = filepath.Join(config.Scanner.TargetDirectory, fileName)
|
||||
} else {
|
||||
// Сохраняем структуру директорий
|
||||
outputFile = filepath.Join(config.Scanner.TargetDirectory, relPath)
|
||||
// Создаем директорию для выходного файла
|
||||
outputDir := filepath.Dir(outputFile)
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
results <- &entities.CompressionResult{
|
||||
CurrentFile: inputFile,
|
||||
Success: false,
|
||||
Error: fmt.Errorf("не удалось создать директорию %s: %w", outputDir, err),
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем информацию о файле
|
||||
fileInfo, err := uc.fileRepo.GetFileInfo(inputFile)
|
||||
if err != nil {
|
||||
results <- &entities.CompressionResult{
|
||||
CurrentFile: inputFile,
|
||||
Success: false,
|
||||
Error: fmt.Errorf("ошибка получения информации о файле: %w", err),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Выполняем сжатие с повторными попытками
|
||||
var result *entities.CompressionResult
|
||||
for attempt := 0; attempt < config.Processing.RetryAttempts; attempt++ {
|
||||
result, err = uc.compressor.Compress(inputFile, outputFile, compressionConfig)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if attempt < config.Processing.RetryAttempts-1 {
|
||||
if uc.logger != nil {
|
||||
uc.logger.Warning("Попытка %d/%d для файла %s не удалась: %v",
|
||||
attempt+1, config.Processing.RetryAttempts, fileName, err)
|
||||
}
|
||||
time.Sleep(time.Second * 2) // Пауза перед повторной попыткой
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
results <- &entities.CompressionResult{
|
||||
CurrentFile: inputFile,
|
||||
OriginalSize: fileInfo.Size,
|
||||
Success: false,
|
||||
Error: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Устанавливаем исходный размер и пересчитываем статистику
|
||||
result.CurrentFile = inputFile
|
||||
result.OriginalSize = fileInfo.Size
|
||||
result.CalculateCompressionRatio()
|
||||
|
||||
// Если заменяем оригинал, переименовываем временный файл
|
||||
if config.Scanner.ReplaceOriginal {
|
||||
if err := uc.replaceOriginalFile(inputFile, outputFile); err != nil {
|
||||
result.Success = false
|
||||
result.Error = fmt.Errorf("ошибка замены оригинального файла: %w", err)
|
||||
// Удаляем временный файл при ошибке
|
||||
_ = os.Remove(outputFile)
|
||||
if uc.logger != nil {
|
||||
uc.logger.Error("Не удалось заменить оригинальный файл %s: %v", inputFile, err)
|
||||
}
|
||||
} else {
|
||||
// Успешно заменили - обновляем путь к файлу в результате
|
||||
result.CurrentFile = inputFile
|
||||
if uc.logger != nil {
|
||||
uc.logger.Info("Файл %s успешно заменен сжатой версией", inputFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results <- result
|
||||
}
|
||||
}
|
||||
|
||||
// replaceOriginalFile заменяет оригинальный файл сжатым
|
||||
func (uc *ProcessPDFsUseCase) replaceOriginalFile(originalFile, tempFile string) error {
|
||||
// Проверяем существование временного файла
|
||||
if _, err := os.Stat(tempFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("временный файл не существует: %s", tempFile)
|
||||
}
|
||||
|
||||
if uc.logger != nil {
|
||||
uc.logger.Info("Замена оригинального файла: %s", originalFile)
|
||||
}
|
||||
|
||||
backupFile := originalFile + ".backup"
|
||||
|
||||
// Создаем резервную копию оригинала
|
||||
if err := os.Rename(originalFile, backupFile); err != nil {
|
||||
if uc.logger != nil {
|
||||
uc.logger.Error("Ошибка создания резервной копии %s: %v", originalFile, err)
|
||||
}
|
||||
return fmt.Errorf("ошибка создания резервной копии: %w", err)
|
||||
}
|
||||
|
||||
// Переименовываем временный файл в оригинальный
|
||||
if err := os.Rename(tempFile, originalFile); err != nil {
|
||||
if uc.logger != nil {
|
||||
uc.logger.Error("Ошибка замены файла %s: %v", originalFile, err)
|
||||
}
|
||||
// Восстанавливаем оригинальный файл из резервной копии
|
||||
_ = os.Rename(backupFile, originalFile)
|
||||
return fmt.Errorf("ошибка замены файла: %w", err)
|
||||
}
|
||||
|
||||
// Удаляем резервную копию
|
||||
if err := os.Remove(backupFile); err != nil {
|
||||
if uc.logger != nil {
|
||||
uc.logger.Warning("Не удалось удалить резервную копию %s: %v", backupFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if uc.logger != nil {
|
||||
uc.logger.Info("Оригинальный файл успешно заменен: %s", originalFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Методы для логирования
|
||||
func (uc *ProcessPDFsUseCase) logInfo(format string, args ...interface{}) {
|
||||
if uc.logger != nil {
|
||||
uc.logger.Info(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ProcessPDFsUseCase) logSuccess(format string, args ...interface{}) {
|
||||
if uc.logger != nil {
|
||||
uc.logger.Success(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ProcessPDFsUseCase) logWarning(format string, args ...interface{}) {
|
||||
if uc.logger != nil {
|
||||
uc.logger.Warning(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ProcessPDFsUseCase) logError(format string, args ...interface{}) {
|
||||
if uc.logger != nil {
|
||||
uc.logger.Error(format, args...)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user