161 lines
4.2 KiB
Go
161 lines
4.2 KiB
Go
package bundle
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
type rawManifest struct {
|
|
SchemaVersion *int `json:"schema_version"`
|
|
ID *string `json:"id"`
|
|
Digest *string `json:"digest"`
|
|
Created *string `json:"created"`
|
|
Files []rawManifestFile `json:"files"`
|
|
}
|
|
|
|
type rawManifestFile struct {
|
|
Path *string `json:"path"`
|
|
SHA256 *string `json:"sha256"`
|
|
Size *int64 `json:"size"`
|
|
}
|
|
|
|
func ParseManifest(data []byte) (Manifest, error) {
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
var raw rawManifest
|
|
if err := decoder.Decode(&raw); err != nil {
|
|
return Manifest{}, fmt.Errorf("parse manifest: %w", err)
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); err != io.EOF {
|
|
return Manifest{}, fmt.Errorf("parse manifest: trailing data")
|
|
}
|
|
|
|
var manifest Manifest
|
|
if raw.SchemaVersion == nil {
|
|
return Manifest{}, fmt.Errorf("manifest schema_version is required")
|
|
}
|
|
manifest.SchemaVersion = *raw.SchemaVersion
|
|
if raw.ID == nil || *raw.ID == "" {
|
|
return Manifest{}, fmt.Errorf("manifest id is required")
|
|
}
|
|
manifest.ID = *raw.ID
|
|
if raw.Digest == nil || *raw.Digest == "" {
|
|
return Manifest{}, fmt.Errorf("manifest digest is required")
|
|
}
|
|
manifest.Digest = *raw.Digest
|
|
if raw.Created == nil || *raw.Created == "" {
|
|
return Manifest{}, fmt.Errorf("manifest created is required")
|
|
}
|
|
created, err := time.Parse(time.RFC3339, *raw.Created)
|
|
if err != nil {
|
|
return Manifest{}, fmt.Errorf("manifest created must be RFC3339: %w", err)
|
|
}
|
|
manifest.Created = created
|
|
if len(raw.Files) == 0 {
|
|
return Manifest{}, fmt.Errorf("manifest files is required")
|
|
}
|
|
|
|
for index, rawFile := range raw.Files {
|
|
file, err := parseManifestFile(index, rawFile)
|
|
if err != nil {
|
|
return Manifest{}, err
|
|
}
|
|
manifest.Files = append(manifest.Files, file)
|
|
}
|
|
if err := ValidateManifest(manifest); err != nil {
|
|
return Manifest{}, fmt.Errorf("manifest %w", err)
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
func parseManifestFile(index int, raw rawManifestFile) (ManifestFile, error) {
|
|
if raw.Path == nil || *raw.Path == "" {
|
|
return ManifestFile{}, fmt.Errorf("manifest files[%d].path is required", index)
|
|
}
|
|
if raw.SHA256 == nil || *raw.SHA256 == "" {
|
|
return ManifestFile{}, fmt.Errorf("manifest files[%d].sha256 is required", index)
|
|
}
|
|
if raw.Size == nil {
|
|
return ManifestFile{}, fmt.Errorf("manifest files[%d].size is required", index)
|
|
}
|
|
return ManifestFile{
|
|
Path: *raw.Path,
|
|
SHA256: *raw.SHA256,
|
|
Size: *raw.Size,
|
|
}, nil
|
|
}
|
|
|
|
func MarshalManifest(manifest Manifest) ([]byte, error) {
|
|
if err := ValidateManifest(manifest); err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(data, '\n'), nil
|
|
}
|
|
|
|
func LoadManifest(root string) (Manifest, error) {
|
|
data, err := os.ReadFile(filepath.Join(root, ManifestName))
|
|
if err != nil {
|
|
return Manifest{}, fmt.Errorf("read manifest: %w", err)
|
|
}
|
|
return ParseManifest(data)
|
|
}
|
|
|
|
func WriteManifest(root string, manifest Manifest, opts WriteManifestOptions) error {
|
|
data, err := MarshalManifest(manifest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
manifestPath := filepath.Join(root, ManifestName)
|
|
if !opts.Overwrite {
|
|
file, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
|
|
if err != nil {
|
|
return fmt.Errorf("write manifest: %w", err)
|
|
}
|
|
if _, err := file.Write(data); err != nil {
|
|
_ = file.Close()
|
|
return fmt.Errorf("write manifest: %w", err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return fmt.Errorf("write manifest: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
dir := root
|
|
if dir == "" {
|
|
dir = "."
|
|
}
|
|
tmp, err := os.CreateTemp(dir, ".manifest-*.tmp")
|
|
if err != nil {
|
|
return fmt.Errorf("write manifest temp: %w", err)
|
|
}
|
|
tmpPath := tmp.Name()
|
|
cleanup := true
|
|
defer func() {
|
|
if cleanup {
|
|
_ = os.Remove(tmpPath)
|
|
}
|
|
}()
|
|
if _, err := tmp.Write(data); err != nil {
|
|
_ = tmp.Close()
|
|
return fmt.Errorf("write manifest temp: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return fmt.Errorf("write manifest temp: %w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, manifestPath); err != nil {
|
|
return fmt.Errorf("replace manifest: %w", err)
|
|
}
|
|
cleanup = false
|
|
return nil
|
|
}
|