2 Commits

Author SHA1 Message Date
693f21915d Updated workflow to include CSV examples folder
All checks were successful
Release zip package / release (push) Successful in 1s
2026-03-17 11:03:23 +01:00
ab39e55194 Added mass-transfer
All checks were successful
Release zip package / release (push) Successful in 1s
2026-03-17 10:57:11 +01:00
4 changed files with 381 additions and 199 deletions

View File

@@ -24,7 +24,7 @@ jobs:
cd repo cd repo
VERSION="${{ gitea.ref_name }}" VERSION="${{ gitea.ref_name }}"
ZIP="SharePoint_ToolBox_${VERSION}.zip" ZIP="SharePoint_ToolBox_${VERSION}.zip"
zip -r "../${ZIP}" Sharepoint_ToolBox.ps1 lang/ zip -r "../${ZIP}" Sharepoint_ToolBox.ps1 lang/ examples/
echo "ZIP=${ZIP}" >> "$GITHUB_ENV" echo "ZIP=${ZIP}" >> "$GITHUB_ENV"
echo "VERSION=${VERSION}" >> "$GITHUB_ENV" echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
@@ -34,7 +34,7 @@ jobs:
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \ "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \ -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"tag_name\":\"${{ env.VERSION }}\",\"name\":\"SharePoint ToolBox ${{ env.VERSION }}\",\"body\":\"### How to use\\n1. Download and extract the archive\\n2. Launch Sharepoint_ToolBox.ps1 with PowerShell\\n\"}" \ -d "{\"tag_name\":\"${{ env.VERSION }}\",\"name\":\"SharePoint ToolBox ${{ env.VERSION }}\",\"body\":\"1. Download and extract the archive\\n2. Launch Sharepoint_ToolBox.ps1 with PowerShell\\n\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "RELEASE_ID=${RELEASE_ID}" >> "$GITHUB_ENV" echo "RELEASE_ID=${RELEASE_ID}" >> "$GITHUB_ENV"

View File

@@ -2823,6 +2823,11 @@ $script:LangDefault = @{
"ph.xfer.site" = "https://tenant.sharepoint.com/sites/xxx" "ph.xfer.site" = "https://tenant.sharepoint.com/sites/xxx"
"ph.xfer.library" = "Shared Documents/subfolder" "ph.xfer.library" = "Shared Documents/subfolder"
"xfer.note" = "Only the current version of each file is transferred (no version history)." "xfer.note" = "Only the current version of each file is transferred (no version history)."
"chk.xfer.create.folders" = "Create missing folders"
"btn.xfer.csv" = "Import CSV..."
"btn.xfer.csv.clear" = "Clear"
"lbl.xfer.csv.info" = "{0} transfer(s) loaded"
"lbl.xfer.report.fmt" = "Report:"
"tab.bulk" = " Bulk Create " "tab.bulk" = " Bulk Create "
"grp.bulk.list" = "Sites to create" "grp.bulk.list" = "Sites to create"
"btn.bulk.add" = "Add Site..." "btn.bulk.add" = "Add Site..."
@@ -3546,27 +3551,56 @@ $txtXferDstLib.PlaceholderText = T "ph.xfer.library"
$grpXferDst.Controls.AddRange(@($lblXferDstSite, $txtXferDstSite, $lblXferDstLib, $txtXferDstLib)) $grpXferDst.Controls.AddRange(@($lblXferDstSite, $txtXferDstSite, $lblXferDstLib, $txtXferDstLib))
# ── GroupBox: Options (y=160, h=58) ────────────────────────────────────────── # ── GroupBox: Options (y=160, h=96) ──────────────────────────────────────────
$grpXferOpts = New-Group (T "grp.xfer.options") 10 160 620 58 $grpXferOpts = New-Group (T "grp.xfer.options") 10 160 620 96
$chkXferRecursive = New-Check (T "chk.xfer.recursive") 10 18 260 $true $chkXferRecursive = New-Check (T "chk.xfer.recursive") 10 18 250 $true
$chkXferOverwrite = New-Check (T "chk.xfer.overwrite") 280 18 230 $chkXferOverwrite = New-Check (T "chk.xfer.overwrite") 270 18 180
$chkXferCreateFolders = New-Check (T "chk.xfer.create.folders") 460 18 155 $true
$lblXferFmt = New-Object System.Windows.Forms.Label
$lblXferFmt.Text = T "lbl.xfer.report.fmt"
$lblXferFmt.Location = New-Object System.Drawing.Point(10, 42)
$lblXferFmt.Size = New-Object System.Drawing.Size(55, 20)
$radXferCsv = New-Radio "CSV" 68 42 55 $true
$radXferHtml = New-Radio "HTML" 125 42 60 $false
$btnXferCsvImport = New-Object System.Windows.Forms.Button
$btnXferCsvImport.Text = T "btn.xfer.csv"
$btnXferCsvImport.Location = New-Object System.Drawing.Point(250, 40)
$btnXferCsvImport.Size = New-Object System.Drawing.Size(118, 24)
$lblXferCsvInfo = New-Object System.Windows.Forms.Label
$lblXferCsvInfo.Text = ""
$lblXferCsvInfo.Location = New-Object System.Drawing.Point(374, 43)
$lblXferCsvInfo.Size = New-Object System.Drawing.Size(175, 18)
$lblXferCsvInfo.ForeColor = [System.Drawing.Color]::FromArgb(0, 120, 212)
$btnXferCsvClear = New-Object System.Windows.Forms.Button
$btnXferCsvClear.Text = T "btn.xfer.csv.clear"
$btnXferCsvClear.Location = New-Object System.Drawing.Point(554, 40)
$btnXferCsvClear.Size = New-Object System.Drawing.Size(55, 24)
$btnXferCsvClear.Visible = $false
$lblXferNote = New-Object System.Windows.Forms.Label $lblXferNote = New-Object System.Windows.Forms.Label
$lblXferNote.Text = T "xfer.note" $lblXferNote.Text = T "xfer.note"
$lblXferNote.Location = New-Object System.Drawing.Point(10, 40) $lblXferNote.Location = New-Object System.Drawing.Point(10, 72)
$lblXferNote.Size = New-Object System.Drawing.Size(600, 16) $lblXferNote.Size = New-Object System.Drawing.Size(600, 16)
$lblXferNote.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Italic) $lblXferNote.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Italic)
$lblXferNote.ForeColor = [System.Drawing.Color]::DimGray $lblXferNote.ForeColor = [System.Drawing.Color]::DimGray
$grpXferOpts.Controls.AddRange(@($chkXferRecursive, $chkXferOverwrite, $lblXferNote)) $grpXferOpts.Controls.AddRange(@($chkXferRecursive, $chkXferOverwrite, $chkXferCreateFolders,
$lblXferFmt, $radXferCsv, $radXferHtml,
$btnXferCsvImport, $lblXferCsvInfo, $btnXferCsvClear,
$lblXferNote))
# ── Buttons (y=224) ────────────────────────────────────────────────────────── # ── Buttons (y=260) ──────────────────────────────────────────────────────────
$btnXferStart = New-ActionBtn (T "btn.xfer.start") 10 224 ([System.Drawing.Color]::FromArgb(0, 120, 60)) $btnXferStart = New-ActionBtn (T "btn.xfer.start") 10 260 ([System.Drawing.Color]::FromArgb(0, 120, 60))
$btnXferVerify = New-Object System.Windows.Forms.Button $btnXferVerify = New-Object System.Windows.Forms.Button
$btnXferVerify.Text = T "btn.xfer.verify" $btnXferVerify.Text = T "btn.xfer.verify"
$btnXferVerify.Location = New-Object System.Drawing.Point(175, 224) $btnXferVerify.Location = New-Object System.Drawing.Point(175, 260)
$btnXferVerify.Size = New-Object System.Drawing.Size(130, 34) $btnXferVerify.Size = New-Object System.Drawing.Size(130, 34)
$btnXferVerify.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 212) $btnXferVerify.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 212)
$btnXferVerify.ForeColor = [System.Drawing.Color]::White $btnXferVerify.ForeColor = [System.Drawing.Color]::White
@@ -3575,7 +3609,7 @@ $btnXferVerify.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Dr
$btnXferOpen = New-Object System.Windows.Forms.Button $btnXferOpen = New-Object System.Windows.Forms.Button
$btnXferOpen.Text = T "btn.xfer.open" $btnXferOpen.Text = T "btn.xfer.open"
$btnXferOpen.Location = New-Object System.Drawing.Point(315, 224) $btnXferOpen.Location = New-Object System.Drawing.Point(315, 260)
$btnXferOpen.Size = New-Object System.Drawing.Size(130, 34) $btnXferOpen.Size = New-Object System.Drawing.Size(130, 34)
$btnXferOpen.Enabled = $false $btnXferOpen.Enabled = $false
@@ -3937,6 +3971,10 @@ $_reg = {
& $_reg $script:i18nMap $grpXferOpts "grp.xfer.options" & $_reg $script:i18nMap $grpXferOpts "grp.xfer.options"
& $_reg $script:i18nMap $chkXferRecursive "chk.xfer.recursive" & $_reg $script:i18nMap $chkXferRecursive "chk.xfer.recursive"
& $_reg $script:i18nMap $chkXferOverwrite "chk.xfer.overwrite" & $_reg $script:i18nMap $chkXferOverwrite "chk.xfer.overwrite"
& $_reg $script:i18nMap $chkXferCreateFolders "chk.xfer.create.folders"
& $_reg $script:i18nMap $lblXferFmt "lbl.xfer.report.fmt"
& $_reg $script:i18nMap $btnXferCsvImport "btn.xfer.csv"
& $_reg $script:i18nMap $btnXferCsvClear "btn.xfer.csv.clear"
& $_reg $script:i18nMap $lblXferNote "xfer.note" & $_reg $script:i18nMap $lblXferNote "xfer.note"
& $_reg $script:i18nMap $btnXferStart "btn.xfer.start" & $_reg $script:i18nMap $btnXferStart "btn.xfer.start"
& $_reg $script:i18nMap $btnXferVerify "btn.xfer.verify" & $_reg $script:i18nMap $btnXferVerify "btn.xfer.verify"
@@ -4965,30 +5003,156 @@ $btnOpenDupes.Add_Click({
# ── Transfer ────────────────────────────────────────────────────────────────── # ── Transfer ──────────────────────────────────────────────────────────────────
$btnXferStart.Add_Click({ # ── CSV Import for bulk transfers ─────────────────────────────────────────────
$clientId = $txtClientId.Text.Trim() $script:_XferCsvEntries = [System.Collections.Generic.List[object]]::new()
$btnXferCsvImport.Add_Click({
$ofd = New-Object System.Windows.Forms.OpenFileDialog
$ofd.Filter = "CSV Files (*.csv)|*.csv"
$ofd.Title = "Import Transfer CSV"
if ($ofd.ShowDialog() -ne "OK") { return }
$script:_XferCsvEntries.Clear()
try {
$rows = Import-Csv -Path $ofd.FileName -Delimiter ';' -Encoding UTF8
foreach ($r in $rows) {
$src = ($r.PSObject.Properties | Where-Object { $_.Name -match '^SourceSite$' }).Value
$dst = ($r.PSObject.Properties | Where-Object { $_.Name -match '^DestSite$' }).Value
$sl = ($r.PSObject.Properties | Where-Object { $_.Name -match '^SourceLibrary$' }).Value
$dl = ($r.PSObject.Properties | Where-Object { $_.Name -match '^DestLibrary$' }).Value
if ($src -and $sl) {
$script:_XferCsvEntries.Add(@{
SrcSite = $src.Trim()
DstSite = if ($dst) { $dst.Trim() } else { $src.Trim() }
SrcLib = $sl.Trim()
DstLib = if ($dl) { $dl.Trim() } else { $sl.Trim() }
})
}
}
$n = $script:_XferCsvEntries.Count
$lblXferCsvInfo.Text = (T "lbl.xfer.csv.info") -f $n
$btnXferCsvClear.Visible = $true
$txtXferSrcSite.Enabled = $false; $txtXferSrcLib.Enabled = $false
$txtXferDstSite.Enabled = $false; $txtXferDstLib.Enabled = $false
Write-Log "CSV loaded: $n transfer(s)" "White"
} catch {
Write-Log "CSV import error: $($_.Exception.Message)" "Red"
}
})
$btnXferCsvClear.Add_Click({
$script:_XferCsvEntries.Clear()
$lblXferCsvInfo.Text = ""
$btnXferCsvClear.Visible = $false
$txtXferSrcSite.Enabled = $true; $txtXferSrcLib.Enabled = $true
$txtXferDstSite.Enabled = $true; $txtXferDstLib.Enabled = $true
})
# ── Helper: build transfer jobs list (from CSV or manual fields) ──────────────
function Get-XferJobs {
if ($script:_XferCsvEntries.Count -gt 0) {
return @($script:_XferCsvEntries)
}
$srcSite = $txtXferSrcSite.Text.Trim() $srcSite = $txtXferSrcSite.Text.Trim()
$dstSite = $txtXferDstSite.Text.Trim() $dstSite = $txtXferDstSite.Text.Trim()
$srcLib = $txtXferSrcLib.Text.Trim() $srcLib = $txtXferSrcLib.Text.Trim()
$dstLib = $txtXferDstLib.Text.Trim() $dstLib = $txtXferDstLib.Text.Trim()
if (-not $srcSite) { Write-Log "URL du site source requis." "Red"; return @() }
if (-not $clientId) { Write-Log "Client ID requis." "Red"; return } if (-not $dstSite) { Write-Log "URL du site destination requis." "Red"; return @() }
if (-not $srcSite) { Write-Log "URL du site source requis." "Red"; return } if (-not $srcLib) { Write-Log "Bibliotheque source requise." "Red"; return @() }
if (-not $dstSite) { Write-Log "URL du site destination requis." "Red"; return }
if (-not $srcLib) { Write-Log "Bibliotheque source requise." "Red"; return }
if (-not $dstLib) { $dstLib = $srcLib } if (-not $dstLib) { $dstLib = $srcLib }
if ($srcSite -eq $dstSite -and $srcLib -eq $dstLib) { if ($srcSite -eq $dstSite -and $srcLib -eq $dstLib) {
Write-Log "Source et destination identiques." "Red"; return Write-Log "Source et destination identiques." "Red"; return @()
} }
return @(@{ SrcSite = $srcSite; DstSite = $dstSite; SrcLib = $srcLib; DstLib = $dstLib })
}
# ── Helper: export transfer report ────────────────────────────────────────────
function Export-XferReport([System.Collections.Generic.List[object]]$Results, [string]$OutDir, [string]$Prefix, [bool]$AsHtml) {
if ($Results.Count -eq 0) { return $null }
if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
if (-not $AsHtml) {
$f = Join-Path $OutDir "${Prefix}_$stamp.csv"
$Results | Export-Csv -Path $f -NoTypeInformation -Encoding UTF8
return $f
}
# HTML report
$f = Join-Path $OutDir "${Prefix}_$stamp.html"
$okN = @($Results | Where-Object { $_.Status -eq "OK" }).Count
$errN = @($Results | Where-Object { $_.Status -eq "ERROR" }).Count
$skipN = @($Results | Where-Object { $_.Status -eq "SKIPPED" }).Count
$missN = @($Results | Where-Object { $_.Status -eq "MISSING" }).Count
$mmN = @($Results | Where-Object { $_.Status -eq "SIZE_MISMATCH" }).Count
$exN = @($Results | Where-Object { $_.Status -eq "EXTRA" }).Count
$rows = ""
foreach ($r in $Results) {
$color = switch ($r.Status) {
"OK" { "#e6f4ea" }
"ERROR" { "#fce8e6" }
"SKIPPED" { "#fff3cd" }
"MISSING" { "#fce8e6" }
"SIZE_MISMATCH" { "#fff3cd" }
"EXTRA" { "#e8f0fe" }
default { "#ffffff" }
}
$srcSz = if ($null -ne $r.SourceSize) { '{0:N0}' -f $r.SourceSize } else { "-" }
$dstSz = if ($null -ne $r.DestSize) { '{0:N0}' -f $r.DestSize } else { "-" }
$msg = if ($r.Message) { [System.Web.HttpUtility]::HtmlEncode($r.Message) } else { "" }
$rows += "<tr style='background:$color'><td>$([System.Web.HttpUtility]::HtmlEncode($r.SourceSite))</td><td>$([System.Web.HttpUtility]::HtmlEncode($r.File))</td><td><b>$($r.Status)</b></td><td style='text-align:right'>$srcSz</td><td style='text-align:right'>$dstSz</td><td>$msg</td></tr>`n"
}
$html = @"
<!DOCTYPE html><html><head><meta charset='utf-8'><title>Transfer Report</title>
<style>body{font-family:'Segoe UI',sans-serif;margin:20px;background:#f5f5f5}
h1{color:#1e3a5f}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:6px 10px;font-size:13px}
th{background:#1e3a5f;color:#fff;position:sticky;top:0}.summary{display:flex;gap:15px;margin:12px 0}
.badge{padding:4px 12px;border-radius:4px;font-weight:bold;font-size:13px}
.ok{background:#e6f4ea;color:#1e7e34}.err{background:#fce8e6;color:#c62828}
.warn{background:#fff3cd;color:#856404}.info{background:#e8f0fe;color:#1565c0}
input[type=text]{padding:6px;margin:8px 0;width:300px;border:1px solid #ccc;border-radius:4px}</style>
<script>function filterTable(){var v=document.getElementById('q').value.toLowerCase();var rows=document.querySelectorAll('tbody tr');
rows.forEach(function(r){r.style.display=r.textContent.toLowerCase().indexOf(v)>-1?'':'none'})}</script>
</head><body><h1>Transfer Report</h1>
<div class='summary'>
<span class='badge ok'>OK: $okN</span><span class='badge err'>Error: $errN</span>
<span class='badge warn'>Skipped: $skipN / Mismatch: $mmN</span><span class='badge err'>Missing: $missN</span>
<span class='badge info'>Extra: $exN</span></div>
<input type='text' id='q' onkeyup='filterTable()' placeholder='Filter...'>
<table><thead><tr><th>Site</th><th>File</th><th>Status</th><th>Source Size</th><th>Dest Size</th><th>Message</th></tr></thead>
<tbody>$rows</tbody></table></body></html>
"@
$html | Set-Content -Path $f -Encoding UTF8
return $f
}
# ── Transfer Start ────────────────────────────────────────────────────────────
$btnXferStart.Add_Click({
$clientId = $txtClientId.Text.Trim()
if (-not $clientId) { Write-Log "Client ID requis." "Red"; return }
$jobs = Get-XferJobs
if ($jobs.Count -eq 0) { return }
$recursive = $chkXferRecursive.Checked
$overwrite = $chkXferOverwrite.Checked
$createFolder = $chkXferCreateFolders.Checked
$outDir = $txtOutput.Text.Trim()
if (-not $outDir) { $outDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path } }
$asHtml = $radXferHtml.Checked
$params = @{ $params = @{
ClientId = $clientId ClientId = $clientId
SrcSite = $srcSite Jobs = @($jobs)
DstSite = $dstSite Recursive = $recursive
SrcLib = $srcLib Overwrite = $overwrite
DstLib = $dstLib CreateFolders = $createFolder
Recursive = $chkXferRecursive.Checked OutFolder = $outDir
Overwrite = $chkXferOverwrite.Checked AsHtml = $asHtml
} }
$btnXferStart.Enabled = $false $btnXferStart.Enabled = $false
@@ -4996,10 +5160,11 @@ $btnXferStart.Add_Click({
$btnXferOpen.Enabled = $false $btnXferOpen.Enabled = $false
$txtLog.Clear() $txtLog.Clear()
Start-ProgressAnim Start-ProgressAnim
Write-Log "=== TRANSFER ===" "White" Write-Log "=== TRANSFER ($($jobs.Count) job(s)) ===" "White"
Write-Log "Source : $srcSite / $srcLib" "Gray" foreach ($j in $jobs) {
Write-Log "Destination : $dstSite / $dstLib" "Gray" Write-Log " $($j.SrcSite)/$($j.SrcLib) -> $($j.DstSite)/$($j.DstLib)" "Gray"
Write-Log "Recursive : $($params.Recursive) Overwrite: $($params.Overwrite)" "Gray" }
Write-Log "Recursive: $recursive Overwrite: $overwrite Create folders: $createFolder" "Gray"
Write-Log ("-" * 52) "DarkGray" Write-Log ("-" * 52) "DarkGray"
$bgTransfer = { $bgTransfer = {
@@ -5008,7 +5173,7 @@ $btnXferStart.Add_Click({
$Sync.Queue.Enqueue(@{ Text = $m; Color = $c }) $Sync.Queue.Enqueue(@{ Text = $m; Color = $c })
} }
function Get-AllSPFiles([string]$BasePath, [string]$Rel = "") { function Get-AllSPFiles([string]$BasePath, [bool]$Recurse, [string]$Rel = "") {
$files = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType File -ErrorAction SilentlyContinue) $files = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType File -ErrorAction SilentlyContinue)
foreach ($f in $files) { foreach ($f in $files) {
if ($f.ServerRelativeUrl -match '/_vti_history/') { continue } if ($f.ServerRelativeUrl -match '/_vti_history/') { continue }
@@ -5020,74 +5185,116 @@ $btnXferStart.Add_Click({
RelativeFolder = $Rel RelativeFolder = $Rel
} }
} }
if ($Params.Recursive) { if ($Recurse) {
$folders = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType Folder -ErrorAction SilentlyContinue) $folders = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType Folder -ErrorAction SilentlyContinue)
foreach ($d in $folders) { foreach ($d in $folders) {
if ($d.Name -in @("Forms", "_vti_cnf", "_vti_history")) { continue } if ($d.Name -in @("Forms", "_vti_cnf", "_vti_history")) { continue }
Get-AllSPFiles "$BasePath/$($d.Name)" "$Rel$($d.Name)/" Get-AllSPFiles "$BasePath/$($d.Name)" $Recurse "$Rel$($d.Name)/"
} }
} }
} }
try { try {
Import-Module PnP.PowerShell -ErrorAction Stop Import-Module PnP.PowerShell -ErrorAction Stop
$report = [System.Collections.Generic.List[object]]::new()
$totalOk = 0; $totalErr = 0; $totalSkip = 0
# ── Enumerate source ── foreach ($job in $Params.Jobs) {
BgLog "Connexion au site source..." "Cyan" BgLog "--- $($job.SrcSite) / $($job.SrcLib) -> $($job.DstSite) / $($job.DstLib) ---" "White"
Connect-PnPOnline -Url $Params.SrcSite -Interactive -ClientId $Params.ClientId
BgLog "Enumeration des fichiers source ($($Params.SrcLib))..." "Cyan" # Enumerate source
$srcFiles = @(Get-AllSPFiles $Params.SrcLib) BgLog "Connecting to source..." "Cyan"
BgLog " $($srcFiles.Count) fichier(s) trouves" "LightGreen" Connect-PnPOnline -Url $job.SrcSite -Interactive -ClientId $Params.ClientId
$srcFiles = @(Get-AllSPFiles $job.SrcLib $Params.Recursive)
BgLog " $($srcFiles.Count) file(s) found" "LightGreen"
if ($srcFiles.Count -eq 0) { if ($srcFiles.Count -eq 0) {
BgLog "Aucun fichier a transferer." "Orange" BgLog " No files to transfer." "DarkOrange"
$Sync.TransferCount = 0 continue
return
} }
# ── Download to temp ── # Download to temp
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "SPToolbox_Transfer_$(Get-Date -Format 'yyyyMMdd_HHmmss')" $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "SPToolbox_Xfer_$(Get-Date -Format 'yyyyMMdd_HHmmss')_$([guid]::NewGuid().ToString('N').Substring(0,6))"
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
BgLog "Dossier temporaire : $tempRoot" "DarkGray"
$idx = 0 $idx = 0
foreach ($f in $srcFiles) { foreach ($f in $srcFiles) {
$idx++ $idx++
$localDir = Join-Path $tempRoot $f.RelativeFolder $localDir = Join-Path $tempRoot $f.RelativeFolder
if (-not (Test-Path $localDir)) { if (-not (Test-Path $localDir)) { New-Item -ItemType Directory -Path $localDir -Force | Out-Null }
New-Item -ItemType Directory -Path $localDir -Force | Out-Null try {
}
Get-PnPFile -Url $f.ServerRelativeUrl -Path $localDir -FileName $f.Name -AsFile -Force Get-PnPFile -Url $f.ServerRelativeUrl -Path $localDir -FileName $f.Name -AsFile -Force
BgLog " [$idx/$($srcFiles.Count)] Downloaded: $($f.RelativePath) ($('{0:N0}' -f $f.Length) bytes)" "LightGreen" } catch {
BgLog " ERROR downloading $($f.RelativePath): $($_.Exception.Message)" "Red"
$report.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$f.RelativePath; Status="ERROR"; SourceSize=$f.Length; DestSize=$null; Message="Download: $($_.Exception.Message)" })
$totalErr++
continue
}
} }
BgLog "Download termine." "White"
# ── Upload to destination ── # Upload to destination
BgLog "Connexion au site destination..." "Cyan" BgLog "Connecting to destination..." "Cyan"
Connect-PnPOnline -Url $Params.DstSite -Interactive -ClientId $Params.ClientId Connect-PnPOnline -Url $job.DstSite -Interactive -ClientId $Params.ClientId
$idx = 0 $idx = 0
foreach ($f in $srcFiles) { foreach ($f in $srcFiles) {
$idx++ $idx++
$dstFolder = if ($f.RelativeFolder) { $dstFolder = if ($f.RelativeFolder) {
"$($Params.DstLib.TrimEnd('/'))/$($f.RelativeFolder.TrimEnd('/'))" "$($job.DstLib.TrimEnd('/'))/$($f.RelativeFolder.TrimEnd('/'))"
} else { $Params.DstLib } } else { $job.DstLib }
Resolve-PnPFolder -SiteRelativePath $dstFolder -ErrorAction SilentlyContinue | Out-Null
$localFile = Join-Path (Join-Path $tempRoot $f.RelativeFolder) $f.Name # Check if dest folder exists / create if needed
Add-PnPFile -Path $localFile -Folder $dstFolder -ErrorAction Stop | Out-Null if ($Params.CreateFolders) {
BgLog " [$idx/$($srcFiles.Count)] Uploaded: $($f.RelativePath)" "LightGreen" try { Resolve-PnPFolder -SiteRelativePath $dstFolder -ErrorAction Stop | Out-Null } catch {
BgLog " ERROR creating folder $dstFolder : $($_.Exception.Message)" "Red"
$report.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$f.RelativePath; Status="ERROR"; SourceSize=$f.Length; DestSize=$null; Message="Folder creation: $($_.Exception.Message)" })
$totalErr++
continue
} }
BgLog "Upload termine." "White" }
$Sync.TransferCount = $srcFiles.Count
$localFile = Join-Path (Join-Path $tempRoot $f.RelativeFolder) $f.Name
if (-not (Test-Path $localFile)) { continue }
# Check for existing file if not overwriting
if (-not $Params.Overwrite) {
try {
$existing = Get-PnPFile -Url "$dstFolder/$($f.Name)" -ErrorAction SilentlyContinue
if ($existing) {
BgLog " [$idx/$($srcFiles.Count)] SKIPPED (exists): $($f.RelativePath)" "DarkOrange"
$report.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$f.RelativePath; Status="SKIPPED"; SourceSize=$f.Length; DestSize=$null; Message="File already exists" })
$totalSkip++
continue
}
} catch {}
}
try {
Add-PnPFile -Path $localFile -Folder $dstFolder -ErrorAction Stop | Out-Null
BgLog " [$idx/$($srcFiles.Count)] OK: $($f.RelativePath)" "LightGreen"
$report.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$f.RelativePath; Status="OK"; SourceSize=$f.Length; DestSize=$f.Length; Message="" })
$totalOk++
} catch {
BgLog " [$idx/$($srcFiles.Count)] ERROR: $($f.RelativePath) - $($_.Exception.Message)" "Red"
$report.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$f.RelativePath; Status="ERROR"; SourceSize=$f.Length; DestSize=$null; Message=$_.Exception.Message })
$totalErr++
}
}
# Cleanup temp
if ($tempRoot -and (Test-Path $tempRoot)) {
Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
$Sync.Report = @($report)
$Sync.TotalOk = $totalOk
$Sync.TotalErr = $totalErr
$Sync.TotalSkip = $totalSkip
} catch { } catch {
$Sync.Error = $_.Exception.Message $Sync.Error = $_.Exception.Message
BgLog "Erreur : $($_.Exception.Message)" "Red" BgLog "Erreur : $($_.Exception.Message)" "Red"
} finally { } finally {
# Cleanup temp
if ($tempRoot -and (Test-Path $tempRoot)) {
Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
BgLog "Fichiers temporaires nettoyes." "DarkGray"
}
$Sync.Done = $true $Sync.Done = $true
} }
} }
@@ -5096,7 +5303,10 @@ $btnXferStart.Add_Click({
Queue = [System.Collections.Generic.Queue[object]]::new() Queue = [System.Collections.Generic.Queue[object]]::new()
Done = $false Done = $false
Error = $null Error = $null
TransferCount = 0 Report = $null
TotalOk = 0
TotalErr = 0
TotalSkip = 0
}) })
$script:_XferSync = $sync $script:_XferSync = $sync
$script:_XferParams = $params $script:_XferParams = $params
@@ -5136,8 +5346,20 @@ $btnXferStart.Add_Click({
return return
} }
$cnt = $script:_XferSync.TransferCount $ok = $script:_XferSync.TotalOk; $er = $script:_XferSync.TotalErr; $sk = $script:_XferSync.TotalSkip
Write-Log "=== TRANSFERT TERMINE : $cnt fichier(s) ===" "White" Write-Log "=== TRANSFER COMPLETE: $ok OK, $er error(s), $sk skipped ===" "White"
# Generate report
$report = $script:_XferSync.Report
if ($report -and $report.Count -gt 0) {
$p = $script:_XferParams
$rptFile = Export-XferReport ([System.Collections.Generic.List[object]]$report) $p.OutFolder "Transfer" $p.AsHtml
if ($rptFile) {
Write-Log "Report: $rptFile" "White"
$script:_XferLastReport = $rptFile
$btnXferOpen.Enabled = $true
}
}
} }
}) })
$tmr.Start() $tmr.Start()
@@ -5147,27 +5369,22 @@ $btnXferStart.Add_Click({
$btnXferVerify.Add_Click({ $btnXferVerify.Add_Click({
$clientId = $txtClientId.Text.Trim() $clientId = $txtClientId.Text.Trim()
$srcSite = $txtXferSrcSite.Text.Trim()
$dstSite = $txtXferDstSite.Text.Trim()
$srcLib = $txtXferSrcLib.Text.Trim()
$dstLib = $txtXferDstLib.Text.Trim()
$outDir = $txtOutput.Text.Trim()
if (-not $clientId) { Write-Log "Client ID requis." "Red"; return } if (-not $clientId) { Write-Log "Client ID requis." "Red"; return }
if (-not $srcSite) { Write-Log "URL du site source requis." "Red"; return }
if (-not $dstSite) { Write-Log "URL du site destination requis." "Red"; return } $jobs = Get-XferJobs
if (-not $srcLib) { Write-Log "Bibliotheque source requise." "Red"; return } if ($jobs.Count -eq 0) { return }
if (-not $dstLib) { $dstLib = $srcLib }
$recursive = $chkXferRecursive.Checked
$outDir = $txtOutput.Text.Trim()
if (-not $outDir) { $outDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path } } if (-not $outDir) { $outDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path } }
$asHtml = $radXferHtml.Checked
$params = @{ $params = @{
ClientId = $clientId ClientId = $clientId
SrcSite = $srcSite Jobs = @($jobs)
DstSite = $dstSite Recursive = $recursive
SrcLib = $srcLib
DstLib = $dstLib
Recursive = $chkXferRecursive.Checked
OutFolder = $outDir OutFolder = $outDir
AsHtml = $asHtml
} }
$btnXferStart.Enabled = $false $btnXferStart.Enabled = $false
@@ -5175,9 +5392,7 @@ $btnXferVerify.Add_Click({
$btnXferOpen.Enabled = $false $btnXferOpen.Enabled = $false
$txtLog.Clear() $txtLog.Clear()
Start-ProgressAnim Start-ProgressAnim
Write-Log "=== VERIFICATION ===" "White" Write-Log "=== VERIFICATION ($($jobs.Count) job(s)) ===" "White"
Write-Log "Source : $srcSite / $srcLib" "Gray"
Write-Log "Destination : $dstSite / $dstLib" "Gray"
Write-Log ("-" * 52) "DarkGray" Write-Log ("-" * 52) "DarkGray"
$bgVerify = { $bgVerify = {
@@ -5186,7 +5401,7 @@ $btnXferVerify.Add_Click({
$Sync.Queue.Enqueue(@{ Text = $m; Color = $c }) $Sync.Queue.Enqueue(@{ Text = $m; Color = $c })
} }
function Get-AllSPFiles([string]$BasePath, [string]$Rel = "") { function Get-AllSPFiles([string]$BasePath, [bool]$Recurse, [string]$Rel = "") {
$files = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType File -ErrorAction SilentlyContinue) $files = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType File -ErrorAction SilentlyContinue)
foreach ($f in $files) { foreach ($f in $files) {
if ($f.ServerRelativeUrl -match '/_vti_history/') { continue } if ($f.ServerRelativeUrl -match '/_vti_history/') { continue }
@@ -5196,94 +5411,59 @@ $btnXferVerify.Add_Click({
RelativePath = "$Rel$($f.Name)" RelativePath = "$Rel$($f.Name)"
} }
} }
if ($Params.Recursive) { if ($Recurse) {
$folders = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType Folder -ErrorAction SilentlyContinue) $folders = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType Folder -ErrorAction SilentlyContinue)
foreach ($d in $folders) { foreach ($d in $folders) {
if ($d.Name -in @("Forms", "_vti_cnf", "_vti_history")) { continue } if ($d.Name -in @("Forms", "_vti_cnf", "_vti_history")) { continue }
Get-AllSPFiles "$BasePath/$($d.Name)" "$Rel$($d.Name)/" Get-AllSPFiles "$BasePath/$($d.Name)" $Recurse "$Rel$($d.Name)/"
} }
} }
} }
try { try {
Import-Module PnP.PowerShell -ErrorAction Stop Import-Module PnP.PowerShell -ErrorAction Stop
$allResults = [System.Collections.Generic.List[object]]::new()
# Enumerate source foreach ($job in $Params.Jobs) {
BgLog "Connexion au site source..." "Cyan" BgLog "--- Verifying $($job.SrcSite)/$($job.SrcLib) vs $($job.DstSite)/$($job.DstLib) ---" "White"
Connect-PnPOnline -Url $Params.SrcSite -Interactive -ClientId $Params.ClientId
BgLog "Enumeration des fichiers source..." "Cyan"
$srcFiles = @(Get-AllSPFiles $Params.SrcLib)
BgLog " $($srcFiles.Count) fichier(s) source" "LightGreen"
$srcMap = @{} BgLog "Connecting to source..." "Cyan"
foreach ($f in $srcFiles) { $srcMap[$f.RelativePath] = $f } Connect-PnPOnline -Url $job.SrcSite -Interactive -ClientId $Params.ClientId
$srcFiles = @(Get-AllSPFiles $job.SrcLib $Params.Recursive)
BgLog " $($srcFiles.Count) source file(s)" "LightGreen"
$srcMap = @{}; foreach ($f in $srcFiles) { $srcMap[$f.RelativePath] = $f }
# Enumerate destination BgLog "Connecting to destination..." "Cyan"
BgLog "Connexion au site destination..." "Cyan" Connect-PnPOnline -Url $job.DstSite -Interactive -ClientId $Params.ClientId
Connect-PnPOnline -Url $Params.DstSite -Interactive -ClientId $Params.ClientId $dstFiles = @(Get-AllSPFiles $job.DstLib $Params.Recursive)
BgLog "Enumeration des fichiers destination..." "Cyan" BgLog " $($dstFiles.Count) destination file(s)" "LightGreen"
$dstFiles = @(Get-AllSPFiles $Params.DstLib) $dstMap = @{}; foreach ($f in $dstFiles) { $dstMap[$f.RelativePath] = $f }
BgLog " $($dstFiles.Count) fichier(s) destination" "LightGreen"
$dstMap = @{}
foreach ($f in $dstFiles) { $dstMap[$f.RelativePath] = $f }
# Compare
BgLog "Comparaison en cours..." "Cyan"
$results = [System.Collections.Generic.List[object]]::new()
foreach ($key in $srcMap.Keys) { foreach ($key in $srcMap.Keys) {
$src = $srcMap[$key] $src = $srcMap[$key]
if ($dstMap.ContainsKey($key)) { if ($dstMap.ContainsKey($key)) {
$dst = $dstMap[$key] $dst = $dstMap[$key]
if ([long]$src.Length -eq [long]$dst.Length) { $st = if ([long]$src.Length -eq [long]$dst.Length) { "OK" } else { "SIZE_MISMATCH" }
$results.Add([PSCustomObject]@{ $allResults.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$key; Status=$st; SourceSize=[long]$src.Length; DestSize=[long]$dst.Length; Message="" })
Name = $src.Name
RelativePath = $key
Status = "OK"
SourceSize = [long]$src.Length
DestSize = [long]$dst.Length
})
} else { } else {
$results.Add([PSCustomObject]@{ $allResults.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$key; Status="MISSING"; SourceSize=[long]$src.Length; DestSize=$null; Message="" })
Name = $src.Name
RelativePath = $key
Status = "SIZE_MISMATCH"
SourceSize = [long]$src.Length
DestSize = [long]$dst.Length
})
}
} else {
$results.Add([PSCustomObject]@{
Name = $src.Name
RelativePath = $key
Status = "MISSING"
SourceSize = [long]$src.Length
DestSize = $null
})
} }
} }
foreach ($key in $dstMap.Keys) { foreach ($key in $dstMap.Keys) {
if (-not $srcMap.ContainsKey($key)) { if (-not $srcMap.ContainsKey($key)) {
$dst = $dstMap[$key] $dst = $dstMap[$key]
$results.Add([PSCustomObject]@{ $allResults.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$key; Status="EXTRA"; SourceSize=$null; DestSize=[long]$dst.Length; Message="" })
Name = $dst.Name
RelativePath = $key
Status = "EXTRA"
SourceSize = $null
DestSize = [long]$dst.Length
})
} }
} }
$okN = @($results | Where-Object { $_.Status -eq "OK" }).Count $okN = @($allResults | Where-Object { $_.Status -eq "OK" }).Count
$missN = @($results | Where-Object { $_.Status -eq "MISSING" }).Count $missN = @($allResults | Where-Object { $_.Status -eq "MISSING" }).Count
$mmN = @($results | Where-Object { $_.Status -eq "SIZE_MISMATCH" }).Count $mmN = @($allResults | Where-Object { $_.Status -eq "SIZE_MISMATCH" }).Count
$exN = @($results | Where-Object { $_.Status -eq "EXTRA" }).Count $exN = @($allResults | Where-Object { $_.Status -eq "EXTRA" }).Count
BgLog "Resultats : $okN OK, $missN manquant(s), $mmN taille(s) differente(s), $exN extra" "White" BgLog " Results: $okN OK, $missN missing, $mmN size mismatch, $exN extra" "White"
}
$Sync.VerifyResults = @($results) $Sync.VerifyResults = @($allResults)
} catch { } catch {
$Sync.Error = $_.Exception.Message $Sync.Error = $_.Exception.Message
BgLog "Erreur : $($_.Exception.Message)" "Red" BgLog "Erreur : $($_.Exception.Message)" "Red"
@@ -5343,20 +5523,13 @@ $btnXferVerify.Add_Click({
} }
$p = $script:_VerParams $p = $script:_VerParams
$stamp = Get-Date -Format "yyyyMMdd_HHmmss" $rptFile = Export-XferReport ([System.Collections.Generic.List[object]]$results) $p.OutFolder "TransferVerify" $p.AsHtml
$outDir = $p.OutFolder if ($rptFile) {
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null } Write-Log "Report: $rptFile" "White"
$script:_XferLastReport = $rptFile
$outFile = Join-Path $outDir "TransferVerify_$stamp.html"
$html = Export-TransferVerifyToHTML -Results $results `
-SrcSite $p.SrcSite -SrcLib $p.SrcLib `
-DstSite $p.DstSite -DstLib $p.DstLib
$html | Set-Content -Path $outFile -Encoding UTF8
Write-Log "Rapport : $outFile" "White"
$script:_XferLastReport = $outFile
$btnXferOpen.Enabled = $true $btnXferOpen.Enabled = $true
} }
}
}) })
$tmr.Start() $tmr.Start()
}) })

View File

@@ -0,0 +1,4 @@
SourceSite;SourceLibrary;DestSite;DestLibrary
https://contoso.sharepoint.com/sites/ProjectA;Shared Documents;https://contoso.sharepoint.com/sites/Archive;Shared Documents/ProjectA
https://contoso.sharepoint.com/sites/ProjectB;Shared Documents/Reports;https://contoso.sharepoint.com/sites/Archive;Shared Documents/ProjectB/Reports
https://contoso.sharepoint.com/sites/HR;Documents RH;https://contoso.sharepoint.com/sites/HR-Backup;Documents RH
1 SourceSite SourceLibrary DestSite DestLibrary
2 https://contoso.sharepoint.com/sites/ProjectA Shared Documents https://contoso.sharepoint.com/sites/Archive Shared Documents/ProjectA
3 https://contoso.sharepoint.com/sites/ProjectB Shared Documents/Reports https://contoso.sharepoint.com/sites/Archive Shared Documents/ProjectB/Reports
4 https://contoso.sharepoint.com/sites/HR Documents RH https://contoso.sharepoint.com/sites/HR-Backup Documents RH

View File

@@ -106,6 +106,11 @@
"ph.xfer.site": "https://tenant.sharepoint.com/sites/xxx", "ph.xfer.site": "https://tenant.sharepoint.com/sites/xxx",
"ph.xfer.library": "Documents partagés/sous-dossier", "ph.xfer.library": "Documents partagés/sous-dossier",
"xfer.note": "Seule la version actuelle de chaque fichier est transférée (pas d'historique de versions).", "xfer.note": "Seule la version actuelle de chaque fichier est transférée (pas d'historique de versions).",
"chk.xfer.create.folders": "Créer les dossiers manquants",
"btn.xfer.csv": "Importer CSV...",
"btn.xfer.csv.clear": "Effacer",
"lbl.xfer.csv.info": "{0} transfert(s) chargé(s)",
"lbl.xfer.report.fmt": "Rapport :",
"tab.bulk": " Création en masse ", "tab.bulk": " Création en masse ",
"grp.bulk.list": "Sites à créer", "grp.bulk.list": "Sites à créer",