148 lines
4.4 KiB
Go
148 lines
4.4 KiB
Go
package apt
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// InitEmptyRepo creates the directory structure for an APT mirror and writes an
|
|
// empty Release file so APT clients don't get a 404 before the first clone.
|
|
func InitEmptyRepo(localDir string, cfg Config) error {
|
|
if err := os.MkdirAll(filepath.Join(localDir, "pool"), 0o755); err != nil {
|
|
return err
|
|
}
|
|
for _, component := range cfg.Components {
|
|
for _, arch := range cfg.Architectures {
|
|
dir := filepath.Join(localDir, "dists", cfg.Suite, component, "binary-"+arch)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
releasePath := filepath.Join(localDir, "dists", cfg.Suite, "Release")
|
|
if _, err := os.Stat(releasePath); os.IsNotExist(err) {
|
|
releaseData := GenerateRelease(cfg.Suite, cfg.Suite, cfg.Components, cfg.Architectures, nil)
|
|
if err := atomicWrite(releasePath, releaseData); err != nil {
|
|
return fmt.Errorf("write empty Release: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RegenerateMetadata rebuilds local Packages, Packages.gz, and Release files
|
|
// for all configured component/arch pairs, keeping only packages present on disk.
|
|
func RegenerateMetadata(localDir string, cfg Config) error {
|
|
suite := cfg.Suite
|
|
var releaseEntries []ReleaseEntry
|
|
|
|
for _, component := range cfg.Components {
|
|
for _, arch := range cfg.Architectures {
|
|
entries, err := regenerateComponentArch(localDir, suite, component, arch)
|
|
if err != nil {
|
|
return fmt.Errorf("regenerate %s/%s: %w", component, arch, err)
|
|
}
|
|
releaseEntries = append(releaseEntries, entries...)
|
|
}
|
|
}
|
|
|
|
// Write dists/{suite}/Release.
|
|
releaseData := GenerateRelease(suite, suite, cfg.Components, cfg.Architectures, releaseEntries)
|
|
releasePath := filepath.Join(localDir, "dists", suite, "Release")
|
|
if err := os.MkdirAll(filepath.Dir(releasePath), 0o755); err != nil {
|
|
return fmt.Errorf("create dists dir: %w", err)
|
|
}
|
|
if err := atomicWrite(releasePath, releaseData); err != nil {
|
|
return fmt.Errorf("write Release: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// regenerateComponentArch regenerates Packages and Packages.gz for one component/arch
|
|
// and returns the two ReleaseEntry values (plain then gz) for the Release file.
|
|
func regenerateComponentArch(localDir, suite, component, arch string) ([]ReleaseEntry, error) {
|
|
binDir := filepath.Join(localDir, "dists", suite, component, "binary-"+arch)
|
|
upstreamGzPath := filepath.Join(binDir, upstreamPackagesGz)
|
|
|
|
// 1. Read upstream Packages.gz.
|
|
gzData, err := os.ReadFile(upstreamGzPath)
|
|
if err != nil {
|
|
// Not cloned yet — skip silently.
|
|
return nil, nil
|
|
}
|
|
|
|
// 2. Decompress.
|
|
gz, err := gzip.NewReader(bytes.NewReader(gzData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open gzip %s: %w", upstreamGzPath, err)
|
|
}
|
|
rawPackages, err := io.ReadAll(gz)
|
|
gz.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decompress %s: %w", upstreamGzPath, err)
|
|
}
|
|
|
|
// 3. Parse all stanzas.
|
|
allPkgs, err := ParsePackages(rawPackages)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse upstream Packages: %w", err)
|
|
}
|
|
|
|
// 4. Filter: keep only packages present on disk.
|
|
checker := diskPresenceChecker(localDir)
|
|
localPkgs := FilterPackages(allPkgs, checker)
|
|
|
|
// 5. Re-emit filtered stanzas.
|
|
plainBytes := emitPackages(localPkgs)
|
|
|
|
// 6. Gzip the filtered Packages.
|
|
var gzBuf bytes.Buffer
|
|
gzw := gzip.NewWriter(&gzBuf)
|
|
if _, err := gzw.Write(plainBytes); err != nil {
|
|
return nil, fmt.Errorf("gzip write: %w", err)
|
|
}
|
|
if err := gzw.Close(); err != nil {
|
|
return nil, fmt.Errorf("gzip close: %w", err)
|
|
}
|
|
gzBytes := gzBuf.Bytes()
|
|
|
|
// 7. Write Packages and Packages.gz.
|
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
plainPath := filepath.Join(binDir, "Packages")
|
|
if err := atomicWrite(plainPath, plainBytes); err != nil {
|
|
return nil, fmt.Errorf("write Packages: %w", err)
|
|
}
|
|
gzPath := filepath.Join(binDir, "Packages.gz")
|
|
if err := atomicWrite(gzPath, gzBytes); err != nil {
|
|
return nil, fmt.Errorf("write Packages.gz: %w", err)
|
|
}
|
|
|
|
// 8. Compute SHA256 hashes.
|
|
plainSum := sha256.Sum256(plainBytes)
|
|
gzSum := sha256.Sum256(gzBytes)
|
|
|
|
// Release paths are relative to dists/{suite}/.
|
|
relBase := component + "/binary-" + arch + "/"
|
|
|
|
return []ReleaseEntry{
|
|
{
|
|
Hash: hex.EncodeToString(plainSum[:]),
|
|
Size: int64(len(plainBytes)),
|
|
Path: relBase + "Packages",
|
|
},
|
|
{
|
|
Hash: hex.EncodeToString(gzSum[:]),
|
|
Size: int64(len(gzBytes)),
|
|
Path: relBase + "Packages.gz",
|
|
},
|
|
}, nil
|
|
}
|
|
|