7 Commits

Author SHA1 Message Date
10bfe6debc Merge branch 'main' of https://git.azuze.fr/kawa/Sharepoint-Toolbox
All checks were successful
Release zip package / release (push) Successful in 1s
2026-04-01 17:12:30 +02:00
945a4e110d Update TODO.md 2026-04-01 17:12:24 +02:00
109d0d5f1e Update TODO.md 2026-03-27 09:57:13 +01:00
b4f0fecad2 Merge branch 'main' of https://git.azuze.fr/kawa/Sharepoint-Toolbox
All checks were successful
Release zip package / release (push) Successful in 1s
2026-03-27 09:54:11 +01:00
903fa17f8a Updated workflow to include CSV examples folder 2026-03-27 09:54:01 +01:00
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
5 changed files with 553 additions and 214 deletions

View File

@@ -24,7 +24,7 @@ jobs:
cd repo
VERSION="${{ gitea.ref_name }}"
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 "VERSION=${VERSION}" >> "$GITHUB_ENV"
@@ -34,7 +34,7 @@ jobs:
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-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'])")
echo "RELEASE_ID=${RELEASE_ID}" >> "$GITHUB_ENV"

View File

@@ -29,7 +29,8 @@ function Format-Bytes([long]$b) {
function Validate-Inputs {
if ([string]::IsNullOrWhiteSpace($script:txtClientId.Text)) {
[System.Windows.Forms.MessageBox]::Show("Please enter a Client ID.", "Missing Field", "OK", "Warning")
$msg = T "validate.missing.clientid.hint"
[System.Windows.Forms.MessageBox]::Show($msg, (T "validate.missing.title"), "OK", "Warning")
return $false
}
$hasSites = ($script:SelectedSites -and $script:SelectedSites.Count -gt 0)
@@ -2823,6 +2824,11 @@ $script:LangDefault = @{
"ph.xfer.site" = "https://tenant.sharepoint.com/sites/xxx"
"ph.xfer.library" = "Shared Documents/subfolder"
"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 "
"grp.bulk.list" = "Sites to create"
"btn.bulk.add" = "Add Site..."
@@ -2880,6 +2886,21 @@ $script:LangDefault = @{
"chk.ver.dryrun" = "Dry run (preview only, no deletion)"
"btn.ver.run" = "Clean Versions"
"btn.ver.open" = "Open Report"
"btn.register.app" = "Register"
"reg.title" = "App Registration"
"reg.offer" = "No Client ID provided. Register a new app on this tenant?"
"reg.confirm" = "Register 'SharePoint Toolbox' app on tenant {0}?"
"reg.in.progress" = "Registering..."
"reg.success" = "App registered successfully!`nClient ID: {0}`nYou can save this profile to reuse it."
"reg.err.tenant" = "Cannot determine tenant from the provided URL."
"reg.err.nocmd" = "PnP.PowerShell module does not support app registration. Please register the app manually in Entra ID."
"reg.err.no.id" = "Registration completed but no Client ID was returned."
"reg.err.failed" = "Registration failed:`n{0}"
"reg.err.no.tenant" = "Please enter a Tenant URL first."
"reg.err.nopwsh" = "PowerShell 7+ (pwsh) is required for app registration but was not found. Install it from https://aka.ms/powershell"
"validate.missing.clientid" = "Please enter a Client ID."
"validate.missing.clientid.hint" = "Please enter a Client ID or use the 'Register' button to create one."
"validate.missing.title" = "Missing Field"
}
$script:Lang = $null # null = use LangDefault
@@ -3055,9 +3076,18 @@ $btnBrowseSites.Size = New-Object System.Drawing.Size(92, 26)
$lblClientId = (& $lbl (T "client.id") 20 108)
$txtClientId = New-Object System.Windows.Forms.TextBox
$txtClientId.Location = New-Object System.Drawing.Point(140, 108)
$txtClientId.Size = New-Object System.Drawing.Size(500, 22)
$txtClientId.Size = New-Object System.Drawing.Size(400, 22)
$txtClientId.Font = New-Object System.Drawing.Font("Consolas", 9)
$btnRegisterApp = New-Object System.Windows.Forms.Button
$btnRegisterApp.Text = T "btn.register.app"
$btnRegisterApp.Location = New-Object System.Drawing.Point(548, 106)
$btnRegisterApp.Size = New-Object System.Drawing.Size(92, 26)
$txtClientId.Add_TextChanged({
$btnRegisterApp.Enabled = [string]::IsNullOrWhiteSpace($txtClientId.Text)
})
$lblSiteURL = (& $lbl (T "site.url") 20 140)
$txtSiteURL = New-Object System.Windows.Forms.TextBox
$txtSiteURL.Location = New-Object System.Drawing.Point(140, 140)
@@ -3546,27 +3576,56 @@ $txtXferDstLib.PlaceholderText = T "ph.xfer.library"
$grpXferDst.Controls.AddRange(@($lblXferDstSite, $txtXferDstSite, $lblXferDstLib, $txtXferDstLib))
# ── GroupBox: Options (y=160, h=58) ──────────────────────────────────────────
$grpXferOpts = New-Group (T "grp.xfer.options") 10 160 620 58
# ── GroupBox: Options (y=160, h=96) ──────────────────────────────────────────
$grpXferOpts = New-Group (T "grp.xfer.options") 10 160 620 96
$chkXferRecursive = New-Check (T "chk.xfer.recursive") 10 18 260 $true
$chkXferOverwrite = New-Check (T "chk.xfer.overwrite") 280 18 230
$chkXferRecursive = New-Check (T "chk.xfer.recursive") 10 18 250 $true
$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.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.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Italic)
$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) ──────────────────────────────────────────────────────────
$btnXferStart = New-ActionBtn (T "btn.xfer.start") 10 224 ([System.Drawing.Color]::FromArgb(0, 120, 60))
# ── Buttons (y=260) ──────────────────────────────────────────────────────────
$btnXferStart = New-ActionBtn (T "btn.xfer.start") 10 260 ([System.Drawing.Color]::FromArgb(0, 120, 60))
$btnXferVerify = New-Object System.Windows.Forms.Button
$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.BackColor = [System.Drawing.Color]::FromArgb(0, 120, 212)
$btnXferVerify.ForeColor = [System.Drawing.Color]::White
@@ -3575,7 +3634,7 @@ $btnXferVerify.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Dr
$btnXferOpen = New-Object System.Windows.Forms.Button
$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.Enabled = $false
@@ -3832,7 +3891,7 @@ $form.Controls.AddRange(@(
$lblProfile, $cboProfile,
$btnProfileNew, $btnProfileSave, $btnProfileRename, $btnProfileDelete,
$lblTenantUrl, $txtTenantUrl, $btnBrowseSites,
$lblClientId, $txtClientId,
$lblClientId, $txtClientId, $btnRegisterApp,
$lblSiteURL, $txtSiteURL,
$lblOutput, $txtOutput, $btnBrowse,
$sep, $tabs,
@@ -3857,6 +3916,7 @@ $_reg = {
& $_reg $script:i18nMap $btnProfileRename "btn.rename"
& $_reg $script:i18nMap $btnProfileDelete "btn.delete"
& $_reg $script:i18nMap $btnBrowseSites "btn.view.sites"
& $_reg $script:i18nMap $btnRegisterApp "btn.register.app"
& $_reg $script:i18nMap $lblTenantUrl "tenant.url"
& $_reg $script:i18nMap $lblClientId "client.id"
& $_reg $script:i18nMap $lblSiteURL "site.url"
@@ -3936,11 +3996,15 @@ $_reg = {
& $_reg $script:i18nMap $lblXferDstLib "lbl.xfer.library"
& $_reg $script:i18nMap $grpXferOpts "grp.xfer.options"
& $_reg $script:i18nMap $chkXferRecursive "chk.xfer.recursive"
& $_reg $script:i18nMap $chkXferOverwrite "chk.xfer.overwrite"
& $_reg $script:i18nMap $lblXferNote "xfer.note"
& $_reg $script:i18nMap $btnXferStart "btn.xfer.start"
& $_reg $script:i18nMap $btnXferVerify "btn.xfer.verify"
& $_reg $script:i18nMap $btnXferOpen "btn.xfer.open"
& $_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 $btnXferStart "btn.xfer.start"
& $_reg $script:i18nMap $btnXferVerify "btn.xfer.verify"
& $_reg $script:i18nMap $btnXferOpen "btn.xfer.open"
# Bulk Create tab controls
& $_reg $script:i18nMap $grpBulkList "grp.bulk.list"
@@ -4118,6 +4182,126 @@ foreach ($mi in @($menuLang.DropDownItems | Where-Object { $_ -is [System.Window
$mi.Add_Click({ Switch-AppLanguage $args[0].Tag })
}
$btnRegisterApp.Add_Click({
$tenantUrl = $txtTenantUrl.Text.Trim()
if ([string]::IsNullOrWhiteSpace($tenantUrl)) {
[System.Windows.Forms.MessageBox]::Show(
(T "reg.err.no.tenant"), (T "reg.title"), "OK", "Warning")
return
}
$confirm = [System.Windows.Forms.MessageBox]::Show(
((T "reg.confirm") -f $tenantUrl),
(T "reg.title"), "YesNo", "Question")
if ($confirm -ne "Yes") { return }
# ── Derive tenant identifier ──────────────────────────────────────────────
if ($tenantUrl -match 'https://([^.]+)\.sharepoint\.com') {
$tenantId = "$($Matches[1]).onmicrosoft.com"
} else {
[System.Windows.Forms.MessageBox]::Show(
(T "reg.err.tenant"), (T "reg.title"), "OK", "Error")
return
}
$btnRegisterApp.Enabled = $false
$btnRegisterApp.Text = T "reg.in.progress"
Write-Log "Registering app on $tenantId ..."
# ── Write a temp script and launch a real PowerShell console ──────────────
$resultFile = Join-Path ([System.IO.Path]::GetTempPath()) "SPToolbox_RegResult.json"
if (Test-Path $resultFile) { Remove-Item $resultFile -Force }
$script:_regResultFile = $resultFile
$scriptContent = @"
`$Host.UI.RawUI.WindowTitle = "SharePoint Toolbox - App Registration"
try {
Import-Module PnP.PowerShell -ErrorAction Stop
} catch {
Write-Host "ERROR: PnP.PowerShell module not found." -ForegroundColor Red
Write-Host `$_.Exception.Message -ForegroundColor Red
@{ Error = `$_.Exception.Message } | ConvertTo-Json | Set-Content -Path "$resultFile" -Encoding UTF8
Read-Host "Press Enter to close"
exit
}
Write-Host "Registering app on $tenantId ..." -ForegroundColor Cyan
Write-Host "A browser window will open for authentication." -ForegroundColor Yellow
Write-Host ""
try {
`$result = Register-PnPEntraIDAppForInteractiveLogin ``
-ApplicationName "SharePoint Toolbox" ``
-Tenant "$tenantId"
`$clientId = `$result.'AzureAppId/ClientId'
if (`$clientId) {
Write-Host "Success! Client ID: `$clientId" -ForegroundColor Green
} else {
Write-Host "WARNING: No Client ID returned." -ForegroundColor Yellow
}
@{ ClientId = `$clientId } | ConvertTo-Json | Set-Content -Path "$resultFile" -Encoding UTF8
} catch {
Write-Host "ERROR: `$(`$_.Exception.Message)" -ForegroundColor Red
@{ Error = `$_.Exception.Message } | ConvertTo-Json | Set-Content -Path "$resultFile" -Encoding UTF8
}
Read-Host "Press Enter to close"
"@
$scriptFile = Join-Path ([System.IO.Path]::GetTempPath()) "SPToolbox_RegApp.ps1"
$scriptContent | Set-Content -Path $scriptFile -Encoding UTF8
$pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
if (-not $pwshPath) {
$btnRegisterApp.Enabled = [string]::IsNullOrWhiteSpace($txtClientId.Text)
$btnRegisterApp.Text = T "btn.register.app"
Write-Log "PowerShell 7+ (pwsh) not found." "Red"
[System.Windows.Forms.MessageBox]::Show(
(T "reg.err.nopwsh"), (T "reg.title"), "OK", "Error")
return
}
Start-Process $pwshPath -ArgumentList "-ExecutionPolicy Bypass -File `"$scriptFile`""
# ── Timer polls for the result file ──────────────────────────────────────
$tmr = New-Object System.Windows.Forms.Timer
$tmr.Interval = 500
$script:_regTimer = $tmr
$tmr.Add_Tick({
if (Test-Path $script:_regResultFile) {
$script:_regTimer.Stop(); $script:_regTimer.Dispose()
$btnRegisterApp.Text = T "btn.register.app"
try {
$res = Get-Content $script:_regResultFile -Raw | ConvertFrom-Json
Remove-Item $script:_regResultFile -Force -ErrorAction SilentlyContinue
} catch {
Write-Log "Failed to read registration result." "Red"
$btnRegisterApp.Enabled = [string]::IsNullOrWhiteSpace($txtClientId.Text)
return
}
if ($res.Error) {
Write-Log "App registration failed: $($res.Error)" "Red"
$btnRegisterApp.Enabled = $true
[System.Windows.Forms.MessageBox]::Show(
((T "reg.err.failed") -f $res.Error),
(T "reg.title"), "OK", "Error")
} elseif ($res.ClientId) {
$script:txtClientId.Text = $res.ClientId
Write-Log "App registered. Client ID: $($res.ClientId)"
[System.Windows.Forms.MessageBox]::Show(
((T "reg.success") -f $res.ClientId),
(T "reg.title"), "OK", "Information")
} else {
Write-Log "Registration returned no Client ID." "Red"
$btnRegisterApp.Enabled = $true
[System.Windows.Forms.MessageBox]::Show(
(T "reg.err.no.id"), (T "reg.title"), "OK", "Error")
}
} else {
$dot = "." * (([System.DateTime]::Now.Second % 4) + 1)
$btnRegisterApp.Text = (T "reg.in.progress") -replace '\.\.\.$', $dot
}
})
$tmr.Start()
})
$btnBrowseSites.Add_Click({
$tenantUrl = $txtTenantUrl.Text.Trim()
$clientId = $txtClientId.Text.Trim()
@@ -4965,30 +5149,156 @@ $btnOpenDupes.Add_Click({
# ── Transfer ──────────────────────────────────────────────────────────────────
$btnXferStart.Add_Click({
$clientId = $txtClientId.Text.Trim()
$srcSite = $txtXferSrcSite.Text.Trim()
$dstSite = $txtXferDstSite.Text.Trim()
$srcLib = $txtXferSrcLib.Text.Trim()
$dstLib = $txtXferDstLib.Text.Trim()
# ── CSV Import for bulk transfers ─────────────────────────────────────────────
$script:_XferCsvEntries = [System.Collections.Generic.List[object]]::new()
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 }
if (-not $srcLib) { Write-Log "Bibliotheque source requise." "Red"; return }
if (-not $dstLib) { $dstLib = $srcLib }
$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()
$dstSite = $txtXferDstSite.Text.Trim()
$srcLib = $txtXferSrcLib.Text.Trim()
$dstLib = $txtXferDstLib.Text.Trim()
if (-not $srcSite) { Write-Log "URL du site source requis." "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 ($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 = @{
ClientId = $clientId
SrcSite = $srcSite
DstSite = $dstSite
SrcLib = $srcLib
DstLib = $dstLib
Recursive = $chkXferRecursive.Checked
Overwrite = $chkXferOverwrite.Checked
ClientId = $clientId
Jobs = @($jobs)
Recursive = $recursive
Overwrite = $overwrite
CreateFolders = $createFolder
OutFolder = $outDir
AsHtml = $asHtml
}
$btnXferStart.Enabled = $false
@@ -4996,10 +5306,11 @@ $btnXferStart.Add_Click({
$btnXferOpen.Enabled = $false
$txtLog.Clear()
Start-ProgressAnim
Write-Log "=== TRANSFER ===" "White"
Write-Log "Source : $srcSite / $srcLib" "Gray"
Write-Log "Destination : $dstSite / $dstLib" "Gray"
Write-Log "Recursive : $($params.Recursive) Overwrite: $($params.Overwrite)" "Gray"
Write-Log "=== TRANSFER ($($jobs.Count) job(s)) ===" "White"
foreach ($j in $jobs) {
Write-Log " $($j.SrcSite)/$($j.SrcLib) -> $($j.DstSite)/$($j.DstLib)" "Gray"
}
Write-Log "Recursive: $recursive Overwrite: $overwrite Create folders: $createFolder" "Gray"
Write-Log ("-" * 52) "DarkGray"
$bgTransfer = {
@@ -5008,7 +5319,7 @@ $btnXferStart.Add_Click({
$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)
foreach ($f in $files) {
if ($f.ServerRelativeUrl -match '/_vti_history/') { continue }
@@ -5020,83 +5331,128 @@ $btnXferStart.Add_Click({
RelativeFolder = $Rel
}
}
if ($Params.Recursive) {
if ($Recurse) {
$folders = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType Folder -ErrorAction SilentlyContinue)
foreach ($d in $folders) {
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 {
Import-Module PnP.PowerShell -ErrorAction Stop
$report = [System.Collections.Generic.List[object]]::new()
$totalOk = 0; $totalErr = 0; $totalSkip = 0
# ── Enumerate source ──
BgLog "Connexion au site source..." "Cyan"
Connect-PnPOnline -Url $Params.SrcSite -Interactive -ClientId $Params.ClientId
BgLog "Enumeration des fichiers source ($($Params.SrcLib))..." "Cyan"
$srcFiles = @(Get-AllSPFiles $Params.SrcLib)
BgLog " $($srcFiles.Count) fichier(s) trouves" "LightGreen"
foreach ($job in $Params.Jobs) {
BgLog "--- $($job.SrcSite) / $($job.SrcLib) -> $($job.DstSite) / $($job.DstLib) ---" "White"
if ($srcFiles.Count -eq 0) {
BgLog "Aucun fichier a transferer." "Orange"
$Sync.TransferCount = 0
return
}
# Enumerate source
BgLog "Connecting to source..." "Cyan"
Connect-PnPOnline -Url $job.SrcSite -Interactive -ClientId $Params.ClientId
$srcFiles = @(Get-AllSPFiles $job.SrcLib $Params.Recursive)
BgLog " $($srcFiles.Count) file(s) found" "LightGreen"
# ── Download to temp ──
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "SPToolbox_Transfer_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
BgLog "Dossier temporaire : $tempRoot" "DarkGray"
$idx = 0
foreach ($f in $srcFiles) {
$idx++
$localDir = Join-Path $tempRoot $f.RelativeFolder
if (-not (Test-Path $localDir)) {
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
if ($srcFiles.Count -eq 0) {
BgLog " No files to transfer." "DarkOrange"
continue
}
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"
}
BgLog "Download termine." "White"
# ── Upload to destination ──
BgLog "Connexion au site destination..." "Cyan"
Connect-PnPOnline -Url $Params.DstSite -Interactive -ClientId $Params.ClientId
# Download to temp
$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
$idx = 0
foreach ($f in $srcFiles) {
$idx++
$dstFolder = if ($f.RelativeFolder) {
"$($Params.DstLib.TrimEnd('/'))/$($f.RelativeFolder.TrimEnd('/'))"
} else { $Params.DstLib }
Resolve-PnPFolder -SiteRelativePath $dstFolder -ErrorAction SilentlyContinue | Out-Null
$localFile = Join-Path (Join-Path $tempRoot $f.RelativeFolder) $f.Name
Add-PnPFile -Path $localFile -Folder $dstFolder -ErrorAction Stop | Out-Null
BgLog " [$idx/$($srcFiles.Count)] Uploaded: $($f.RelativePath)" "LightGreen"
$idx = 0
foreach ($f in $srcFiles) {
$idx++
$localDir = Join-Path $tempRoot $f.RelativeFolder
if (-not (Test-Path $localDir)) { New-Item -ItemType Directory -Path $localDir -Force | Out-Null }
try {
Get-PnPFile -Url $f.ServerRelativeUrl -Path $localDir -FileName $f.Name -AsFile -Force
} 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
}
}
# Upload to destination
BgLog "Connecting to destination..." "Cyan"
Connect-PnPOnline -Url $job.DstSite -Interactive -ClientId $Params.ClientId
$idx = 0
foreach ($f in $srcFiles) {
$idx++
$dstFolder = if ($f.RelativeFolder) {
"$($job.DstLib.TrimEnd('/'))/$($f.RelativeFolder.TrimEnd('/'))"
} else { $job.DstLib }
# Check if dest folder exists / create if needed
if ($Params.CreateFolders) {
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
}
}
$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
}
}
BgLog "Upload termine." "White"
$Sync.TransferCount = $srcFiles.Count
$Sync.Report = @($report)
$Sync.TotalOk = $totalOk
$Sync.TotalErr = $totalErr
$Sync.TotalSkip = $totalSkip
} catch {
$Sync.Error = $_.Exception.Message
BgLog "Erreur : $($_.Exception.Message)" "Red"
} 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 = [hashtable]::Synchronized(@{
Queue = [System.Collections.Generic.Queue[object]]::new()
Done = $false
Error = $null
TransferCount = 0
Queue = [System.Collections.Generic.Queue[object]]::new()
Done = $false
Error = $null
Report = $null
TotalOk = 0
TotalErr = 0
TotalSkip = 0
})
$script:_XferSync = $sync
$script:_XferParams = $params
@@ -5136,8 +5492,20 @@ $btnXferStart.Add_Click({
return
}
$cnt = $script:_XferSync.TransferCount
Write-Log "=== TRANSFERT TERMINE : $cnt fichier(s) ===" "White"
$ok = $script:_XferSync.TotalOk; $er = $script:_XferSync.TotalErr; $sk = $script:_XferSync.TotalSkip
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()
@@ -5147,27 +5515,22 @@ $btnXferStart.Add_Click({
$btnXferVerify.Add_Click({
$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 $srcSite) { Write-Log "URL du site source requis." "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 $outDir) { $outDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path } }
$jobs = Get-XferJobs
if ($jobs.Count -eq 0) { return }
$recursive = $chkXferRecursive.Checked
$outDir = $txtOutput.Text.Trim()
if (-not $outDir) { $outDir = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path } }
$asHtml = $radXferHtml.Checked
$params = @{
ClientId = $clientId
SrcSite = $srcSite
DstSite = $dstSite
SrcLib = $srcLib
DstLib = $dstLib
Recursive = $chkXferRecursive.Checked
Jobs = @($jobs)
Recursive = $recursive
OutFolder = $outDir
AsHtml = $asHtml
}
$btnXferStart.Enabled = $false
@@ -5175,9 +5538,7 @@ $btnXferVerify.Add_Click({
$btnXferOpen.Enabled = $false
$txtLog.Clear()
Start-ProgressAnim
Write-Log "=== VERIFICATION ===" "White"
Write-Log "Source : $srcSite / $srcLib" "Gray"
Write-Log "Destination : $dstSite / $dstLib" "Gray"
Write-Log "=== VERIFICATION ($($jobs.Count) job(s)) ===" "White"
Write-Log ("-" * 52) "DarkGray"
$bgVerify = {
@@ -5186,7 +5547,7 @@ $btnXferVerify.Add_Click({
$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)
foreach ($f in $files) {
if ($f.ServerRelativeUrl -match '/_vti_history/') { continue }
@@ -5196,94 +5557,59 @@ $btnXferVerify.Add_Click({
RelativePath = "$Rel$($f.Name)"
}
}
if ($Params.Recursive) {
if ($Recurse) {
$folders = @(Get-PnPFolderItem -FolderSiteRelativeUrl $BasePath -ItemType Folder -ErrorAction SilentlyContinue)
foreach ($d in $folders) {
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 {
Import-Module PnP.PowerShell -ErrorAction Stop
$allResults = [System.Collections.Generic.List[object]]::new()
# Enumerate source
BgLog "Connexion au site source..." "Cyan"
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"
foreach ($job in $Params.Jobs) {
BgLog "--- Verifying $($job.SrcSite)/$($job.SrcLib) vs $($job.DstSite)/$($job.DstLib) ---" "White"
$srcMap = @{}
foreach ($f in $srcFiles) { $srcMap[$f.RelativePath] = $f }
BgLog "Connecting to source..." "Cyan"
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 "Connexion au site destination..." "Cyan"
Connect-PnPOnline -Url $Params.DstSite -Interactive -ClientId $Params.ClientId
BgLog "Enumeration des fichiers destination..." "Cyan"
$dstFiles = @(Get-AllSPFiles $Params.DstLib)
BgLog " $($dstFiles.Count) fichier(s) destination" "LightGreen"
BgLog "Connecting to destination..." "Cyan"
Connect-PnPOnline -Url $job.DstSite -Interactive -ClientId $Params.ClientId
$dstFiles = @(Get-AllSPFiles $job.DstLib $Params.Recursive)
BgLog " $($dstFiles.Count) destination file(s)" "LightGreen"
$dstMap = @{}; foreach ($f in $dstFiles) { $dstMap[$f.RelativePath] = $f }
$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) {
$src = $srcMap[$key]
if ($dstMap.ContainsKey($key)) {
$dst = $dstMap[$key]
if ([long]$src.Length -eq [long]$dst.Length) {
$results.Add([PSCustomObject]@{
Name = $src.Name
RelativePath = $key
Status = "OK"
SourceSize = [long]$src.Length
DestSize = [long]$dst.Length
})
foreach ($key in $srcMap.Keys) {
$src = $srcMap[$key]
if ($dstMap.ContainsKey($key)) {
$dst = $dstMap[$key]
$st = if ([long]$src.Length -eq [long]$dst.Length) { "OK" } else { "SIZE_MISMATCH" }
$allResults.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$key; Status=$st; SourceSize=[long]$src.Length; DestSize=[long]$dst.Length; Message="" })
} else {
$results.Add([PSCustomObject]@{
Name = $src.Name
RelativePath = $key
Status = "SIZE_MISMATCH"
SourceSize = [long]$src.Length
DestSize = [long]$dst.Length
})
$allResults.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$key; Status="MISSING"; SourceSize=[long]$src.Length; DestSize=$null; Message="" })
}
} else {
$results.Add([PSCustomObject]@{
Name = $src.Name
RelativePath = $key
Status = "MISSING"
SourceSize = [long]$src.Length
DestSize = $null
})
}
foreach ($key in $dstMap.Keys) {
if (-not $srcMap.ContainsKey($key)) {
$dst = $dstMap[$key]
$allResults.Add([PSCustomObject]@{ SourceSite=$job.SrcSite; DestSite=$job.DstSite; File=$key; Status="EXTRA"; SourceSize=$null; DestSize=[long]$dst.Length; Message="" })
}
}
$okN = @($allResults | Where-Object { $_.Status -eq "OK" }).Count
$missN = @($allResults | Where-Object { $_.Status -eq "MISSING" }).Count
$mmN = @($allResults | Where-Object { $_.Status -eq "SIZE_MISMATCH" }).Count
$exN = @($allResults | Where-Object { $_.Status -eq "EXTRA" }).Count
BgLog " Results: $okN OK, $missN missing, $mmN size mismatch, $exN extra" "White"
}
foreach ($key in $dstMap.Keys) {
if (-not $srcMap.ContainsKey($key)) {
$dst = $dstMap[$key]
$results.Add([PSCustomObject]@{
Name = $dst.Name
RelativePath = $key
Status = "EXTRA"
SourceSize = $null
DestSize = [long]$dst.Length
})
}
}
$okN = @($results | Where-Object { $_.Status -eq "OK" }).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
BgLog "Resultats : $okN OK, $missN manquant(s), $mmN taille(s) differente(s), $exN extra" "White"
$Sync.VerifyResults = @($results)
$Sync.VerifyResults = @($allResults)
} catch {
$Sync.Error = $_.Exception.Message
BgLog "Erreur : $($_.Exception.Message)" "Red"
@@ -5342,20 +5668,13 @@ $btnXferVerify.Add_Click({
return
}
$p = $script:_VerParams
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
$outDir = $p.OutFolder
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
$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
$p = $script:_VerParams
$rptFile = Export-XferReport ([System.Collections.Generic.List[object]]$results) $p.OutFolder "TransferVerify" $p.AsHtml
if ($rptFile) {
Write-Log "Report: $rptFile" "White"
$script:_XferLastReport = $rptFile
$btnXferOpen.Enabled = $true
}
}
})
$tmr.Start()
@@ -5884,16 +6203,12 @@ $btnVerOpen.Add_Click({
$btnVerRun.Add_Click({
# --- Gather all selected site URLs ---
$siteUrls = @()
if ($script:_CachedSites -and $script:_CachedSites.Count -gt 0) {
foreach ($s in $script:_CachedSites) {
if ($s.Checked) { $siteUrls += $s.Url }
}
}
if ($siteUrls.Count -eq 0) {
$single = $txtSiteUrl.Text.Trim()
if ($single) { $siteUrls = @($single) }
$siteUrls = if ($script:SelectedSites -and $script:SelectedSites.Count -gt 0) {
@($script:SelectedSites)
} else {
@($txtSiteURL.Text.Trim())
}
$siteUrls = @($siteUrls | Where-Object { $_ })
if ($siteUrls.Count -eq 0) { Write-Log "Site URL required." "Red"; return }
$clientId = $txtClientId.Text.Trim()

View File

@@ -1,4 +1,3 @@
# Features à ajouter :
- Sauvegarde du contexte d'authentification en plus des profils
- Possibilité de demander la liste de site auquels un user precis a acces
- Barre de recherche dans les fichiers HTML exportés

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.library": "Documents partagés/sous-dossier",
"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 ",
"grp.bulk.list": "Sites à créer",
@@ -165,5 +170,21 @@
"chk.ver.subsites": "Inclure les sous-sites",
"chk.ver.dryrun": "Simulation (aperçu uniquement, aucune suppression)",
"btn.ver.run": "Nettoyer les versions",
"btn.ver.open": "Ouvrir le rapport"
"btn.ver.open": "Ouvrir le rapport",
"btn.register.app": "Enregistrer",
"reg.title": "Enregistrement de l'application",
"reg.offer": "Aucun Client ID fourni. Enregistrer une nouvelle application sur ce tenant ?",
"reg.confirm": "Enregistrer l'application 'SharePoint Toolbox' sur le tenant {0} ?",
"reg.in.progress": "En cours...",
"reg.success": "Application enregistrée avec succès !\nClient ID : {0}\nVous pouvez sauvegarder ce profil pour le réutiliser.",
"reg.err.tenant": "Impossible de déterminer le tenant à partir de l'URL fournie.",
"reg.err.nocmd": "Le module PnP.PowerShell ne supporte pas l'enregistrement d'applications. Veuillez enregistrer l'application manuellement dans Entra ID.",
"reg.err.no.id": "Enregistrement terminé mais aucun Client ID n'a été retourné.",
"reg.err.failed": "Échec de l'enregistrement :\n{0}",
"reg.err.no.tenant": "Veuillez d'abord saisir une URL de tenant.",
"reg.err.nopwsh": "PowerShell 7+ (pwsh) est requis pour l'enregistrement d'application mais n'a pas été trouvé. Installez-le depuis https://aka.ms/powershell",
"validate.missing.clientid": "Veuillez saisir un Client ID.",
"validate.missing.clientid.hint": "Veuillez saisir un Client ID ou utilisez le bouton 'Enregistrer' pour en créer un.",
"validate.missing.title": "Champ manquant"
}