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") }