debanator/package.go

161 lines
4.0 KiB
Go

package debanator
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/go-chi/chi/v5"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"pault.ag/go/debian/control"
"pault.ag/go/debian/dependency"
"pault.ag/go/debian/hashio"
"pault.ag/go/debian/version"
)
// A group of debs of a package for different arches/version
type LogicalPackage struct {
Name string
// arch:version:package
Arches map[dependency.Arch]map[version.Version]control.BinaryIndex
}
type hashedFile struct {
buf []byte
md5Hash control.MD5FileHash
sha1Hash control.SHA1FileHash
sha256Hash control.SHA256FileHash
}
type Repo struct {
packages []LogicalPackage
cache map[string]hashedFile
release []byte
}
func (r *Repo) GetArches() []dependency.Arch {
arches := make(map[dependency.Arch]struct{})
for _, lp := range r.packages {
for arch := range lp.Arches {
arches[arch] = struct{}{}
}
}
return maps.Keys(arches)
}
// Find the latest versions of all packages for the given arch
func (r *Repo) GetPackagesForArch(a dependency.Arch) []control.BinaryIndex {
out := []control.BinaryIndex{}
for _, p := range r.packages {
if versions, ok := p.Arches[a]; ok {
var latest version.Version
for v := range versions {
if version.Compare(v, latest) > 0 {
latest = v
}
}
out = append(out, p.Arches[a][latest])
}
}
return out
}
func (r *Repo) makePackagesFileForArch(arch dependency.Arch) error {
var b bytes.Buffer
w, hashers, err := hashio.NewHasherWriters([]string{"md5", "sha256", "sha1"}, &b)
enc, _ := control.NewEncoder(w)
for _, d := range r.GetPackagesForArch(arch) {
if err = enc.Encode(d); err != nil {
return fmt.Errorf("encoding package %s: %w", d.Package, err)
}
}
fname := fmt.Sprintf("main/binary-%s/Packages", arch)
hashes := make(map[string]control.FileHash)
for _, h := range hashers {
hashes[h.Name()] = control.FileHashFromHasher(fname, *h)
}
r.cache[fname] = hashedFile{
buf: b.Bytes(),
sha256Hash: control.SHA256FileHash{hashes["sha256"]},
sha1Hash: control.SHA1FileHash{hashes["sha1"]},
md5Hash: control.MD5FileHash{hashes["md5"]},
}
return nil
}
// Generate and cache all the Package/Repo files
func (r *Repo) GenerateFiles() error {
for _, arch := range r.GetArches() {
if err := r.makePackagesFileForArch(arch); err != nil {
return fmt.Errorf("generating files for arch %s: %w", arch, err)
}
}
r.makeRelease()
return nil
}
func (r *Repo) makeRelease() {
var rel bytes.Buffer
enc, _ := control.NewEncoder(&rel)
const dateFmt = "Mon, 02 Jan 2006 15:04:05 MST"
var md5s []control.MD5FileHash
var sha1s []control.SHA1FileHash
var sha256s []control.SHA256FileHash
for _, f := range r.cache {
md5s = append(md5s, f.md5Hash)
sha1s = append(sha1s, f.sha1Hash)
sha256s = append(sha256s, f.sha256Hash)
}
if err := enc.Encode(Release{
Suite: "stable",
Architectures: r.GetArches(),
Components: "main",
Date: time.Now().UTC().Format(dateFmt),
MD5Sum: md5s,
SHA1: sha1s,
SHA256: sha256s,
}); err != nil {
log.Fatal(err)
}
r.release = rel.Bytes()
return
}
// Handle a deb/apt repository http request
func (r *Repo) GetHandler(keyring *crypto.KeyRing) http.Handler {
router := chi.NewRouter()
router.Get("/Release", func(w http.ResponseWriter, req *http.Request) {
if _, err := w.Write(r.release); err != nil {
log.Fatal(err)
}
})
router.Get("/Release.gpg", func(w http.ResponseWriter, req *http.Request) {
msg := crypto.NewPlainMessage(r.release)
sig, err := keyring.SignDetached(msg)
if err != nil {
log.Fatal(err)
}
sigStr, err := sig.GetArmored()
if err != nil {
log.Fatal(err)
}
io.WriteString(w, sigStr)
})
router.Get("/main/{arch}/Packages", func(w http.ResponseWriter, req *http.Request) {
h, ok := r.cache[fmt.Sprintf("main/%s/Packages", chi.URLParam(req, "arch"))]
if !ok {
w.WriteHeader(http.StatusBadRequest)
return
}
_, err := w.Write(h.buf); if err != nil {
log.Error(err)
}
})
return router
}