first with full handle over rpm
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
7948368573
commit
274ea454dd
50 changed files with 4309 additions and 0 deletions
83
internal/core/clone.go
Normal file
83
internal/core/clone.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"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" {
|
||||
return 0, fmt.Errorf("clone is only supported for rpm 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.SourceURL, repo.Type)
|
||||
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, sourceURL, repoType string) {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := s.jobStore.MarkCloneJobStarted(ctx, jobID); err != nil {
|
||||
log.Printf("clone job %d: failed to mark started: %v", jobID, err)
|
||||
return
|
||||
}
|
||||
|
||||
destDir := filepath.Join(s.dataDir, "repos", fmt.Sprintf("%d", repoID), repoType)
|
||||
cloner := rpm.New(sourceURL, destDir)
|
||||
cloner.OnProgress = func(p rpm.Progress) {
|
||||
log.Printf("clone job %d: %s (%d bytes)", jobID, p.File, p.BytesDone)
|
||||
}
|
||||
|
||||
if err := cloner.Clone(ctx); err != nil {
|
||||
errMsg := err.Error()
|
||||
_ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobFailed, &errMsg)
|
||||
log.Printf("clone job %d: failed: %v", jobID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rpm.RegenerateMetadata(destDir); err != nil {
|
||||
log.Printf("clone job %d: metadata regeneration failed: %v", jobID, err)
|
||||
}
|
||||
|
||||
_ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobCompleted, nil)
|
||||
log.Printf("clone job %d: completed", jobID)
|
||||
}
|
||||
5
internal/core/errors.go
Normal file
5
internal/core/errors.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package core
|
||||
|
||||
import "github.com/syonad/clonepack/internal/store"
|
||||
|
||||
var ErrNotFound = store.ErrNotFound
|
||||
82
internal/core/repo.go
Normal file
82
internal/core/repo.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
rpmclone "github.com/syonad/clonepack/internal/clone/rpm"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
var validTypes = map[string]bool{"apt": true, "rpm": true, "docker": true, "binary": true}
|
||||
|
||||
type RepoService struct {
|
||||
store store.RepoStore
|
||||
dataDir string
|
||||
}
|
||||
|
||||
func NewRepoService(s store.RepoStore, dataDir string) *RepoService {
|
||||
return &RepoService{store: s, dataDir: dataDir}
|
||||
}
|
||||
|
||||
var validSyncModes = map[string]bool{"auto": true, "manual": true}
|
||||
|
||||
type CreateRepoInput struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SourceURL string `json:"source_url"`
|
||||
SyncMode string `json:"sync_mode"`
|
||||
}
|
||||
|
||||
func (s *RepoService) Create(ctx context.Context, in CreateRepoInput) (*store.Repo, error) {
|
||||
if in.Name == "" {
|
||||
return nil, fmt.Errorf("name is required")
|
||||
}
|
||||
if !validTypes[in.Type] {
|
||||
return nil, fmt.Errorf("invalid type: %s (must be apt, rpm, docker, or binary)", in.Type)
|
||||
}
|
||||
if in.SourceURL == "" {
|
||||
return nil, fmt.Errorf("source_url is required")
|
||||
}
|
||||
if in.SyncMode == "" {
|
||||
in.SyncMode = "auto"
|
||||
} else if !validSyncModes[in.SyncMode] {
|
||||
return nil, fmt.Errorf("invalid sync_mode: %s (must be auto or manual)", in.SyncMode)
|
||||
}
|
||||
|
||||
r := &store.Repo{Name: in.Name, Type: in.Type, SourceURL: in.SourceURL, SyncMode: in.SyncMode}
|
||||
id, err := s.store.CreateRepo(ctx, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo, err := s.store.GetRepo(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.initStorage(repo)
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func (s *RepoService) initStorage(repo *store.Repo) {
|
||||
switch repo.Type {
|
||||
case "rpm":
|
||||
localDir := filepath.Join(s.dataDir, "repos", fmt.Sprintf("%d", repo.ID), "rpm")
|
||||
if err := rpmclone.InitEmptyRepo(localDir); err != nil {
|
||||
log.Printf("init storage for repo %d: %v", repo.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RepoService) List(ctx context.Context) ([]store.Repo, error) {
|
||||
return s.store.ListRepos(ctx)
|
||||
}
|
||||
|
||||
func (s *RepoService) Get(ctx context.Context, id int64) (*store.Repo, error) {
|
||||
return s.store.GetRepo(ctx, id)
|
||||
}
|
||||
|
||||
func (s *RepoService) Delete(ctx context.Context, id int64) error {
|
||||
return s.store.DeleteRepo(ctx, id)
|
||||
}
|
||||
260
internal/core/snapshot.go
Normal file
260
internal/core/snapshot.go
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rpmclone "github.com/syonad/clonepack/internal/clone/rpm"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
type SnapshotService struct {
|
||||
snapshotStore store.SnapshotStore
|
||||
repoStore store.RepoStore
|
||||
dataDir string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewSnapshotService(
|
||||
snapshotStore store.SnapshotStore,
|
||||
repoStore store.RepoStore,
|
||||
dataDir string,
|
||||
) *SnapshotService {
|
||||
return &SnapshotService{
|
||||
snapshotStore: snapshotStore,
|
||||
repoStore: repoStore,
|
||||
dataDir: dataDir,
|
||||
httpClient: &http.Client{Timeout: 30 * time.Minute},
|
||||
}
|
||||
}
|
||||
|
||||
type SnapshotDiff struct {
|
||||
From *store.Snapshot
|
||||
To *store.Snapshot
|
||||
Added []store.SnapshotPackage
|
||||
Removed []store.SnapshotPackage
|
||||
Unchanged int
|
||||
}
|
||||
|
||||
func (s *SnapshotService) TakeSnapshot(ctx context.Context, repoID int64, label string) (int64, error) {
|
||||
if _, err := s.repoStore.GetRepo(ctx, repoID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
localDir := filepath.Join(s.dataDir, "repos", fmt.Sprintf("%d", repoID), "rpm")
|
||||
|
||||
repomdData, err := os.ReadFile(filepath.Join(localDir, "repodata", "repomd.xml"))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read repomd.xml: %w", err)
|
||||
}
|
||||
|
||||
var repomd rpmclone.RepoMD
|
||||
if err := xml.Unmarshal(repomdData, &repomd); err != nil {
|
||||
return 0, fmt.Errorf("parse repomd.xml: %w", err)
|
||||
}
|
||||
|
||||
var primaryHref string
|
||||
for _, entry := range repomd.Data {
|
||||
if entry.Type == "primary" {
|
||||
primaryHref = entry.Location.Href
|
||||
break
|
||||
}
|
||||
}
|
||||
if primaryHref == "" {
|
||||
return 0, fmt.Errorf("no primary entry in repomd.xml")
|
||||
}
|
||||
|
||||
gzData, err := os.ReadFile(filepath.Join(localDir, filepath.FromSlash(primaryHref)))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read primary.xml.gz: %w", err)
|
||||
}
|
||||
|
||||
gz, err := gzip.NewReader(bytes.NewReader(gzData))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("open gzip: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
var primary rpmclone.PrimaryMetadata
|
||||
if err := xml.NewDecoder(gz).Decode(&primary); err != nil {
|
||||
return 0, fmt.Errorf("parse primary.xml: %w", err)
|
||||
}
|
||||
|
||||
pkgs := make([]store.SnapshotPackage, len(primary.Packages))
|
||||
for i, p := range primary.Packages {
|
||||
pkgs[i] = store.SnapshotPackage{
|
||||
Name: p.Name,
|
||||
Version: p.Version.Ver + "-" + p.Version.Rel,
|
||||
Arch: p.Arch,
|
||||
Location: p.Location.Href,
|
||||
Checksum: p.Checksum.Value,
|
||||
ChecksumType: p.Checksum.Type,
|
||||
Size: p.Size.Package,
|
||||
}
|
||||
}
|
||||
|
||||
snapID, err := s.snapshotStore.CreateSnapshot(ctx, repoID, label)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(pkgs) > 0 {
|
||||
if err := s.snapshotStore.AddSnapshotPackages(ctx, snapID, pkgs); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return snapID, nil
|
||||
}
|
||||
|
||||
func (s *SnapshotService) List(ctx context.Context, repoID int64) ([]store.Snapshot, error) {
|
||||
if _, err := s.repoStore.GetRepo(ctx, repoID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.snapshotStore.ListSnapshots(ctx, repoID)
|
||||
}
|
||||
|
||||
func (s *SnapshotService) Get(ctx context.Context, repoID, id int64) (*store.Snapshot, []store.SnapshotPackage, error) {
|
||||
snap, err := s.snapshotStore.GetSnapshot(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if snap.RepoID != repoID {
|
||||
return nil, nil, store.ErrNotFound
|
||||
}
|
||||
pkgs, err := s.snapshotStore.GetSnapshotPackages(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return snap, pkgs, nil
|
||||
}
|
||||
|
||||
func (s *SnapshotService) Delete(ctx context.Context, repoID, id int64) error {
|
||||
snap, err := s.snapshotStore.GetSnapshot(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if snap.RepoID != repoID {
|
||||
return store.ErrNotFound
|
||||
}
|
||||
return s.snapshotStore.DeleteSnapshot(ctx, id)
|
||||
}
|
||||
|
||||
func (s *SnapshotService) Diff(ctx context.Context, repoID, fromID, toID int64) (*SnapshotDiff, error) {
|
||||
from, err := s.snapshotStore.GetSnapshot(ctx, fromID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
to, err := s.snapshotStore.GetSnapshot(ctx, toID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from.RepoID != repoID || to.RepoID != repoID {
|
||||
return nil, fmt.Errorf("snapshots do not belong to repo %d", repoID)
|
||||
}
|
||||
|
||||
fromPkgs, err := s.snapshotStore.GetSnapshotPackages(ctx, fromID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
toPkgs, err := s.snapshotStore.GetSnapshotPackages(ctx, toID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fromSet := make(map[string]struct{}, len(fromPkgs))
|
||||
for _, p := range fromPkgs {
|
||||
fromSet[p.Name+"|"+p.Version+"|"+p.Arch] = struct{}{}
|
||||
}
|
||||
toSet := make(map[string]struct{}, len(toPkgs))
|
||||
for _, p := range toPkgs {
|
||||
toSet[p.Name+"|"+p.Version+"|"+p.Arch] = struct{}{}
|
||||
}
|
||||
|
||||
diff := &SnapshotDiff{From: from, To: to}
|
||||
for _, p := range toPkgs {
|
||||
if _, ok := fromSet[p.Name+"|"+p.Version+"|"+p.Arch]; ok {
|
||||
diff.Unchanged++
|
||||
} else {
|
||||
diff.Added = append(diff.Added, p)
|
||||
}
|
||||
}
|
||||
for _, p := range fromPkgs {
|
||||
if _, ok := toSet[p.Name+"|"+p.Version+"|"+p.Arch]; !ok {
|
||||
diff.Removed = append(diff.Removed, p)
|
||||
}
|
||||
}
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func (s *SnapshotService) Rollback(ctx context.Context, repoID, snapshotID int64) error {
|
||||
repo, err := s.repoStore.GetRepo(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snap, err := s.snapshotStore.GetSnapshot(ctx, snapshotID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if snap.RepoID != repoID {
|
||||
return store.ErrNotFound
|
||||
}
|
||||
|
||||
pkgs, err := s.snapshotStore.GetSnapshotPackages(ctx, snapshotID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localDir := filepath.Join(s.dataDir, "repos", fmt.Sprintf("%d", repoID), "rpm")
|
||||
|
||||
snapSet := make(map[string]store.SnapshotPackage, len(pkgs))
|
||||
for _, p := range pkgs {
|
||||
snapSet[p.Location] = p
|
||||
}
|
||||
|
||||
var errs []error
|
||||
if err := filepath.WalkDir(localDir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() || filepath.Ext(path) != ".rpm" {
|
||||
return err
|
||||
}
|
||||
rel, _ := filepath.Rel(localDir, path)
|
||||
rel = filepath.ToSlash(rel)
|
||||
if _, ok := snapSet[rel]; !ok {
|
||||
if rmErr := os.Remove(path); rmErr != nil {
|
||||
errs = append(errs, rmErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
for _, p := range pkgs {
|
||||
dest := filepath.Join(localDir, filepath.FromSlash(p.Location))
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
continue
|
||||
}
|
||||
url := strings.TrimRight(repo.SourceURL, "/") + "/" + p.Location
|
||||
if _, dlErr := rpmclone.DownloadAndVerify(ctx, s.httpClient, url, dest, p.ChecksumType, p.Checksum); dlErr != nil {
|
||||
errs = append(errs, fmt.Errorf("re-download %s: %w", p.Name, dlErr))
|
||||
}
|
||||
}
|
||||
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if repo.Type == "rpm" {
|
||||
if err := rpmclone.RegenerateMetadata(localDir); err != nil {
|
||||
return fmt.Errorf("regenerate metadata: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
268
internal/core/sync.go
Normal file
268
internal/core/sync.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rpmclone "github.com/syonad/clonepack/internal/clone/rpm"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
type SyncService struct {
|
||||
repoStore store.RepoStore
|
||||
pendingStore store.PendingPackageStore
|
||||
blockedStore store.BlockedPackageStore
|
||||
cloneSvc *CloneService
|
||||
snapshotSvc *SnapshotService
|
||||
dataDir string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewSyncService(
|
||||
repoStore store.RepoStore,
|
||||
pendingStore store.PendingPackageStore,
|
||||
blockedStore store.BlockedPackageStore,
|
||||
cloneSvc *CloneService,
|
||||
snapshotSvc *SnapshotService,
|
||||
dataDir string,
|
||||
) *SyncService {
|
||||
return &SyncService{
|
||||
repoStore: repoStore,
|
||||
pendingStore: pendingStore,
|
||||
blockedStore: blockedStore,
|
||||
cloneSvc: cloneSvc,
|
||||
snapshotSvc: snapshotSvc,
|
||||
dataDir: dataDir,
|
||||
httpClient: &http.Client{Timeout: 5 * time.Minute},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SyncService) ValidateRepo(ctx context.Context, repoID int64) error {
|
||||
_, err := s.repoStore.GetRepo(ctx, repoID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SyncService) ScanRepo(ctx context.Context, repoID int64) error {
|
||||
repo, err := s.repoStore.GetRepo(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if repo.Type != "rpm" {
|
||||
return fmt.Errorf("sync only supported for rpm repos")
|
||||
}
|
||||
|
||||
localDir := filepath.Join(s.dataDir, "repos", fmt.Sprintf("%d", repoID), "rpm")
|
||||
scanner := rpmclone.NewScanner(repo.SourceURL, localDir)
|
||||
newPkgs, err := scanner.Scan(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scan repo %d: %w", repoID, err)
|
||||
}
|
||||
|
||||
if len(newPkgs) == 0 {
|
||||
log.Printf("sync: repo %d is up to date", repoID)
|
||||
return nil
|
||||
}
|
||||
|
||||
blocked, err := s.blockedStore.ListBlocked(ctx, repoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list blocked for repo %d: %w", repoID, err)
|
||||
}
|
||||
blockedLocations := make(map[string]bool, len(blocked))
|
||||
blockedNames := make(map[string]bool, len(blocked))
|
||||
for _, b := range blocked {
|
||||
blockedLocations[b.Location] = true
|
||||
if b.Name != "" {
|
||||
blockedNames[b.Name] = true
|
||||
}
|
||||
}
|
||||
filtered := newPkgs[:0]
|
||||
for _, p := range newPkgs {
|
||||
if !blockedLocations[p.Location] && !blockedNames[p.Name] {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
newPkgs = filtered
|
||||
|
||||
if len(newPkgs) == 0 {
|
||||
log.Printf("sync: repo %d is up to date (all new packages are blocked)", repoID)
|
||||
return nil
|
||||
}
|
||||
log.Printf("sync: repo %d has %d new package(s)", repoID, len(newPkgs))
|
||||
|
||||
switch repo.SyncMode {
|
||||
case "auto":
|
||||
_, err = s.cloneSvc.StartClone(ctx, repoID)
|
||||
if err != nil && err != ErrCloneAlreadyRunning {
|
||||
return fmt.Errorf("start clone for repo %d: %w", repoID, err)
|
||||
}
|
||||
case "manual":
|
||||
pending := make([]store.PendingPackage, len(newPkgs))
|
||||
for i, p := range newPkgs {
|
||||
pending[i] = store.PendingPackage{
|
||||
RepoID: repoID,
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Arch: p.Arch,
|
||||
Location: p.Location,
|
||||
Checksum: p.Checksum,
|
||||
ChecksumType: p.ChecksumType,
|
||||
Size: p.Size,
|
||||
}
|
||||
}
|
||||
if err := s.pendingStore.UpsertPending(ctx, pending); err != nil {
|
||||
return fmt.Errorf("upsert pending for repo %d: %w", repoID, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown sync_mode %q for repo %d", repo.SyncMode, repoID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SyncService) ListPending(ctx context.Context, repoID int64) ([]store.PendingPackage, error) {
|
||||
if _, err := s.repoStore.GetRepo(ctx, repoID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.pendingStore.ListPending(ctx, repoID)
|
||||
}
|
||||
|
||||
func (s *SyncService) ApprovePending(ctx context.Context, repoID int64, ids []int64) error {
|
||||
repo, err := s.repoStore.GetRepo(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
all, err := s.pendingStore.ListPending(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wanted := make(map[int64]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
wanted[id] = true
|
||||
}
|
||||
|
||||
localDir := filepath.Join(s.dataDir, "repos", fmt.Sprintf("%d", repoID), "rpm")
|
||||
var downloadErrors []error
|
||||
var anyApproved bool
|
||||
for _, pkg := range all {
|
||||
if !wanted[pkg.ID] {
|
||||
continue
|
||||
}
|
||||
url := strings.TrimRight(repo.SourceURL, "/") + "/" + pkg.Location
|
||||
dest := filepath.Join(localDir, filepath.FromSlash(pkg.Location))
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||
downloadErrors = append(downloadErrors, fmt.Errorf("mkdirall %s: %w", pkg.Location, err))
|
||||
continue
|
||||
}
|
||||
if _, err := rpmclone.DownloadAndVerify(ctx, s.httpClient, url, dest, pkg.ChecksumType, pkg.Checksum); err != nil {
|
||||
downloadErrors = append(downloadErrors, fmt.Errorf("download %s: %w", pkg.Name, err))
|
||||
continue
|
||||
}
|
||||
// Supprime immédiatement de la liste — même si la suite échoue, le paquet est acquis.
|
||||
if err := s.pendingStore.DeletePending(ctx, []int64{pkg.ID}); err != nil {
|
||||
log.Printf("delete pending %d: %v", pkg.ID, err)
|
||||
}
|
||||
anyApproved = true
|
||||
}
|
||||
|
||||
if anyApproved {
|
||||
if repo.Type == "rpm" {
|
||||
if err := rpmclone.RegenerateMetadata(localDir); err != nil {
|
||||
log.Printf("metadata regeneration for repo %d failed: %v", repoID, err)
|
||||
}
|
||||
}
|
||||
label := "auto-" + time.Now().UTC().Format(time.RFC3339)
|
||||
if _, err := s.snapshotSvc.TakeSnapshot(context.Background(), repoID, label); err != nil {
|
||||
log.Printf("auto-snapshot for repo %d failed: %v", repoID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(downloadErrors...)
|
||||
}
|
||||
|
||||
func (s *SyncService) BlockPackages(ctx context.Context, repoID int64, pendingIDs []int64) error {
|
||||
if _, err := s.repoStore.GetRepo(ctx, repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
all, err := s.pendingStore.ListPending(ctx, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wanted := make(map[int64]bool, len(pendingIDs))
|
||||
for _, id := range pendingIDs {
|
||||
wanted[id] = true
|
||||
}
|
||||
var toBlock []store.BlockedPackage
|
||||
var toDelete []int64
|
||||
for _, p := range all {
|
||||
if wanted[p.ID] {
|
||||
toBlock = append(toBlock, store.BlockedPackage{Name: p.Name, Location: p.Location})
|
||||
toDelete = append(toDelete, p.ID)
|
||||
}
|
||||
}
|
||||
if len(toBlock) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := s.blockedStore.BlockPackages(ctx, repoID, toBlock); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.pendingStore.DeletePending(ctx, toDelete)
|
||||
}
|
||||
|
||||
func (s *SyncService) UnblockPackages(ctx context.Context, repoID int64, ids []int64) error {
|
||||
if _, err := s.repoStore.GetRepo(ctx, repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.blockedStore.UnblockPackages(ctx, repoID, ids)
|
||||
}
|
||||
|
||||
func (s *SyncService) ListBlocked(ctx context.Context, repoID int64) ([]store.BlockedPackage, error) {
|
||||
if _, err := s.repoStore.GetRepo(ctx, repoID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.blockedStore.ListBlocked(ctx, repoID)
|
||||
}
|
||||
|
||||
func (s *SyncService) RejectPending(ctx context.Context, repoID int64, ids []int64) error {
|
||||
if _, err := s.repoStore.GetRepo(ctx, repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.pendingStore.DeletePending(ctx, ids)
|
||||
}
|
||||
|
||||
func (s *SyncService) StartScheduler(ctx context.Context, interval time.Duration) {
|
||||
go func() {
|
||||
log.Printf("sync scheduler started (interval=%s)", interval)
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("sync scheduler stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.runScheduledScan(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *SyncService) runScheduledScan(ctx context.Context) {
|
||||
repos, err := s.repoStore.ListRepos(ctx)
|
||||
if err != nil {
|
||||
log.Printf("sync scheduler: list repos error: %v", err)
|
||||
return
|
||||
}
|
||||
for _, repo := range repos {
|
||||
if err := s.ScanRepo(ctx, repo.ID); err != nil {
|
||||
log.Printf("sync scheduler: scan repo %d error: %v", repo.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue