# CareTV DrivePool Existing-Data Adoption # Version: v001 # # PURPOSE # Make files that already exist on StableBit DrivePool member disks appear # inside the pool WITHOUT copy/paste/delete of the file contents. # # HOW IT WORKS # StableBit DrivePool stores pooled files inside a hidden PoolPart.* folder # on each member volume. This script finds the PoolPart.* folder on each # selected member drive and MOVES existing top-level data into that folder # on the SAME NTFS/ReFS volume. Same-volume moves normally update filesystem # metadata rather than rewriting all file contents. # # SAFETY # - Does NOT format, initialize, delete, or rebalance disks. # - Does NOT cross-copy data between volumes. # - Skips Windows/system/application folders. # - Skips PoolPart.*, $RECYCLE.BIN, System Volume Information, etc. # - Refuses ambiguous disks containing more than one PoolPart.* folder. # - Writes a log. # - Requires explicit confirmation unless -Yes is supplied. # # RECOMMENDED USE # Run once whenever an already-populated disk has been added to DrivePool. param( [string[]]$DriveLetters, [switch]$Yes ) $ErrorActionPreference = "Stop" $LogDir = Join-Path $env:ProgramData "CareTV\Logs" New-Item -ItemType Directory -Path $LogDir -Force | Out-Null $LogFile = Join-Path $LogDir ("DrivePool-Adopt-" + (Get-Date -Format "yyyyMMdd-HHmmss") + ".log") function Log([string]$Text) { $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Text" Write-Host $line Add-Content -Path $LogFile -Value $line } function Require-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw "Run the BAT/PowerShell script as Administrator." } } Require-Administrator # Intentionally conservative skip list. $SkipNames = @( '$RECYCLE.BIN', 'System Volume Information', 'Recovery', 'Windows', 'Program Files', 'Program Files (x86)', 'ProgramData', 'Users', 'PerfLogs', 'Documents and Settings', 'MSOCache', 'Config.Msi', 'EFI', 'Boot', 'bootmgr' ) function Get-CandidateDrives { if ($DriveLetters -and $DriveLetters.Count -gt 0) { return $DriveLetters | ForEach-Object { $d = $_.Trim().TrimEnd(':') + ':' Get-PSDrive -Name $d.TrimEnd(':') -PSProvider FileSystem -ErrorAction Stop } } return Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -match '^[A-Z]:\\$' -and $_.Name -ne 'C' } } function Get-PoolPart([string]$Root) { $parts = Get-ChildItem -LiteralPath $Root -Force -Directory -ErrorAction Stop | Where-Object { $_.Name -like 'PoolPart.*' } if ($parts.Count -eq 0) { return $null } if ($parts.Count -gt 1) { throw "Ambiguous DrivePool membership at $Root : more than one PoolPart.* folder found." } return $parts[0] } $drives = Get-CandidateDrives $plans = @() foreach ($drive in $drives) { $root = $drive.Root $poolPart = Get-PoolPart $root if (-not $poolPart) { continue } $items = Get-ChildItem -LiteralPath $root -Force -ErrorAction Stop | Where-Object { $_.Name -notlike 'PoolPart.*' -and $_.Name -notin $SkipNames -and -not ($_.Attributes -band [IO.FileAttributes]::System) } if ($items.Count -eq 0) { Log "Nothing to adopt on $root" continue } foreach ($item in $items) { $destination = Join-Path $poolPart.FullName $item.Name # Avoid merging over an existing destination automatically. if (Test-Path -LiteralPath $destination) { Log "SKIP collision: $($item.FullName) -> $destination already exists" continue } $plans += [pscustomobject]@{ Drive = $root Source = $item.FullName Destination = $destination } } } if ($plans.Count -eq 0) { Log "No adoptable existing data found on DrivePool member volumes." Write-Host "" Write-Host "Log: $LogFile" exit 0 } Write-Host "" Write-Host "CareTV DrivePool Adoption Plan" Write-Host "==============================" $plans | Format-Table Drive, Source, Destination -AutoSize Write-Host "" Write-Host "These are SAME-VOLUME moves into each disk's existing PoolPart folder." Write-Host "No disk formatting or cross-volume copy is performed." Write-Host "" if (-not $Yes) { $answer = Read-Host "Type ADOPT to continue" if ($answer -ne "ADOPT") { Log "Cancelled by user." exit 1 } } foreach ($plan in $plans) { try { $srcRoot = [IO.Path]::GetPathRoot($plan.Source) $dstRoot = [IO.Path]::GetPathRoot($plan.Destination) if ($srcRoot -ne $dstRoot) { throw "Safety stop: source and destination are not on the same volume." } Log "MOVE $($plan.Source) -> $($plan.Destination)" Move-Item -LiteralPath $plan.Source -Destination $plan.Destination -ErrorAction Stop } catch { Log "ERROR $($plan.Source): $($_.Exception.Message)" } } Log "DrivePool adoption pass complete." Write-Host "" Write-Host "StableBit DrivePool should now see the adopted files." Write-Host "If Explorer is stale, refresh it or wait briefly for DrivePool to rescan." Write-Host "" Write-Host "Log: $LogFile"