202 lines
6.2 KiB
Python
Executable file
202 lines
6.2 KiB
Python
Executable file
#! /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",
|
|
)
|
|
|
|
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 1.0 "
|
|
if not args.quiet:
|
|
print(
|
|
len(startstring) * "=" + "\n" + startstring + "\n" + len(startstring) * "="
|
|
)
|
|
|
|
# Test Folder availability
|
|
if not os.path.exists(inpath):
|
|
print(f"ERROR: 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"ERROR: The output folder ({outpath}] is unavailable or non-existant.")
|
|
if not args.quiet:
|
|
input("\nPress ENTER to exit...")
|
|
exit(1)
|
|
|
|
print("Analyzing 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)):
|
|
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("No changes found.")
|
|
input("\nPress ENTER to exit...")
|
|
exit(0)
|
|
|
|
# Print overview
|
|
if not args.quiet:
|
|
print("Found the following changes:\n")
|
|
if new:
|
|
print("Added:")
|
|
for i in new:
|
|
print(f" + {i}")
|
|
print()
|
|
if changed:
|
|
print("Changed:")
|
|
for i in changed:
|
|
print(f" ~ {i}")
|
|
if deleted:
|
|
print("Deleted:")
|
|
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("Write new and changed files? (y/N): ").strip().lower()
|
|
)
|
|
if copy_confirm in ["ja", "j", "yes", "y"]:
|
|
if not args.quiet:
|
|
print("\nCopying files...")
|
|
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...")
|
|
|
|
# Delete files
|
|
if not args.dry_run:
|
|
if deleted:
|
|
alert = " ALERT: Some files only exist at the destination. "
|
|
if not args.quiet:
|
|
print(f"{len(alert) * '*'}\n{alert}\n{len(alert) * '*'}")
|
|
if args.delete:
|
|
delete_confirm = "y"
|
|
else:
|
|
delete_confirm = (
|
|
input("Delete files on the target? (y/N): ").strip().lower()
|
|
)
|
|
if delete_confirm in ["ja", "j", "yes", "y"]:
|
|
if not args.quiet:
|
|
print("\nDeleting files...")
|
|
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()
|