Add actionable permission-error message with macOS Full Disk Access hint
When the scanner hits a permission-denied error (TCC-protected directories on macOS without Full Disk Access, or any other EPERM), the error now names the offending path and includes platform-specific remediation instructions. On macOS it points the user at System Settings -> Privacy & Security -> Full Disk Access. On other platforms it suggests --skip-errors. The error wraps os.ErrPermission so errors.Is still works for callers that care about the underlying error. README quickstart and snapshot create docs now mention the macOS FDA requirement.
This commit is contained in:
42
internal/snapshot/permission_error_test.go
Normal file
42
internal/snapshot/permission_error_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package snapshot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWrapPermissionError(t *testing.T) {
|
||||
// Non-permission errors pass through unchanged.
|
||||
plain := errors.New("disk on fire")
|
||||
if got := wrapPermissionError("/some/path", plain); got != plain {
|
||||
t.Errorf("non-permission error should pass through, got %v", got)
|
||||
}
|
||||
|
||||
// Permission errors get remediation instructions.
|
||||
permErr := fmt.Errorf("open /x: %w", os.ErrPermission)
|
||||
wrapped := wrapPermissionError("/Users/u/Library/Calendars", permErr)
|
||||
|
||||
if !errors.Is(wrapped, os.ErrPermission) {
|
||||
t.Error("wrapped error should still match os.ErrPermission")
|
||||
}
|
||||
if !strings.Contains(wrapped.Error(), "/Users/u/Library/Calendars") {
|
||||
t.Error("wrapped error should name the offending path")
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
if !strings.Contains(wrapped.Error(), "Full Disk Access") {
|
||||
t.Errorf("macOS permission error should mention Full Disk Access:\n%s", wrapped.Error())
|
||||
}
|
||||
if !strings.Contains(wrapped.Error(), "System Settings") {
|
||||
t.Errorf("macOS permission error should point at System Settings:\n%s", wrapped.Error())
|
||||
}
|
||||
} else {
|
||||
if !strings.Contains(wrapped.Error(), "--skip-errors") {
|
||||
t.Errorf("non-macOS permission error should mention --skip-errors:\n%s", wrapped.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -631,7 +632,7 @@ func (s *Scanner) scanPhase(ctx context.Context, path string, result *ScanResult
|
||||
return nil // Continue scanning
|
||||
}
|
||||
log.Debug("Error accessing filesystem entry", "path", filePath, "error", err)
|
||||
return err
|
||||
return wrapPermissionError(filePath, err)
|
||||
}
|
||||
|
||||
// Check context cancellation
|
||||
@@ -1290,7 +1291,7 @@ func (s *Scanner) processFileStreaming(ctx context.Context, fileToProcess *FileT
|
||||
|
||||
file, err := s.fs.Open(fileToProcess.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening file: %w", err)
|
||||
return fmt.Errorf("opening file: %w", wrapPermissionError(fileToProcess.Path, err))
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
@@ -1447,6 +1448,24 @@ func (s *Scanner) detectDeletedFilesFromMap(ctx context.Context, knownFiles map[
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrapPermissionError augments permission errors with platform-specific
|
||||
// remediation instructions. On macOS, TCC-protected directories (Calendars,
|
||||
// Reminders, Photos, etc.) return EPERM unless the running application has
|
||||
// been granted Full Disk Access.
|
||||
func wrapPermissionError(path string, err error) error {
|
||||
if !errors.Is(err, os.ErrPermission) {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
return fmt.Errorf("cannot read %s: %w\n\n"+
|
||||
"macOS is blocking access to this path. Grant Full Disk Access to your\n"+
|
||||
"terminal application (or the app running vaultik):\n\n"+
|
||||
" System Settings → Privacy & Security → Full Disk Access\n\n"+
|
||||
"then quit and reopen the terminal and re-run the backup", path, err)
|
||||
}
|
||||
return fmt.Errorf("cannot read %s: %w (check file permissions, or run with --skip-errors to continue past unreadable files)", path, err)
|
||||
}
|
||||
|
||||
// compileExcludePatterns compiles the exclude patterns into glob matchers
|
||||
func compileExcludePatterns(patterns []string) []compiledPattern {
|
||||
var compiled []compiledPattern
|
||||
|
||||
Reference in New Issue
Block a user