- Добавлен файл 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 для двоичных файлов.
402 lines
15 KiB
Go
402 lines
15 KiB
Go
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...)
|
||
}
|
||
}
|