#! /usr/bin/python3 import os import argparse import shutil import filecmp # Argument Parsing parser = argparse.ArgumentParser( prog="DataPuller", description="Check differences between folders and update files if needed", ) parser.add_argument("-i", "--input", help="Path to input folder") parser.add_argument("-o", "--output", help="Path to output folder") parser.add_argument( "-q", "--quiet", action="store_true", help="Suppress non-essential commandline output", ) parser.add_argument( "-y", "--confirm", action="store_true", help="Auto-confirm user queries" ) parser.add_argument( "-d", "--delete", action="store_true", help="Auto-confirm file deletion" ) parser.add_argument( "--dry-run", action="store_true", help="List affected files without copying or deleting", ) parser.add_argument( "-s", "--shallow-diff", action="store_true", help="Use filecmp.cmp() shallow mode for file diff (faster, but might miss changes or generate false positives)", ) args = parser.parse_args() # Paths if not args.input: inpath = input("Source Path: ") else: inpath = args.input if not args.output: outpath = input("Target Path: ") else: outpath = args.output def get_files(path): file_info = [] if not os.path.exists(path): return file_info for folder, _, files in os.walk(path): for file in files: full_path = os.path.join(folder, file) rel_path = os.path.relpath(full_path, path) file_info.append(rel_path) return file_info def main(): startstring = " FOLDER-SYNC 0.1.3 " if not args.quiet: print( len(startstring) * "=" + "\n" + startstring + "\n" + len(startstring) * "=" ) # Test Folder availability if not os.path.exists(inpath): print(f"\nERROR: The input folder ({inpath}) is unavailable or non-existant.") if not args.quiet: input("\nPress ENTER to exit...") exit(1) if not os.path.exists(outpath): print(f"\nERROR: The output folder ({outpath}] is unavailable or non-existant.") if not args.quiet: input("\nPress ENTER to exit...") exit(1) print("\nAnalyzing files. Please wait...\n") source = get_files(inpath) target = get_files(outpath) new = [] changed = [] deleted = [] # Check for file additions and changes for path in source: if path not in target: new.append(path) elif not filecmp.cmp( os.path.join(inpath, path), os.path.join(outpath, path), shallow=args.shallow_diff, ): changed.append(path) # Check for file deletions for path in target: if path not in source: deleted.append(path) if not new and not changed and not deleted: if not args.quiet: print("\nNo changes found.") input("\nPress ENTER to exit...") exit(0) # Print overview if not args.quiet: print("\nFound the following changes:") if new: print("\nAdded:") for i in new: print(f" + {i}") if changed: print("\nChanged:") for i in changed: print(f" ~ {i}") if deleted: print("\nDeleted:") for i in deleted: print(f" - {i}") # Copy and update files if not args.dry_run: if new or changed: if args.confirm: copy_confirm = "y" else: copy_confirm = ( input("\nWrite new and changed files? (y/N): ").strip().lower() ) if copy_confirm in ["ja", "j", "yes", "y"]: if not args.quiet: print("\nCopying files...\n") for path in new + changed: source_path = os.path.join(inpath, path) target_path = os.path.join(outpath, path) # Create new folders if needed os.makedirs(os.path.dirname(target_path), exist_ok=True) # Copy file try: shutil.copy(source_path, target_path) if not args.quiet: print(f"Successfully copied {path}") except Exception as error: print(f"Failed to copy {path} with error {error}") if not args.confirm: if not input("Continue? (y/N): ") in [ "ja", "j", "yes", "y", ]: exit(1) elif not args.quiet: print("\nSkipping changes...\n") # Delete files if not args.dry_run: if deleted: alert = " ALERT: Some files only exist at the destination. " if not args.quiet: print(f"\n{len(alert) * '*'}\n{alert}\n{len(alert) * '*'}") if args.delete: delete_confirm = "y" else: delete_confirm = ( input("\nDelete files on the target? (y/N): ").strip().lower() ) if delete_confirm in ["ja", "j", "yes", "y"]: if not args.quiet: print("\nDeleting files...\n") for path in deleted: target_path = os.path.join(outpath, path) try: os.remove(target_path) if not args.quiet: print(f"Successfully deleted {path}") except Exception as error: print(f"Failed to delete {path} with error {error}") if not args.confirm: if not input("Continue? (y/N): ") in [ "ja", "j", "yes", "y", ]: exit(1) elif not args.quiet: print("\nSkipping deletion...") if not args.quiet: if not args.dry_run: print("\nFolder sync finished!") else: print("\nDry run complete!") input("\nPress ENTER to exit...") if __name__ == "__main__": main()