check / check (push) Waiting to run
Error messages in mfer/ and internal/cli/ are lowercase except names and acronyms, carry no "failed to" or command-name prefix, and each wrap names only the operation and thing the wrapped error does not already name, so a stacked message names what failed once. Wraps around errors that already name their operation and path (os and afero path errors, url.Error, the gpg helpers' own errors) are dropped. runGPG appends gpg's stderr only when gpg wrote some, so no message ends in a colon. errHTTPStatus reads "unexpected HTTP status"; both inner-not-set sentinels read "inner message not set". No sentinel, errors.Is result or exit status changes. Message tests pin the new text through the real call sites. Model: opus-5-5
84 lines
1.8 KiB
Go
84 lines
1.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/urfave/cli/v3"
|
|
"sneak.berlin/go/mfer/mfer"
|
|
)
|
|
|
|
// ExportEntry represents a single file entry in the exported JSON output.
|
|
type ExportEntry struct {
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
Hashes []string `json:"hashes"`
|
|
Mtime *string `json:"mtime,omitempty"`
|
|
Ctime *string `json:"ctime,omitempty"`
|
|
Mode string `json:"mode"` // octal, "0000" when none was recorded
|
|
}
|
|
|
|
func (mfa *CLIApp) exportManifestOperation(
|
|
ctx context.Context, cmd *cli.Command,
|
|
) error {
|
|
pathOrURL, err := mfa.resolveManifestArg(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rc, err := mfa.openManifestReader(ctx, pathOrURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = rc.Close() }()
|
|
|
|
//nolint:contextcheck // mfer loads a manifest without a context
|
|
manifest, err := mfer.NewManifestFromReader(rc)
|
|
if err != nil {
|
|
return fmt.Errorf("parse manifest: %w", err)
|
|
}
|
|
|
|
files := manifest.Files()
|
|
entries := make([]ExportEntry, 0, len(files))
|
|
|
|
for _, f := range files {
|
|
entry := ExportEntry{
|
|
Path: f.GetPath(),
|
|
Size: f.GetSize(),
|
|
Hashes: make([]string, 0, len(f.GetHashes())),
|
|
Mode: fmt.Sprintf("%04o", f.GetMode()),
|
|
}
|
|
|
|
for _, h := range f.GetHashes() {
|
|
entry.Hashes = append(entry.Hashes, hex.EncodeToString(h.GetMultiHash()))
|
|
}
|
|
|
|
if mtime, ok := entryMtime(f); ok {
|
|
t := mtime.UTC().Format(time.RFC3339Nano)
|
|
entry.Mtime = &t
|
|
}
|
|
|
|
if f.GetCtime() != nil {
|
|
t := time.Unix(f.GetCtime().GetSeconds(), int64(f.GetCtime().GetNanos())).
|
|
UTC().Format(time.RFC3339Nano)
|
|
entry.Ctime = &t
|
|
}
|
|
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
enc := json.NewEncoder(mfa.Stdout)
|
|
enc.SetIndent("", " ")
|
|
|
|
err = enc.Encode(entries)
|
|
if err != nil {
|
|
return fmt.Errorf("encode JSON: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|