Last active
September 5, 2021 22:26
-
-
Save zhovner/3f2496da87528c76cac0ba312b4b20d3 to your computer and use it in GitHub Desktop.
Time Machine helper for travelers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/opt/homebrew/bin/python3 | |
################################################### | |
# Remote Time Machine helper for remote SMB share # | |
# Run backup only if SMB server speed is high # | |
################################################### | |
### Requirements: | |
# brew install iperf3 python3 | |
# pip install osascript | |
### How to use: | |
# 1. Configure Time Machine to smb share in normal mode. SMB credential should be saved in system keychain. | |
# 2. Disable automatic backup in Time Machine | |
# 3. Run this script in background via Launchd N times a day | |
### EDIT YOUR VARIABLES | |
# --------------------------------------------------------------------- | |
SMB_SHARE_ADDRESS = 'share.lan.example.com' | |
WORKGROUP_NAME = 'WORKGROUP' | |
SMB_MOUNT_PATH = '/Volumes/ivanfolder' | |
SMB_USER = 'ivan' | |
SMB_SHARE_PATH = 'ivanfolder' | |
SPEED_TEST_SERVER = "iperf.lan.example.com" | |
SPEED_TEST_DURATION = "3" # seconds | |
SPEED_TEST_TIMEOUT = "60" # milliseconds | |
MIN_SPEED = "60" # Min upload speed in Mbit/s to activate Time Machine | |
MAX_LOAD_AVERAGE = 5 # Max load average when starting backup | |
# --------------------------------------------------------------------- | |
import subprocess | |
import json | |
import glob | |
import os | |
import osascript | |
import time | |
import sys | |
import datetime | |
print(''' | |
############################## | |
# Remote Time Machine helper # | |
############################## | |
''') | |
print("Started: " + datetime.datetime.now().strftime("%d %b %Y %H:%M")) | |
# Print Time Mahcine settings | |
TMUTIL_SETTINGS_SUB = subprocess.run(["tmutil", "destinationinfo"], capture_output=True) | |
print("Time Machine Settings:") | |
print(str(TMUTIL_SETTINGS_SUB.stdout, 'utf-8')) | |
# Check if Time Machine running | |
# exit if backup running | |
# | |
# tmutil function if need more info from running time machine | |
# https://gist.github.com/andrewbenson/cc5fd79ff6999f0524b8979fe17937a3 | |
# | |
TMUTIL_PHASE_SUB = subprocess.run(["tmutil", "currentphase"], capture_output=True) | |
TMUTIL_PHASE = str(TMUTIL_PHASE_SUB.stdout, 'utf-8').strip() | |
if TMUTIL_PHASE != "BackupNotRunning": | |
print("Backup process is running. Exiting...") | |
exit() | |
# Check Load Average. Exit if system busy | |
LA = round(os.getloadavg()[0]) | |
if LA > MAX_LOAD_AVERAGE: | |
print("Current Load Average is to high: " + str(LA)) | |
print("Exiting...") | |
osacode,osaout,osaerr = osascript.run('display notification "🥵CPU usage is to high! Skipping backup." with Title "Time Machine Helper"') | |
exit() | |
# Check if SMB share availible | |
SMBUTIL_SUB = subprocess.run(["smbutil", "status", SMB_SHARE_ADDRESS], capture_output=True) | |
SMBUTIL_RESULT = str(SMBUTIL_SUB.stdout, 'utf-8') | |
# Check if smbutil respone have WORKGROUP_NAME | |
if WORKGROUP_NAME in SMBUTIL_RESULT: | |
print("SMB " + SMB_SHARE_ADDRESS + " Available\n") | |
print(SMBUTIL_RESULT) | |
else: | |
print("Error: SMB " + SMB_SHARE_ADDRESS + " not availible") | |
exit() | |
# Run Network Speed Test | |
print("Running speed test...") | |
try: | |
IPERF_SUB = subprocess.run( | |
["/opt/homebrew/bin/iperf3", | |
"--connect-timeout", SPEED_TEST_TIMEOUT, # Drop connection in laggy network | |
"--time", SPEED_TEST_DURATION, # Run speed test shorter then default 10 seconds | |
"--json", # Output in JSON format | |
"--client", SPEED_TEST_SERVER], # Connects to Speed Test server in client mode | |
capture_output=True) | |
except: | |
print("Iperf3 failed. Exiting") | |
exit() | |
# Convert iperf3 output to valid JSON | |
IPERF_RESULT = str(IPERF_SUB.stdout, 'utf-8') # Convert bytes to string | |
IPERF_JSON = json.loads(IPERF_RESULT) | |
# Exit if error key found | |
if "error" in IPERF_JSON: | |
print("Iperf3 failed:") | |
print(IPERF_JSON["error"]) | |
exit() | |
# Else calculate the upload speed and decide if it's enough to start Time Machine | |
else: | |
USPEED_FLOAT = IPERF_JSON["end"]["sum_sent"]["bits_per_second"] | |
UPLOAD_SPEED = round(USPEED_FLOAT) // 1000000 | |
print("Upload Speed: " + str(UPLOAD_SPEED) + " Mbits/sec") | |
# If Upload Speed slow then exit | |
if int(UPLOAD_SPEED) < int(MIN_SPEED): | |
print("Internet is too slow. Minimum speed is: " + str(MIN_SPEED) + " Mbits/sec") | |
OSA_ARGS = 'display notification "🐌 Internet is to slow for backup: ' + str(int(UPLOAD_SPEED)) + ' Mbit/s "' + 'with Title "Time Machine Helper"' | |
osacode,osaout,osaerr = osascript.run(OSA_ARGS) | |
exit() | |
# Check if SMB share mounted and Time Machine backup existed on it | |
print("Looking for Time Machine backups on " + SMB_MOUNT_PATH) | |
# Mounting SMB share if not mounted. Using osascript (hardest part) | |
if not os.path.isdir(SMB_MOUNT_PATH): | |
print(SMB_MOUNT_PATH + " not founded. Trying to mount...") | |
OSA_ARGS = 'tell application "Finder" to mount volume ' + '"smb://' + SMB_USER + '@' + SMB_SHARE_ADDRESS + '/' + SMB_SHARE_PATH + '"' | |
osacode,osaout,osaerr = osascript.run(OSA_ARGS) | |
time.sleep(1) | |
# Looking for a SMB Path again after mount | |
if not os.path.isdir(SMB_MOUNT_PATH): | |
print("Mount " + SMB_MOUNT_PATH + " Failed") | |
print("Command used to mount vis osascript: " + str(OSA_ARGS)) | |
exit() | |
# Looking for *.sparsebundle files on SMB share | |
# exit if not founded | |
# Create Time Machine backup manually before using this script | |
TIME_MACHINE_BACKUPS = glob.glob(SMB_MOUNT_PATH + "/*.sparsebundle") | |
if TIME_MACHINE_BACKUPS: | |
print("Founded " + str(len(TIME_MACHINE_BACKUPS)) + " Time Machine files:") | |
print(*TIME_MACHINE_BACKUPS, sep='\n') | |
else: | |
print("Error: no Time Machine files found. Create it manually before using this script") | |
osacode,osaout,osaerr = osascript.run('display notification "⚠️ No Time Machine Files Found on Share" with Title "Time Machine Helper"') | |
print("Exiting...") | |
exit() | |
# Everything looks good | |
# Lets start backup | |
print("Staring Time Machine backup...") | |
TIME_MACHINE_SUB = subprocess.run( | |
["tmutil", | |
"startbackup"], | |
capture_output=True) | |
if TIME_MACHINE_SUB.returncode == 0: | |
print("Time Machine Started successfully!") | |
osacode,osaout,osaerr = osascript.run('display notification "💾 Backup started..." with Title "Time Machine Helper"') | |
else: | |
print("ERROR: Time Machine Starting Failed!") | |
osacode,osaout,osaerr = osascript.run('display notification "⚠️ ERROR: Time Machine Starting Failed!" with Title "Time Machine Helper"') | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment