1
0
mirror of https://github.com/gilbertchen/duplicacy synced 2025-12-06 00:03:38 +00:00
Files
duplicacy/src/duplicacy_shadowcopy_darwin.go
Michael Cook 0762c448c4 gofmt -s
2018-12-29 13:20:10 +01:00

171 lines
3.9 KiB
Go
Executable File

//
// 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 (
"context"
"errors"
"io/ioutil"
"os"
"os/exec"
"strings"
"syscall"
"time"
)
var snapshotPath string
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)
if ba[i] == 0 {
len = i
break
}
}
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()
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("/sbin/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
}
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 = ""
}
func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadowTop string) {
if !shadowCopy {
return top
}
// Check repository filesystem is APFS
stat := syscall.Statfs_t{}
err := syscall.Statfs(top, &stat)
if err != nil {
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
}
// 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
}
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
}
if timeoutInSeconds <= 60 {
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)
return top
}
colonPos := strings.IndexByte(tmutilOutput, ':')
if colonPos < 0 {
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,
"/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
}
LOG_INFO("VSS_DONE", "Shadow copy created and mounted at %s", snapshotPath)
return snapshotPath + top
}