102 lines
4.9 KiB
PowerShell
102 lines
4.9 KiB
PowerShell
# 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"
|