Skip to content

Commit 41c130d

Browse files
committed
Add basic CLI
1 parent 637b02b commit 41c130d

File tree

4 files changed

+473
-0
lines changed

4 files changed

+473
-0
lines changed

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Development files
2+
/bucket/
3+
4+
# Binaries for programs and plugins
5+
/aptblob
6+
*.exe
7+
*.exe~
8+
*.dll
9+
*.so
10+
*.dylib
11+
12+
# Test binary, built with `go test -c`
13+
*.test
14+
15+
# Output of the go coverage tool, specifically when used with LiteIDE
16+
*.out

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# aptblob
2+
3+
## Starting a Repository
4+
5+
```
6+
mkdir bucket
7+
BUCKET="file://$(pwd)/bucket"
8+
gpg --gen-key
9+
KEYID=42CAFE...
10+
11+
go run . init -k $KEYID "$BUCKET" stable <<EOF
12+
Origin: stable
13+
Label: stable
14+
Codename: stable
15+
Architectures: amd64
16+
Components: main
17+
Description: Apt repository for Foo
18+
EOF
19+
20+
go run . upload -k $KEYID "$BUCKET" stable mypackage.deb
21+
```

aptblob.go

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/hex"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"strconv"
11+
"strings"
12+
13+
"github.com/spf13/cobra"
14+
"gocloud.dev/blob"
15+
_ "gocloud.dev/blob/fileblob"
16+
_ "gocloud.dev/blob/gcsblob"
17+
_ "gocloud.dev/blob/s3blob"
18+
"gocloud.dev/gcerrors"
19+
"zombiezen.com/go/aptblob/internal/deb"
20+
)
21+
22+
func cmdInit(ctx context.Context, bucket *blob.Bucket, dist string, keyID string) error {
23+
fmt.Fprintln(os.Stderr, "aptblob: reading Release from stdin...")
24+
newRelease, err := deb.ParseReleaseIndex(os.Stdin)
25+
if err != nil {
26+
return fmt.Errorf("read stdin: %w", err)
27+
}
28+
oldRelease, err := downloadReleaseIndex(ctx, bucket, dist)
29+
if err != nil {
30+
return fmt.Errorf("read old release: %w", err)
31+
}
32+
keys := []string{"MD5Sum", "SHA1", "SHA256"}
33+
for _, k := range keys {
34+
if v := oldRelease.Get(k); v != "" {
35+
newRelease.Set(k, v)
36+
}
37+
}
38+
err = uploadReleaseIndex(ctx, bucket, dist, newRelease, keyID)
39+
if err != nil {
40+
return err
41+
}
42+
return nil
43+
}
44+
45+
func downloadReleaseIndex(ctx context.Context, bucket *blob.Bucket, dist string) (deb.Paragraph, error) {
46+
key := releaseIndexPath(dist)
47+
blob, err := bucket.NewReader(ctx, key, nil)
48+
if err != nil {
49+
if gcerrors.Code(err) == gcerrors.NotFound {
50+
return nil, nil
51+
}
52+
return nil, err
53+
}
54+
index, err := deb.ParseReleaseIndex(blob)
55+
blob.Close()
56+
if err != nil {
57+
return nil, fmt.Errorf("%s: %w", key, err)
58+
}
59+
return index, nil
60+
}
61+
62+
const componentName = "main"
63+
64+
func cmdUpload(ctx context.Context, bucket *blob.Bucket, dist string, keyID string, debPath string) error {
65+
// Extract package metadata.
66+
debFile, err := os.Open(debPath)
67+
if err != nil {
68+
return err
69+
}
70+
defer debFile.Close()
71+
control, err := deb.ExtractControl(debFile)
72+
if err != nil {
73+
return fmt.Errorf("%s: %w", debPath, err)
74+
}
75+
p := deb.NewParser(bytes.NewReader(control))
76+
p.Fields = deb.ControlFields
77+
if !p.Single() {
78+
if err := p.Err(); err != nil {
79+
return fmt.Errorf("%s: %w", debPath, err)
80+
}
81+
}
82+
newPackage := p.Paragraph()
83+
arch := newPackage.Get("Architecture")
84+
if arch == "" {
85+
return fmt.Errorf("%s: no Architecture", debPath)
86+
}
87+
poolPath := "pool/" + filepath.Base(debPath)
88+
packageHashes, err := upload(ctx, bucket, poolPath, "application/vnd.debian.binary-package", "immutable", debFile)
89+
if err != nil {
90+
return err
91+
}
92+
newPackage.Set("Filename", poolPath)
93+
newPackage.Set("Size", strconv.FormatInt(packageHashes.size, 10))
94+
newPackage.Set("MD5sum", hex.EncodeToString(packageHashes.md5[:]))
95+
newPackage.Set("SHA1", hex.EncodeToString(packageHashes.sha1[:]))
96+
newPackage.Set("SHA256", hex.EncodeToString(packageHashes.sha256[:]))
97+
98+
// List existing packages.
99+
var packages []deb.Paragraph
100+
if packagesReader, err := bucket.NewReader(ctx, binaryPackagesIndexPath(dist, componentName, arch), nil); err == nil {
101+
p := deb.NewParser(packagesReader)
102+
p.Fields = deb.ControlFields
103+
for p.Next() {
104+
packages = append(packages, p.Paragraph())
105+
}
106+
packagesReader.Close()
107+
if err := p.Err(); err != nil {
108+
return fmt.Errorf("%s: %w", binaryPackagesIndexPath(dist, componentName, arch), err)
109+
}
110+
} else if gcerrors.Code(err) != gcerrors.NotFound {
111+
return err
112+
}
113+
114+
// Append package to index.
115+
packages = append(packages, newPackage)
116+
packageIndexHashes, packageIndexGzipHashes, err := uploadPackageIndex(
117+
ctx, bucket, dist, componentName, arch, packages)
118+
if err != nil {
119+
return err
120+
}
121+
122+
// Update release index.
123+
release, err := downloadReleaseIndex(ctx, bucket, dist)
124+
if err != nil {
125+
return err
126+
}
127+
packagesDistPath := strings.TrimPrefix(
128+
binaryPackagesIndexPath(dist, componentName, arch),
129+
distDirPath(dist)+"/",
130+
)
131+
packagesGzipDistPath := strings.TrimPrefix(
132+
binaryPackagesGzipIndexPath(dist, componentName, arch),
133+
distDirPath(dist)+"/",
134+
)
135+
err = updateSignature(&release, "MD5Sum",
136+
deb.IndexSignature{
137+
Filename: packagesDistPath,
138+
Checksum: packageIndexHashes.md5[:],
139+
Size: packageIndexHashes.size,
140+
},
141+
deb.IndexSignature{
142+
Filename: packagesGzipDistPath,
143+
Checksum: packageIndexGzipHashes.md5[:],
144+
Size: packageIndexGzipHashes.size,
145+
},
146+
)
147+
if err != nil {
148+
return fmt.Errorf("%s: %w", releaseIndexPath(dist), err)
149+
}
150+
err = updateSignature(&release, "SHA1",
151+
deb.IndexSignature{
152+
Filename: packagesDistPath,
153+
Checksum: packageIndexHashes.sha1[:],
154+
Size: packageIndexHashes.size,
155+
},
156+
deb.IndexSignature{
157+
Filename: packagesGzipDistPath,
158+
Checksum: packageIndexGzipHashes.sha1[:],
159+
Size: packageIndexGzipHashes.size,
160+
},
161+
)
162+
if err != nil {
163+
return fmt.Errorf("%s: %w", releaseIndexPath(dist), err)
164+
}
165+
err = updateSignature(&release, "SHA256",
166+
deb.IndexSignature{
167+
Filename: packagesDistPath,
168+
Checksum: packageIndexHashes.sha256[:],
169+
Size: packageIndexHashes.size,
170+
},
171+
deb.IndexSignature{
172+
Filename: packagesGzipDistPath,
173+
Checksum: packageIndexGzipHashes.sha256[:],
174+
Size: packageIndexGzipHashes.size,
175+
},
176+
)
177+
if err != nil {
178+
return fmt.Errorf("%s: %w", releaseIndexPath(dist), err)
179+
}
180+
if err := uploadReleaseIndex(ctx, bucket, dist, release, keyID); err != nil {
181+
return err
182+
}
183+
184+
return nil
185+
}
186+
187+
func updateSignature(para *deb.Paragraph, key string, newSigs ...deb.IndexSignature) error {
188+
if len(newSigs) == 0 {
189+
return nil
190+
}
191+
sigs, err := deb.ParseIndexSignatures(para.Get(key), len(newSigs[0].Checksum))
192+
if err != nil {
193+
return fmt.Errorf("%s: %w", key, err)
194+
}
195+
newMap := make(map[string]deb.IndexSignature, len(newSigs))
196+
for _, sig := range newSigs {
197+
newMap[sig.Filename] = sig
198+
}
199+
for i := range sigs {
200+
newSig, ok := newMap[sigs[i].Filename]
201+
if !ok {
202+
continue
203+
}
204+
sigs[i].Checksum = newSig.Checksum
205+
sigs[i].Size = newSig.Size
206+
delete(newMap, sigs[i].Filename)
207+
}
208+
for _, sig := range newSigs {
209+
if _, ok := newMap[sig.Filename]; !ok {
210+
// Already added.
211+
continue
212+
}
213+
sigs = append(sigs, sig)
214+
delete(newMap, sig.Filename)
215+
}
216+
sb := new(strings.Builder)
217+
for _, sig := range sigs {
218+
sb.WriteString("\n ")
219+
sb.WriteString(sig.String())
220+
}
221+
para.Set(key, sb.String())
222+
return nil
223+
}
224+
225+
func main() {
226+
rootCmd := &cobra.Command{
227+
Use: "aptblob",
228+
Short: "Manager for blob-storage-based APT repositories",
229+
SilenceErrors: true,
230+
SilenceUsage: true,
231+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
232+
if len(args) == 0 {
233+
return fmt.Errorf("must have at least one argument for bucket")
234+
}
235+
var err error
236+
return err
237+
},
238+
}
239+
keyID := rootCmd.PersistentFlags().StringP("keyid", "k", "", "GPG key to sign with")
240+
rootCmd.AddCommand(&cobra.Command{
241+
Use: "init [options] BUCKET DIST",
242+
Short: "Set up a distribution",
243+
Args: cobra.ExactArgs(2),
244+
DisableFlagsInUseLine: true,
245+
SilenceErrors: true,
246+
SilenceUsage: true,
247+
RunE: func(cmd *cobra.Command, args []string) error {
248+
bucket, err := blob.OpenBucket(cmd.Context(), args[0])
249+
if err != nil {
250+
return err
251+
}
252+
return cmdInit(cmd.Context(), bucket, args[1], *keyID)
253+
},
254+
})
255+
rootCmd.AddCommand(&cobra.Command{
256+
Use: "upload [options] BUCKET DIST DEB",
257+
Short: "Upload a deb package",
258+
Args: cobra.ExactArgs(3),
259+
DisableFlagsInUseLine: true,
260+
SilenceErrors: true,
261+
SilenceUsage: true,
262+
RunE: func(cmd *cobra.Command, args []string) error {
263+
bucket, err := blob.OpenBucket(cmd.Context(), args[0])
264+
if err != nil {
265+
return err
266+
}
267+
return cmdUpload(cmd.Context(), bucket, args[1], *keyID, args[2])
268+
},
269+
})
270+
if err := rootCmd.Execute(); err != nil {
271+
fmt.Fprintln(os.Stderr, "aptblob:", err)
272+
os.Exit(1)
273+
}
274+
}

0 commit comments

Comments
 (0)