Skip to content

Commit 912dd77

Browse files
authored
Merge pull request #1381 from presztak/simplestreams_prune
incus-simplestreams: Add prune command
2 parents ce639bb + 811e479 commit 912dd77

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

cmd/incus-simplestreams/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ func main() {
6262
verifyCmd := cmdVerify{global: &globalCmd}
6363
app.AddCommand(verifyCmd.Command())
6464

65+
pruneCmd := cmdPrune{global: &globalCmd}
66+
app.AddCommand(pruneCmd.Command())
67+
6568
// Run the main command and handle errors.
6669
err := app.Execute()
6770
if err != nil {

cmd/incus-simplestreams/main_prune.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
"slices"
11+
"sort"
12+
13+
"github.com/spf13/cobra"
14+
15+
cli "github.com/lxc/incus/v6/internal/cmd"
16+
"github.com/lxc/incus/v6/shared/simplestreams"
17+
)
18+
19+
type cmdPrune struct {
20+
global *cmdGlobal
21+
22+
flagDryRun bool
23+
flagRetention int
24+
flagVerbose bool
25+
}
26+
27+
// Command generates the command definition.
28+
func (c *cmdPrune) Command() *cobra.Command {
29+
cmd := &cobra.Command{}
30+
cmd.Use = "prune"
31+
cmd.Short = "Clean up obsolete files and data"
32+
cmd.Long = cli.FormatSection("Description",
33+
`Cleans up obsolete tarball files and removes outdated versions of a product
34+
35+
The prune command scans the project directory for tarball files that do not have corresponding references
36+
in the 'images.json' file. Any tarball file that is not listed in images.json is considered orphaned
37+
and will be deleted.
38+
Additionally this command will delete older images, keeping a configurable number of older images per product.`)
39+
40+
cmd.RunE = c.Run
41+
cmd.Flags().BoolVarP(&c.flagDryRun, "dry-run", "d", false, "Preview changes without executing actual operations")
42+
cmd.Flags().IntVarP(&c.flagRetention, "retention", "r", 2, "Number of older versions of the product to preserve"+"``")
43+
cmd.Flags().BoolVarP(&c.flagVerbose, "verbose", "v", false, "Show all information messages")
44+
45+
return cmd
46+
}
47+
48+
// Run runs the actual command logic.
49+
func (c *cmdPrune) Run(cmd *cobra.Command, args []string) error {
50+
// Quick checks.
51+
exit, err := c.global.CheckArgs(cmd, args, 0, 0)
52+
if exit {
53+
return err
54+
}
55+
56+
if c.flagDryRun {
57+
c.flagVerbose = true
58+
}
59+
60+
err = c.prune()
61+
if err != nil {
62+
return err
63+
}
64+
65+
return nil
66+
}
67+
68+
func (c *cmdPrune) pruneFiles(products *simplestreams.Products, filesToPreserve []string) error {
69+
deletedFiles := []string{}
70+
err := filepath.WalkDir("./images", func(path string, d fs.DirEntry, err error) error {
71+
if err != nil {
72+
return err
73+
}
74+
75+
// Omit the path if it is a directory or if it exists in the images.json file.
76+
if d.IsDir() || slices.Contains(filesToPreserve, path) {
77+
return nil
78+
}
79+
80+
if c.flagVerbose {
81+
deletedFiles = append(deletedFiles, path)
82+
}
83+
84+
if !c.flagDryRun {
85+
e := os.Remove(path)
86+
if e != nil {
87+
return e
88+
}
89+
}
90+
91+
return nil
92+
})
93+
if err != nil {
94+
return err
95+
}
96+
97+
if c.flagVerbose && len(deletedFiles) > 0 {
98+
fmt.Printf("Following files were removed:\n")
99+
for _, file := range deletedFiles {
100+
fmt.Println(file)
101+
}
102+
}
103+
104+
return nil
105+
}
106+
107+
func (c *cmdPrune) prune() error {
108+
body, err := os.ReadFile("streams/v1/images.json")
109+
if err != nil {
110+
return err
111+
}
112+
113+
products := simplestreams.Products{}
114+
err = json.Unmarshal(body, &products)
115+
if err != nil {
116+
return err
117+
}
118+
119+
filesToPreserve := []string{}
120+
deletedItems := []string{}
121+
deletedVersions := []string{}
122+
for kProduct, product := range products.Products {
123+
versionNames := []string{}
124+
for kVersion, version := range product.Versions {
125+
for kItem, item := range version.Items {
126+
_, err := os.Stat(item.Path)
127+
if err != nil {
128+
if !errors.Is(err, os.ErrNotExist) {
129+
return err
130+
}
131+
132+
if c.flagVerbose {
133+
deletedItems = append(deletedItems, fmt.Sprintf("%s:%s:%s", kProduct, kVersion, item.Path))
134+
}
135+
136+
// Corresponding file doesn't exist on disk. Remove item from products.
137+
delete(version.Items, kItem)
138+
}
139+
140+
filesToPreserve = append(filesToPreserve, item.Path)
141+
}
142+
143+
if len(version.Items) == 0 {
144+
delete(product.Versions, kVersion)
145+
continue
146+
}
147+
148+
versionNames = append(versionNames, kVersion)
149+
}
150+
151+
if len(product.Versions) == 0 {
152+
delete(products.Products, kProduct)
153+
continue
154+
}
155+
156+
sort.Strings(versionNames)
157+
158+
updatedVersions := map[string]simplestreams.ProductVersion{}
159+
iteration := 0
160+
for i := len(versionNames) - 1; i >= 0; i-- {
161+
version := versionNames[i]
162+
if iteration <= c.flagRetention {
163+
updatedVersions[version] = product.Versions[version]
164+
} else if c.flagVerbose {
165+
deletedVersions = append(deletedVersions, fmt.Sprintf("%s:%s", kProduct, version))
166+
}
167+
168+
iteration += 1
169+
}
170+
171+
p := products.Products[kProduct]
172+
p.Versions = updatedVersions
173+
products.Products[kProduct] = p
174+
}
175+
176+
if c.flagVerbose {
177+
if len(deletedItems) > 0 {
178+
fmt.Printf("Following items were removed from images.json:\n")
179+
for _, item := range deletedItems {
180+
fmt.Println(item)
181+
}
182+
}
183+
184+
if len(deletedVersions) > 0 {
185+
fmt.Printf("Following versions were removed:\n")
186+
for _, version := range deletedVersions {
187+
fmt.Println(version)
188+
}
189+
}
190+
}
191+
192+
if !c.flagDryRun {
193+
// Write back the images file.
194+
body, err = json.Marshal(&products)
195+
if err != nil {
196+
return err
197+
}
198+
199+
err = os.WriteFile("streams/v1/images.json", body, 0644)
200+
if err != nil {
201+
return err
202+
}
203+
204+
// Re-generate the index.
205+
err = writeIndex(&products)
206+
if err != nil {
207+
return err
208+
}
209+
}
210+
211+
err = c.pruneFiles(&products, filesToPreserve)
212+
if err != nil {
213+
return err
214+
}
215+
216+
return nil
217+
}

0 commit comments

Comments
 (0)