Files
adb-autosync/main.py
2025-10-27 23:59:05 -04:00

237 lines
7.4 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
waiting_for_connection = 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 waiting_for_connection:
logging.info("No device connected. Waiting silently until a device is connected.")
waiting_for_connection = True
time.sleep(10)
continue
waiting_for_connection = False
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()