Initial commit: Go webhook server for Quartz site rebuild
This commit is contained in:
292
main.go
Normal file
292
main.go
Normal file
@@ -0,0 +1,292 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user