refactor: разделение кода на пакеты и улучшение архитектуры
- Создана новая структура проекта с разделением на пакеты - Добавлены интерфейсы для всех сервисов (Git, Quartz, Files, Build) - Реализован Dependency Injection для сервисов - Добавлены middleware для логирования, Request ID и Response Time - Создан пакет конфигурации с валидацией - Улучшено логирование через интерфейс - Добавлены обработчики HTTP в отдельных пакетах - Создана структура для тестирования - Добавлены конфигурационные файлы и документация
This commit is contained in:
51
Dockerfile-refactored
Normal file
51
Dockerfile-refactored
Normal 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
155
Makefile-refactored
Normal 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
212
README-REFACTORED.md
Normal 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
30
api/routes.go
Normal 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
76
cmd/server/main.go
Normal 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
12
configs/config.yaml
Normal 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
101
internal/config/config.go
Normal 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
|
||||||
|
}
|
||||||
37
internal/handlers/health.go
Normal file
37
internal/handlers/health.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
50
internal/handlers/webhook.go
Normal file
50
internal/handlers/webhook.go
Normal 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
67
internal/middleware/logging.go
Normal file
67
internal/middleware/logging.go
Normal 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)
|
||||||
|
}
|
||||||
88
internal/services/build.go
Normal file
88
internal/services/build.go
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
||||||
78
internal/services/files.go
Normal file
78
internal/services/files.go
Normal 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
93
internal/services/git.go
Normal 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
|
||||||
|
}
|
||||||
85
internal/services/quartz.go
Normal file
85
internal/services/quartz.go
Normal 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
|
||||||
|
}
|
||||||
28
internal/services/types.go
Normal file
28
internal/services/types.go
Normal 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
130
pkg/logger/logger.go
Normal 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}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user