Достижение: Добавлены скрипты и документация для релиза PDF Compressor.

- Добавлен release-body.md для подробных заметок о релизе на русском языке.
- Реализован release-gitea.ps1 для автоматизированного релиза Gitea с помощью PowerShell.
- Создан release-gitea.sh для автоматизированного релиза Gitea с помощью Bash.
- Добавлен release.sh для сборки и маркировки релизов с поддержкой нескольких платформ.
- Улучшен пользовательский интерфейс благодаря информативному логированию и обработке ошибок.
- Добавлена ​​поддержка переменных окружения и управления конфигурацией.
- Добавлена ​​функция создания архивов и загрузки ресурсов в Gitea.
This commit is contained in:
Dmitriy Fofanov
2025-11-05 09:33:12 +03:00
parent f328d67080
commit ec65cfd05a
43 changed files with 5792 additions and 2 deletions

View 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
}

View 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
}

View 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
}

View 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
}

View 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...)
}
}