Files
go-webhook/main.go
2025-08-11 19:26:57 +04:00

293 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
type Config struct {
Port string
ObsidianPath string
QuartzPath string
PublicPath string
GitBranch string
GitRemote string
}
type BuildResult struct {
Success bool `json:"success"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
}
var (
config Config
logger *logrus.Logger
)
func init() {
// Инициализация логгера
logger = logrus.New()
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
logger.SetLevel(logrus.InfoLevel)
// Загрузка конфигурации
config = Config{
Port: getEnv("PORT", "3000"),
ObsidianPath: getEnv("OBSIDIAN_PATH", "/obsidian"),
QuartzPath: getEnv("QUARTZ_PATH", "/quartz"),
PublicPath: getEnv("PUBLIC_PATH", "/public"),
GitBranch: getEnv("GIT_BRANCH", "main"),
GitRemote: getEnv("GIT_REMOTE", "origin"),
}
logger.Infof("Configuration loaded: Port=%s, ObsidianPath=%s, QuartzPath=%s, PublicPath=%s, GitBranch=%s, GitRemote=%s",
config.Port, config.ObsidianPath, config.QuartzPath, config.PublicPath, config.GitBranch, config.GitRemote)
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func buildSite() BuildResult {
logger.Info("Starting site build process...")
// Проверяем существование репозитория
if _, err := os.Stat(filepath.Join(config.ObsidianPath, ".git")); os.IsNotExist(err) {
logger.Error("Repository not found")
return BuildResult{
Success: false,
Message: "Repository not found",
Error: "Git repository does not exist at specified path",
}
}
// Обновляем репозиторий
if err := updateRepository(); err != nil {
logger.Errorf("Failed to update repository: %v", err)
return BuildResult{
Success: false,
Message: "Failed to update repository",
Error: err.Error(),
}
}
// Собираем сайт с помощью Quartz
if err := buildQuartzSite(); err != nil {
logger.Errorf("Failed to build Quartz site: %v", err)
return BuildResult{
Success: false,
Message: "Failed to build Quartz site",
Error: err.Error(),
}
}
// Копируем собранные файлы в публичную директорию
if err := copyBuiltSite(); err != nil {
logger.Errorf("Failed to copy built site: %v", err)
return BuildResult{
Success: false,
Message: "Failed to copy built site",
Error: err.Error(),
}
}
logger.Info("Site built successfully!")
return BuildResult{
Success: true,
Message: "Site built successfully",
}
}
func updateRepository() error {
logger.Info("Updating repository...")
// Открываем репозиторий
repo, err := git.PlainOpen(config.ObsidianPath)
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)
}
// Выполняем git pull
err = worktree.Pull(&git.PullOptions{
RemoteName: config.GitRemote,
})
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to pull from remote: %w", err)
}
if err == git.NoErrAlreadyUpToDate {
logger.Info("Repository is already up to date")
} else {
logger.Info("Repository updated successfully")
}
return nil
}
func buildQuartzSite() error {
logger.Info("Building site with Quartz...")
// Проверяем существование package.json в директории Quartz
packageJSONPath := filepath.Join(config.QuartzPath, "package.json")
if _, err := os.Stat(packageJSONPath); os.IsNotExist(err) {
return fmt.Errorf("package.json not found in Quartz directory")
}
// Выполняем npm install если node_modules не существует
nodeModulesPath := filepath.Join(config.QuartzPath, "node_modules")
if _, err := os.Stat(nodeModulesPath); os.IsNotExist(err) {
logger.Info("Installing npm dependencies...")
cmd := exec.Command("npm", "install")
cmd.Dir = config.QuartzPath
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to install npm dependencies: %w", err)
}
}
// Выполняем сборку Quartz
cmd := exec.Command("npm", "run", "quartz", "build", "--", "-d", config.ObsidianPath)
cmd.Dir = config.QuartzPath
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to build Quartz site: %w", err)
}
logger.Info("Quartz build completed successfully")
return nil
}
func copyBuiltSite() error {
logger.Info("Copying built site to public directory...")
// Очищаем публичную директорию
if err := os.RemoveAll(config.PublicPath); err != nil {
return fmt.Errorf("failed to clear public directory: %w", err)
}
// Создаем публичную директорию заново
if err := os.MkdirAll(config.PublicPath, 0755); err != nil {
return fmt.Errorf("failed to create public directory: %w", err)
}
// Копируем собранные файлы
quartzPublicPath := filepath.Join(config.QuartzPath, "public")
if _, err := os.Stat(quartzPublicPath); os.IsNotExist(err) {
return fmt.Errorf("Quartz public directory not found")
}
// Используем cp команду для копирования
cmd := exec.Command("cp", "-r", quartzPublicPath+"/.", config.PublicPath+"/")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to copy built site: %w", err)
}
logger.Info("Site files copied successfully")
return nil
}
func webhookHandler(c *gin.Context) {
logger.Info("Webhook received, starting site rebuild...")
// Запускаем сборку в горутине для асинхронной обработки
go func() {
result := 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",
})
}
func healthHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"service": "go-webhook-server",
"version": "1.0.0",
})
}
func main() {
// Настраиваем Gin
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// Добавляем middleware для логирования
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
logger.WithFields(logrus.Fields{
"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 ""
}))
// Роуты
router.POST("/webhook", webhookHandler)
router.GET("/health", healthHandler)
// Запуск сервера
addr := ":" + config.Port
logger.Infof("Starting webhook server on port %s", config.Port)
server := &http.Server{
Addr: addr,
Handler: router,
}
// Graceful shutdown
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Failed to start server: %v", err)
}
}()
// Ожидаем сигнал для graceful shutdown
<-make(chan os.Signal, 1)
logger.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Errorf("Server forced to shutdown: %v", err)
}
logger.Info("Server exited")
}