From 714a45c34a0e216e362394bcfa972e5f136f32d9 Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Sun, 29 Apr 2018 04:11:10 +0100 Subject: [PATCH] Improved macOS VSS checks VSS support now determined by: 1) Checking that repository filesystem is APFS rather than by inspecting macOS version 2) Checking that repository resides on local device (as external APFS formatted drives are not supported) rather than crudely by path name --- src/duplicacy_shadowcopy_osx.go | 77 +++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/src/duplicacy_shadowcopy_osx.go b/src/duplicacy_shadowcopy_osx.go index d9979f8..0308381 100755 --- a/src/duplicacy_shadowcopy_osx.go +++ b/src/duplicacy_shadowcopy_osx.go @@ -16,33 +16,41 @@ import ( "errors" "time" "syscall" - "strconv" ) var snapshotPath string var snapshotDate string -func GetKernelVersion() (major int, minor int, component int, err error) { +// Converts char array to string +func CharsToString(ca []int8) string { - versionString, err := syscall.Sysctl("kern.osrelease") - if err != nil { - return 0, 0, 0, err; + len := len(ca) + ba := make([]byte, len) + + for i, v := range ca { + ba[i] = byte(v) + if ba[i] == 0 { + len = i + break + } } - versionSplit := strings.Split(versionString, ".") - if len(versionSplit) != 3 { - return 0, 0, 0, errors.New("Sysctl returned invalid kernel version string") - } - - major, _ = strconv.Atoi(versionSplit[0]) - minor, _ = strconv.Atoi(versionSplit[1]) - component, _ = strconv.Atoi(versionSplit[2]) - - return major, minor, component, nil; + return string(ba[:len]) } +// Get ID of device containing path +func GetPathDeviceId(path string) (deviceId int32, err error) { + stat := syscall.Stat_t{} + err = syscall.Stat(path, &stat) + if err != nil { + return 0, err + } + return stat.Dev, nil +} + +// Executes shell command with timeout and returns stdout func CommandWithTimeout(timeoutInSeconds int, name string, arg ...string) (output string, err error) { - + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutInSeconds) * time.Second) defer cancel() @@ -88,19 +96,31 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow return top } - major, _, _, err := GetKernelVersion() + // Check repository filesystem is APFS + stat := syscall.Statfs_t{} + err := syscall.Statfs(top, &stat) if err != nil { - LOG_ERROR("VSS_INIT", "Failed to get kernel version: " + err.Error()) + LOG_ERROR("VSS_INIT", "Unable to determine filesystem of repository path") + return top + } + if CharsToString(stat.Fstypename[:]) != "apfs" { + LOG_WARN("VSS_INIT", "VSS requires APFS filesystem") return top } - if major < 17 { - LOG_WARN("VSS_INIT", "VSS requires macOS 10.13 High Sierra or higher") + // Check path is local as tmutil snapshots will not support APFS formatted external drives + deviceIdLocal, err := GetPathDeviceId("/") + if err != nil { + LOG_ERROR("VSS_INIT", "Unable to get device ID of path: /") return top } - - if top == "/Volumes" || strings.HasPrefix(top, "/Volumes/") { - LOG_ERROR("VSS_PATH", "Invalid repository path: %s", top) + deviceIdRepository, err := GetPathDeviceId(top) + if err != nil { + LOG_ERROR("VSS_INIT", "Unable to get device ID of path: ", top) + return top + } + if deviceIdLocal != deviceIdRepository { + LOG_WARN("VSS_PATH", "VSS not supported for non-local repository path: ", top) return top } @@ -108,29 +128,32 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow timeoutInSeconds = 60 } + // Create mount point snapshotPath, err = ioutil.TempDir("/tmp/", "snp_") if err != nil { LOG_ERROR("VSS_CREATE", "Failed to create temporary mount directory") return top } + // Use tmutil to create snapshot tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot") if err != nil { - LOG_ERROR("VSS_CREATE", "Error while calling tmutil: " + err.Error()) + LOG_ERROR("VSS_CREATE", "Error while calling tmutil: ", err) return top } colonPos := strings.IndexByte(tmutilOutput, ':') if colonPos < 0 { - LOG_ERROR("VSS_CREATE", "Snapshot creation failed: " + tmutilOutput) + LOG_ERROR("VSS_CREATE", "Snapshot creation failed: ", tmutilOutput) return top } snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:]) - + + // Mount snapshot as readonly and hide from GUI i.e. Finder _, err = CommandWithTimeout(timeoutInSeconds, "mount", "-t", "apfs", "-o", "nobrowse,-r,-s=com.apple.TimeMachine." + snapshotDate, "/", snapshotPath) if err != nil { - LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: " + err.Error()) + LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: ", err) return top }