#!/usr/bin/env python3 """ Sync SharePoint Quality Systems Department to match Datto exactly. Deletes any files in SharePoint that aren't in the Datto source. """ import requests import json import sys import subprocess import time from datetime import datetime, timedelta from pathlib import Path # Configuration RMM_API = "http://172.16.3.30:3001" RMM_ADMIN_EMAIL = "claude-api@azcomputerguru.com" RMM_ADMIN_PASSWORD = "ClaudeAPI2026!@#" ACG_DWP_AGENT_ID = "a4524e85-8a07-45d0-91b1-51ce7e2ca74a" TENANT_ID = "19a568e8-9e88-413b-9341-cbc224b39145" TENANT_ADMIN_CLIENT_ID = "709e6eed-0711-4875-9c44-2d3518c47063" # Paths DATTO_SOURCE = r"C:\Users\Public\Desktop\Datto Workplace Server Projects\Quality Department" SHAREPOINT_SITE = "https://birthbiologic.sharepoint.com/sites/QualitySystemsDepartment" SHAREPOINT_DRIVE = "Documents" # Default document library def get_vault_secret(vault_path, field): """Get a secret from the SOPS vault.""" result = subprocess.run( ["bash", ".claude/scripts/vault.sh", "get-field", vault_path, field], capture_output=True, text=True, cwd="/Users/azcomputerguru/ClaudeTools" ) secret = result.stdout.strip() if secret == "null" or not secret: raise ValueError(f"Failed to get vault secret: {vault_path} -> {field}") return secret def get_rmm_token(): """Get RMM API token.""" response = requests.post( f"{RMM_API}/api/auth/login", json={"email": RMM_ADMIN_EMAIL, "password": RMM_ADMIN_PASSWORD} ) response.raise_for_status() return response.json()["token"] def get_graph_token(): """Get Graph API token for Tenant Admin app.""" client_secret = get_vault_secret( "msp-tools/computerguru-tenant-admin", "credentials.client_secret" ) response = requests.post( f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token", data={ "client_id": TENANT_ADMIN_CLIENT_ID, "client_secret": client_secret, "scope": "https://graph.microsoft.com/.default", "grant_type": "client_credentials" } ) response.raise_for_status() return response.json()["access_token"] def run_rmm_command(rmm_token, agent_id, script): """Run a PowerShell command via RMM and wait for result.""" # Submit command response = requests.post( f"{RMM_API}/api/agents/{agent_id}/command", headers={"Authorization": f"Bearer {rmm_token}"}, json={ "command_type": "powershell", "command": script, "timeout_seconds": 300 } ) response.raise_for_status() command_id = response.json()["command_id"] print(f"[INFO] RMM command {command_id} submitted, waiting for result...") # Poll for result for _ in range(60): time.sleep(5) response = requests.get( f"{RMM_API}/api/commands/{command_id}", headers={"Authorization": f"Bearer {rmm_token}"} ) response.raise_for_status() cmd = response.json() if cmd["status"] == "completed": return cmd.get("stdout", "") elif cmd["status"] == "failed": raise Exception(f"RMM command failed: {cmd.get('stderr', 'Unknown error')}") raise Exception("RMM command timed out") def get_datto_file_list(rmm_token): """Get recursive file listing from Datto source.""" script = f""" $ErrorActionPreference = 'Stop' $path = '{DATTO_SOURCE}' Get-ChildItem -Path $path -Recurse -File | ForEach-Object {{ $relativePath = $_.FullName.Substring($path.Length).TrimStart('\\') [PSCustomObject]@{{ Path = $relativePath Size = $_.Length LastModified = $_.LastWriteTime.ToString('o') }} }} | ConvertTo-Json -Compress """ result = run_rmm_command(rmm_token, ACG_DWP_AGENT_ID, script) if not result or result == "null": return [] # Handle single object (not array) case data = json.loads(result) if isinstance(data, dict): return [data] return data def get_sharepoint_file_list(graph_token): """Get recursive file listing from SharePoint.""" # Get site ID response = requests.get( f"https://graph.microsoft.com/v1.0/sites/birthbiologic.sharepoint.com:/sites/QualitySystemsDepartment", headers={"Authorization": f"Bearer {graph_token}"} ) response.raise_for_status() site_id = response.json()["id"] # Get drive ID response = requests.get( f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives", headers={"Authorization": f"Bearer {graph_token}"} ) response.raise_for_status() drives = response.json()["value"] doc_drive = next((d for d in drives if d["name"] == SHAREPOINT_DRIVE), None) if not doc_drive: raise Exception(f"Drive '{SHAREPOINT_DRIVE}' not found") drive_id = doc_drive["id"] # Get all files recursively files = [] def get_items(item_id="root", prefix=""): response = requests.get( f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{item_id}/children?$top=5000", headers={"Authorization": f"Bearer {graph_token}"} ) response.raise_for_status() items = response.json()["value"] for item in items: name = item["name"] path = f"{prefix}/{name}".lstrip("/") if "folder" in item: # Recurse into folder get_items(item["id"], path) else: # File files.append({ "Path": path.replace("/", "\\"), # Match Datto format "Size": item["size"], "LastModified": item["lastModifiedDateTime"], "ItemId": item["id"], "WebUrl": item.get("webUrl", "") }) get_items() return files def delete_sharepoint_item(graph_token, site_id, drive_id, item_id): """Delete a SharePoint item.""" response = requests.delete( f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{item_id}", headers={"Authorization": f"Bearer {graph_token}"} ) response.raise_for_status() def main(): print("[INFO] Starting Quality Department sync to Datto ground truth...") print() # Get tokens print("[INFO] Getting RMM token...") rmm_token = get_rmm_token() print("[INFO] Getting Graph token...") graph_token = get_graph_token() # Get file lists print("[INFO] Getting Datto file list...") datto_files = get_datto_file_list(rmm_token) print(f"[INFO] Found {len(datto_files)} files in Datto") print("[INFO] Getting SharePoint file list...") sp_files = get_sharepoint_file_list(graph_token) print(f"[INFO] Found {len(sp_files)} files in SharePoint") print() # Build Datto path set (normalized) datto_paths = {f["Path"].replace("/", "\\").lower() for f in datto_files} # Find files to delete (in SharePoint but not in Datto) # Exclude files modified in last 24 hours (the ~11 files modified yesterday) cutoff = datetime.now() - timedelta(hours=24) to_delete = [] for sp_file in sp_files: sp_path = sp_file["Path"].lower() last_modified = datetime.fromisoformat(sp_file["LastModified"].replace("Z", "+00:00")) if sp_path not in datto_paths: # File exists in SharePoint but not in Datto if last_modified > cutoff: print(f"[SKIP] Recent file (modified <24h ago): {sp_file['Path']}") else: to_delete.append(sp_file) print() print(f"[INFO] {len(to_delete)} files to delete from SharePoint") print() if not to_delete: print("[OK] SharePoint already matches Datto (no deletions needed)") return 0 # Show what will be deleted print("[WARNING] The following files will be DELETED from SharePoint:") print() for f in to_delete[:20]: # Show first 20 print(f" - {f['Path']}") if len(to_delete) > 20: print(f" ... and {len(to_delete) - 20} more") print() # Confirm response = input(f"Delete {len(to_delete)} files? (yes/no): ") if response.lower() != "yes": print("[CANCELLED] No files deleted") return 1 # Get site and drive IDs for deletion response = requests.get( f"https://graph.microsoft.com/v1.0/sites/birthbiologic.sharepoint.com:/sites/QualitySystemsDepartment", headers={"Authorization": f"Bearer {graph_token}"} ) response.raise_for_status() site_id = response.json()["id"] response = requests.get( f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives", headers={"Authorization": f"Bearer {graph_token}"} ) response.raise_for_status() drives = response.json()["value"] doc_drive = next((d for d in drives if d["name"] == SHAREPOINT_DRIVE), None) drive_id = doc_drive["id"] # Delete files print() print("[INFO] Deleting files...") deleted = 0 errors = [] for f in to_delete: try: delete_sharepoint_item(graph_token, site_id, drive_id, f["ItemId"]) deleted += 1 print(f"[DELETED] {f['Path']}") except Exception as e: errors.append((f["Path"], str(e))) print(f"[ERROR] Failed to delete {f['Path']}: {e}") print() print(f"[OK] Deleted {deleted}/{len(to_delete)} files") if errors: print() print(f"[WARNING] {len(errors)} errors:") for path, err in errors[:10]: print(f" - {path}: {err}") return 0 if not errors else 1 if __name__ == "__main__": sys.exit(main())