293 lines
7.8 KiB
Go
293 lines
7.8 KiB
Go
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")
|
||
}
|