From 4c3d5dbc2f8252fe40c7584fd1f3695258090c1a Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Sat, 28 Apr 2018 17:58:47 +0100 Subject: [PATCH 1/9] Add APFS snapshot support Add's VSS support for macOS using APFS snapshot's --- src/duplicacy_shadowcopy.go | 1 + src/duplicacy_shadowcopy_osx.go | 106 ++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) mode change 100644 => 100755 src/duplicacy_shadowcopy.go create mode 100755 src/duplicacy_shadowcopy_osx.go diff --git a/src/duplicacy_shadowcopy.go b/src/duplicacy_shadowcopy.go old mode 100644 new mode 100755 index 0e75ac2..aeab70e --- a/src/duplicacy_shadowcopy.go +++ b/src/duplicacy_shadowcopy.go @@ -3,6 +3,7 @@ // Commercial use requires per-user licenses available from https://duplicacy.com // +build !windows +// +build !darwin package duplicacy diff --git a/src/duplicacy_shadowcopy_osx.go b/src/duplicacy_shadowcopy_osx.go new file mode 100755 index 0000000..189f747 --- /dev/null +++ b/src/duplicacy_shadowcopy_osx.go @@ -0,0 +1,106 @@ +// +// Shadow copy module for Mac OSX using APFS snapshot +// +// +// This module copyright 2018 Adam Marcus (https://github.com/amarcu5) +// and may be distributed under the same terms as Duplicacy. + +package duplicacy + +import ( + "os" + "strings" + "io/ioutil" + "os/exec" + "context" + "errors" + "time" +) + +var snapshotPath string +var snapshotDate string + +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() + + cmd := exec.CommandContext(ctx, name, arg...) + out, err := cmd.Output() + + if ctx.Err() == context.DeadlineExceeded { + err = errors.New("Command '" + name + "' timed out") + } + + output = string(out) + return output, err; +} + +func DeleteShadowCopy() { + + if snapshotPath == "" { + return + } + + err := exec.Command("umount", "-f", snapshotPath).Run() + if err != nil { + LOG_ERROR("VSS_DELETE", "Error while unmounting snapshot") + return + } + + err = exec.Command("tmutil", "deletelocalsnapshots", snapshotDate).Run() + if err != nil { + LOG_ERROR("VSS_DELETE", "Error while deleting local snapshot") + return + } + + os.RemoveAll(snapshotPath) + + LOG_INFO("VSS_DELETE", "Shadow copy unmounted and deleted at %s", snapshotPath) + + snapshotPath = "" + +} + +func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadowTop string) { + + if !shadowCopy { + return top + } + + if timeoutInSeconds <= 60 { + timeoutInSeconds = 60 + } + + tmpDir, err := ioutil.TempDir("/tmp/", "snp_") + if err != nil { + LOG_ERROR("VSS_CREATE", "Failed to create temporary mount directory") + return top + } + snapshotPath = tmpDir + + tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot", "/") + if err != nil { + LOG_ERROR("VSS_CREATE", "Error while calling tmutil: " + err.Error()) + return top + } + + colonPos := strings.IndexByte(tmutilOutput, ':') + if colonPos < 0 { + LOG_ERROR("VSS_CREATE", "Snapshot creation failed") + return top + } + snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:]) + + _, err = CommandWithTimeout(timeoutInSeconds, "mount", "-t", "apfs", "-o", "nobrowse,-s=com.apple.TimeMachine." + snapshotDate, "/", snapshotPath) + if err != nil { + LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: " + err.Error()) + return top + } + + LOG_DEBUG("VSS_PROPERTY", "Returned path: %s", snapshotPath + top) + + LOG_INFO("VSS_DONE", "Shadow copy created and mounted at %s", snapshotPath) + + return snapshotPath + top +} From 8a3c5847a8de6117523b58f23ba1d87f636accf1 Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Sat, 28 Apr 2018 21:23:00 +0100 Subject: [PATCH 2/9] Cleaned up formatting --- src/duplicacy_shadowcopy_osx.go | 58 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/duplicacy_shadowcopy_osx.go b/src/duplicacy_shadowcopy_osx.go index 189f747..b4c8879 100755 --- a/src/duplicacy_shadowcopy_osx.go +++ b/src/duplicacy_shadowcopy_osx.go @@ -22,14 +22,14 @@ var snapshotDate string 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() cmd := exec.CommandContext(ctx, name, arg...) out, err := cmd.Output() - if ctx.Err() == context.DeadlineExceeded { - err = errors.New("Command '" + name + "' timed out") + if ctx.Err() == context.DeadlineExceeded { + err = errors.New("Command '" + name + "' timed out") } output = string(out) @@ -38,28 +38,27 @@ func CommandWithTimeout(timeoutInSeconds int, name string, arg ...string) (outpu func DeleteShadowCopy() { - if snapshotPath == "" { - return - } + if snapshotPath == "" { + return + } err := exec.Command("umount", "-f", snapshotPath).Run() - if err != nil { - LOG_ERROR("VSS_DELETE", "Error while unmounting snapshot") + if err != nil { + LOG_ERROR("VSS_DELETE", "Error while unmounting snapshot") return } err = exec.Command("tmutil", "deletelocalsnapshots", snapshotDate).Run() - if err != nil { - LOG_ERROR("VSS_DELETE", "Error while deleting local snapshot") + if err != nil { + LOG_ERROR("VSS_DELETE", "Error while deleting local snapshot") return } - os.RemoveAll(snapshotPath) - - LOG_INFO("VSS_DELETE", "Shadow copy unmounted and deleted at %s", snapshotPath) - - snapshotPath = "" + os.RemoveAll(snapshotPath) + LOG_INFO("VSS_DELETE", "Shadow copy unmounted and deleted at %s", snapshotPath) + + snapshotPath = "" } func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadowTop string) { @@ -69,38 +68,37 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow } if timeoutInSeconds <= 60 { - timeoutInSeconds = 60 + timeoutInSeconds = 60 } - tmpDir, err := ioutil.TempDir("/tmp/", "snp_") + tmpDir, err := ioutil.TempDir("/tmp/", "snp_") 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 } snapshotPath = tmpDir tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot", "/") - if err != nil { - LOG_ERROR("VSS_CREATE", "Error while calling tmutil: " + err.Error()) + if err != nil { + LOG_ERROR("VSS_CREATE", "Error while calling tmutil: " + err.Error()) return top } - + colonPos := strings.IndexByte(tmutilOutput, ':') if colonPos < 0 { - LOG_ERROR("VSS_CREATE", "Snapshot creation failed") + LOG_ERROR("VSS_CREATE", "Snapshot creation failed") return top } snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:]) - _, err = CommandWithTimeout(timeoutInSeconds, "mount", "-t", "apfs", "-o", "nobrowse,-s=com.apple.TimeMachine." + snapshotDate, "/", snapshotPath) - if err != nil { - LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: " + err.Error()) - return top + _, 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()) + return top } - - LOG_DEBUG("VSS_PROPERTY", "Returned path: %s", snapshotPath + top) - - LOG_INFO("VSS_DONE", "Shadow copy created and mounted at %s", snapshotPath) + + LOG_INFO("VSS_DONE", "Shadow copy created and mounted at %s", snapshotPath) return snapshotPath + top } From 23b98a3034cd305a7af257caef722c8ad11f727e Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Sat, 28 Apr 2018 23:09:59 +0100 Subject: [PATCH 3/9] Added macOS VSS checks Restricts VSS under macOS to version 10.13 High Sierra or higher and local volume only --- src/duplicacy_shadowcopy_osx.go | 44 ++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/duplicacy_shadowcopy_osx.go b/src/duplicacy_shadowcopy_osx.go index b4c8879..d9979f8 100755 --- a/src/duplicacy_shadowcopy_osx.go +++ b/src/duplicacy_shadowcopy_osx.go @@ -15,11 +15,32 @@ import ( "context" "errors" "time" + "syscall" + "strconv" ) var snapshotPath string var snapshotDate string +func GetKernelVersion() (major int, minor int, component int, err error) { + + versionString, err := syscall.Sysctl("kern.osrelease") + if err != nil { + return 0, 0, 0, err; + } + + 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; +} + func CommandWithTimeout(timeoutInSeconds int, name string, arg ...string) (output string, err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutInSeconds) * time.Second) @@ -66,19 +87,34 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow if !shadowCopy { return top } + + major, _, _, err := GetKernelVersion() + if err != nil { + LOG_ERROR("VSS_INIT", "Failed to get kernel version: " + err.Error()) + return top + } + + if major < 17 { + LOG_WARN("VSS_INIT", "VSS requires macOS 10.13 High Sierra or higher") + return top + } + + if top == "/Volumes" || strings.HasPrefix(top, "/Volumes/") { + LOG_ERROR("VSS_PATH", "Invalid repository path: %s", top) + return top + } if timeoutInSeconds <= 60 { timeoutInSeconds = 60 } - tmpDir, err := ioutil.TempDir("/tmp/", "snp_") + snapshotPath, err = ioutil.TempDir("/tmp/", "snp_") if err != nil { LOG_ERROR("VSS_CREATE", "Failed to create temporary mount directory") return top } - snapshotPath = tmpDir - tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot", "/") + tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot") if err != nil { LOG_ERROR("VSS_CREATE", "Error while calling tmutil: " + err.Error()) return top @@ -86,7 +122,7 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow colonPos := strings.IndexByte(tmutilOutput, ':') if colonPos < 0 { - LOG_ERROR("VSS_CREATE", "Snapshot creation failed") + LOG_ERROR("VSS_CREATE", "Snapshot creation failed: " + tmutilOutput) return top } snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:]) From 714a45c34a0e216e362394bcfa972e5f136f32d9 Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Sun, 29 Apr 2018 04:11:10 +0100 Subject: [PATCH 4/9] 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 } From ef1d316f33b4dfce0347901bd8a46f3f039b96bf Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Sun, 29 Apr 2018 04:21:28 +0100 Subject: [PATCH 5/9] Cleaned up formatting Fixed tabbing --- src/duplicacy_shadowcopy_osx.go | 86 ++++++++++++++++----------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/duplicacy_shadowcopy_osx.go b/src/duplicacy_shadowcopy_osx.go index 0308381..4ae494c 100755 --- a/src/duplicacy_shadowcopy_osx.go +++ b/src/duplicacy_shadowcopy_osx.go @@ -23,19 +23,19 @@ var snapshotDate string // Converts char array to string func CharsToString(ca []int8) string { - + len := len(ca) ba := make([]byte, len) - - for i, v := range ca { - ba[i] = byte(v) + + for i, v := range ca { + ba[i] = byte(v) if ba[i] == 0 { len = i break } } - - return string(ba[:len]) + + return string(ba[:len]) } // Get ID of device containing path @@ -51,43 +51,43 @@ func GetPathDeviceId(path string) (deviceId int32, err error) { // 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) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutInSeconds) * time.Second) defer cancel() - + cmd := exec.CommandContext(ctx, name, arg...) out, err := cmd.Output() - - if ctx.Err() == context.DeadlineExceeded { - err = errors.New("Command '" + name + "' timed out") + + if ctx.Err() == context.DeadlineExceeded { + err = errors.New("Command '" + name + "' timed out") } - + output = string(out) - return output, err; + return output, err } func DeleteShadowCopy() { - - if snapshotPath == "" { - return - } + + if snapshotPath == "" { + return + } err := exec.Command("umount", "-f", snapshotPath).Run() - if err != nil { - LOG_ERROR("VSS_DELETE", "Error while unmounting snapshot") + if err != nil { + LOG_ERROR("VSS_DELETE", "Error while unmounting snapshot") return } - + err = exec.Command("tmutil", "deletelocalsnapshots", snapshotDate).Run() - if err != nil { - LOG_ERROR("VSS_DELETE", "Error while deleting local snapshot") + if err != nil { + LOG_ERROR("VSS_DELETE", "Error while deleting local snapshot") return } - - os.RemoveAll(snapshotPath) - - LOG_INFO("VSS_DELETE", "Shadow copy unmounted and deleted at %s", snapshotPath) - - snapshotPath = "" + + os.RemoveAll(snapshotPath) + + LOG_INFO("VSS_DELETE", "Shadow copy unmounted and deleted at %s", snapshotPath) + + snapshotPath = "" } func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadowTop string) { @@ -95,7 +95,7 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow if !shadowCopy { return top } - + // Check repository filesystem is APFS stat := syscall.Statfs_t{} err := syscall.Statfs(top, &stat) @@ -107,7 +107,7 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow LOG_WARN("VSS_INIT", "VSS requires APFS filesystem") return top } - + // Check path is local as tmutil snapshots will not support APFS formatted external drives deviceIdLocal, err := GetPathDeviceId("/") if err != nil { @@ -125,26 +125,26 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow } if timeoutInSeconds <= 60 { - timeoutInSeconds = 60 + timeoutInSeconds = 60 } - + // Create mount point - snapshotPath, err = ioutil.TempDir("/tmp/", "snp_") + snapshotPath, err = ioutil.TempDir("/tmp/", "snp_") 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 } - + // Use tmutil to create snapshot tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot") - if err != nil { - LOG_ERROR("VSS_CREATE", "Error while calling tmutil: ", err) + if err != nil { + 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:]) @@ -153,11 +153,11 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow _, 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) - return top + LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: ", err) + return top } - - LOG_INFO("VSS_DONE", "Shadow copy created and mounted at %s", snapshotPath) - + + LOG_INFO("VSS_DONE", "Shadow copy created and mounted at %s", snapshotPath) + return snapshotPath + top } From 5747d6763f6d19e3119c5fe762ae554ed52d1a33 Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Sun, 29 Apr 2018 13:33:17 +0100 Subject: [PATCH 6/9] Improved error handling Minor improvement to error handling and small formatting changes --- src/duplicacy_shadowcopy_osx.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/duplicacy_shadowcopy_osx.go b/src/duplicacy_shadowcopy_osx.go index 4ae494c..01a586b 100755 --- a/src/duplicacy_shadowcopy_osx.go +++ b/src/duplicacy_shadowcopy_osx.go @@ -8,14 +8,14 @@ package duplicacy import ( - "os" - "strings" - "io/ioutil" - "os/exec" "context" "errors" - "time" + "io/ioutil" + "os" + "os/exec" + "strings" "syscall" + "time" ) var snapshotPath string @@ -40,11 +40,14 @@ func CharsToString(ca []int8) string { // 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 } @@ -83,8 +86,12 @@ func DeleteShadowCopy() { return } - os.RemoveAll(snapshotPath) - + err = os.RemoveAll(snapshotPath) + if err != nil { + LOG_ERROR("VSS_DELETE", "Error while deleting temporary mount directory") + return + } + LOG_INFO("VSS_DELETE", "Shadow copy unmounted and deleted at %s", snapshotPath) snapshotPath = "" @@ -141,7 +148,7 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow 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) From c23ea30da4f041a2d5c5f4663ae3cd1e8bc7b623 Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Sun, 29 Apr 2018 23:24:22 +0100 Subject: [PATCH 7/9] Updated VSS flag usage VSS flag usage now indicates support for macOS using APFS in addition to Windows --- duplicacy/duplicacy_main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/duplicacy/duplicacy_main.go b/duplicacy/duplicacy_main.go index c54cf8c..874c86f 100644 --- a/duplicacy/duplicacy_main.go +++ b/duplicacy/duplicacy_main.go @@ -1324,7 +1324,7 @@ func main() { }, cli.BoolFlag{ Name: "vss", - Usage: "enable the Volume Shadow Copy service (Windows only)", + Usage: "enable the Volume Shadow Copy service (Windows and macOS using APFS only)", }, cli.IntFlag{ Name: "vss-timeout", From acd7addc9a64d7e813aac8d4709e4ca4b94decbb Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Mon, 30 Apr 2018 00:36:11 +0100 Subject: [PATCH 8/9] Fixed Windows complication --- ...duplicacy_shadowcopy_osx.go => duplicacy_shadowcopy_darwin.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{duplicacy_shadowcopy_osx.go => duplicacy_shadowcopy_darwin.go} (100%) diff --git a/src/duplicacy_shadowcopy_osx.go b/src/duplicacy_shadowcopy_darwin.go similarity index 100% rename from src/duplicacy_shadowcopy_osx.go rename to src/duplicacy_shadowcopy_darwin.go From 8c3ef6cae9e31a8e90343ce387a4763ddd1f14de Mon Sep 17 00:00:00 2001 From: amarcu5 Date: Mon, 30 Apr 2018 00:40:34 +0100 Subject: [PATCH 9/9] Improved cron support Now calls mount and unmount using absolute path --- src/duplicacy_shadowcopy_darwin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/duplicacy_shadowcopy_darwin.go b/src/duplicacy_shadowcopy_darwin.go index 01a586b..b48c294 100755 --- a/src/duplicacy_shadowcopy_darwin.go +++ b/src/duplicacy_shadowcopy_darwin.go @@ -74,7 +74,7 @@ func DeleteShadowCopy() { return } - err := exec.Command("umount", "-f", snapshotPath).Run() + err := exec.Command("/sbin/umount", "-f", snapshotPath).Run() if err != nil { LOG_ERROR("VSS_DELETE", "Error while unmounting snapshot") return @@ -158,7 +158,7 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow // 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) + "/sbin/mount", "-t", "apfs", "-o", "nobrowse,-r,-s=com.apple.TimeMachine." + snapshotDate, "/", snapshotPath) if err != nil { LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: ", err) return top