Merge fix/macos-fda-error-message
All checks were successful
check / check (push) Successful in 2m5s
All checks were successful
check / check (push) Successful in 2m5s
This commit is contained in:
12
README.md
12
README.md
@@ -24,8 +24,12 @@ grep 'public key' vaultik_backup_private_key.txt
|
||||
vaultik config set age_recipients.0 age1YOUR_PUBLIC_KEY_HERE
|
||||
vaultik config set storage_url "file:///Volumes/usbstick/mybackup"
|
||||
|
||||
# back up your home directory (the default config includes a "home"
|
||||
# snapshot of ~ with sensible excludes)
|
||||
# macOS only: grant your terminal app Full Disk Access first
|
||||
# (System Settings → Privacy & Security → Full Disk Access), otherwise
|
||||
# the backup will abort with a permission error on protected directories
|
||||
|
||||
# run your first backup (the default config backs up ~ and /Applications
|
||||
# with sensible excludes)
|
||||
vaultik snapshot create
|
||||
|
||||
# see what you have
|
||||
@@ -159,6 +163,10 @@ in the file are preserved; intermediate maps are created as needed.
|
||||
|
||||
**`snapshot create`**: Perform incremental backup of configured snapshots.
|
||||
* Optional snapshot names argument to create specific snapshots (default: all)
|
||||
* On macOS, the terminal application running vaultik needs Full Disk Access
|
||||
(System Settings → Privacy & Security → Full Disk Access) to read
|
||||
TCC-protected directories; without it the backup aborts with a permission
|
||||
error that explains how to fix it
|
||||
* `--cron`: Silent unless error (for crontab)
|
||||
* `--prune`: After backup, drop older snapshots of each backed-up name and
|
||||
remove orphaned blobs from remote storage. By default keeps only the latest
|
||||
|
||||
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