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 }