1
0
mirror of https://github.com/gilbertchen/duplicacy synced 2025-12-06 00:03:38 +00:00

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
This commit is contained in:
amarcu5
2018-04-29 04:11:10 +01:00
parent 23b98a3034
commit 714a45c34a

View File

@@ -16,33 +16,41 @@ import (
"errors" "errors"
"time" "time"
"syscall" "syscall"
"strconv"
) )
var snapshotPath string var snapshotPath string
var snapshotDate 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") len := len(ca)
if err != nil { ba := make([]byte, len)
return 0, 0, 0, err;
for i, v := range ca {
ba[i] = byte(v)
if ba[i] == 0 {
len = i
break
}
} }
versionSplit := strings.Split(versionString, ".") return string(ba[:len])
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;
} }
// 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) { func CommandWithTimeout(timeoutInSeconds int, name string, arg ...string) (output string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutInSeconds) * time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutInSeconds) * time.Second)
defer cancel() defer cancel()
@@ -88,19 +96,31 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow
return top return top
} }
major, _, _, err := GetKernelVersion() // Check repository filesystem is APFS
stat := syscall.Statfs_t{}
err := syscall.Statfs(top, &stat)
if err != nil { 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 return top
} }
if major < 17 { // Check path is local as tmutil snapshots will not support APFS formatted external drives
LOG_WARN("VSS_INIT", "VSS requires macOS 10.13 High Sierra or higher") deviceIdLocal, err := GetPathDeviceId("/")
if err != nil {
LOG_ERROR("VSS_INIT", "Unable to get device ID of path: /")
return top return top
} }
deviceIdRepository, err := GetPathDeviceId(top)
if top == "/Volumes" || strings.HasPrefix(top, "/Volumes/") { if err != nil {
LOG_ERROR("VSS_PATH", "Invalid repository path: %s", top) 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 return top
} }
@@ -108,29 +128,32 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow
timeoutInSeconds = 60 timeoutInSeconds = 60
} }
// Create mount point
snapshotPath, err = ioutil.TempDir("/tmp/", "snp_") snapshotPath, err = ioutil.TempDir("/tmp/", "snp_")
if err != nil { if err != nil {
LOG_ERROR("VSS_CREATE", "Failed to create temporary mount directory") LOG_ERROR("VSS_CREATE", "Failed to create temporary mount directory")
return top return top
} }
// Use tmutil to create snapshot
tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot") tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot")
if err != nil { if err != nil {
LOG_ERROR("VSS_CREATE", "Error while calling tmutil: " + err.Error()) LOG_ERROR("VSS_CREATE", "Error while calling tmutil: ", err)
return top return top
} }
colonPos := strings.IndexByte(tmutilOutput, ':') colonPos := strings.IndexByte(tmutilOutput, ':')
if colonPos < 0 { if colonPos < 0 {
LOG_ERROR("VSS_CREATE", "Snapshot creation failed: " + tmutilOutput) LOG_ERROR("VSS_CREATE", "Snapshot creation failed: ", tmutilOutput)
return top return top
} }
snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:]) snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:])
// Mount snapshot as readonly and hide from GUI i.e. Finder
_, err = CommandWithTimeout(timeoutInSeconds, _, err = CommandWithTimeout(timeoutInSeconds,
"mount", "-t", "apfs", "-o", "nobrowse,-r,-s=com.apple.TimeMachine." + snapshotDate, "/", snapshotPath) "mount", "-t", "apfs", "-o", "nobrowse,-r,-s=com.apple.TimeMachine." + snapshotDate, "/", snapshotPath)
if err != nil { if err != nil {
LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: " + err.Error()) LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: ", err)
return top return top
} }