refactor: разделение кода на пакеты и улучшение архитектуры

- Создана новая структура проекта с разделением на пакеты
- Добавлены интерфейсы для всех сервисов (Git, Quartz, Files, Build)
- Реализован Dependency Injection для сервисов
- Добавлены middleware для логирования, Request ID и Response Time
- Создан пакет конфигурации с валидацией
- Улучшено логирование через интерфейс
- Добавлены обработчики HTTP в отдельных пакетах
- Создана структура для тестирования
- Добавлены конфигурационные файлы и документация
This commit is contained in:
Andrey Epifancev
2025-08-11 19:45:54 +04:00
parent 1b340362be
commit 04cea69d6e
16 changed files with 1293 additions and 0 deletions

51
Dockerfile-refactored Normal file
View File

@@ -0,0 +1,51 @@
# Многоэтапная сборка для Go приложения
FROM golang:1.21-alpine AS builder
# Устанавливаем необходимые пакеты для сборки
RUN apk add --no-cache git ca-certificates tzdata
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем go mod файлы
COPY go.mod go.sum ./
# Скачиваем зависимости
RUN go mod download
# Копируем исходный код
COPY . .
# Собираем приложение из cmd/server
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
# Финальный образ
FROM alpine:latest
# Устанавливаем необходимые пакеты для работы
RUN apk --no-cache add ca-certificates git tzdata
# Создаем пользователя для безопасности
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем собранное приложение из builder этапа
COPY --from=builder /app/main .
# Копируем конфигурационные файлы
COPY --from=builder /app/configs ./configs
# Меняем владельца файлов
RUN chown -R appuser:appgroup /app
# Переключаемся на непривилегированного пользователя
USER appuser
# Открываем порт
EXPOSE 3000
# Запускаем приложение
CMD ["./main"]

155
Makefile-refactored Normal file
View File

@@ -0,0 +1,155 @@
.PHONY: help build run test clean docker-build docker-run docker-stop install-deps
# Переменные
BINARY_NAME=go-webhook-server
DOCKER_IMAGE=go-webhook-server
DOCKER_CONTAINER=go-webhook-server
BUILD_DIR=cmd/server
# Помощь
help: ## Показать справку по командам
@echo "Доступные команды:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
# Установка зависимостей
install-deps: ## Установить Go зависимости
go mod download
go mod tidy
# Сборка
build: install-deps ## Собрать бинарный файл
@echo "Сборка $(BINARY_NAME)..."
go build -o $(BINARY_NAME) $(BUILD_DIR)/main.go
@echo "Сборка завершена: $(BINARY_NAME)"
# Запуск
run: build ## Запустить сервис локально
@echo "Запуск $(BINARY_NAME)..."
./$(BINARY_NAME)
# Запуск без сборки
run-dev: ## Запустить сервис в режиме разработки
@echo "Запуск в режиме разработки..."
cd $(BUILD_DIR) && go run main.go
# Тестирование
test: install-deps ## Запустить тесты
@echo "Запуск тестов..."
go test -v ./internal/...
go test -v ./pkg/...
# Тестирование с покрытием
test-coverage: install-deps ## Запустить тесты с покрытием
@echo "Запуск тестов с покрытием..."
go test -v -coverprofile=coverage.out ./internal/...
go test -v -coverprofile=pkg-coverage.out ./pkg/...
go tool cover -html=coverage.out -o coverage.html
go tool cover -html=pkg-coverage.out -o pkg-coverage.html
@echo "Отчеты покрытия созданы: coverage.html, pkg-coverage.html"
# Очистка
clean: ## Очистить собранные файлы
@echo "Очистка..."
rm -f $(BINARY_NAME)
rm -f coverage.out pkg-coverage.out
rm -f coverage.html pkg-coverage.html
@echo "Очистка завершена"
# Docker команды
docker-build: ## Собрать Docker образ
@echo "Сборка Docker образа..."
docker build -t $(DOCKER_IMAGE) .
@echo "Docker образ собран: $(DOCKER_IMAGE)"
docker-run: docker-build ## Запустить Docker контейнер
@echo "Запуск Docker контейнера..."
docker run -d \
--name $(DOCKER_CONTAINER) \
-p 3000:3000 \
-v obsidian_repo:/obsidian:ro \
-v quartz_repo:/quartz:ro \
-v public_site:/public \
$(DOCKER_IMAGE)
@echo "Docker контейнер запущен: $(DOCKER_CONTAINER)"
docker-stop: ## Остановить Docker контейнер
@echo "Остановка Docker контейнера..."
docker stop $(DOCKER_CONTAINER) || true
docker rm $(DOCKER_CONTAINER) || true
@echo "Docker контейнер остановлен"
docker-logs: ## Показать логи Docker контейнера
docker logs -f $(DOCKER_CONTAINER)
# Docker Compose команды
compose-up: ## Запустить сервис через Docker Compose
@echo "Запуск через Docker Compose..."
docker-compose up -d
@echo "Сервис запущен"
compose-down: ## Остановить сервис через Docker Compose
@echo "Остановка через Docker Compose..."
docker-compose down
@echo "Сервис остановлен"
compose-logs: ## Показать логи Docker Compose
docker-compose logs -f
# Проверка состояния
status: ## Показать статус сервиса
@echo "Проверка статуса сервиса..."
@curl -s http://localhost:3000/health || echo "Сервис недоступен"
# Webhook тест
test-webhook: ## Отправить тестовый webhook
@echo "Отправка тестового webhook..."
@curl -X POST http://localhost:3000/webhook
@echo ""
# Проверка структуры
lint: ## Проверить код линтером
@echo "Проверка кода..."
@if command -v golangci-lint > /dev/null; then \
golangci-lint run; \
else \
echo "golangci-lint не установлен. Установите: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
fi
# Форматирование кода
fmt: ## Отформатировать код
@echo "Форматирование кода..."
go fmt ./...
@echo "Код отформатирован"
# Проверка зависимостей
deps-check: ## Проверить зависимости
@echo "Проверка зависимостей..."
go mod verify
go list -m all
# Полная пересборка
rebuild: clean build ## Полная пересборка проекта
# Разработка
dev: ## Запуск в режиме разработки с автоперезагрузкой
@echo "Запуск в режиме разработки..."
@if command -v air > /dev/null; then \
air; \
else \
echo "Air не установлен. Установите: go install github.com/cosmtrek/air@latest"; \
cd $(BUILD_DIR) && go run main.go; \
fi
# Создание структуры проекта
create-structure: ## Создать структуру директорий
@echo "Создание структуры проекта..."
mkdir -p cmd/server
mkdir -p internal/{config,handlers,services,middleware}
mkdir -p pkg/logger
mkdir -p api
mkdir -p configs
mkdir -p tests/{unit,integration}
mkdir -p scripts
mkdir -p docs
mkdir -p deployments
@echo "Структура проекта создана"

212
README-REFACTORED.md Normal file
View File

@@ -0,0 +1,212 @@
# Go Webhook Server - Рефакторенная версия
Рефакторенная версия webhook сервера с разделением на пакеты и улучшенной архитектурой.
## 🏗️ **Новая структура проекта**
```
go-webhook-server/
├── cmd/
│ └── server/
│ └── main.go # Точка входа приложения
├── internal/
│ ├── config/
│ │ └── config.go # Конфигурация приложения
│ ├── handlers/
│ │ ├── webhook.go # Обработчик webhook'ов
│ │ └── health.go # Health check обработчик
│ ├── services/
│ │ ├── build.go # Основной сервис сборки
│ │ ├── git.go # Git операции
│ │ ├── quartz.go # Сборка Quartz
│ │ ├── files.go # Файловые операции
│ │ └── types.go # Общие типы
│ └── middleware/
│ └── logging.go # HTTP логирование
├── pkg/
│ └── logger/
│ └── logger.go # Интерфейс логгера
├── api/
│ └── routes.go # Определение роутов
├── configs/
│ └── config.yaml # YAML конфигурация
├── tests/ # Тесты
├── scripts/ # Скрипты
└── docs/ # Документация
```
## 🔧 **Основные улучшения**
### **1. Разделение ответственности**
- **Config** - управление конфигурацией
- **Services** - бизнес-логика
- **Handlers** - HTTP обработчики
- **Middleware** - промежуточное ПО
- **API** - определение роутов
### **2. Интерфейсы и абстракции**
```go
type GitService interface {
UpdateRepository() error
IsRepositoryExists() bool
}
type QuartzService interface {
BuildSite() error
InstallDependencies() error
}
type BuildService interface {
BuildSite() BuildResult
}
```
### **3. Dependency Injection**
```go
// Инициализация сервисов
gitService := services.NewGitService(cfg, log)
quartzService := services.NewQuartzService(cfg, log)
fileService := services.NewFileService(cfg, log)
buildService := services.NewBuildService(cfg, log, gitService, quartzService, fileService)
```
### **4. Улучшенное логирование**
- Структурированное логирование
- Request ID для отслеживания
- Уровни логирования
- Контекстная информация
### **5. Middleware**
- **RequestIDMiddleware** - уникальный ID для каждого запроса
- **ResponseTimeMiddleware** - время ответа
- **LoggingMiddleware** - детальное логирование HTTP
## 🚀 **Запуск**
### **Локально**
```bash
cd cmd/server
go run main.go
```
### **Сборка**
```bash
go build -o go-webhook-server cmd/server/main.go
```
### **Docker**
```bash
docker build -t go-webhook-server .
docker run -p 3000:3000 go-webhook-server
```
## 📝 **Конфигурация**
### **Переменные окружения**
```bash
PORT=3000
OBSIDIAN_PATH=/obsidian
QUARTZ_PATH=/quartz
PUBLIC_PATH=/public
GIT_BRANCH=main
GIT_REMOTE=origin
SERVER_TIMEOUT=30
```
### **YAML конфигурация**
```yaml
server:
port: "3000"
timeout: 30
paths:
obsidian: "/obsidian"
quartz: "/quartz"
public: "/public"
git:
branch: "main"
remote: "origin"
```
## 🧪 **Тестирование**
### **Unit тесты**
```bash
go test ./internal/...
```
### **Integration тесты**
```bash
go test ./tests/integration/...
```
## 🔄 **API эндпоинты**
### **POST /webhook**
Запускает процесс сборки сайта.
**Ответ:**
```json
{
"status": "accepted",
"message": "Build process started",
"request_id": "20250127103000-abc123",
"timestamp": "2025-01-27T10:30:00Z"
}
```
### **GET /health**
Проверка состояния сервиса.
**Ответ:**
```json
{
"status": "ok",
"timestamp": "2025-01-27T10:30:00Z",
"service": "go-webhook-server",
"version": "1.0.0",
"request_id": "20250127103000-abc123"
}
```
## 📊 **Мониторинг**
### **Заголовки ответа**
- `X-Request-ID` - уникальный ID запроса
- `X-Response-Time` - время ответа
### **Логирование**
- Все HTTP запросы логируются с деталями
- Request ID для отслеживания цепочки запросов
- Структурированные логи в формате JSON
## 🚀 **Преимущества новой архитектуры**
1. **Тестируемость** - легко писать unit тесты
2. **Переиспользование** - компоненты можно использовать в других проектах
3. **Читаемость** - код легче понимать и поддерживать
4. **Расширяемость** - проще добавлять новую функциональность
5. **Соответствие стандартам** - структура соответствует Go best practices
6. **Dependency Injection** - легко заменять реализации
7. **Интерфейсы** - четкое разделение контрактов
## 🔮 **Планы развития**
- [ ] Добавление метрик Prometheus
- [ ] Конфигурация через файлы
- [ ] Graceful shutdown для сборки
- [ ] Очередь сборок
- [ ] Уведомления о результатах
- [ ] API для мониторинга сборок
- [ ] Аутентификация webhook'ов
- [ ] Rate limiting
- [ ] OpenAPI документация
## 📚 **Зависимости**
- **Go 1.20+**
- **Gin** - веб-фреймворк
- **go-git/v5** - Git клиент
- **logrus** - логирование
- **YAML** - конфигурация (планируется)

30
api/routes.go Normal file
View File

@@ -0,0 +1,30 @@
package api
import (
"github.com/gin-gonic/gin"
"go-webhook-server/internal/handlers"
"go-webhook-server/internal/middleware"
"go-webhook-server/pkg/logger"
)
// SetupRoutes настраивает все роуты приложения
func SetupRoutes(
webhookHandler *handlers.WebhookHandler,
healthHandler *handlers.HealthHandler,
log logger.Logger,
) *gin.Engine {
// Настраиваем Gin
gin.SetMode(gin.ReleaseMode)
router := gin.New()
// Добавляем middleware
router.Use(middleware.RequestIDMiddleware())
router.Use(middleware.ResponseTimeMiddleware())
router.Use(middleware.LoggingMiddleware(log))
// Роуты
router.POST("/webhook", webhookHandler.HandleWebhook)
router.GET("/health", healthHandler.HandleHealth)
return router
}

76
cmd/server/main.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"go-webhook-server/api"
"go-webhook-server/internal/config"
"go-webhook-server/internal/handlers"
"go-webhook-server/internal/services"
"go-webhook-server/pkg/logger"
)
func main() {
// Инициализация логгера
log := logger.New()
log.Info("Starting go-webhook-server...")
// Загрузка конфигурации
cfg := config.Load()
if err := cfg.Validate(); err != nil {
log.Fatalf("Configuration validation failed: %v", err)
}
log.Infof("Configuration loaded: Port=%s, ObsidianPath=%s, QuartzPath=%s, PublicPath=%s, GitBranch=%s, GitRemote=%s",
cfg.Server.Port, cfg.Paths.Obsidian, cfg.Paths.Quartz, cfg.Paths.Public, cfg.Git.Branch, cfg.Git.Remote)
// Инициализация сервисов
gitService := services.NewGitService(cfg, log)
quartzService := services.NewQuartzService(cfg, log)
fileService := services.NewFileService(cfg, log)
buildService := services.NewBuildService(cfg, log, gitService, quartzService, fileService)
// Инициализация обработчиков
webhookHandler := handlers.NewWebhookHandler(buildService, log)
healthHandler := handlers.NewHealthHandler(log)
// Настройка роутов
router := api.SetupRoutes(webhookHandler, healthHandler, log)
// Создание HTTP сервера
addr := ":" + cfg.Server.Port
server := &http.Server{
Addr: addr,
Handler: router,
}
// Запуск сервера в горутине
go func() {
log.Infof("Starting webhook server on port %s", cfg.Server.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
// Ожидаем сигнал для graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info("Shutting down server...")
// Graceful shutdown с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.Server.Timeout)*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Errorf("Server forced to shutdown: %v", err)
}
log.Info("Server exited")
}

12
configs/config.yaml Normal file
View File

@@ -0,0 +1,12 @@
server:
port: "3000"
timeout: 30
paths:
obsidian: "/obsidian"
quartz: "/quartz"
public: "/public"
git:
branch: "main"
remote: "origin"

101
internal/config/config.go Normal file
View File

@@ -0,0 +1,101 @@
package config
import (
"errors"
"os"
"strconv"
)
// Config содержит все настройки приложения
type Config struct {
Server ServerConfig `yaml:"server"`
Paths PathsConfig `yaml:"paths"`
Git GitConfig `yaml:"git"`
}
// ServerConfig содержит настройки HTTP сервера
type ServerConfig struct {
Port string `yaml:"port"`
Timeout int `yaml:"timeout"` // в секундах
}
// PathsConfig содержит пути к директориям
type PathsConfig struct {
Obsidian string `yaml:"obsidian"`
Quartz string `yaml:"quartz"`
Public string `yaml:"public"`
}
// GitConfig содержит настройки Git
type GitConfig struct {
Branch string `yaml:"branch"`
Remote string `yaml:"remote"`
}
// Load загружает конфигурацию из переменных окружения
func Load() *Config {
config := &Config{
Server: ServerConfig{
Port: getEnv("PORT", "3000"),
Timeout: getEnvAsInt("SERVER_TIMEOUT", 30),
},
Paths: PathsConfig{
Obsidian: getEnv("OBSIDIAN_PATH", "/obsidian"),
Quartz: getEnv("QUARTZ_PATH", "/quartz"),
Public: getEnv("PUBLIC_PATH", "/public"),
},
Git: GitConfig{
Branch: getEnv("GIT_BRANCH", "main"),
Remote: getEnv("GIT_REMOTE", "origin"),
},
}
return config
}
// Validate проверяет корректность конфигурации
func (c *Config) Validate() error {
if c.Server.Port == "" {
return errors.New("server port is required")
}
if c.Paths.Obsidian == "" {
return errors.New("obsidian path is required")
}
if c.Paths.Quartz == "" {
return errors.New("quartz path is required")
}
if c.Paths.Public == "" {
return errors.New("public path is required")
}
if c.Git.Branch == "" {
return errors.New("git branch is required")
}
if c.Git.Remote == "" {
return errors.New("git remote is required")
}
return nil
}
// getEnv получает значение переменной окружения или возвращает значение по умолчанию
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// getEnvAsInt получает значение переменной окружения как int или возвращает значение по умолчанию
func getEnvAsInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}

View File

@@ -0,0 +1,37 @@
package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"go-webhook-server/pkg/logger"
)
// HealthHandler обработчик для health check эндпоинта
type HealthHandler struct {
logger logger.Logger
}
// NewHealthHandler создает новый экземпляр health обработчика
func NewHealthHandler(log logger.Logger) *HealthHandler {
return &HealthHandler{
logger: log,
}
}
// HandleHealth обрабатывает health check запрос
func (h *HealthHandler) HandleHealth(c *gin.Context) {
requestID := c.GetString("request_id")
logger := h.logger.WithField("request_id", requestID)
logger.Debug("Health check request received")
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"service": "go-webhook-server",
"version": "1.0.0",
"request_id": requestID,
})
}

View File

@@ -0,0 +1,50 @@
package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"go-webhook-server/internal/services"
"go-webhook-server/pkg/logger"
)
// WebhookHandler обработчик для webhook эндпоинта
type WebhookHandler struct {
buildService services.BuildService
logger logger.Logger
}
// NewWebhookHandler создает новый экземпляр webhook обработчика
func NewWebhookHandler(buildService services.BuildService, log logger.Logger) *WebhookHandler {
return &WebhookHandler{
buildService: buildService,
logger: log,
}
}
// HandleWebhook обрабатывает webhook запрос
func (h *WebhookHandler) HandleWebhook(c *gin.Context) {
requestID := c.GetString("request_id")
logger := h.logger.WithField("request_id", requestID)
logger.Info("Webhook received, starting site rebuild...")
// Запускаем сборку в горутине для асинхронной обработки
go func() {
result := h.buildService.BuildSite()
if result.Success {
logger.Info("Webhook build completed successfully")
} else {
logger.Errorf("Webhook build failed: %s", result.Error)
}
}()
// Сразу возвращаем ответ
c.JSON(http.StatusAccepted, gin.H{
"status": "accepted",
"message": "Build process started",
"request_id": requestID,
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}

View File

@@ -0,0 +1,67 @@
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"go-webhook-server/pkg/logger"
)
// LoggingMiddleware создает middleware для логирования HTTP запросов
func LoggingMiddleware(log logger.Logger) gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// Логируем детали запроса
log.WithFields(map[string]interface{}{
"status": param.StatusCode,
"latency": param.Latency,
"client_ip": param.ClientIP,
"method": param.Method,
"path": param.Path,
"user_agent": param.Request.UserAgent(),
}).Info("HTTP Request")
// Возвращаем пустую строку, так как логирование уже выполнено
return ""
})
}
// RequestIDMiddleware добавляет уникальный ID к каждому запросу
func RequestIDMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
c.Header("X-Request-ID", requestID)
c.Next()
}
}
// ResponseTimeMiddleware добавляет время ответа в заголовки
func ResponseTimeMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start)
c.Header("X-Response-Time", duration.String())
}
}
// generateRequestID генерирует простой ID запроса
func generateRequestID() string {
return time.Now().Format("20060102150405") + "-" + randomString(6)
}
// randomString генерирует случайную строку указанной длины
func randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(b)
}

View File

@@ -0,0 +1,88 @@
package services
import (
"go-webhook-server/internal/config"
"go-webhook-server/pkg/logger"
)
// BuildService интерфейс для сборки сайта
type BuildService interface {
BuildSite() BuildResult
}
// buildServiceImpl реализация сервиса сборки
type buildServiceImpl struct {
config *config.Config
logger logger.Logger
gitService GitService
quartzService QuartzService
fileService FileService
}
// NewBuildService создает новый экземпляр сервиса сборки
func NewBuildService(
cfg *config.Config,
log logger.Logger,
git GitService,
quartz QuartzService,
files FileService,
) BuildService {
return &buildServiceImpl{
config: cfg,
logger: log,
gitService: git,
quartzService: quartz,
fileService: files,
}
}
// BuildSite выполняет полную сборку сайта
func (b *buildServiceImpl) BuildSite() BuildResult {
b.logger.Info("Starting site build process...")
// Проверяем существование репозитория
if !b.gitService.IsRepositoryExists() {
b.logger.Error("Repository not found")
return BuildResult{
Success: false,
Message: "Repository not found",
Error: "Git repository does not exist at specified path",
}
}
// Обновляем репозиторий
if err := b.gitService.UpdateRepository(); err != nil {
b.logger.Errorf("Failed to update repository: %v", err)
return BuildResult{
Success: false,
Message: "Failed to update repository",
Error: err.Error(),
}
}
// Собираем сайт с помощью Quartz
if err := b.quartzService.BuildSite(); err != nil {
b.logger.Errorf("Failed to build Quartz site: %v", err)
return BuildResult{
Success: false,
Message: "Failed to build Quartz site",
Error: err.Error(),
}
}
// Копируем собранные файлы в публичную директорию
if err := b.fileService.CopyBuiltSite(); err != nil {
b.logger.Errorf("Failed to copy built site: %v", err)
return BuildResult{
Success: false,
Message: "Failed to copy built site",
Error: err.Error(),
}
}
b.logger.Info("Site built successfully!")
return BuildResult{
Success: true,
Message: "Site built successfully",
}
}

View File

@@ -0,0 +1,78 @@
package services
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"go-webhook-server/internal/config"
"go-webhook-server/pkg/logger"
)
// FileService интерфейс для файловых операций
type FileService interface {
CopyBuiltSite() error
ClearPublicDirectory() error
EnsurePublicDirectory() error
}
// fileServiceImpl реализация файлового сервиса
type fileServiceImpl struct {
config *config.Config
logger logger.Logger
}
// NewFileService создает новый экземпляр файлового сервиса
func NewFileService(cfg *config.Config, log logger.Logger) FileService {
return &fileServiceImpl{
config: cfg,
logger: log,
}
}
// CopyBuiltSite копирует собранные файлы в публичную директорию
func (f *fileServiceImpl) CopyBuiltSite() error {
f.logger.Info("Copying built site to public directory...")
// Очищаем публичную директорию
if err := f.ClearPublicDirectory(); err != nil {
return fmt.Errorf("failed to clear public directory: %w", err)
}
// Создаем публичную директорию заново
if err := f.EnsurePublicDirectory(); err != nil {
return fmt.Errorf("failed to create public directory: %w", err)
}
// Проверяем существование директории с собранными файлами
quartzPublicPath := filepath.Join(f.config.Paths.Quartz, "public")
if _, err := os.Stat(quartzPublicPath); os.IsNotExist(err) {
return fmt.Errorf("Quartz public directory not found: %s", quartzPublicPath)
}
// Копируем файлы с помощью cp команды
cmd := exec.Command("cp", "-r", quartzPublicPath+"/.", f.config.Paths.Public+"/")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to copy built site: %w", err)
}
f.logger.Info("Site files copied successfully")
return nil
}
// ClearPublicDirectory очищает публичную директорию
func (f *fileServiceImpl) ClearPublicDirectory() error {
if err := os.RemoveAll(f.config.Paths.Public); err != nil {
return fmt.Errorf("failed to clear public directory: %w", err)
}
return nil
}
// EnsurePublicDirectory создает публичную директорию если она не существует
func (f *fileServiceImpl) EnsurePublicDirectory() error {
if err := os.MkdirAll(f.config.Paths.Public, 0755); err != nil {
return fmt.Errorf("failed to create public directory: %w", err)
}
return nil
}

93
internal/services/git.go Normal file
View File

@@ -0,0 +1,93 @@
package services
import (
"fmt"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"go-webhook-server/internal/config"
"go-webhook-server/pkg/logger"
)
// GitService интерфейс для Git операций
type GitService interface {
UpdateRepository() error
IsRepositoryExists() bool
}
// gitServiceImpl реализация Git сервиса
type gitServiceImpl struct {
config *config.Config
logger logger.Logger
}
// NewGitService создает новый экземпляр Git сервиса
func NewGitService(cfg *config.Config, log logger.Logger) GitService {
return &gitServiceImpl{
config: cfg,
logger: log,
}
}
// UpdateRepository обновляет репозиторий из удаленного источника
func (g *gitServiceImpl) UpdateRepository() error {
g.logger.Info("Updating repository...")
// Открываем репозиторий
repo, err := git.PlainOpen(g.config.Paths.Obsidian)
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}
// Получаем worktree
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
// Проверяем текущую ветку
head, err := repo.Head()
if err != nil {
return fmt.Errorf("failed to get HEAD: %w", err)
}
g.logger.Infof("Current branch: %s", head.Name().Short())
// Выполняем git pull
err = worktree.Pull(&git.PullOptions{
RemoteName: g.config.Git.Remote,
ReferenceName: plumbing.NewBranchReferenceName(g.config.Git.Branch),
})
if err != nil {
if err == git.NoErrAlreadyUpToDate {
g.logger.Info("Repository is already up to date")
return nil
}
return fmt.Errorf("failed to pull from remote: %w", err)
}
g.logger.Info("Repository updated successfully")
return nil
}
// IsRepositoryExists проверяет существование Git репозитория
func (g *gitServiceImpl) IsRepositoryExists() bool {
gitPath := filepath.Join(g.config.Paths.Obsidian, ".git")
// Проверяем существование .git директории
if _, err := filepath.Abs(gitPath); err != nil {
return false
}
// Пытаемся открыть репозиторий
repo, err := git.PlainOpen(g.config.Paths.Obsidian)
if err != nil {
return false
}
// Проверяем что это действительно Git репозиторий
_, err = repo.Head()
return err == nil
}

View File

@@ -0,0 +1,85 @@
package services
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"go-webhook-server/internal/config"
"go-webhook-server/pkg/logger"
)
// QuartzService интерфейс для сборки Quartz сайта
type QuartzService interface {
BuildSite() error
InstallDependencies() error
}
// quartzServiceImpl реализация Quartz сервиса
type quartzServiceImpl struct {
config *config.Config
logger logger.Logger
}
// NewQuartzService создает новый экземпляр Quartz сервиса
func NewQuartzService(cfg *config.Config, log logger.Logger) QuartzService {
return &quartzServiceImpl{
config: cfg,
logger: log,
}
}
// BuildSite собирает сайт с помощью Quartz
func (q *quartzServiceImpl) BuildSite() error {
q.logger.Info("Building site with Quartz...")
// Проверяем существование package.json в директории Quartz
packageJSONPath := filepath.Join(q.config.Paths.Quartz, "package.json")
if _, err := os.Stat(packageJSONPath); os.IsNotExist(err) {
return fmt.Errorf("package.json not found in Quartz directory: %s", q.config.Paths.Quartz)
}
// Устанавливаем зависимости если необходимо
if err := q.InstallDependencies(); err != nil {
return fmt.Errorf("failed to install dependencies: %w", err)
}
// Выполняем сборку Quartz
cmd := exec.Command("npm", "run", "quartz", "build", "--", "-d", q.config.Paths.Obsidian)
cmd.Dir = q.config.Paths.Quartz
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
q.logger.Info("Executing Quartz build command...")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to build Quartz site: %w", err)
}
q.logger.Info("Quartz build completed successfully")
return nil
}
// InstallDependencies устанавливает npm зависимости
func (q *quartzServiceImpl) InstallDependencies() error {
// Проверяем существование node_modules
nodeModulesPath := filepath.Join(q.config.Paths.Quartz, "node_modules")
if _, err := os.Stat(nodeModulesPath); os.IsNotExist(err) {
q.logger.Info("Installing npm dependencies...")
cmd := exec.Command("npm", "install")
cmd.Dir = q.config.Paths.Quartz
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to install npm dependencies: %w", err)
}
q.logger.Info("npm dependencies installed successfully")
} else {
q.logger.Debug("node_modules already exists, skipping npm install")
}
return nil
}

View File

@@ -0,0 +1,28 @@
package services
// BuildResult результат сборки сайта
type BuildResult struct {
Success bool `json:"success"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
}
// BuildStatus статус сборки
type BuildStatus string
const (
BuildStatusPending BuildStatus = "pending"
BuildStatusRunning BuildStatus = "running"
BuildStatusCompleted BuildStatus = "completed"
BuildStatusFailed BuildStatus = "failed"
)
// BuildInfo детальная информация о сборке
type BuildInfo struct {
ID string `json:"id"`
Status BuildStatus `json:"status"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time,omitempty"`
Duration string `json:"duration,omitempty"`
Result BuildResult `json:"result,omitempty"`
}

130
pkg/logger/logger.go Normal file
View File

@@ -0,0 +1,130 @@
package logger
import (
"github.com/sirupsen/logrus"
)
// Logger интерфейс для логирования
type Logger interface {
Info(args ...interface{})
Infof(format string, args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
Warn(args ...interface{})
Warnf(format string, args ...interface{})
Debug(args ...interface{})
Debugf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
WithField(key string, value interface{}) Logger
WithFields(fields map[string]interface{}) Logger
}
// logrusLogger реализация логгера через logrus
type logrusLogger struct {
logger *logrus.Logger
}
// New создает новый экземпляр логгера
func New() Logger {
logger := logrus.New()
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
logger.SetLevel(logrus.InfoLevel)
return &logrusLogger{logger: logger}
}
// NewWithLevel создает логгер с указанным уровнем
func NewWithLevel(level string) Logger {
logger := logrus.New()
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
switch level {
case "debug":
logger.SetLevel(logrus.DebugLevel)
case "info":
logger.SetLevel(logrus.InfoLevel)
case "warn":
logger.SetLevel(logrus.WarnLevel)
case "error":
logger.SetLevel(logrus.ErrorLevel)
default:
logger.SetLevel(logrus.InfoLevel)
}
return &logrusLogger{logger: logger}
}
// Info логирует информационное сообщение
func (l *logrusLogger) Info(args ...interface{}) {
l.logger.Info(args...)
}
// Fatalf логирует критическую ошибку и завершает программу
func (l *logrusLogger) Fatalf(format string, args ...interface{}) {
l.logger.Fatalf(format, args...)
}
// Infof логирует форматированное информационное сообщение
func (l *logrusLogger) Infof(format string, args ...interface{}) {
l.logger.Infof(format, args...)
}
// Error логирует сообщение об ошибке
func (l *logrusLogger) Error(args ...interface{}) {
l.logger.Error(args...)
}
// Errorf логирует форматированное сообщение об ошибке
func (l *logrusLogger) Errorf(format string, args ...interface{}) {
l.logger.Errorf(format, args...)
}
// Warn логирует предупреждение
func (l *logrusLogger) Warn(args ...interface{}) {
l.logger.Warn(args...)
}
// Warnf логирует форматированное предупреждение
func (l *logrusLogger) Warnf(format string, args ...interface{}) {
l.logger.Warnf(format, args...)
}
// Debug логирует отладочное сообщение
func (l *logrusLogger) Debug(args ...interface{}) {
l.logger.Debug(args...)
}
// Debugf логирует форматированное отладочное сообщение
func (l *logrusLogger) Debugf(format string, args ...interface{}) {
l.logger.Debugf(format, args...)
}
// WithField добавляет поле к логгеру
func (l *logrusLogger) WithField(key string, value interface{}) Logger {
// Создаем новый логгер с полем
newLogger := logrus.New()
newLogger.SetFormatter(l.logger.Formatter)
newLogger.SetLevel(l.logger.Level)
newLogger.SetOutput(l.logger.Out)
// Добавляем поле к контексту
newLogger.WithField(key, value)
return &logrusLogger{logger: newLogger}
}
// WithFields добавляет поля к логгеру
func (l *logrusLogger) WithFields(fields map[string]interface{}) Logger {
// Создаем новый логгер с полями
newLogger := logrus.New()
newLogger.SetFormatter(l.logger.Formatter)
newLogger.SetLevel(l.logger.Level)
newLogger.SetOutput(l.logger.Out)
// Добавляем поля к контексту
newLogger.WithFields(fields)
return &logrusLogger{logger: newLogger}
}