From 57c93177adaa0484a799df1c4424d5d0241d8d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Roth?= Date: Sat, 13 Jun 2026 12:42:00 +0200 Subject: [PATCH] publish: support MultiDist toggle --- api/publish.go | 24 ++++++++++++++++++++++++ deb/publish.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/api/publish.go b/api/publish.go index 7637caa1..e13da535 100644 --- a/api/publish.go +++ b/api/publish.go @@ -502,6 +502,9 @@ func apiPublishUpdateSwitch(c *gin.Context) { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } + // Capture MultiDist before mutations to detect a false→true transition. + prevMultiDist := published.MultiDist + // Apply field mutations on the freshly loaded object. if b.SkipContents != nil { published.SkipContents = *b.SkipContents @@ -549,6 +552,15 @@ func apiPublishUpdateSwitch(c *gin.Context) { if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } + // When MultiDist is toggled, the old pool layout still has files that + // CleanupPrefixComponentFiles won't touch (it only scans the new layout). + // Run a second pass over the previous layout to remove stale files. + if prevMultiDist != published.MultiDist { + err = taskCollection.CleanupAfterMultiDistToggle(context, published, prevMultiDist, cleanComponents, taskCollectionFactory, out) + if err != nil { + return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to clean legacy pool: %s", err) + } + } } return &task.ProcessReturnValue{Code: http.StatusOK, Value: published}, nil @@ -1148,6 +1160,9 @@ func apiPublishUpdate(c *gin.Context) { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } + // Capture MultiDist before mutations to detect a false→true transition. + prevMultiDist := published.MultiDist + // Apply field mutations on the freshly loaded object. if b.SkipContents != nil { published.SkipContents = *b.SkipContents @@ -1184,6 +1199,15 @@ func apiPublishUpdate(c *gin.Context) { if err != nil { return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err) } + // When MultiDist is toggled, the old pool layout still has files that + // CleanupPrefixComponentFiles won't touch (it only scans the new layout). + // Run a second pass over the previous layout to remove stale files. + if prevMultiDist != published.MultiDist { + err = taskCollection.CleanupAfterMultiDistToggle(context, published, prevMultiDist, cleanComponents, taskCollectionFactory, out) + if err != nil { + return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to clean legacy pool: %s", err) + } + } } return &task.ProcessReturnValue{Code: http.StatusOK, Value: published}, nil diff --git a/deb/publish.go b/deb/publish.go index 8b67c626..79c086f6 100644 --- a/deb/publish.go +++ b/deb/publish.go @@ -1540,6 +1540,52 @@ func (collection *PublishedRepoCollection) listReferencedFilesByComponent(prefix return referencedFiles, nil } +// CleanupAfterMultiDistToggle cleans up stale pool files left behind when the +// MultiDist flag is toggled on a published repository. +// +// - false→true: Publish() wrote packages into pool/// +// but the old flat pool// files were not removed because +// CleanupPrefixComponentFiles only scans the new MultiDist tree. +// A second pass with MultiDist=false cleans the legacy flat layout by +// reusing the existing orphan-detection logic (the repo is now MultiDist=true +// so it is excluded from the referenced-files scan, making its old pool +// entries appear orphaned). +// +// - true→false: Publish() wrote packages into pool// but the old +// per-distribution pool/// directories were not +// removed. The orphan-detection approach cannot be used here because the +// repo's RefList still contains all packages (they just moved locations). +// Instead we directly remove each pool/// directory. +// This is safe because per-distribution pool dirs are exclusive to a single +// prefix+distribution combination — no other published repo can share them. +func (collection *PublishedRepoCollection) CleanupAfterMultiDistToggle(publishedStorageProvider aptly.PublishedStorageProvider, + published *PublishedRepo, prevMultiDist bool, cleanComponents []string, collectionFactory *CollectionFactory, progress aptly.Progress) error { + if prevMultiDist == published.MultiDist { + return nil + } + + if !prevMultiDist && published.MultiDist { + // false→true: use orphan-detection via the existing cleanup, but with + // MultiDist temporarily set to false so it scans the flat pool layout. + legacy := *published + legacy.MultiDist = false + return collection.CleanupPrefixComponentFiles(publishedStorageProvider, &legacy, cleanComponents, collectionFactory, progress) + } + + // true→false: directly remove the per-distribution pool directories. + publishedStorage := publishedStorageProvider.GetPublishedStorage(published.Storage) + for _, component := range cleanComponents { + poolDir := filepath.Join(published.Prefix, "pool", published.Distribution, component) + if err := publishedStorage.RemoveDirs(poolDir, progress); err != nil { + return err + } + } + // Remove the distribution-level pool dir if it is now empty. + distPoolDir := filepath.Join(published.Prefix, "pool", published.Distribution) + _ = publishedStorage.RemoveDirs(distPoolDir, progress) + return nil +} + // CleanupPrefixComponentFiles removes all unreferenced files in published storage under prefix/component pair func (collection *PublishedRepoCollection) CleanupPrefixComponentFiles(publishedStorageProvider aptly.PublishedStorageProvider, published *PublishedRepo, cleanComponents []string, collectionFactory *CollectionFactory, progress aptly.Progress) error {