2025-09-25 15:39:17 +03:00

208 lines
3.9 KiB
Go

package archive
import (
"archive/tar"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"time"
"kisekinopureya.com.tr/updater/internal/version"
)
// Output metadata structure
type Output struct {
Metadata struct {
Date int64 `json:"date"`
Version string `json:"version"`
} `json:"metadata"`
Files []struct {
Name string `json:"name"`
Type string `json:"type"`
Sha256 string `json:"sha256"`
} `json:"files"`
}
type fileInfo struct {
Name string
Type string
Path string
Sha256 string
}
// Pack creates a tar.xz archive with metadata.json and the given images
func Pack(rootfsPath, verityPath, kernelPath, outPath string) error {
paths := []struct {
Path string
Type string
}{
{rootfsPath, "rootfs"},
{verityPath, "dm-verity"},
{kernelPath, "kernel"},
}
var infos []fileInfo
for _, p := range paths {
log.Printf("Hashing %s...", p.Path)
sum, err := sha256File(p.Path)
if err != nil {
return fmt.Errorf("hashing %s: %w", p.Path, err)
}
infos = append(infos, fileInfo{
Name: filepath.Base(p.Path),
Type: p.Type,
Path: p.Path,
Sha256: sum,
})
}
output, err := exec.Command("blkid", rootfsPath).Output()
if err != nil {
log.Fatal(err)
}
versionFromImage, err := version.Parse(string(output))
if err != nil {
log.Fatal(err)
}
var out Output
out.Metadata.Date = time.Now().Unix()
// NOTE: version should be filled by caller if needed
out.Metadata.Version = versionFromImage
for _, fi := range infos {
out.Files = append(out.Files, struct {
Name string `json:"name"`
Type string `json:"type"`
Sha256 string `json:"sha256"`
}{
Name: fi.Name,
Type: fi.Type,
Sha256: fi.Sha256,
})
}
metaBytes, err := json.MarshalIndent(out, "", " ")
if err != nil {
return fmt.Errorf("marshal metadata: %w", err)
}
log.Printf("Metadata: %+v\n", out)
if err := writeTarXz(outPath, metaBytes, infos); err != nil {
return fmt.Errorf("create tar.xz: %w", err)
}
sigFile := outPath + ".sig"
cmd := exec.Command("gpg", "--output", sigFile, "--detach-sign", "--armor", outPath)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(fmt.Errorf("gpg signing failed: %w", err))
}
fmt.Printf("Created %s and signed as %s\n", outPath, sigFile)
return nil
}
func sha256File(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func writeTarXz(outPath string, metadata []byte, files []fileInfo) error {
outFile, err := os.Create(outPath)
if err != nil {
return fmt.Errorf("create output: %w", err)
}
defer outFile.Close()
cmd := exec.Command("xz", "-z", "-c")
cmd.Stdout = outFile
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
tw := tar.NewWriter(stdin)
// metadata.json
metaHeader := &tar.Header{
Name: "metadata.json",
Mode: 0644,
Size: int64(len(metadata)),
}
if err := tw.WriteHeader(metaHeader); err != nil {
return err
}
if _, err := tw.Write(metadata); err != nil {
return err
}
// files
for _, fi := range files {
if err := addFileToTar(tw, fi.Path, fi.Name); err != nil {
return err
}
}
if err := tw.Close(); err != nil {
return err
}
if err := stdin.Close(); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
func addFileToTar(tw *tar.Writer, srcPath, destName string) error {
f, err := os.Open(srcPath)
if err != nil {
return err
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return err
}
hdr := &tar.Header{
Name: destName,
Mode: int64(stat.Mode().Perm()),
Size: stat.Size(),
ModTime: stat.ModTime(),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
_, err = io.Copy(tw, f)
return err
}