package core import ( "context" "encoding/json" "errors" "fmt" "log/slog" "path/filepath" aptclone "github.com/syonad/clonepack/internal/clone/apt" "github.com/syonad/clonepack/internal/clone/rpm" "github.com/syonad/clonepack/internal/store" ) var ErrCloneAlreadyRunning = errors.New("a clone job is already running for this repo") type CloneService struct { repoStore store.RepoStore jobStore store.CloneJobStore dataDir string } func NewCloneService(repoStore store.RepoStore, jobStore store.CloneJobStore, dataDir string) *CloneService { return &CloneService{repoStore: repoStore, jobStore: jobStore, dataDir: dataDir} } func (s *CloneService) StartClone(ctx context.Context, repoID int64) (int64, error) { repo, err := s.repoStore.GetRepo(ctx, repoID) if err != nil { return 0, err } if repo.Type != "rpm" && repo.Type != "apt" { return 0, fmt.Errorf("clone is only supported for rpm and apt repositories") } running, err := s.jobStore.HasRunningCloneJob(ctx, repoID) if err != nil { return 0, err } if running { return 0, ErrCloneAlreadyRunning } jobID, err := s.jobStore.CreateCloneJob(ctx, repoID) if err != nil { return 0, err } go s.runClone(repoID, jobID, repo) return jobID, nil } func (s *CloneService) GetLatestCloneJob(ctx context.Context, repoID int64) (*store.CloneJob, error) { return s.jobStore.GetLatestCloneJob(ctx, repoID) } func (s *CloneService) runClone(repoID, jobID int64, repo *store.Repo) { ctx := context.Background() if err := s.jobStore.MarkCloneJobStarted(ctx, jobID); err != nil { slog.Error("clone job: failed to mark started", "job_id", jobID, "error", err) return } destDir := filepath.Join(s.dataDir, "repos", fmt.Sprintf("%d", repoID), repo.Type) var cloneErr error switch repo.Type { case "rpm": cloner := rpm.New(repo.SourceURL, destDir) cloner.OnProgress = func(p rpm.Progress) { slog.Debug("clone progress", "job_id", jobID, "file", p.File, "bytes", p.BytesDone) } if err := cloner.Clone(ctx); err != nil { cloneErr = err } else if err := rpm.RegenerateMetadata(destDir); err != nil { slog.Warn("clone job: metadata regeneration failed", "job_id", jobID, "error", err) } case "apt": var cfg aptclone.Config if err := json.Unmarshal([]byte(repo.Config), &cfg); err != nil { cloneErr = fmt.Errorf("parse apt config: %w", err) break } cloner := aptclone.New(repo.SourceURL, destDir, cfg) cloner.OnProgress = func(file string, bytes int64) { slog.Debug("clone progress", "job_id", jobID, "file", file, "bytes", bytes) } cloneErr = cloner.Clone(ctx) } if cloneErr != nil { errMsg := cloneErr.Error() _ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobFailed, &errMsg) slog.Error("clone job failed", "job_id", jobID, "error", cloneErr) return } _ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobCompleted, nil) slog.Info("clone job completed", "job_id", jobID) }