Skip to content

Commit ffd2b27

Browse files
committed
Add manifest inspection command with JSON and verify modes
1 parent 4a114f0 commit ffd2b27

2 files changed

Lines changed: 531 additions & 0 deletions

File tree

cmd/manifest.go

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"sort"
10+
"strings"
11+
12+
"github.com/spf13/cobra"
13+
14+
"github.com/substantialcattle5/sietch/internal/config"
15+
"github.com/substantialcattle5/sietch/internal/fs"
16+
manifestbuilder "github.com/substantialcattle5/sietch/internal/manifest"
17+
)
18+
19+
type manifestSummaryOutput struct {
20+
VaultName string `json:"vault_name"`
21+
VaultPath string `json:"vault_path"`
22+
EncryptionType string `json:"encryption_type"`
23+
KeyPath string `json:"key_path,omitempty"`
24+
ChunkSize string `json:"chunk_size"`
25+
ManifestCount int `json:"manifest_count"`
26+
}
27+
28+
type fileManifestOutput struct {
29+
ManifestID string `json:"manifest_id"`
30+
File string `json:"file"`
31+
Destination string `json:"destination"`
32+
FullPath string `json:"full_path"`
33+
ChunkCount int `json:"chunk_count"`
34+
ChunkRefs []config.ChunkRef `json:"chunk_refs"`
35+
Tags []string `json:"tags,omitempty"`
36+
ModifiedAt string `json:"modified_at"`
37+
AddedAt string `json:"added_at"`
38+
LastSynced string `json:"last_synced,omitempty"`
39+
LastVerified string `json:"last_verified,omitempty"`
40+
}
41+
42+
type verifyOutput struct {
43+
Passed bool `json:"passed"`
44+
Failures []string `json:"failures"`
45+
}
46+
47+
var manifestCmd = &cobra.Command{
48+
Use: "manifest",
49+
Short: "Inspect vault and file manifests",
50+
Long: `Inspect vault metadata and file manifest entries.
51+
52+
Examples:
53+
sietch manifest
54+
sietch manifest --file docs/report.pdf
55+
sietch manifest --file report.pdf --json
56+
sietch manifest --verify`,
57+
RunE: runManifestCommand,
58+
}
59+
60+
func runManifestCommand(cmd *cobra.Command, _ []string) error {
61+
fileArg, _ := cmd.Flags().GetString("file")
62+
jsonOutput, _ := cmd.Flags().GetBool("json")
63+
verify, _ := cmd.Flags().GetBool("verify")
64+
65+
vaultRoot, err := fs.FindVaultRoot()
66+
if err != nil {
67+
return fmt.Errorf("not inside a vault: %v", err)
68+
}
69+
70+
if verify {
71+
result := verifyVaultManifests(vaultRoot)
72+
if jsonOutput {
73+
if err := writeJSON(cmd, result); err != nil {
74+
return err
75+
}
76+
if !result.Passed {
77+
return errors.New("manifest verification failed")
78+
}
79+
return nil
80+
}
81+
for _, failure := range result.Failures {
82+
fmt.Fprintf(cmd.OutOrStdout(), "FAIL: %s\n", failure)
83+
}
84+
if result.Passed {
85+
fmt.Fprintln(cmd.OutOrStdout(), "Manifest verification passed")
86+
return nil
87+
}
88+
return errors.New("manifest verification failed")
89+
}
90+
91+
if fileArg != "" {
92+
manifestID, mf, err := resolveFileManifest(vaultRoot, fileArg)
93+
if err != nil {
94+
return err
95+
}
96+
output := buildFileManifestOutput(manifestID, mf)
97+
if jsonOutput {
98+
return writeJSON(cmd, output)
99+
}
100+
printFileManifestSummary(cmd, output)
101+
return nil
102+
}
103+
104+
summary, err := buildManifestSummary(vaultRoot)
105+
if err != nil {
106+
return err
107+
}
108+
if jsonOutput {
109+
return writeJSON(cmd, summary)
110+
}
111+
printManifestSummary(cmd, summary)
112+
return nil
113+
}
114+
115+
func buildManifestSummary(vaultRoot string) (manifestSummaryOutput, error) {
116+
vaultCfg, err := manifestbuilder.LoadVaultConfig(vaultRoot)
117+
if err != nil {
118+
return manifestSummaryOutput{}, fmt.Errorf("failed to load vault metadata: %w", err)
119+
}
120+
121+
manifestIDs, err := manifestbuilder.ListFileManifests(vaultRoot)
122+
if err != nil {
123+
return manifestSummaryOutput{}, fmt.Errorf("failed to list file manifests: %w", err)
124+
}
125+
126+
return manifestSummaryOutput{
127+
VaultName: vaultCfg.Name,
128+
VaultPath: vaultRoot,
129+
EncryptionType: vaultCfg.Encryption.Type,
130+
KeyPath: vaultCfg.Encryption.KeyPath,
131+
ChunkSize: vaultCfg.Chunking.ChunkSize,
132+
ManifestCount: len(manifestIDs),
133+
}, nil
134+
}
135+
136+
func resolveFileManifest(vaultRoot, fileArg string) (string, *config.FileManifest, error) {
137+
manifestIDs, err := manifestbuilder.ListFileManifests(vaultRoot)
138+
if err != nil {
139+
return "", nil, fmt.Errorf("failed to list file manifests: %w", err)
140+
}
141+
if len(manifestIDs) == 0 {
142+
return "", nil, fmt.Errorf("no files found in vault")
143+
}
144+
145+
sort.Strings(manifestIDs)
146+
for _, id := range manifestIDs {
147+
if id == fileArg {
148+
mf, loadErr := manifestbuilder.LoadFileManifest(vaultRoot, id)
149+
if loadErr != nil {
150+
return "", nil, fmt.Errorf("failed to load manifest '%s': %w", id, loadErr)
151+
}
152+
return id, mf, nil
153+
}
154+
}
155+
156+
targetBase := filepath.Base(fileArg)
157+
var suggestions []string
158+
for _, id := range manifestIDs {
159+
mf, loadErr := manifestbuilder.LoadFileManifest(vaultRoot, id)
160+
if loadErr != nil {
161+
continue
162+
}
163+
fullPath := mf.Destination + mf.FilePath
164+
if fullPath == fileArg || mf.FilePath == fileArg || filepath.Base(mf.FilePath) == fileArg || filepath.Base(fullPath) == fileArg {
165+
return id, mf, nil
166+
}
167+
if filepath.Base(fullPath) == targetBase {
168+
suggestions = append(suggestions, fullPath)
169+
}
170+
}
171+
172+
if len(suggestions) > 0 {
173+
return "", nil, fmt.Errorf("no file found matching '%s'. Did you mean one of: %v", fileArg, suggestions)
174+
}
175+
176+
return "", nil, fmt.Errorf("no file found matching '%s'. Use 'sietch ls' to see available files", fileArg)
177+
}
178+
179+
func buildFileManifestOutput(manifestID string, mf *config.FileManifest) fileManifestOutput {
180+
out := fileManifestOutput{
181+
ManifestID: manifestID,
182+
File: mf.FilePath,
183+
Destination: mf.Destination,
184+
FullPath: mf.Destination + mf.FilePath,
185+
ChunkCount: len(mf.Chunks),
186+
ChunkRefs: mf.Chunks,
187+
Tags: mf.Tags,
188+
ModifiedAt: mf.ModTime,
189+
AddedAt: mf.AddedAt.Format("2006-01-02T15:04:05Z07:00"),
190+
}
191+
if !mf.LastSynced.IsZero() {
192+
out.LastSynced = mf.LastSynced.Format("2006-01-02T15:04:05Z07:00")
193+
}
194+
if !mf.LastVerified.IsZero() {
195+
out.LastVerified = mf.LastVerified.Format("2006-01-02T15:04:05Z07:00")
196+
}
197+
return out
198+
}
199+
200+
func verifyVaultManifests(vaultRoot string) verifyOutput {
201+
failures := make([]string, 0)
202+
203+
manifestIDs, err := manifestbuilder.ListFileManifests(vaultRoot)
204+
if err != nil {
205+
failures = append(failures, fmt.Sprintf("unable to list manifests: %v", err))
206+
return verifyOutput{Passed: false, Failures: failures}
207+
}
208+
209+
seenIDs := make(map[string]bool)
210+
seenFullPaths := make(map[string]string)
211+
for _, id := range manifestIDs {
212+
if seenIDs[id] {
213+
failures = append(failures, fmt.Sprintf("duplicate manifest ID: %s", id))
214+
continue
215+
}
216+
seenIDs[id] = true
217+
218+
mf, loadErr := manifestbuilder.LoadFileManifest(vaultRoot, id)
219+
if loadErr != nil {
220+
failures = append(failures, fmt.Sprintf("missing or unreadable manifest '%s': %v", id, loadErr))
221+
continue
222+
}
223+
224+
fullPath := mf.Destination + mf.FilePath
225+
if priorID, exists := seenFullPaths[fullPath]; exists {
226+
failures = append(failures, fmt.Sprintf("duplicate manifest ID for file '%s': %s and %s", fullPath, priorID, id))
227+
} else {
228+
seenFullPaths[fullPath] = id
229+
}
230+
231+
if len(mf.Chunks) == 0 {
232+
failures = append(failures, fmt.Sprintf("zero-chunk manifest entry: %s", id))
233+
}
234+
}
235+
236+
manifestsDir := filepath.Join(vaultRoot, ".sietch", "manifests")
237+
for _, id := range manifestIDs {
238+
manifestPath := filepath.Join(manifestsDir, id+".yaml")
239+
if _, statErr := os.Stat(manifestPath); statErr != nil {
240+
failures = append(failures, fmt.Sprintf("missing manifest file for ID '%s'", id))
241+
}
242+
}
243+
244+
return verifyOutput{Passed: len(failures) == 0, Failures: failures}
245+
}
246+
247+
func printManifestSummary(cmd *cobra.Command, summary manifestSummaryOutput) {
248+
fmt.Fprintf(cmd.OutOrStdout(), "Vault Name: %s\n", summary.VaultName)
249+
fmt.Fprintf(cmd.OutOrStdout(), "Vault Path: %s\n", summary.VaultPath)
250+
fmt.Fprintf(cmd.OutOrStdout(), "Encryption: %s\n", summary.EncryptionType)
251+
if summary.KeyPath != "" {
252+
fmt.Fprintf(cmd.OutOrStdout(), "Key Path: %s\n", summary.KeyPath)
253+
}
254+
fmt.Fprintf(cmd.OutOrStdout(), "Chunk Size: %s\n", summary.ChunkSize)
255+
fmt.Fprintf(cmd.OutOrStdout(), "Manifest Count: %d\n", summary.ManifestCount)
256+
}
257+
258+
func printFileManifestSummary(cmd *cobra.Command, output fileManifestOutput) {
259+
fmt.Fprintf(cmd.OutOrStdout(), "Manifest ID: %s\n", output.ManifestID)
260+
fmt.Fprintf(cmd.OutOrStdout(), "Destination: %s\n", output.Destination)
261+
fmt.Fprintf(cmd.OutOrStdout(), "File: %s\n", output.File)
262+
fmt.Fprintf(cmd.OutOrStdout(), "Full Path: %s\n", output.FullPath)
263+
fmt.Fprintf(cmd.OutOrStdout(), "Chunks: %d\n", output.ChunkCount)
264+
if len(output.Tags) > 0 {
265+
fmt.Fprintf(cmd.OutOrStdout(), "Tags: %s\n", strings.Join(output.Tags, ", "))
266+
}
267+
fmt.Fprintf(cmd.OutOrStdout(), "Modified: %s\n", output.ModifiedAt)
268+
fmt.Fprintf(cmd.OutOrStdout(), "Added: %s\n", output.AddedAt)
269+
if output.LastSynced != "" {
270+
fmt.Fprintf(cmd.OutOrStdout(), "Last Synced: %s\n", output.LastSynced)
271+
}
272+
if output.LastVerified != "" {
273+
fmt.Fprintf(cmd.OutOrStdout(), "Last Verified: %s\n", output.LastVerified)
274+
}
275+
}
276+
277+
func writeJSON(cmd *cobra.Command, payload any) error {
278+
encoder := json.NewEncoder(cmd.OutOrStdout())
279+
encoder.SetIndent("", " ")
280+
return encoder.Encode(payload)
281+
}
282+
283+
func init() {
284+
rootCmd.AddCommand(manifestCmd)
285+
manifestCmd.Flags().String("file", "", "File path, file name, or manifest ID to inspect")
286+
manifestCmd.Flags().Bool("json", false, "Output machine-readable JSON")
287+
manifestCmd.Flags().Bool("verify", false, "Run manifest consistency checks")
288+
}

0 commit comments

Comments
 (0)