Достижение: Добавлены скрипты и документация для релиза PDF Compressor.
- Добавлен release-body.md для подробных заметок о релизе на русском языке. - Реализован release-gitea.ps1 для автоматизированного релиза Gitea с помощью PowerShell. - Создан release-gitea.sh для автоматизированного релиза Gitea с помощью Bash. - Добавлен release.sh для сборки и маркировки релизов с поддержкой нескольких платформ. - Улучшен пользовательский интерфейс благодаря информативному логированию и обработке ошибок. - Добавлена поддержка переменных окружения и управления конфигурацией. - Добавлена функция создания архивов и загрузки ресурсов в Gitea.
This commit is contained in:
259
internal/infrastructure/compressors/image_compressor.go
Normal file
259
internal/infrastructure/compressors/image_compressor.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package compressors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/nfnt/resize"
|
||||
)
|
||||
|
||||
// ImageCompressor интерфейс для сжатия изображений
|
||||
type ImageCompressor interface {
|
||||
CompressJPEG(inputPath, outputPath string, quality int) error
|
||||
CompressPNG(inputPath, outputPath string, quality int) error
|
||||
}
|
||||
|
||||
// DefaultImageCompressor реализация компрессора изображений
|
||||
type DefaultImageCompressor struct{}
|
||||
|
||||
// NewImageCompressor создает новый компрессор изображений
|
||||
func NewImageCompressor() ImageCompressor {
|
||||
return &DefaultImageCompressor{}
|
||||
}
|
||||
|
||||
// CompressJPEG сжимает JPEG файл с указанным качеством
|
||||
func (c *DefaultImageCompressor) CompressJPEG(inputPath, outputPath string, quality int) error {
|
||||
// Открываем исходный файл
|
||||
inputFile, err := os.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось открыть файл %s: %w", inputPath, err)
|
||||
}
|
||||
defer inputFile.Close()
|
||||
|
||||
// Декодируем изображение
|
||||
img, err := jpeg.Decode(inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось декодировать JPEG файл %s: %w", inputPath, err)
|
||||
}
|
||||
|
||||
// Получаем размер исходного файла для сравнения
|
||||
inputFileInfo, err := inputFile.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось получить информацию о файле %s: %w", inputPath, err)
|
||||
}
|
||||
originalSize := inputFileInfo.Size()
|
||||
|
||||
// Вычисляем новый размер на основе качества
|
||||
bounds := img.Bounds()
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
|
||||
// Более агрессивное уменьшение размера для достижения реального сжатия
|
||||
// quality 10 -> 0.5 (50%), quality 50 -> 0.9 (90%)
|
||||
scaleFactor := 0.5 + float64(quality-10)/40.0*0.4
|
||||
if scaleFactor > 1.0 {
|
||||
scaleFactor = 1.0
|
||||
}
|
||||
|
||||
newWidth := uint(float64(width) * scaleFactor)
|
||||
newHeight := uint(float64(height) * scaleFactor)
|
||||
|
||||
// Изменяем размер изображения только если есть реальная польза
|
||||
var finalImg image.Image
|
||||
if newWidth < uint(width) && newHeight < uint(height) {
|
||||
finalImg = resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
|
||||
} else {
|
||||
finalImg = img
|
||||
}
|
||||
|
||||
// Маппинг качества: 10->30, 30->55, 50->75 (более консервативно)
|
||||
jpegQuality := 20 + int(float64(quality-10)/40.0*55.0)
|
||||
if jpegQuality < 20 {
|
||||
jpegQuality = 20
|
||||
}
|
||||
if jpegQuality > 75 {
|
||||
jpegQuality = 75
|
||||
}
|
||||
|
||||
// Создаем временный файл для проверки результата
|
||||
tmpPath := outputPath + ".tmp"
|
||||
tmpFile, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось создать временный файл: %w", err)
|
||||
}
|
||||
|
||||
// Кодируем во временный файл
|
||||
options := &jpeg.Options{Quality: jpegQuality}
|
||||
err = jpeg.Encode(tmpFile, finalImg, options)
|
||||
tmpFile.Close()
|
||||
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("не удалось закодировать JPEG: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем размер результата
|
||||
tmpInfo, err := os.Stat(tmpPath)
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("не удалось получить информацию о временном файле: %w", err)
|
||||
}
|
||||
|
||||
// Если сжатие неэффективно (файл больше или почти такой же), копируем оригинал
|
||||
if tmpInfo.Size() >= originalSize*95/100 {
|
||||
os.Remove(tmpPath)
|
||||
// Копируем оригинал
|
||||
inputFile.Seek(0, 0)
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось создать выходной файл: %w", err)
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
_, err = io.Copy(outputFile, inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось скопировать файл: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Переименовываем временный файл в выходной
|
||||
err = os.Rename(tmpPath, outputPath)
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("не удалось переименовать временный файл: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompressPNG сжимает PNG файл с указанным качеством
|
||||
func (c *DefaultImageCompressor) CompressPNG(inputPath, outputPath string, quality int) error {
|
||||
// Открываем исходный файл
|
||||
inputFile, err := os.Open(inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось открыть файл %s: %w", inputPath, err)
|
||||
}
|
||||
defer inputFile.Close()
|
||||
|
||||
// Получаем размер исходного файла для сравнения
|
||||
inputFileInfo, err := inputFile.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось получить информацию о файле %s: %w", inputPath, err)
|
||||
}
|
||||
originalSize := inputFileInfo.Size()
|
||||
|
||||
// Декодируем изображение
|
||||
img, err := png.Decode(inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось декодировать PNG файл %s: %w", inputPath, err)
|
||||
}
|
||||
|
||||
// Вычисляем новый размер на основе качества
|
||||
bounds := img.Bounds()
|
||||
width := bounds.Dx()
|
||||
height := bounds.Dy()
|
||||
|
||||
// Более консервативное масштабирование для PNG
|
||||
// quality 10 -> 0.6 (60%), quality 50 -> 0.9 (90%)
|
||||
scaleFactor := 0.6 + float64(quality-10)/40.0*0.3
|
||||
if scaleFactor > 1.0 {
|
||||
scaleFactor = 1.0
|
||||
}
|
||||
|
||||
newWidth := uint(float64(width) * scaleFactor)
|
||||
newHeight := uint(float64(height) * scaleFactor)
|
||||
|
||||
// Не изменяем размер для маленьких изображений
|
||||
if width < 400 && height < 400 {
|
||||
newWidth = uint(width)
|
||||
newHeight = uint(height)
|
||||
}
|
||||
|
||||
// Изменяем размер изображения только если это даст выигрыш
|
||||
var finalImg image.Image
|
||||
if newWidth < uint(width) && newHeight < uint(height) {
|
||||
finalImg = resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
|
||||
} else {
|
||||
finalImg = img
|
||||
}
|
||||
|
||||
// Создаем временный файл для проверки результата
|
||||
tmpPath := outputPath + ".tmp"
|
||||
tmpFile, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось создать временный файл: %w", err)
|
||||
}
|
||||
|
||||
// Для PNG используем максимальное сжатие
|
||||
encoder := &png.Encoder{
|
||||
CompressionLevel: png.BestCompression,
|
||||
}
|
||||
|
||||
err = encoder.Encode(tmpFile, finalImg)
|
||||
tmpFile.Close()
|
||||
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("не удалось закодировать PNG: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем размер результата
|
||||
tmpInfo, err := os.Stat(tmpPath)
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("не удалось получить информацию о временном файле: %w", err)
|
||||
}
|
||||
|
||||
// Если сжатие неэффективно (файл больше или почти такой же), копируем оригинал
|
||||
if tmpInfo.Size() >= originalSize*95/100 {
|
||||
os.Remove(tmpPath)
|
||||
// Копируем оригинал
|
||||
inputFile.Seek(0, 0)
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось создать выходной файл: %w", err)
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
_, err = io.Copy(outputFile, inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("не удалось скопировать файл: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Переименовываем временный файл в выходной
|
||||
err = os.Rename(tmpPath, outputPath)
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("не удалось переименовать временный файл: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsImageFile проверяет, является ли файл изображением поддерживаемого формата
|
||||
func IsImageFile(filename string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
return ext == ".jpg" || ext == ".jpeg" || ext == ".png"
|
||||
}
|
||||
|
||||
// GetImageFormat возвращает формат изображения по расширению файла
|
||||
func GetImageFormat(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
return "jpeg"
|
||||
case ".png":
|
||||
return "png"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
69
internal/infrastructure/compressors/pdfcpu_compressor.go
Normal file
69
internal/infrastructure/compressors/pdfcpu_compressor.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package compressors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
|
||||
"compressor/internal/domain/entities"
|
||||
)
|
||||
|
||||
// PDFCPUCompressor реализация компрессора с использованием PDFCPU
|
||||
type PDFCPUCompressor struct{}
|
||||
|
||||
// NewPDFCPUCompressor создает новый PDFCPU компрессор
|
||||
func NewPDFCPUCompressor() *PDFCPUCompressor {
|
||||
return &PDFCPUCompressor{}
|
||||
}
|
||||
|
||||
// Compress сжимает PDF файл используя PDFCPU библиотеку
|
||||
func (p *PDFCPUCompressor) Compress(inputPath, outputPath string, config *entities.CompressionConfig) (*entities.CompressionResult, error) {
|
||||
fmt.Printf("🔄 Сжатие PDF с уровнем %d%% (PDFCPU)...\n", config.Level)
|
||||
|
||||
// Получаем исходный размер файла
|
||||
originalInfo, err := os.Stat(inputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка получения информации об исходном файле: %w", err)
|
||||
}
|
||||
|
||||
// Применяем настройки в зависимости от уровня сжатия
|
||||
if config.ImageCompression {
|
||||
fmt.Printf("📸 Включено сжатие изображений (качество: %d%%)\n", config.ImageQuality)
|
||||
}
|
||||
|
||||
if config.RemoveDuplicates {
|
||||
fmt.Println("🔄 Удаление дубликатов объектов")
|
||||
}
|
||||
|
||||
// Выполняем оптимизацию с базовыми настройками
|
||||
err = api.OptimizeFile(inputPath, outputPath, nil)
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка оптимизации PDFCPU: %w", err)
|
||||
}
|
||||
|
||||
// Получаем размер сжатого файла
|
||||
compressedInfo, err := os.Stat(outputPath)
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка получения информации о сжатом файле: %w", err)
|
||||
}
|
||||
|
||||
result := &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
CompressedSize: compressedInfo.Size(),
|
||||
Success: true,
|
||||
}
|
||||
|
||||
result.CalculateCompressionRatio()
|
||||
|
||||
fmt.Printf("✅ Сжатие завершено: %s\n", outputPath)
|
||||
return result, nil
|
||||
}
|
||||
145
internal/infrastructure/compressors/unipdf_compressor.go
Normal file
145
internal/infrastructure/compressors/unipdf_compressor.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package compressors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/unidoc/unipdf/v3/common"
|
||||
"github.com/unidoc/unipdf/v3/model"
|
||||
"github.com/unidoc/unipdf/v3/model/optimize"
|
||||
|
||||
"compressor/internal/domain/entities"
|
||||
)
|
||||
|
||||
// UniPDFCompressor реализация компрессора с использованием UniPDF
|
||||
type UniPDFCompressor struct{}
|
||||
|
||||
// NewUniPDFCompressor создает новый UniPDF компрессор
|
||||
func NewUniPDFCompressor() *UniPDFCompressor {
|
||||
return &UniPDFCompressor{}
|
||||
}
|
||||
|
||||
// Compress сжимает PDF файл используя UniPDF библиотеку
|
||||
func (u *UniPDFCompressor) Compress(inputPath, outputPath string, config *entities.CompressionConfig) (*entities.CompressionResult, error) {
|
||||
fmt.Printf("🔄 Сжатие PDF с уровнем %d%% (UniPDF)...\n", config.Level)
|
||||
|
||||
// Инициализируем логгер
|
||||
common.SetLogger(common.NewConsoleLogger(common.LogLevelInfo))
|
||||
|
||||
// Проверяем лицензионный ключ из конфигурации или переменной окружения
|
||||
licenseKey := config.UniPDFLicenseKey
|
||||
if licenseKey == "" {
|
||||
licenseKey = os.Getenv("UNIDOC_LICENSE_API_KEY")
|
||||
}
|
||||
|
||||
if licenseKey == "" {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: 0,
|
||||
Success: false,
|
||||
Error: fmt.Errorf("UniPDF требует лицензионный ключ. Установите его в конфигурации или в переменной UNIDOC_LICENSE_API_KEY. Используйте алгоритм 'pdfcpu' для бесплатной обработки или получите ключ на https://cloud.unidoc.io"),
|
||||
}, fmt.Errorf("UniPDF лицензия не настроена. Установите лицензионный ключ в конфигурации или используйте алгоритм 'pdfcpu'")
|
||||
}
|
||||
|
||||
// Устанавливаем лицензионный ключ
|
||||
fmt.Printf("🔑 Устанавливаем лицензионный ключ UniPDF...\n")
|
||||
os.Setenv("UNIDOC_LICENSE_API_KEY", licenseKey) // Получаем исходный размер файла
|
||||
originalInfo, err := os.Stat(inputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка получения информации об исходном файле: %w", err)
|
||||
}
|
||||
|
||||
// Открываем исходный PDF файл
|
||||
pdfReader, file, err := model.NewPdfReaderFromFile(inputPath, nil)
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка открытия файла: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Создаем writer с оптимизацией
|
||||
pdfWriter := model.NewPdfWriter()
|
||||
|
||||
// Настраиваем оптимизацию в зависимости от уровня сжатия
|
||||
optimizer := optimize.New(optimize.Options{
|
||||
CombineDuplicateDirectObjects: true,
|
||||
CombineIdenticalIndirectObjects: true,
|
||||
ImageUpperPPI: float64(150 - config.Level), // чем выше уровень, тем ниже PPI
|
||||
ImageQuality: 100 - config.Level, // чем выше уровень, тем ниже качество
|
||||
})
|
||||
|
||||
pdfWriter.SetOptimizer(optimizer)
|
||||
|
||||
// Копируем страницы
|
||||
numPages, err := pdfReader.GetNumPages()
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка получения количества страниц: %w", err)
|
||||
}
|
||||
|
||||
for i := 1; i <= numPages; i++ {
|
||||
page, err := pdfReader.GetPage(i)
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка получения страницы %d: %w", i, err)
|
||||
}
|
||||
|
||||
err = pdfWriter.AddPage(page)
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка добавления страницы %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем оптимизированный файл
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка создания выходного файла: %w", err)
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
err = pdfWriter.Write(outputFile)
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка записи файла: %w", err)
|
||||
}
|
||||
|
||||
// Получаем размер сжатого файла
|
||||
compressedInfo, err := os.Stat(outputPath)
|
||||
if err != nil {
|
||||
return &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, fmt.Errorf("ошибка получения информации о сжатом файле: %w", err)
|
||||
}
|
||||
|
||||
result := &entities.CompressionResult{
|
||||
OriginalSize: originalInfo.Size(),
|
||||
CompressedSize: compressedInfo.Size(),
|
||||
Success: true,
|
||||
}
|
||||
|
||||
result.CalculateCompressionRatio()
|
||||
|
||||
fmt.Printf("✅ Сжатие завершено: %s\n", outputPath)
|
||||
return result, nil
|
||||
}
|
||||
74
internal/infrastructure/config/repository.go
Normal file
74
internal/infrastructure/config/repository.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"compressor/internal/domain/entities"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Repository реализация репозитория конфигурации
|
||||
type Repository struct{}
|
||||
|
||||
// NewRepository создает новый репозиторий конфигурации
|
||||
func NewRepository() *Repository {
|
||||
return &Repository{}
|
||||
}
|
||||
|
||||
// Load загружает конфигурацию из файла
|
||||
func (r *Repository) Load(configPath string) (*entities.Config, error) {
|
||||
// Если файл не существует, создаем конфигурацию по умолчанию
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return r.createDefaultConfig(), nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config entities.Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Save сохраняет конфигурацию в файл
|
||||
func (r *Repository) Save(configPath string, config *entities.Config) error {
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
// createDefaultConfig создает конфигурацию по умолчанию
|
||||
func (r *Repository) createDefaultConfig() *entities.Config {
|
||||
return &entities.Config{
|
||||
Scanner: entities.ScannerConfig{
|
||||
SourceDirectory: "./pdfs",
|
||||
TargetDirectory: "./compressed",
|
||||
ReplaceOriginal: false,
|
||||
},
|
||||
Compression: entities.AppCompressionConfig{
|
||||
Level: 50,
|
||||
Algorithm: "pdfcpu",
|
||||
AutoStart: false,
|
||||
},
|
||||
Processing: entities.ProcessingConfig{
|
||||
ParallelWorkers: 2,
|
||||
TimeoutSeconds: 30,
|
||||
RetryAttempts: 3,
|
||||
},
|
||||
Output: entities.OutputConfig{
|
||||
LogLevel: "info",
|
||||
ProgressBar: true,
|
||||
LogToFile: true,
|
||||
LogFileName: "compressor.log",
|
||||
LogMaxSizeMB: 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
110
internal/infrastructure/logging/file_logger.go
Normal file
110
internal/infrastructure/logging/file_logger.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileLogger реализация логгера в файл
|
||||
type FileLogger struct {
|
||||
file *os.File
|
||||
logger *log.Logger
|
||||
logLevel string
|
||||
}
|
||||
|
||||
// NewFileLogger создает новый файловый логгер
|
||||
func NewFileLogger(filename, logLevel string, maxSizeMB int, logToFile bool) (*FileLogger, error) {
|
||||
if !logToFile {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger := log.New(file, "", log.LstdFlags)
|
||||
|
||||
return &FileLogger{
|
||||
file: file,
|
||||
logger: logger,
|
||||
logLevel: strings.ToLower(logLevel),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Debug логирует отладочное сообщение
|
||||
func (l *FileLogger) Debug(format string, args ...interface{}) {
|
||||
if l.shouldLog("debug") {
|
||||
l.writeLog("DEBUG", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Info логирует информационное сообщение
|
||||
func (l *FileLogger) Info(format string, args ...interface{}) {
|
||||
if l.shouldLog("info") {
|
||||
l.writeLog("INFO", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Warning логирует предупреждение
|
||||
func (l *FileLogger) Warning(format string, args ...interface{}) {
|
||||
if l.shouldLog("warning") {
|
||||
l.writeLog("WARNING", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Error логирует ошибку
|
||||
func (l *FileLogger) Error(format string, args ...interface{}) {
|
||||
if l.shouldLog("error") {
|
||||
l.writeLog("ERROR", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Success логирует успешное выполнение
|
||||
func (l *FileLogger) Success(format string, args ...interface{}) {
|
||||
if l.shouldLog("info") {
|
||||
l.writeLog("SUCCESS", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Close закрывает логгер
|
||||
func (l *FileLogger) Close() error {
|
||||
if l.file != nil {
|
||||
return l.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeLog записывает лог
|
||||
func (l *FileLogger) writeLog(level, format string, args ...interface{}) {
|
||||
if l.logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(format, args...)
|
||||
l.logger.Printf("[%s] %s", level, message)
|
||||
}
|
||||
|
||||
// shouldLog проверяет, нужно ли логировать на данном уровне
|
||||
func (l *FileLogger) shouldLog(level string) bool {
|
||||
levels := map[string]int{
|
||||
"debug": 0,
|
||||
"info": 1,
|
||||
"warning": 2,
|
||||
"error": 3,
|
||||
}
|
||||
|
||||
currentLevel, ok := levels[l.logLevel]
|
||||
if !ok {
|
||||
currentLevel = 1 // default to info
|
||||
}
|
||||
|
||||
messageLevel, ok := levels[level]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return messageLevel >= currentLevel
|
||||
}
|
||||
24
internal/infrastructure/repositories/config_repository.go
Normal file
24
internal/infrastructure/repositories/config_repository.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"compressor/internal/domain/entities"
|
||||
)
|
||||
|
||||
// ConfigRepository реализация репозитория конфигурации
|
||||
type ConfigRepository struct{}
|
||||
|
||||
// NewConfigRepository создает новый репозиторий конфигурации
|
||||
func NewConfigRepository() *ConfigRepository {
|
||||
return &ConfigRepository{}
|
||||
}
|
||||
|
||||
// GetCompressionConfig получает конфигурацию сжатия по уровню
|
||||
func (r *ConfigRepository) GetCompressionConfig(level int) (*entities.CompressionConfig, error) {
|
||||
config := entities.NewCompressionConfig(level)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// ValidateConfig валидирует конфигурацию
|
||||
func (r *ConfigRepository) ValidateConfig(config *entities.CompressionConfig) error {
|
||||
return config.Validate()
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"compressor/internal/domain/entities"
|
||||
)
|
||||
|
||||
// FileSystemRepository реализация репозитория для работы с файловой системой
|
||||
type FileSystemRepository struct{}
|
||||
|
||||
// NewFileSystemRepository создает новый репозиторий файловой системы
|
||||
func NewFileSystemRepository() *FileSystemRepository {
|
||||
return &FileSystemRepository{}
|
||||
}
|
||||
|
||||
// GetFileInfo получает информацию о PDF файле
|
||||
func (r *FileSystemRepository) GetFileInfo(path string) (*entities.PDFDocument, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &entities.PDFDocument{
|
||||
Path: path,
|
||||
Size: info.Size(),
|
||||
ModifiedTime: info.ModTime(),
|
||||
Pages: 0, // TODO: Можно добавить определение количества страниц
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FileExists проверяет существование файла
|
||||
func (r *FileSystemRepository) FileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// CreateDirectory создает директорию
|
||||
func (r *FileSystemRepository) CreateDirectory(path string) error {
|
||||
return os.MkdirAll(path, 0755)
|
||||
}
|
||||
|
||||
// ListPDFFiles возвращает список PDF файлов в директории и всех подпапках
|
||||
func (r *FileSystemRepository) ListPDFFiles(directory string) ([]string, error) {
|
||||
var pdfFiles []string
|
||||
|
||||
err := filepath.WalkDir(directory, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(filepath.Ext(d.Name()), ".pdf") {
|
||||
pdfFiles = append(pdfFiles, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Strings(pdfFiles)
|
||||
return pdfFiles, nil
|
||||
}
|
||||
Reference in New Issue
Block a user