Skip to content

Commit b09a963

Browse files
committed
local snapshot save always has .zip extension
1 parent e084b0b commit b09a963

3 files changed

Lines changed: 55 additions & 25 deletions

File tree

internal/snapshot/destination.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ import (
1010

1111
// ParseDestination resolves the user-supplied path to an absolute local path,
1212
// or returns an error for cloud/bare names. When dest is empty, a default name
13-
// based on now (UTC) is used, e.g. "snapshot-2026-05-11T21-04-32".
13+
// based on now (UTC) is used, e.g. "snapshot-2026-05-11T21-04-32.zip".
14+
// The returned path always has a .zip extension.
1415
func ParseDestination(dest string, now time.Time) (string, error) {
1516
if dest == "" {
1617
dest = "./" + now.UTC().Format("snapshot-2006-01-02T15-04-05")
1718
} else if strings.Contains(dest, "://") {
18-
return "", fmt.Errorf("cloud destinations are not yet supported — use a file path like ./my-snapshot")
19+
return "", fmt.Errorf("cloud destinations are not yet supported — use a file path like ./my-snapshot.zip")
1920
} else if !strings.HasPrefix(dest, ".") && !strings.HasPrefix(dest, "~") && !filepath.IsAbs(dest) && filepath.Base(dest) == dest {
2021
// bare name with no path separators: reserved for future cloud pod names
21-
return "", fmt.Errorf("cloud destinations are not yet supported — use a file path like ./my-snapshot")
22+
return "", fmt.Errorf("cloud destinations are not yet supported — use a file path like ./my-snapshot.zip")
2223
}
2324
if dest == "~" || strings.HasPrefix(dest, "~/") || strings.HasPrefix(dest, `~\`) {
2425
home, err := os.UserHomeDir()
@@ -31,5 +32,11 @@ func ParseDestination(dest string, now time.Time) (string, error) {
3132
if err != nil {
3233
return "", fmt.Errorf("resolve path: %w", err)
3334
}
35+
if info, err := os.Stat(abs); err == nil && info.IsDir() {
36+
return "", fmt.Errorf("%q is a directory — specify a file path like ./my-snapshot.zip", abs)
37+
}
38+
if !strings.EqualFold(filepath.Ext(abs), ".zip") {
39+
abs += ".zip"
40+
}
3441
return abs, nil
3542
}

internal/snapshot/destination_test.go

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ import (
1212
"github.com/stretchr/testify/require"
1313
)
1414

15+
func TestParseDestinationRejectsDirectory(t *testing.T) {
16+
t.Parallel()
17+
dir := t.TempDir()
18+
now := time.Date(2026, 5, 11, 21, 4, 32, 0, time.UTC)
19+
_, err := snapshot.ParseDestination(dir, now)
20+
require.Error(t, err)
21+
assert.Contains(t, err.Error(), "is a directory")
22+
}
23+
1524
func TestParseDestinationDefault(t *testing.T) {
1625
t.Parallel()
1726
wd, err := os.Getwd()
@@ -20,7 +29,7 @@ func TestParseDestinationDefault(t *testing.T) {
2029
now := time.Date(2026, 5, 11, 21, 4, 32, 0, time.UTC)
2130
got, err := snapshot.ParseDestination("", now)
2231
require.NoError(t, err)
23-
assert.Equal(t, filepath.Join(wd, "snapshot-2026-05-11T21-04-32"), got)
32+
assert.Equal(t, filepath.Join(wd, "snapshot-2026-05-11T21-04-32.zip"), got)
2433
}
2534

2635
func TestParseDestination(t *testing.T) {
@@ -33,51 +42,63 @@ func TestParseDestination(t *testing.T) {
3342
now := time.Date(2026, 5, 11, 21, 4, 32, 0, time.UTC)
3443

3544
type testCase struct {
36-
input string
37-
wantPath string
38-
wantErr string
45+
input string
46+
wantPath string
47+
wantErr string
48+
wantCloudErr bool
3949
}
4050

4151
tests := []testCase{
4252
{
4353
input: "./my-state",
44-
wantPath: filepath.Join(wd, "my-state"),
54+
wantPath: filepath.Join(wd, "my-state.zip"),
4555
},
4656
{
4757
input: filepath.Join(os.TempDir(), "state"),
48-
wantPath: filepath.Join(os.TempDir(), "state"),
58+
wantPath: filepath.Join(os.TempDir(), "state.zip"),
4959
},
5060
{
5161
input: "~",
52-
wantPath: home,
62+
wantErr: "is a directory",
5363
},
5464
{
5565
input: "~/snapshots/s",
56-
wantPath: filepath.Join(home, "snapshots", "s"),
66+
wantPath: filepath.Join(home, "snapshots", "s.zip"),
5767
},
5868
{
5969
input: "subdir/state",
60-
wantPath: filepath.Join(wd, "subdir", "state"),
70+
wantPath: filepath.Join(wd, "subdir", "state.zip"),
71+
},
72+
{
73+
input: "./checkpoint.zip",
74+
wantPath: filepath.Join(wd, "checkpoint.zip"),
75+
},
76+
{
77+
input: "./already.ZIP",
78+
wantPath: filepath.Join(wd, "already.ZIP"),
6179
},
6280
{
63-
input: "my-pod",
64-
wantErr: "cloud destinations are not yet supported",
81+
input: "my-pod",
82+
wantErr: "cloud destinations are not yet supported",
83+
wantCloudErr: true,
6584
},
6685
{
67-
input: "cloud://my-pod",
68-
wantErr: "cloud destinations are not yet supported",
86+
input: "cloud://my-pod",
87+
wantErr: "cloud destinations are not yet supported",
88+
wantCloudErr: true,
6989
},
7090
{
71-
input: "s3://bucket/key",
72-
wantErr: "cloud destinations are not yet supported",
91+
input: "s3://bucket/key",
92+
wantErr: "cloud destinations are not yet supported",
93+
wantCloudErr: true,
7394
},
7495
}
7596

7697
if runtime.GOOS == "windows" {
7798
tests = append(tests,
78-
testCase{input: `~\snapshots\s`, wantPath: filepath.Join(home, "snapshots", "s")},
79-
testCase{input: `C:\Users\user\snap`, wantPath: `C:\Users\user\snap`},
80-
testCase{input: `C:/Users/user/snap`, wantPath: `C:\Users\user\snap`},
99+
testCase{input: `~\snapshots\s`, wantPath: filepath.Join(home, "snapshots", "s.zip")},
100+
testCase{input: `C:\Users\user\snap`, wantPath: `C:\Users\user\snap.zip`},
101+
testCase{input: `C:/Users/user/snap`, wantPath: `C:\Users\user\snap.zip`},
81102
)
82103
}
83104

@@ -88,7 +109,9 @@ func TestParseDestination(t *testing.T) {
88109
if tc.wantErr != "" {
89110
require.Error(t, err)
90111
assert.Contains(t, err.Error(), tc.wantErr)
91-
assert.Contains(t, err.Error(), "./my-snapshot")
112+
if tc.wantCloudErr {
113+
assert.Contains(t, err.Error(), "./my-snapshot.zip")
114+
}
92115
return
93116
}
94117
require.NoError(t, err)

test/integration/snapshot_save_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func TestSnapshotSaveCustomPath(t *testing.T) {
8282
startTestContainer(t, ctx)
8383
srv := mockStateServer(t)
8484
dir := t.TempDir()
85-
outPath := filepath.Join(dir, "my-snap")
85+
outPath := filepath.Join(dir, "my-snap.zip")
8686

8787
stdout, stderr, err := runLstk(t, ctx, dir,
8888
env.With(env.LocalStackHost, lsHost(srv)),
@@ -118,7 +118,7 @@ func TestSnapshotSaveRelativePath(t *testing.T) {
118118
require.NoError(t, err, "lstk snapshot save failed: %s", stderr)
119119
assert.Contains(t, stdout, "Snapshot saved")
120120

121-
_, statErr := os.Stat(filepath.Join(dir, "my-state"))
121+
_, statErr := os.Stat(filepath.Join(dir, "my-state.zip"))
122122
assert.NoError(t, statErr, "relative output file should exist")
123123
}
124124

@@ -131,7 +131,7 @@ func TestSnapshotSaveOverwritesExistingFile(t *testing.T) {
131131
startTestContainer(t, ctx)
132132
srv := mockStateServer(t)
133133
dir := t.TempDir()
134-
outPath := filepath.Join(dir, "snap")
134+
outPath := filepath.Join(dir, "snap.zip")
135135
require.NoError(t, os.WriteFile(outPath, []byte("OLD"), 0600))
136136

137137
_, stderr, err := runLstk(t, ctx, dir,

0 commit comments

Comments
 (0)