import os import sys import json import subprocess import time folders_created = 0 folders_existed = 0 files_uploaded = 0 files_existed = 0 files_replaced = 0 errors = 0 def run_cmd(cmd): print(f"Running: {cmd}") p = subprocess.run(cmd, shell=True, capture_output=True, text=True) if p.stderr: print(f"Error running command: {p.stderr}") # ERROR global errors errors += 1 time.sleep(1) return run_cmd(cmd) try: return json.loads(p.stdout) except json.JSONDecodeError: print(f"Non-JSON output: {p.stdout}") raise Exception(f"Invalid JSON from: {cmd}") def get_listing(remote_id): cmd = f'internxt list --json --non-interactive --id="{remote_id}"' out = run_cmd(cmd) if out.get('success', False): return out['list'] else: print(f"List failed: {out.get('message', 'Unknown error')}") # ERROR global errors errors += 1 time.sleep(1) return get_listing(remote_id) def find_folder_uuid(parent_id, name): listing = get_listing(parent_id) for folder in listing['folders']: if folder['plainName'] == name: print(f"Folder '{name}' exists in '{parent_id}', uuid: {folder['uuid']}") return folder['uuid'] return None def create_folder(parent_id, name): cmd = f'internxt create-folder --json --non-interactive --name="{name}" --id="{parent_id}"' out = run_cmd(cmd) if out.get('success', False): uuid = out['folder']['uuid'] print(f"Created folder '{name}' in '{parent_id}', uuid: {uuid}") return uuid else: print(f"Failed to create folder '{name}': {out.get('message', 'Unknown error')}") # ERROR global errors errors += 1 time.sleep(1) return create_folder(parent_id, name) def find_or_create_folder(parent_id, name): global folders_created, folders_existed uuid = find_folder_uuid(parent_id, name) if uuid: folders_existed += 1 return uuid, False else: folders_created += 1 return create_folder(parent_id, name), True def find_file_uuid(parent_uuid, full_name): stem, ext = os.path.splitext(full_name) ext = ext.lstrip('.') listing = get_listing(parent_uuid) for file in listing['files']: if file['plainName'] == stem and file.get('type', '') == ext: print(f"File '{full_name}' exists in '{parent_uuid}', uuid: {file['uuid']}") return file['uuid'] return None def upload_file(local_path, dest_uuid): cmd = f'internxt upload-file --json --non-interactive --file="{local_path}" --destination="{dest_uuid}"' out = run_cmd(cmd) if out.get('success', False): print(f"Uploaded '{local_path}' to '{dest_uuid}'") return True else: print(f"Failed to upload '{local_path}': {out.get('message', 'Unknown error')}") return False def trash_file(file_uuid): cmd = f'internxt trash-file --json --non-interactive --id="{file_uuid}"' out = run_cmd(cmd) if out.get('success', False): print(f"Trashed file '{file_uuid}'") return True else: print(f"Failed to trash '{file_uuid}': {out.get('message', 'Unknown error')}") return False def handle_file(local_path, parent_id, mode): global files_uploaded, files_existed, files_replaced full_name = os.path.basename(local_path) print(f"Handling file '{local_path}' in '{parent_id}' (mode: {mode})") existing_uuid = find_file_uuid(parent_id, full_name) if not existing_uuid: if upload_file(local_path, parent_id): files_uploaded += 1 else: if mode == 'append': print(f"File '{full_name}' exists, skipping") files_existed += 1 elif mode == 'replace': if trash_file(existing_uuid): if upload_file(local_path, parent_id): files_replaced += 1 else: print(f"Failed to replace '{local_path}' after trashing") else: print(f"Failed to trash '{full_name}', skipping replacement") def backup_dir(local_dir, parent_id, mode): print(f"Backing up directory '{local_dir}' to '{parent_id}'") base = os.path.basename(local_dir) current_id, created = find_or_create_folder(parent_id, base) if not created and mode == 'append': # TODO: add datetime check and only skip if older print(f"Directory '{local_dir}' exists, skipping in append mode") return for entry in os.listdir(local_dir): local_path = os.path.join(local_dir, entry) if os.path.isdir(local_path): backup_dir(local_path, current_id, mode) elif os.path.isfile(local_path): handle_file(local_path, current_id, mode) else: print(f"Skipping non-file/non-dir '{local_path}'") def get_total_size(local_dir): total = 0 for root, dirs, files in os.walk(local_dir): for f in files: fp = os.path.join(root, f) try: total += os.path.getsize(fp) except (OSError, PermissionError) as e: print(f"Error getting size of '{fp}': {e}") return total if __name__ == "__main__": if len(sys.argv) != 3: print("Usage: python backup.py ") sys.exit(1) local_root = sys.argv[1] mode = sys.argv[2].lower() if mode not in ['append', 'replace']: print("Invalid mode: must be 'append' or 'replace'") sys.exit(1) print(f"Local root directory: {local_root}") start_time = time.time() total_size = get_total_size(local_root) base_name = os.path.basename(os.path.abspath(local_root)) root_parent = "" # remote_root_uuid = find_or_create_folder(root_parent, base_name) backup_dir(local_root, root_parent, mode) end_time = time.time() total_time = end_time - start_time print("\nBackup Summary:") print(f"Folders created: {folders_created}") print(f"Folders existed: {folders_existed}") print(f"Files uploaded (new): {files_uploaded}") print(f"Files existed (skipped): {files_existed}") if mode == 'replace': print(f"Files replaced (trashed + uploaded): {files_replaced}") print(f"Total file size: {total_size / (1024**2):.2f} MB") print(f"Total time: {total_time / 60:.2f} minutes") if total_size > 0: time_per_gb = (total_time / 60) / (total_size / (1024**3)) print(f"Time per GB: {time_per_gb:.2f} minutes/GB") megabits = total_size / (1024 * 128) # 128 is 1024/8 print(f"Megabits: {megabits}mb | total_time: {total_time}") print(f"Speed: {megabits / total_time}mbps") else: print("Time per GB: N/A (zero size)") print(f"Total errors: {errors}")