236 lines
7.3 KiB
Python
Executable File
236 lines
7.3 KiB
Python
Executable File
import subprocess
|
|
import configparser
|
|
import os
|
|
import argparse
|
|
import time
|
|
import logging
|
|
|
|
from BetterADBSync import sync_with_options
|
|
from tcpip import reconnect_as_tcpip
|
|
|
|
# debug vs standard logging
|
|
DEBUG = False
|
|
|
|
if DEBUG:
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format="[%(asctime)s][%(levelname)s] %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S"
|
|
)
|
|
else:
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="[%(asctime)s][%(levelname)s] %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
filename="sync.log",
|
|
filemode="a"
|
|
)
|
|
|
|
|
|
def parse_command_output(output: str) -> dict:
|
|
lines = output.split(os.linesep)
|
|
result = {}
|
|
i = lines.index("[INFO] SYNCING")
|
|
|
|
trees = {
|
|
'deleted': [
|
|
'Deleting delete tree',
|
|
'Empty delete tree',
|
|
'Deleting non-excluded-supporting destination unaccounted tree'
|
|
],
|
|
'copied': [
|
|
'Copying copy tree',
|
|
'Empty copy tree'
|
|
]
|
|
}
|
|
|
|
current_tree = ''
|
|
|
|
for line in lines[i + 2:]:
|
|
actual_line = line[6:].strip()
|
|
for tree, matches in trees.items():
|
|
if actual_line in matches:
|
|
current_tree = tree
|
|
result[current_tree] = []
|
|
break
|
|
else:
|
|
if current_tree:
|
|
if actual_line == '':
|
|
current_tree = ''
|
|
continue
|
|
if actual_line.startswith('Removing '):
|
|
actual_line = actual_line[9:]
|
|
result[current_tree].append(actual_line)
|
|
|
|
return result
|
|
|
|
|
|
def traverse_tree(tree: dict, level: int = 0):
|
|
"""Traverse a tree and print it nicely. Also keep a count of the number of files in the tree."""
|
|
count = 0
|
|
for key, value in tree.items():
|
|
if isinstance(value, dict):
|
|
logging.info(f"{' ' * level}{key}/")
|
|
count += traverse_tree(value, level + 1)
|
|
else:
|
|
logging.info(f"{' ' * level}{key}")
|
|
count += 1
|
|
return count
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Daemon for syncing files between a computer and an Android device.")
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Perform a dry run without actually syncing files. (Only runs once)"
|
|
)
|
|
parser.add_argument(
|
|
"--tcpip",
|
|
action="store_true",
|
|
help="Reconnect to device over TCP/IP before syncing. Linux only."
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.tcpip:
|
|
if os.name != 'posix':
|
|
logging.error("TCP/IP mode is only supported on Linux systems.")
|
|
return
|
|
|
|
# Read config file
|
|
if not os.path.exists("config.ini"):
|
|
# Create a new config file
|
|
config = configparser.ConfigParser()
|
|
config["Global"] = {
|
|
"adb_path": "adb",
|
|
"device": ""
|
|
}
|
|
|
|
config["__example_Path_1"] = {
|
|
"type": "push",
|
|
"source": "C:/path/to/source",
|
|
"destination": "/path/to/destination",
|
|
"exclude_1": "path/to/exclude",
|
|
"exclude_2": "path/to/exclude",
|
|
"delete_files_in_dest": "no"
|
|
}
|
|
|
|
print("New configuration file created. Please edit the file to set paths and other configurations.")
|
|
|
|
with open("config.ini", "w") as configfile:
|
|
config.write(configfile)
|
|
return
|
|
|
|
else:
|
|
config = configparser.ConfigParser()
|
|
config.read("config.ini")
|
|
|
|
adb_path = config["Global"]["adb_path"]
|
|
device = config["Global"]["device"]
|
|
|
|
# First run adb, check if server is running
|
|
result = subprocess.run([adb_path, "start-server"])
|
|
if result.returncode != 0:
|
|
logging.error("Could not start ADB server. Exiting...")
|
|
return
|
|
|
|
# Disconnect all devices to start fresh
|
|
subprocess.run([adb_path, "disconnect"], stdout=subprocess.PIPE)
|
|
|
|
run = True
|
|
first_run = True
|
|
connected = False
|
|
|
|
while run:
|
|
# Get the list of devices
|
|
result = subprocess.run([adb_path, "devices"], stdout=subprocess.PIPE)
|
|
output = result.stdout.decode("utf-8")
|
|
devices = output.split(os.linesep)[1:-2]
|
|
devices = [device.split("\t")[0] for device in devices if "offline" not in device]
|
|
if len(devices) == 0:
|
|
# Device is not yet connected
|
|
device = config["Global"]["device"] # reset device to config value, in case it was TCP/IP before
|
|
first_run = True
|
|
if not connected:
|
|
logging.info("No device connected. Waiting silently until a device is connected.")
|
|
time.sleep(10)
|
|
continue
|
|
|
|
connected = True
|
|
if device and device not in devices:
|
|
time.sleep(60)
|
|
continue
|
|
if not device:
|
|
device = devices[0]
|
|
|
|
if first_run:
|
|
logging.info(f"Device connected: {device}")
|
|
if args.tcpip:
|
|
# Reconnect over TCP/IP
|
|
logging.info("Reconnecting device over TCP/IP...")
|
|
try:
|
|
tcpip_device = reconnect_as_tcpip(device, adb_path)
|
|
except (RuntimeError, TimeoutError) as e:
|
|
logging.warning(f"Failed to reconnect device over TCP/IP: {e}")
|
|
logging.warning("Continuing with the current connection after 10 seconds.")
|
|
time.sleep(10)
|
|
else:
|
|
device = tcpip_device
|
|
logging.info(f"Device reconnected over TCP/IP at {tcpip_device}")
|
|
|
|
logging.info(f"Found {len(config.sections()) - 1} path{'s' if len(config.sections()) > 2 else ''} to sync.")
|
|
logging.info('')
|
|
|
|
# Sync files
|
|
for section in config.sections():
|
|
if section == "Global" or section.startswith("__"):
|
|
continue
|
|
if first_run:
|
|
logging.info(f"Syncing {section}...")
|
|
type = config[section]["type"]
|
|
source = config[section]["source"]
|
|
destination = config[section]["destination"]
|
|
exclude = [config[section][key] for key in config[section] if key.startswith("exclude")]
|
|
delete_files_in_dest = config[section].getboolean("delete_files_in_dest")
|
|
|
|
options = {
|
|
'adb_options': {'s': device},
|
|
}
|
|
|
|
if args.dry_run:
|
|
options['dry_run'] = True
|
|
|
|
if exclude:
|
|
options['exclude'] = exclude
|
|
|
|
if delete_files_in_dest:
|
|
options['delete'] = True
|
|
|
|
options['show_progress'] = True
|
|
options['direction'] = type
|
|
options['source'] = source
|
|
options['dest'] = destination
|
|
|
|
copied, deleted = sync_with_options(**options)
|
|
|
|
if copied:
|
|
count = traverse_tree(copied)
|
|
logging.info(f"Copied {count} files to {destination}\n")
|
|
|
|
if deleted and delete_files_in_dest:
|
|
count = traverse_tree(deleted)
|
|
logging.info(f"Deleted {count} files from {destination}\n")
|
|
|
|
if args.dry_run:
|
|
run = False
|
|
else:
|
|
if first_run:
|
|
logging.info("Entering loop and waiting for file changes")
|
|
first_run = False
|
|
time.sleep(300)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|