Initial commit

This commit is contained in:
2025-10-25 20:59:58 -04:00
commit ea2f98cfc3
9 changed files with 1432 additions and 0 deletions

199
main.py Executable file
View File

@@ -0,0 +1,199 @@
import subprocess
import configparser
import os
import argparse
import time
import logging
from BetterADBSync import sync_with_options
# 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)")
args = parser.parse_args()
# 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
run = True
first_run = True
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 len(devices) == 0:
# Device is not yet connnected
logging.info("No device connected. Retrying in 60 seconds.")
time.sleep(60)
continue
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}")
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()