# 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"
