Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/cloudstic/cmd_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ func (r *runner) runSingleBackup(a *backupArgs) int {
if err != nil {
return r.fail("Backup failed: %v", err)
}
if a.g.jsonEnabled() {
return r.writeJSON(result)
}
r.printBackupSummary(result)
return 0
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/cloudstic/cmd_breaklock.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func (r *runner) runBreakLock(ctx context.Context) int {
if err != nil {
return r.fail("Failed to break lock: %v", err)
}
if a.g.jsonEnabled() {
return r.writeJSON(&breakLockJSONResult{Locks: removed})
}
r.printBreakLockResult(removed)
return 0
}
Expand Down
23 changes: 23 additions & 0 deletions cmd/cloudstic/cmd_breaklock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/json"
"os"
"strings"
"testing"
Expand All @@ -21,6 +22,28 @@ func TestRunBreakLock_NoLock(t *testing.T) {
}
}

func TestRunBreakLock_JSON(t *testing.T) {
os.Args = []string{"cloudstic", "break-lock", "-json"}
var out strings.Builder
r := &runner{out: &out, errOut: &strings.Builder{}, client: &stubClient{
breakLockResult: []*cloudstic.RepoLock{
{Operation: "backup", Holder: "worker-1"},
},
}}

if exit := r.runBreakLock(context.Background()); exit != 0 {
t.Fatalf("runBreakLock() exit = %d, want 0", exit)
}

var got map[string]any
if err := json.Unmarshal([]byte(out.String()), &got); err != nil {
t.Fatalf("json unmarshal: %v\noutput:\n%s", err, out.String())
}
if _, ok := got["locks"]; !ok {
t.Fatalf("expected locks key in JSON output, got: %v", got)
}
}

func TestRunBreakLock_LocksRemoved(t *testing.T) {
os.Args = []string{"cloudstic", "break-lock"}
var errOut strings.Builder
Expand Down
12 changes: 8 additions & 4 deletions cmd/cloudstic/cmd_cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@ import (
type catArgs struct {
g *globalFlags
keys []string
json bool
raw bool
}

func parseCatArgs() *catArgs {
fs := flag.NewFlagSet("cat", flag.ExitOnError)
a := &catArgs{}
a.g = addGlobalFlags(fs)
jsonFlag := fs.Bool("json", false, "Suppress non-JSON output (alias for -quiet)")
rawFlag := fs.Bool("raw", false, "Output raw, unformatted data (useful for hashing)")
mustParse(fs)
if fs.NArg() < 1 {
Expand All @@ -36,7 +34,6 @@ func parseCatArgs() *catArgs {
fmt.Fprintln(os.Stderr, " cloudstic cat -raw filemeta/def456... | sha256sum")
os.Exit(1)
}
a.json = *jsonFlag
a.raw = *rawFlag
a.keys = fs.Args()
return a
Expand All @@ -48,13 +45,20 @@ func (r *runner) runCat(ctx context.Context) int {
return r.fail("Failed to init store: %v", err)
}

quiet := *a.g.quiet || a.json
if a.g.jsonEnabled() && a.raw {
return r.failJSONFlagConflict("-json", "-raw")
}

quiet := *a.g.quiet || a.g.jsonEnabled()

results, err := r.client.Cat(context.Background(), a.keys...)
if err != nil {
return r.fail("Failed to fetch objects: %v", err)
}

if a.g.jsonEnabled() {
return r.writeJSON(makeCatJSONResults(results))
}
r.printCatResult(results, quiet, a.raw)
return 0
}
Expand Down
38 changes: 33 additions & 5 deletions cmd/cloudstic/cmd_cat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/json"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -47,20 +48,34 @@ func TestRunCat_MultipleKeys_HeadersShown(t *testing.T) {
}
}

func TestRunCat_QuietMode_NoHeaders(t *testing.T) {
func TestRunCat_JSON_NoHeadersAndStructuredOutput(t *testing.T) {
os.Args = []string{"cloudstic", "cat", "--json", "config", "index/latest"}
var out strings.Builder
var errOut strings.Builder
r := &runner{out: &strings.Builder{}, errOut: &errOut, client: &stubClient{
r := &runner{out: &out, errOut: &errOut, client: &stubClient{
catResults: []*cloudstic.CatResult{
{Key: "config", Data: []byte(`{}`)},
{Key: "config", Data: []byte(`{"version":1}`)},
{Key: "index/latest", Data: []byte(`{}`)},
},
}}

r.runCat(context.Background())
if exit := r.runCat(context.Background()); exit != 0 {
t.Fatalf("runCat() exit = %d, want 0", exit)
}

if strings.Contains(errOut.String(), "==>") {
t.Errorf("quiet mode should not show headers, got:\n%s", errOut.String())
t.Errorf("json mode should not show headers, got:\n%s", errOut.String())
}

var got []map[string]any
if err := json.Unmarshal([]byte(out.String()), &got); err != nil {
t.Fatalf("json unmarshal: %v\noutput:\n%s", err, out.String())
}
if len(got) != 2 {
t.Fatalf("len(got) = %d, want 2", len(got))
}
if got[0]["key"] != "config" {
t.Fatalf("first key = %v, want config", got[0]["key"])
}
}

Expand Down Expand Up @@ -92,3 +107,16 @@ func TestRunCat_InvalidJSON_PrintsRaw(t *testing.T) {
t.Errorf("expected raw fallback output, got:\n%s", out.String())
}
}

func TestRunCat_JSONAndRawConflict(t *testing.T) {
os.Args = []string{"cloudstic", "cat", "-json", "-raw", "config"}
var errOut strings.Builder
r := &runner{out: &strings.Builder{}, errOut: &errOut, client: &stubClient{}}

if exit := r.runCat(context.Background()); exit != 1 {
t.Fatalf("runCat() exit = %d, want 1", exit)
}
if !strings.Contains(errOut.String(), "-json cannot be combined with -raw") {
t.Fatalf("expected conflict error, got:\n%s", errOut.String())
}
}
9 changes: 9 additions & 0 deletions cmd/cloudstic/cmd_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ func (r *runner) runCheck(ctx context.Context) int {
if err != nil {
return r.fail("Check failed: %v", err)
}
if a.g.jsonEnabled() {
if exit := r.writeJSON(result); exit != 0 {
return exit
}
if len(result.Errors) > 0 {
return 1
}
return 0
}
if r.printCheckResult(result) {
return 1
}
Expand Down
27 changes: 27 additions & 0 deletions cmd/cloudstic/cmd_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/json"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -32,6 +33,32 @@ func TestRunCheck_Healthy(t *testing.T) {
}
}

func TestRunCheck_JSONWithErrorsReturnsExitOne(t *testing.T) {
os.Args = []string{"cloudstic", "check", "-json"}
var out strings.Builder
r := &runner{out: &out, errOut: &strings.Builder{}, client: &stubClient{
checkResult: &cloudstic.CheckResult{
SnapshotsChecked: 1,
ObjectsVerified: 2,
Errors: []engine.CheckError{
{Type: "corrupt", Key: "content/xyz", Message: "checksum mismatch"},
},
},
}}

if exit := r.runCheck(context.Background()); exit != 1 {
t.Fatalf("runCheck() exit = %d, want 1", exit)
}

var got map[string]any
if err := json.Unmarshal([]byte(out.String()), &got); err != nil {
t.Fatalf("json unmarshal: %v\noutput:\n%s", err, out.String())
}
if _, ok := got["Errors"]; !ok {
t.Fatalf("expected Errors key in JSON output, got: %v", got)
}
}

func TestPrintCheckResult_Healthy(t *testing.T) {
var errOut strings.Builder
r := &runner{out: &strings.Builder{}, errOut: &errOut}
Expand Down
3 changes: 3 additions & 0 deletions cmd/cloudstic/cmd_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func (r *runner) runDiff(ctx context.Context) int {
if err != nil {
return r.fail("Diff failed: %v", err)
}
if a.g.jsonEnabled() {
return r.writeJSON(result)
}
r.printDiffResult(result)
return 0
}
Expand Down
27 changes: 27 additions & 0 deletions cmd/cloudstic/cmd_diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/json"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -42,6 +43,32 @@ func TestRunDiff_Success(t *testing.T) {
}
}

func TestRunDiff_JSON(t *testing.T) {
os.Args = []string{"cloudstic", "diff", "-json", "aaa", "bbb"}
var out strings.Builder
r := &runner{out: &out, errOut: &strings.Builder{}, client: &stubClient{
diffResult: &cloudstic.DiffResult{
Ref1: "snapshot/aaa",
Ref2: "snapshot/bbb",
Changes: []engine.FileChange{
{Type: "A", Path: "docs/readme.md"},
},
},
}}

if exit := r.runDiff(context.Background()); exit != 0 {
t.Fatalf("runDiff() exit = %d, want 0", exit)
}

var got map[string]any
if err := json.Unmarshal([]byte(out.String()), &got); err != nil {
t.Fatalf("json unmarshal: %v\noutput:\n%s", err, out.String())
}
if got["Ref1"] != "snapshot/aaa" {
t.Fatalf("Ref1 = %v, want snapshot/aaa", got["Ref1"])
}
}

func TestRunDiff_NoChanges(t *testing.T) {
os.Args = []string{"cloudstic", "diff", "aaa", "bbb"}
var out strings.Builder
Expand Down
9 changes: 9 additions & 0 deletions cmd/cloudstic/cmd_forget.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ func (r *runner) execForgetSingle(a *forgetArgs) int {
if err != nil {
return r.fail("Forget failed: %v", err)
}
if a.g.jsonEnabled() {
return r.writeJSON(&forgetSingleJSONResult{
SnapshotID: a.snapshotID,
Prune: result.Prune,
})
}
_, _ = fmt.Fprintln(r.out)
_, _ = fmt.Fprintln(r.out, "Snapshot removed.")
if result.Prune != nil {
Expand All @@ -192,6 +198,9 @@ func (r *runner) execForgetPolicy(a *forgetArgs) int {
if err != nil {
return r.fail("Forget failed: %v", err)
}
if a.g.jsonEnabled() {
return r.writeJSON(makeForgetPolicyJSONResult(result, a.dryRun))
}
r.printPolicyResult(result, a.dryRun)
return 0
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/cloudstic/cmd_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ func (r *runner) runInit(ctx context.Context) int {
return r.fail("Init failed: %v", err)
}

if a.g.jsonEnabled() {
return r.writeJSON(result)
}
r.printInitResult(result)
return 0
}
Expand Down
9 changes: 9 additions & 0 deletions cmd/cloudstic/cmd_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func (r *runner) runKeyList(ctx context.Context) int {
return r.fail("Failed to list key slots: %v", err)
}

if a.g.jsonEnabled() {
return r.writeJSON(slots)
}
r.printKeySlots(slots)
return 0
}
Expand Down Expand Up @@ -133,6 +136,9 @@ func (r *runner) runKeyPasswd(ctx context.Context) int {
return r.fail("Failed to change password: %v", err)
}

if a.g.jsonEnabled() {
return r.writeJSON(&keyPasswordJSONResult{Changed: true})
}
_, _ = fmt.Fprintln(r.errOut, "Repository password has been changed.")
return 0
}
Expand Down Expand Up @@ -166,6 +172,9 @@ func (r *runner) runAddRecoveryKey(ctx context.Context) int {
return r.fail("Failed to create recovery key: %v", err)
}

if a.g.jsonEnabled() {
return r.writeJSON(&recoveryKeyJSONResult{RecoveryKey: mnemonic})
}
r.printRecoveryKey(mnemonic)
_, _ = fmt.Fprintln(r.errOut, "Recovery key slot has been added to the repository.")
return 0
Expand Down
3 changes: 3 additions & 0 deletions cmd/cloudstic/cmd_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func (r *runner) runList(ctx context.Context) int {
if err != nil {
return r.fail("List failed: %v", err)
}
if a.g.jsonEnabled() {
return r.writeJSON(result)
}
r.printListResult(result, *a.group)
return 0
}
Expand Down
25 changes: 25 additions & 0 deletions cmd/cloudstic/cmd_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/json"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -31,6 +32,30 @@ func TestRunList_Success(t *testing.T) {
}
}

func TestRunList_JSON(t *testing.T) {
os.Args = []string{"cloudstic", "list", "-json"}
var out strings.Builder
r := &runner{out: &out, errOut: &strings.Builder{}, client: &stubClient{
listResult: &cloudstic.ListResult{
Snapshots: []engine.SnapshotEntry{
{Ref: "snapshot/abc", Snap: core.Snapshot{Seq: 1, Created: "2024-01-01"}},
},
},
}}

if exit := r.runList(context.Background()); exit != 0 {
t.Fatalf("runList() exit = %d, want 0", exit)
}

var got map[string]any
if err := json.Unmarshal([]byte(out.String()), &got); err != nil {
t.Fatalf("json unmarshal: %v\noutput:\n%s", err, out.String())
}
if _, ok := got["Snapshots"]; !ok {
t.Fatalf("expected Snapshots key in JSON output, got: %v", got)
}
}

func TestRunList_Empty(t *testing.T) {
os.Args = []string{"cloudstic", "list"}
var out strings.Builder
Expand Down
3 changes: 3 additions & 0 deletions cmd/cloudstic/cmd_ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func (r *runner) runLsSnapshot(ctx context.Context) int {
if err != nil {
return r.fail("Ls failed: %v", err)
}
if a.g.jsonEnabled() {
return r.writeJSON(result)
}
r.printLsResult(result, time.Since(start))
return 0
}
Expand Down
Loading
Loading