Files
compress/internal/usecase/process_pdfs.go
Dmitriy Fofanov eee9a4a093 Добавлены скрипты сборки для кроссплатформенных двоичных файлов и лицензия GPL.
- Добавлен файл LICENSE с лицензией GNU General Public License версии 3.0.
- Создан скрипт PowerShell (build-all.ps1) для сборки двоичных файлов Windows и Linux из Windows с использованием кросс-компиляции.
- Разработан скрипт сборки Linux (build-linux.sh) для сборки двоичных файлов Linux.
- Реализован скрипт PowerShell (build-windows.ps1) для сборки двоичных файлов Windows.
- Каждый скрипт сборки включает упаковку и генерацию контрольной суммы SHA256 для двоичных файлов.
2025-11-05 13:05:49 +03:00

402 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecases
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
"compress/internal/domain/entities"
"compress/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...)
}
}