Достижение: Добавлены скрипты и документация для релиза 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,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...)
}
}