Skip to content

Commit a27236f

Browse files
committed
incus-simplestreams: Add prune command
Signed-off-by: Piotr Resztak <[email protected]>
1 parent ccaa4de commit a27236f

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
"slices"
10+
"sort"
11+
12+
"github.com/spf13/cobra"
13+
14+
cli "github.com/lxc/incus/v6/internal/cmd"
15+
"github.com/lxc/incus/v6/shared/simplestreams"
16+
)
17+
18+
type cmdPrune struct {
19+
global *cmdGlobal
20+
21+
flagDryRun bool
22+
flagPruneVersions bool
23+
flagVerbose bool
24+
}
25+
26+
// Command generates the command definition.
27+
func (c *cmdPrune) Command() *cobra.Command {
28+
cmd := &cobra.Command{}
29+
cmd.Use = "prune"
30+
cmd.Short = "Clean up obsolete files and data"
31+
cmd.Long = cli.FormatSection("Description",
32+
`Cleans up obsolote tarball files and removes outdated versions of a product
33+
34+
The prune command scans the project directory for tarball files that do not have corresponding references
35+
in the 'images.json' file. Any tarball file that is not listed in images.json is considered orphaned
36+
and will be deleted.
37+
The prune command identifies and deletes older versions of product data,
38+
retaining only the most recent existing version.`)
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().BoolVarP(&c.flagPruneVersions, "prune-versions", "p", true, "Prune old versions of a product")
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+
products, err := c.productsWithPrunePreviousVersions()
61+
if err != nil {
62+
return err
63+
}
64+
65+
err = c.pruneImages(products)
66+
if err != nil {
67+
return err
68+
}
69+
70+
return nil
71+
}
72+
73+
func (c *cmdPrune) pruneImages(products *simplestreams.Products) error {
74+
filesToPreserve := []string{}
75+
for _, product := range products.Products {
76+
for _, version := range product.Versions {
77+
for _, item := range version.Items {
78+
filesToPreserve = append(filesToPreserve, item.Path)
79+
}
80+
}
81+
}
82+
83+
err := filepath.WalkDir("./images", func(path string, d fs.DirEntry, err error) error {
84+
if err != nil {
85+
return err
86+
}
87+
88+
// Omit the path if it is a directory or if it exists in the images.json file.
89+
if d.IsDir() || slices.Contains(filesToPreserve, path) {
90+
return nil
91+
}
92+
93+
if c.flagVerbose {
94+
fmt.Printf("Removing: %s\n", path)
95+
}
96+
97+
if !c.flagDryRun {
98+
e := os.Remove(path)
99+
if e != nil {
100+
return e
101+
}
102+
}
103+
104+
return nil
105+
})
106+
if err != nil {
107+
return err
108+
}
109+
110+
return nil
111+
}
112+
113+
func (c *cmdPrune) productsWithPrunePreviousVersions() (*simplestreams.Products, error) {
114+
body, err := os.ReadFile("streams/v1/images.json")
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
products := simplestreams.Products{}
120+
err = json.Unmarshal(body, &products)
121+
if err != nil {
122+
return nil, err
123+
}
124+
125+
if !c.flagPruneVersions {
126+
return &products, nil
127+
}
128+
129+
for kProduct, product := range products.Products {
130+
// If there are fewer than two versions, then there is nothing to do here.
131+
if len(product.Versions) < 2 {
132+
continue
133+
}
134+
135+
versionNames := []string{}
136+
for kVersion := range product.Versions {
137+
versionNames = append(versionNames, kVersion)
138+
}
139+
140+
sort.Strings(versionNames)
141+
142+
recentVersion := versionNames[len(versionNames)-1]
143+
if c.flagVerbose {
144+
fmt.Printf("Leaving version %s for product %s\n", recentVersion, kProduct)
145+
}
146+
147+
// Keep only recent 'version'.
148+
p := products.Products[kProduct]
149+
p.Versions = map[string]simplestreams.ProductVersion{recentVersion: p.Versions[recentVersion]}
150+
products.Products[kProduct] = p
151+
}
152+
153+
if !c.flagDryRun {
154+
// Write back the images file.
155+
body, err = json.Marshal(&products)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
err = os.WriteFile("streams/v1/images.json", body, 0644)
161+
if err != nil {
162+
return nil, err
163+
}
164+
}
165+
166+
return &products, nil
167+
}

0 commit comments

Comments
 (0)