#
Export-GoogleShortcuts.ps1
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$RootPath,
[Parameter(Mandatory = $true)][string]$ClientId,
[Parameter(Mandatory = $true)][string]$ClientSecret,
[string]$TokenCachePath = "$env:LOCALAPPDATA\gdrive-export\token.json",
[switch]$Overwrite
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Ensure-Folder([string]$dir) { if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null } }
function New-RandomString([int]$len) { $b=New-Object byte[] $len; [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); ([Convert]::ToBase64String($b)).TrimEnd('=').Replace('+','-').Replace('/','_').Substring(0,$len) }
function Base64UrlSha256([string]$s) { $sha=[System.Security.Cryptography.SHA256]::Create(); $h=$sha.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($s)); ([Convert]::ToBase64String($h)).TrimEnd('=').Replace('+','-').Replace('/','_') }
function Get-AccessToken {
if (Test-Path $TokenCachePath) {
try {
$cache = Get-Content $TokenCachePath -Raw | ConvertFrom-Json
if ($cache.refresh_token) {
$r = Invoke-RestMethod -Method Post -Uri "https://oauth2.googleapis.com/token" -Body @{client_id=$ClientId;client_secret=$ClientSecret;refresh_token=$cache.refresh_token;grant_type="refresh_token"}
$cache.access_token=$r.access_token; $cache.expires_in=$r.expires_in; $cache | ConvertTo-Json | Set-Content $TokenCachePath
return $r.access_token
}
} catch {}
}
$port=Get-Random -Min 49152 -Max 65535; $red="http://127.0.0.1:$port/"; $st=New-RandomString 24; $cv=New-RandomString 64; $cc=Base64UrlSha256 $cv
$authUrl="https://accounts.google.com/o/oauth2/v2/auth?client_id=$ClientId&redirect_uri=$red&response_type=code&scope=https://www.googleapis.com/auth/drive.readonly&access_type=offline&prompt=consent&state=$st&code_challenge=$cc&code_challenge_method=S256"
$l=New-Object System.Net.HttpListener; $l.Prefixes.Add($red); $l.Start(); Start-Process $authUrl | Out-Null
Write-Host "Autorisez l'accès dans le navigateur..." -ForegroundColor Yellow
$ctx=$l.GetContext(); $b=[System.Text.Encoding]::UTF8.GetBytes("
Authentification OK
");
$ctx.Response.ContentLength64=$b.Length; $ctx.Response.OutputStream.Write($b,0,$b.Length); $ctx.Response.OutputStream.Close(); $l.Stop()
$q=$ctx.Request.Url.Query.TrimStart('?'); $code=""; foreach($k in $q-split'&'){if($k-match'code='){$code=$k.Split('=')[1]}}
$t=Invoke-RestMethod -Method Post -Uri "https://oauth2.googleapis.com/token" -Body @{client_id=$ClientId;client_secret=$ClientSecret;code=$code;code_verifier=$cv;redirect_uri=$red;grant_type="authorization_code"}
$cacheObject = @{refresh_token=$t.refresh_token; access_token=$t.access_token; expires_in=$t.expires_in}
Ensure-Folder (Split-Path $TokenCachePath); $cacheObject | ConvertTo-Json | Set-Content $TokenCachePath
return $t.access_token
}
function Search-GoogleFileIdByName([string]$Name, [string]$SearchMime, [string]$Token) {
$h = @{ Authorization = "Bearer $Token" }
$sn = $Name.Replace("'", "\'")
try {
$url = "https://www.googleapis.com/drive/v3/files?q=$([uri]::EscapeDataString("name = '$sn' and mimeType = '$SearchMime' and trashed = false"))&fields=files(id,name)"
$r = Invoke-RestMethod -Uri $url -Headers $h -Method Get
if ($r.files.Count -ge 1) { return $r.files[0].id }
} catch {}
try {
$url = "https://www.googleapis.com/drive/v3/files?q=$([uri]::EscapeDataString("name contains '$sn' and mimeType = '$SearchMime' and trashed = false"))&fields=files(id,name)"
$r = Invoke-RestMethod -Uri $url -Headers $h -Method Get
if ($r.files.Count -ge 1) { return $r.files[0].id }
} catch {}
return $null
}
$rootFull = (Resolve-Path -LiteralPath $RootPath).Path
Write-Host "=== GOOGLE DRIVE EXPORT TOOL ==="
Write-Host "Source : $rootFull"
$extMap = @{
".gdoc" = @{ exportMime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"; searchMime="application/vnd.google-apps.document"; outExt=".docx"; suffix="_from_gdocs" }
".gsheet" = @{ exportMime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; searchMime="application/vnd.google-apps.spreadsheet"; outExt=".xlsx"; suffix="_from_gsheets" }
".gslides" = @{ exportMime="application/vnd.openxmlformats-officedocument.presentationml.presentation"; searchMime="application/vnd.google-apps.presentation"; outExt=".pptx"; suffix="_from_gslides" }
}
$targets = Get-ChildItem -LiteralPath $RootPath -Recurse -File | Where-Object { $extMap.ContainsKey($_.Extension.ToLowerInvariant()) }
if ($targets.Count -eq 0) { Write-Host "Aucun fichier Google trouvé."; exit }
$token = Get-AccessToken
foreach ($f in $targets) {
$cfg = $extMap[$f.Extension.ToLowerInvariant()]
$outName = $f.BaseName + $cfg.suffix + $cfg.outExt
$outPath = Join-Path $f.DirectoryName $outName
Write-Host "Traitement : $($f.Name)" -NoNewline
if ((-not $Overwrite) -and (Test-Path -LiteralPath $outPath)) { Write-Host " -> SKIP (Existe déjà)" -ForegroundColor Gray; continue }
$id = Search-GoogleFileIdByName -Name $f.BaseName -SearchMime $cfg.searchMime -Token $token
if (-not $id) { Write-Host " -> ECHEC (Introuvable via API)" -ForegroundColor Red; continue }
try {
$url = "https://www.googleapis.com/drive/v3/files/$id/export?mimeType=$([uri]::EscapeDataString($cfg.exportMime))"
Invoke-WebRequest -Uri $url -Headers @{ Authorization = "Bearer $Token" } -OutFile $outPath -ErrorAction Stop
Write-Host " -> OK" -ForegroundColor Green
}
catch {
Write-Host " -> ECHEC (Trop volumineux ou Erreur API)" -ForegroundColor Magenta
}
}
Write-Host "`nTerminé."