- Current state: 3,249/3,768 files uploaded, 519 remaining - Active RMM command: 9e0fcfe8 (running on ACG-DWP-X-BB) - Working upload script with drive ID concatenation fix - Comprehensive continuation instructions - All verification scripts Client very angry - this was promised yesterday Issue: PowerShell escaping ! in drive ID (b! -> b\!) Solution: String concatenation at runtime
143 lines
5.1 KiB
Python
143 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Make SharePoint Quality Systems Department match Datto exactly.
|
|
Quick version - just do it.
|
|
"""
|
|
|
|
import requests
|
|
import subprocess
|
|
import time
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
|
|
RMM_API = "http://172.16.3.30:3001"
|
|
ADMIN_EMAIL = "claude-api@azcomputerguru.com"
|
|
ADMIN_PASS = "ClaudeAPI2026!@#"
|
|
AGENT_ID = "a4524e85-8a07-45d0-91b1-51ce7e2ca74a"
|
|
|
|
TENANT_ID = "19a568e8-9e88-413b-9341-cbc224b39145"
|
|
CLIENT_ID = "709e6eed-0711-4875-9c44-2d3518c47063"
|
|
|
|
def vault(path, field):
|
|
r = subprocess.run(["bash", ".claude/scripts/vault.sh", "get-field", path, field],
|
|
capture_output=True, text=True, cwd="/Users/azcomputerguru/ClaudeTools")
|
|
return r.stdout.strip()
|
|
|
|
def rmm_cmd(script, timeout=300):
|
|
"""Run PS command via RMM, return stdout"""
|
|
token = requests.post(f"{RMM_API}/api/auth/login",
|
|
json={"email": ADMIN_EMAIL, "password": ADMIN_PASS}).json()["token"]
|
|
|
|
r = requests.post(f"{RMM_API}/api/agents/{AGENT_ID}/command",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
json={"command_type": "powershell", "command": script, "timeout_seconds": timeout})
|
|
cmd_id = r.json()["command_id"]
|
|
|
|
for _ in range(int(timeout/5) + 10):
|
|
time.sleep(5)
|
|
r = requests.get(f"{RMM_API}/api/commands/{cmd_id}",
|
|
headers={"Authorization": f"Bearer {token}"})
|
|
cmd = r.json()
|
|
if cmd["status"] == "completed":
|
|
return cmd.get("stdout", "")
|
|
elif cmd["status"] == "failed":
|
|
raise Exception(f"RMM failed: {cmd.get('stderr', '')}")
|
|
raise Exception("RMM timeout")
|
|
|
|
def get_graph_token():
|
|
secret = vault("msp-tools/computerguru-tenant-admin", "credentials.client_secret")
|
|
r = requests.post(f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token",
|
|
data={"client_id": CLIENT_ID, "client_secret": secret,
|
|
"scope": "https://graph.microsoft.com/.default", "grant_type": "client_credentials"})
|
|
return r.json()["access_token"]
|
|
|
|
print("[1/4] Getting Datto file list (handling long paths)...")
|
|
|
|
# Get Datto files with error handling for long paths
|
|
script = r'''
|
|
$files = @()
|
|
$root = "C:\Users\Public\Desktop\Datto Workplace Server Projects\Quality Department"
|
|
Get-ChildItem $root -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {
|
|
try {
|
|
$relPath = $_.FullName.Substring($root.Length + 1)
|
|
$files += [PSCustomObject]@{
|
|
Path = $relPath
|
|
Size = $_.Length
|
|
}
|
|
} catch {}
|
|
}
|
|
$files | ConvertTo-Json -Compress
|
|
'''
|
|
|
|
result = rmm_cmd(script, 600)
|
|
datto_files = json.loads(result) if result and result != "null" else []
|
|
if isinstance(datto_files, dict):
|
|
datto_files = [datto_files]
|
|
|
|
datto_paths = {f["Path"].replace("\\", "/").lower() for f in datto_files}
|
|
print(f" Found {len(datto_paths)} files in Datto")
|
|
|
|
print("[2/4] Getting SharePoint file list...")
|
|
|
|
token = get_graph_token()
|
|
|
|
# Get site/drive IDs
|
|
r = requests.get("https://graph.microsoft.com/v1.0/sites/birthbiologic.sharepoint.com:/sites/QualitySystemsDepartment",
|
|
headers={"Authorization": f"Bearer {token}"})
|
|
site_id = r.json()["id"]
|
|
|
|
r = requests.get(f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives",
|
|
headers={"Authorization": f"Bearer {token}"})
|
|
drive = next(d for d in r.json()["value"] if d["name"] == "Documents")
|
|
drive_id = drive["id"]
|
|
|
|
# Get all files
|
|
sp_files = []
|
|
def get_items(item_id="root", prefix=""):
|
|
r = requests.get(f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{item_id}/children?$top=5000",
|
|
headers={"Authorization": f"Bearer {token}"})
|
|
for item in r.json()["value"]:
|
|
path = f"{prefix}/{item['name']}".lstrip("/")
|
|
if "folder" in item:
|
|
get_items(item["id"], path)
|
|
else:
|
|
sp_files.append({"Path": path, "Id": item["id"],
|
|
"Modified": item["lastModifiedDateTime"]})
|
|
|
|
get_items()
|
|
print(f" Found {len(sp_files)} files in SharePoint")
|
|
|
|
print("[3/4] Identifying files to delete...")
|
|
|
|
cutoff = datetime.now() - timedelta(hours=24)
|
|
to_delete = []
|
|
|
|
for f in sp_files:
|
|
path_norm = f["Path"].replace("\\", "/").lower()
|
|
modified = datetime.fromisoformat(f["Modified"].replace("Z", "+00:00")).replace(tzinfo=None)
|
|
|
|
if path_norm not in datto_paths and modified < cutoff:
|
|
to_delete.append(f)
|
|
|
|
print(f" {len(to_delete)} files to delete (not in Datto, >24h old)")
|
|
|
|
if not to_delete:
|
|
print("[OK] SharePoint already matches Datto!")
|
|
exit(0)
|
|
|
|
print(f"\n[4/4] Deleting {len(to_delete)} files from SharePoint...")
|
|
|
|
deleted = 0
|
|
for f in to_delete:
|
|
try:
|
|
requests.delete(f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{f['Id']}",
|
|
headers={"Authorization": f"Bearer {token}"}).raise_for_status()
|
|
deleted += 1
|
|
if deleted % 10 == 0:
|
|
print(f" Deleted {deleted}/{len(to_delete)}...")
|
|
except Exception as e:
|
|
print(f" ERROR deleting {f['Path']}: {e}")
|
|
|
|
print(f"\n[DONE] Deleted {deleted}/{len(to_delete)} files")
|
|
print(f"[OK] Quality Systems Department now matches Datto")
|