Files
GitM/internal/sync/engine.go
2026-03-31 16:17:50 +08:00

191 lines
4.5 KiB
Go

package sync
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
"gitm/internal/database"
"gitm/internal/gitea"
"gitm/internal/models"
)
type Engine struct {
maxConcurrent int
mu sync.Mutex
activeTasks map[int64]string
}
func NewEngine(maxConcurrent int) *Engine {
return &Engine{
maxConcurrent: maxConcurrent,
activeTasks: make(map[int64]string),
}
}
func (e *Engine) SyncServer(serverID int64) error {
e.mu.Lock()
if _, active := e.activeTasks[serverID]; active {
e.mu.Unlock()
return fmt.Errorf("sync already in progress for server %d", serverID)
}
taskID := fmt.Sprintf("%d-%d", serverID, time.Now().Unix())
e.activeTasks[serverID] = taskID
e.mu.Unlock()
defer func() {
e.mu.Lock()
delete(e.activeTasks, serverID)
e.mu.Unlock()
}()
server, err := database.GetServer(serverID)
if err != nil || server == nil {
return fmt.Errorf("failed to get server: %w", err)
}
syncLog := &models.SyncLog{
ServerID: serverID,
Status: "in_progress",
Message: "Starting sync",
StartedAt: time.Now(),
}
if err := database.CreateSyncLog(syncLog); err != nil {
return fmt.Errorf("failed to create sync log: %w", err)
}
client, err := gitea.NewClient(server.URL, server.Token)
if err != nil {
e.finishLog(syncLog, "failed", fmt.Sprintf("Failed to create client: %v", err))
return err
}
if _, err := client.ValidateToken(); err != nil {
e.finishLog(syncLog, "failed", fmt.Sprintf("Authentication failed: %v", err))
return err
}
giteaRepos, err := client.GetAllRepos()
if err != nil {
e.finishLog(syncLog, "failed", fmt.Sprintf("Failed to fetch repos: %v", err))
return err
}
reposDir, _ := database.GetSetting("repos_dir")
if reposDir == "" {
reposDir = "./data/repos"
}
serverDir := filepath.Join(reposDir, fmt.Sprintf("server_%d_%s", serverID, server.Name))
os.MkdirAll(serverDir, 0755)
successCount := 0
failedCount := 0
sem := make(chan struct{}, e.maxConcurrent)
var wg sync.WaitGroup
var resultsMu sync.Mutex
for _, gr := range giteaRepos {
wg.Add(1)
go func(giteaRepo gitea.GiteaRepo) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
localPath := filepath.Join(serverDir, giteaRepo.FullName+".git")
existingRepo, _ := database.GetRepoByFullName(serverID, giteaRepo.FullName)
var err error
if existingRepo == nil {
err = e.cloneMirror(giteaRepo.CloneURL, localPath)
if err == nil {
repo := &models.Repo{
ServerID: serverID,
Name: giteaRepo.Name,
FullName: giteaRepo.FullName,
CloneURL: giteaRepo.CloneURL,
LocalPath: localPath,
SyncStatus: "success",
}
database.CreateRepo(repo)
}
} else {
err = e.fetchMirror(localPath)
if err == nil {
database.UpdateRepoSyncStatus(existingRepo.ID, "success")
}
}
resultsMu.Lock()
if err != nil {
failedCount++
failLog := &models.SyncLog{
ServerID: serverID,
RepoID: getRepoID(serverID, giteaRepo.FullName),
Status: "failed",
Message: err.Error(),
StartedAt: time.Now(),
}
database.CreateSyncLog(failLog)
} else {
successCount++
}
resultsMu.Unlock()
}(gr)
}
wg.Wait()
database.UpdateServerLastSync(serverID)
message := fmt.Sprintf("Sync completed: %d succeeded, %d failed", successCount, failedCount)
e.finishLog(syncLog, "success", message)
return nil
}
func (e *Engine) cloneMirror(url, path string) error {
if _, err := os.Stat(path); err == nil {
return nil
}
os.MkdirAll(filepath.Dir(path), 0755)
cmd := exec.Command("git", "clone", "--mirror", url, path)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("clone failed: %w - %s", err, string(output))
}
return nil
}
func (e *Engine) fetchMirror(path string) error {
cmd := exec.Command("git", "--git-dir", path, "fetch", "--all", "--prune")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("fetch failed: %w - %s", err, string(output))
}
return nil
}
func (e *Engine) finishLog(log *models.SyncLog, status, message string) {
now := time.Now()
log.Status = status
log.Message = message
log.FinishedAt = &now
database.UpdateSyncLog(log.ID, status, message, log.FinishedAt)
}
func (e *Engine) IsSyncing(serverID int64) bool {
e.mu.Lock()
defer e.mu.Unlock()
_, active := e.activeTasks[serverID]
return active
}
func getRepoID(serverID int64, fullName string) *int64 {
repo, _ := database.GetRepoByFullName(serverID, fullName)
if repo != nil {
return &repo.ID
}
return nil
}