#!/usr/bin/env python3 """Upload remaining files to SharePoint via Graph API - files that OneDrive missed.""" import requests import subprocess import json # Get credentials def get_vault(path, field): result = subprocess.run( ["bash", ".claude/scripts/vault.sh", "get-field", path, field], capture_output=True, text=True, check=True ) return result.stdout.strip() def get_rmm_token(): result = subprocess.run( ["bash", ".claude/scripts/vault.sh", "get-field", "clients/birth-biologic/gururmm-site-main", "credentials.api_key"], capture_output=True, text=True, check=True ) return result.stdout.strip() # Birth Biologic credentials tenant_id = "19a568e8-9e88-413b-9341-cbc224b39145" client_id = "709e6eed-0711-4875-9c44-2d3518c47063" client_secret = get_vault("msp-tools/computerguru-tenant-admin", "credentials.client_secret") drive_id = "b!F8BzMb1YakCIWCyWlmczb09LHqtxDxVMpLT6kAwYmsM7NUY4oPLSRq7ng3tJq-E9" rmm_token = get_rmm_token() agent_id = "a4524e85-8a07-45d0-91b1-51ce7e2ca74a" # Get Graph token print("Getting Graph API token...") token_data = { "client_id": client_id, "client_secret": client_secret, "scope": "https://graph.microsoft.com/.default", "grant_type": "client_credentials" } token_resp = requests.post( f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token", data=token_data ) token_resp.raise_for_status() graph_token = token_resp.json()["access_token"] print("[OK] Graph token acquired") headers = {"Authorization": f"Bearer {graph_token}"} # Get all files from Datto via RMM print("\nGetting file list from Datto on VM...") get_files_script = r''' $source = "C:\Users\Public\Desktop\Datto Workplace Server Projects\Quality Department" Get-ChildItem $source -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object { $relativePath = $_.FullName.Substring($source.Length + 1).Replace('\', '/') [PSCustomObject]@{ Path = $relativePath Size = $_.Length } } | ConvertTo-Json -Compress ''' rmm_headers = { "Authorization": f"Bearer {rmm_token}", "Content-Type": "application/json" } command_payload = { "command_type": "powershell", "command": get_files_script, "timeout": 300 } resp = requests.post( f"https://rmm.azcomputerguru.com/api/agents/{agent_id}/command", headers=rmm_headers, json=command_payload ) resp.raise_for_status() command_id = resp.json()["command_id"] # Wait for result import time for _ in range(60): time.sleep(5) resp = requests.get( f"https://rmm.azcomputerguru.com/api/commands/{command_id}", headers=rmm_headers ) resp.raise_for_status() cmd_data = resp.json() if cmd_data["status"] in ["completed", "failed"]: if cmd_data["status"] == "failed": print(f"[ERROR] Command failed: {cmd_data.get('stderr', 'Unknown error')}") exit(1) break datto_files_json = cmd_data.get("stdout", "[]") datto_files = json.loads(datto_files_json) if datto_files_json.strip() else [] if isinstance(datto_files, dict): datto_files = [datto_files] print(f"[OK] Found {len(datto_files)} files in Datto") # Get all files currently in SharePoint def get_all_sharepoint_files(item_id=None): if item_id: url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{item_id}/children" else: url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/root/children" files = [] while url: resp = requests.get(url, headers=headers) resp.raise_for_status() data = resp.json() for item in data.get("value", []): if "folder" in item: files.extend(get_all_sharepoint_files(item["id"])) else: # Get path from parentReference and name path = item.get("name", "") if "parentReference" in item and "path" in item["parentReference"]: parent_path = item["parentReference"]["path"] # Extract relative path if "/root:" in parent_path: relative_parent = parent_path.split("/root:")[-1] if relative_parent: path = f"{relative_parent}/{item['name']}" files.append({ "path": path.strip("/"), "size": item.get("size", 0) }) url = data.get("@odata.nextLink") return files print("\nGetting file list from SharePoint...") sp_files = get_all_sharepoint_files() print(f"[OK] Found {len(sp_files)} files in SharePoint") # Find missing files sp_paths = {f["path"].lower().replace("\\", "/") for f in sp_files} missing_files = [] for datto_file in datto_files: datto_path = datto_file["Path"].lower().replace("\\", "/") if datto_path not in sp_paths: missing_files.append(datto_file) print(f"\n[INFO] Missing files: {len(missing_files)}") if len(missing_files) == 0: print("[OK] No missing files - sync is complete!") exit(0) # Upload missing files print(f"\nUploading {len(missing_files)} missing files...") upload_script_template = r''' $source = "C:\Users\Public\Desktop\Datto Workplace Server Projects\Quality Department" $driveId = "{drive_id}" $token = "{token}" $filePath = "{file_path}" $fullPath = Join-Path $source $filePath $uploadPath = $filePath.Replace('\', '/') $uploadUrl = "https://graph.microsoft.com/v1.0/drives/$driveId/root:/$uploadPath`:/content" $headers = @{{ "Authorization" = "Bearer $token" "Content-Type" = "application/octet-stream" }} try {{ $fileBytes = [System.IO.File]::ReadAllBytes($fullPath) Invoke-RestMethod -Method Put -Uri $uploadUrl -Headers $headers -Body $fileBytes -UseBasicParsing | Out-Null Write-Host "[OK] $uploadPath" }} catch {{ Write-Host "[ERROR] $uploadPath - $_" exit 1 }} ''' uploaded = 0 errors = [] for missing_file in missing_files[:100]: # Upload first 100 to start file_path = missing_file["Path"] upload_script = upload_script_template.format( drive_id=drive_id, token=graph_token, file_path=file_path.replace('"', '`"') ) command_payload = { "command_type": "powershell", "command": upload_script, "timeout": 120 } try: resp = requests.post( f"https://rmm.azcomputerguru.com/api/agents/{agent_id}/command", headers=rmm_headers, json=command_payload ) resp.raise_for_status() command_id = resp.json()["command_id"] # Wait for completion for _ in range(24): time.sleep(5) resp = requests.get( f"https://rmm.azcomputerguru.com/api/commands/{command_id}", headers=rmm_headers ) resp.raise_for_status() cmd_data = resp.json() if cmd_data["status"] in ["completed", "failed"]: if cmd_data["status"] == "completed": uploaded += 1 if uploaded % 10 == 0: print(f" Uploaded {uploaded} / {len(missing_files)} files...") else: errors.append(file_path) print(f" [ERROR] {file_path}") break except Exception as e: errors.append(file_path) print(f" [ERROR] {file_path}: {e}") print(f"\n[OK] Uploaded {uploaded} files") if errors: print(f"[WARNING] {len(errors)} errors") print("\nFirst 10 errors:") for err in errors[:10]: print(f" {err}")