# Birth Biologic - Quality Systems Department: converge SharePoint to Datto (3768 files) # Idempotent: skips files already present with matching size. Chunked upload sessions for >=4MB. # Long-path safe (\\?\). Refreshes Graph token on long runs. Writes progress to a tailable log. $ErrorActionPreference = 'Stop' $source = 'C:\Users\Public\Desktop\Datto Workplace Server Projects\Quality Department' $driveId = 'b!F8BzMb1YakCIWCyWlmczb09LHqtxDxVMpLT6kAwYmsM7NUY4oPLSRq7ng3tJq-E9' $tenantId = '19a568e8-9e88-413b-9341-cbc224b39145' $clientId = '709e6eed-0711-4875-9c44-2d3518c47063' $clientSecret = 'SECRET_PLACEHOLDER' $logPath = 'C:\Windows\Temp\quality-upload.log' $manifestPath = 'C:\Windows\Temp\quality-manifest.txt' "=== upload start ===" | Set-Content -LiteralPath $logPath -Encoding UTF8 function Log($m){ $line = (Get-Date -Format 'HH:mm:ss') + ' ' + $m; Write-Host $line; Add-Content -LiteralPath $logPath -Value $line } $script:token = $null $script:tokenAt = $null function Get-Token { $body = @{ client_id=$clientId; client_secret=$clientSecret; scope='https://graph.microsoft.com/.default'; grant_type='client_credentials' } $r = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body $body $script:token = $r.access_token $script:tokenAt = Get-Date } function Fresh-Token { if ($null -eq $script:token -or ((Get-Date) - $script:tokenAt).TotalMinutes -gt 40) { Get-Token } return $script:token } function Enc($rel){ ($rel.Split('/') | ForEach-Object { [uri]::EscapeDataString($_) }) -join '/' } function Long($p){ if ($p.StartsWith('\\?\')) { $p } else { '\\?\' + $p } } Get-Token Log "Token acquired." $lines = Get-Content -LiteralPath $manifestPath | Where-Object { $_ -and $_.Contains('|') } $items = foreach($l in $lines){ $idx = $l.LastIndexOf('|') [pscustomobject]@{ rel = $l.Substring(0,$idx); size = [int64]$l.Substring($idx+1) } } # small files first so the visible count climbs fast, then the big ones $items = $items | Sort-Object size Log ("Manifest items: " + $items.Count) # Internal time budget: exit cleanly before any agent-side cap; re-dispatch resumes (idempotent). $deadline = (Get-Date).AddSeconds(9600) $timedOut = $false $uploaded=0; $skipped=0; $errors=0; $bigUp=0; $i=0 foreach($it in $items){ $i++ if ((Get-Date) -gt $deadline){ $timedOut = $true; Log "TIME budget reached at item $i; stopping pass cleanly."; break } $rel = $it.rel; $size = $it.size; $enc = Enc $rel $tok = Fresh-Token $h = @{ Authorization = "Bearer $tok" } $exists=$false try { $meta = Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/drives/$driveId/root:/$enc" -Headers $h if ([int64]$meta.size -eq $size) { $exists=$true } } catch { $exists=$false } if ($exists){ $skipped++; if($i % 250 -eq 0){ Log " $i/$($items.Count) up=$uploaded skip=$skipped err=$errors big=$bigUp" }; continue } $full = Long (Join-Path $source ($rel.Replace('/','\'))) try { if ($size -lt 4194304){ $bytes = [System.IO.File]::ReadAllBytes($full) $hh = @{ Authorization = "Bearer $tok"; 'Content-Type'='application/octet-stream' } Invoke-RestMethod -Method Put -Uri "https://graph.microsoft.com/v1.0/drives/$driveId/root:/$enc`:/content" -Headers $hh -Body $bytes -UseBasicParsing | Out-Null $uploaded++ } else { $sessUri = "https://graph.microsoft.com/v1.0/drives/$driveId/root:/$enc`:/createUploadSession" $sessBody = @{ item = @{ '@microsoft.graph.conflictBehavior'='replace' } } | ConvertTo-Json $sess = Invoke-RestMethod -Method Post -Uri $sessUri -Headers @{ Authorization="Bearer $tok"; 'Content-Type'='application/json' } -Body $sessBody $upUrl = $sess.uploadUrl $fs = [System.IO.File]::Open($full,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::Read) try { $chunk = 10485760 # 10 MB (multiple of 320 KiB) $buf = New-Object byte[] $chunk $pos = [int64]0 while ($pos -lt $size){ $read = $fs.Read($buf,0,$chunk) if ($read -le 0){ break } if ($read -eq $chunk){ $payload = $buf } else { $payload = New-Object byte[] $read; [Array]::Copy($buf,$payload,$read) } $end = $pos + $read - 1 $ch = @{ 'Content-Range' = "bytes $pos-$end/$size" } Invoke-RestMethod -Method Put -Uri $upUrl -Headers $ch -Body $payload | Out-Null $pos += $read } } finally { $fs.Close() } $uploaded++; $bigUp++ Log (" BIG ok " + [math]::Round($size/1MB,1) + "MB: $rel") } } catch { $errors++ Log " ERROR: $rel :: $($_.Exception.Message)" } if($i % 100 -eq 0){ Log " $i/$($items.Count) up=$uploaded skip=$skipped err=$errors big=$bigUp" } } $allDone = (-not $timedOut) Log "DONE up=$uploaded skip=$skipped err=$errors big=$bigUp total=$($items.Count) reachedEnd=$allDone" Write-Host "RESULT up=$uploaded skip=$skipped err=$errors big=$bigUp total=$($items.Count) reachedEnd=$allDone"