refactor: разделение кода на пакеты и улучшение архитектуры

- Создана новая структура проекта с разделением на пакеты
- Добавлены интерфейсы для всех сервисов (Git, Quartz, Files, Build)
- Реализован Dependency Injection для сервисов
- Добавлены middleware для логирования, Request ID и Response Time
- Создан пакет конфигурации с валидацией
- Улучшено логирование через интерфейс
- Добавлены обработчики HTTP в отдельных пакетах
- Создана структура для тестирования
- Добавлены конфигурационные файлы и документация
This commit is contained in:
Andrey Epifancev
2025-08-11 19:45:54 +04:00
parent 1b340362be
commit 04cea69d6e
16 changed files with 1293 additions and 0 deletions

View 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",
}
}

View 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
View 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
}

View 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
}

View 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"`
}