diff --git a/save.go b/save.go index d813cb3..4c54498 100644 --- a/save.go +++ b/save.go @@ -83,12 +83,15 @@ func (c *Cache) Save(ctx context.Context, cacheID string) (SaveResult, error) { c.callProgress(cacheID, "validating", "Validating cache configuration", 0, 0) - // Validate cache paths exist - if err := checkPathsExist(cacheConfig.Paths); err != nil { + // Filter paths to only those that exist, collecting warnings for missing paths + validPaths, warnings, err := filterExistingPaths(cacheConfig.Paths) + if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "invalid cache paths") return result, fmt.Errorf("invalid cache paths: %w", err) } + result.Warnings = warnings + cacheConfig.Paths = validPaths c.callProgress(cacheID, "checking_exists", "Checking if cache already exists", 0, 0) @@ -256,29 +259,37 @@ func (c *Cache) Save(ctx context.Context, cacheID string) (SaveResult, error) { return result, nil } -// checkPathsExist validates that all paths exist on the filesystem -func checkPathsExist(paths []string) error { +// filterExistingPaths validates paths and returns only those that exist. +// Missing paths are returned as warnings rather than causing errors. +func filterExistingPaths(paths []string) (validPaths []string, warnings []string, err error) { if len(paths) == 0 { - return fmt.Errorf("no paths provided") + return nil, nil, fmt.Errorf("no paths provided") } for _, path := range paths { + expandedPath := path // Handle ~ expansion if len(path) > 0 && path[0] == '~' { homeDir, err := os.UserHomeDir() if err != nil { - return fmt.Errorf("failed to get home directory: %w", err) + return nil, nil, fmt.Errorf("failed to get home directory: %w", err) } - path = homeDir + path[1:] + expandedPath = homeDir + path[1:] } // Check if the path exists - if _, err := os.Stat(path); os.IsNotExist(err) { - return fmt.Errorf("path does not exist: %s", path) + if _, err := os.Stat(expandedPath); os.IsNotExist(err) { + warnings = append(warnings, fmt.Sprintf("path does not exist: %s", path)) + } else { + validPaths = append(validPaths, path) } } - return nil + if len(validPaths) == 0 { + return nil, warnings, fmt.Errorf("no valid paths found") + } + + return validPaths, warnings, nil } // validateCacheStore validates the cache store configuration diff --git a/save_test.go b/save_test.go new file mode 100644 index 0000000..20f7237 --- /dev/null +++ b/save_test.go @@ -0,0 +1,96 @@ +package zstash + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFilterExistingPaths(t *testing.T) { + t.Run("returns all paths when all exist", func(t *testing.T) { + dir := t.TempDir() + file1 := filepath.Join(dir, "file1.txt") + file2 := filepath.Join(dir, "file2.txt") + require.NoError(t, os.WriteFile(file1, []byte("test1"), 0o644)) + require.NoError(t, os.WriteFile(file2, []byte("test2"), 0o644)) + + validPaths, warnings, err := filterExistingPaths([]string{file1, file2}) + + require.NoError(t, err) + assert.Equal(t, []string{file1, file2}, validPaths) + assert.Empty(t, warnings) + }) + + t.Run("returns warning for missing path", func(t *testing.T) { + dir := t.TempDir() + existingFile := filepath.Join(dir, "exists.txt") + missingFile := filepath.Join(dir, "missing.txt") + require.NoError(t, os.WriteFile(existingFile, []byte("test"), 0o644)) + + validPaths, warnings, err := filterExistingPaths([]string{existingFile, missingFile}) + + require.NoError(t, err) + assert.Equal(t, []string{existingFile}, validPaths) + assert.Len(t, warnings, 1) + assert.Contains(t, warnings[0], "path does not exist") + assert.Contains(t, warnings[0], missingFile) + }) + + t.Run("returns error when all paths missing", func(t *testing.T) { + validPaths, warnings, err := filterExistingPaths([]string{"/nonexistent/path1", "/nonexistent/path2"}) + + require.Error(t, err) + assert.Contains(t, err.Error(), "no valid paths found") + assert.Nil(t, validPaths) + assert.Len(t, warnings, 2) + }) + + t.Run("returns error for empty paths", func(t *testing.T) { + validPaths, warnings, err := filterExistingPaths([]string{}) + + require.Error(t, err) + assert.Contains(t, err.Error(), "no paths provided") + assert.Nil(t, validPaths) + assert.Nil(t, warnings) + }) + + t.Run("handles directories", func(t *testing.T) { + dir := t.TempDir() + subdir := filepath.Join(dir, "subdir") + require.NoError(t, os.MkdirAll(subdir, 0o755)) + + validPaths, warnings, err := filterExistingPaths([]string{subdir}) + + require.NoError(t, err) + assert.Equal(t, []string{subdir}, validPaths) + assert.Empty(t, warnings) + }) + + t.Run("handles tilde expansion", func(t *testing.T) { + homeDir, err := os.UserHomeDir() + require.NoError(t, err) + + // Create a temp file in home directory for this test + tempFile := filepath.Join(homeDir, ".zstash_test_temp") + require.NoError(t, os.WriteFile(tempFile, []byte("test"), 0o644)) + defer os.Remove(tempFile) + + validPaths, warnings, err := filterExistingPaths([]string{"~/.zstash_test_temp"}) + + require.NoError(t, err) + assert.Equal(t, []string{"~/.zstash_test_temp"}, validPaths) + assert.Empty(t, warnings) + }) + + t.Run("warns for missing tilde path", func(t *testing.T) { + validPaths, warnings, err := filterExistingPaths([]string{"~/.nonexistent_zstash_test_path", "/tmp"}) + + require.NoError(t, err) + assert.Equal(t, []string{"/tmp"}, validPaths) + assert.Len(t, warnings, 1) + assert.Contains(t, warnings[0], "~/.nonexistent_zstash_test_path") + }) +} diff --git a/zstash.go b/zstash.go index 8672a1f..b81271e 100644 --- a/zstash.go +++ b/zstash.go @@ -202,6 +202,15 @@ type SaveResult struct { // TotalDuration is the end-to-end duration of the save operation, // from validation through commit (if created) or early exit (if exists). TotalDuration time.Duration + + // Warnings contains non-fatal issues encountered during the save operation, + // such as paths that were configured but did not exist. + Warnings []string +} + +// HasWarnings one or more warnings are present in the result +func (sr *SaveResult) HasWarnings() bool { + return len(sr.Warnings) > 0 } // RestoreResult contains detailed information about a cache restore operation.