diff --git a/Sharepoint_ToolBox.ps1 b/Sharepoint_ToolBox.ps1 index f5a13fc..605a980 100644 --- a/Sharepoint_ToolBox.ps1 +++ b/Sharepoint_ToolBox.ps1 @@ -2823,6 +2823,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..." @@ -3546,27 +3551,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 +3609,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 @@ -3936,11 +3970,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" @@ -4965,30 +5003,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 += "$([System.Web.HttpUtility]::HtmlEncode($r.SourceSite))$([System.Web.HttpUtility]::HtmlEncode($r.File))$($r.Status)$srcSz$dstSz$msg`n" + } + + $html = @" +Transfer Report + + +

Transfer Report

+
+OK: $okNError: $errN +Skipped: $skipN / Mismatch: $mmNMissing: $missN +Extra: $exN
+ + +$rows
SiteFileStatusSource SizeDest SizeMessage
+"@ + $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 +5160,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 +5173,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 +5185,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 +5346,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 +5369,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 +5392,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 +5401,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 +5411,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 +5522,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() diff --git a/examples/bulk_transfer.csv b/examples/bulk_transfer.csv new file mode 100644 index 0000000..ada924e --- /dev/null +++ b/examples/bulk_transfer.csv @@ -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 diff --git a/lang/fr.json b/lang/fr.json index 222d3e5..509ae26 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -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",