diff --git a/Sonstiges/Get-NtfsStableFolderId.ps1 b/Sonstiges/Get-NtfsStableFolderId.ps1 new file mode 100644 index 0000000..ca3a6fc --- /dev/null +++ b/Sonstiges/Get-NtfsStableFolderId.ps1 @@ -0,0 +1,172 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [string[]]$Path, + + [Parameter()] + [string]$ExportJsonPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) { + throw 'Dieses Demo-Script kann nur unter Windows ausgefuehrt werden.' +} + +$nativeMethods = @" +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +public static class NtfsNativeMethods +{ + [StructLayout(LayoutKind.Sequential)] + public struct BY_HANDLE_FILE_INFORMATION + { + public uint FileAttributes; + public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime; + public uint VolumeSerialNumber; + public uint FileSizeHigh; + public uint FileSizeLow; + public uint NumberOfLinks; + public uint FileIndexHigh; + public uint FileIndexLow; + } + + public const uint FILE_SHARE_READ = 0x00000001; + public const uint FILE_SHARE_WRITE = 0x00000002; + public const uint FILE_SHARE_DELETE = 0x00000004; + public const uint OPEN_EXISTING = 3; + public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern SafeFileHandle CreateFile( + string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr lpSecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetFileInformationByHandle( + SafeFileHandle hFile, + out BY_HANDLE_FILE_INFORMATION lpFileInformation); +} +"@ + +if (-not ('NtfsNativeMethods' -as [type])) { + Add-Type -TypeDefinition $nativeMethods +} + +function Get-Md5Hex { + param( + [Parameter(Mandatory = $true)] + [string]$Text + ) + + $md5 = [System.Security.Cryptography.MD5]::Create() + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) + $hash = $md5.ComputeHash($bytes) + return ([System.BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant() + } + finally { + $md5.Dispose() + } +} + +function Get-NtfsStableFolderId { + param( + [Parameter(Mandatory = $true)] + [string]$LiteralPath + ) + + $item = Get-Item -LiteralPath $LiteralPath -Force + if (-not $item.PSIsContainer) { + throw "Der Pfad '$LiteralPath' ist kein Ordner." + } + + $handle = [NtfsNativeMethods]::CreateFile( + $item.FullName, + 0, + [NtfsNativeMethods]::FILE_SHARE_READ -bor [NtfsNativeMethods]::FILE_SHARE_WRITE -bor [NtfsNativeMethods]::FILE_SHARE_DELETE, + [IntPtr]::Zero, + [NtfsNativeMethods]::OPEN_EXISTING, + [NtfsNativeMethods]::FILE_FLAG_BACKUP_SEMANTICS, + [IntPtr]::Zero) + + if ($handle.IsInvalid) { + $lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "CreateFile fehlgeschlagen fuer '$($item.FullName)' mit Win32-Fehler $lastError." + } + + try { + $info = New-Object NtfsNativeMethods+BY_HANDLE_FILE_INFORMATION + if (-not [NtfsNativeMethods]::GetFileInformationByHandle($handle, [ref]$info)) { + $lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "GetFileInformationByHandle fehlgeschlagen fuer '$($item.FullName)' mit Win32-Fehler $lastError." + } + + $fileIndex = ([UInt64]$info.FileIndexHigh -shl 32) -bor [UInt64]$info.FileIndexLow + $stableAnchor = ('{0:x8}:{1:x16}' -f $info.VolumeSerialNumber, $fileIndex) + + [pscustomobject]@{ + Path = $item.FullName + Name = $item.Name + VolumeSerialNumberHex = ('0x{0:x8}' -f $info.VolumeSerialNumber) + FileIndexHex = ('0x{0:x16}' -f $fileIndex) + StableAnchor = $stableAnchor + StableDataAreaId = (Get-Md5Hex -Text $stableAnchor) + CreatedUtc = $item.CreationTimeUtc.ToString('o') + LastWriteUtc = $item.LastWriteTimeUtc.ToString('o') + } + } + finally { + $handle.Dispose() + } +} + +$results = foreach ($currentPath in $Path) { + Get-NtfsStableFolderId -LiteralPath $currentPath +} + +if ($PSBoundParameters.ContainsKey('ExportJsonPath')) { + $exportDirectory = Split-Path -Parent $ExportJsonPath + if (-not [string]::IsNullOrWhiteSpace($exportDirectory) -and -not (Test-Path -LiteralPath $exportDirectory)) { + New-Item -ItemType Directory -Path $exportDirectory | Out-Null + } + + $results | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $ExportJsonPath -Encoding UTF8 +} + +$results + +<# +Demo fuer Rename/Move auf demselben NTFS-Volume: + +1. Vorher ausfuehren: + .\Get-NtfsStableFolderId.ps1 -Path 'C:\Daten\TeamA' -ExportJsonPath '.\before.json' + +2. Ordner umbenennen oder innerhalb desselben Volumes verschieben. + +3. Nachher erneut ausfuehren: + .\Get-NtfsStableFolderId.ps1 -Path 'C:\Daten\Fachbereich\TeamA_Renamed' -ExportJsonPath '.\after.json' + +4. Vergleichen: + - `Path` ist anders + - `StableAnchor` bleibt gleich + - `StableDataAreaId` bleibt gleich + +Wichtige Einschraenkung: + +- Rename und Move auf demselben NTFS-Volume behalten die Identitaet. +- Copy-and-delete oder Move ueber ein anderes Volume erzeugen in der Regel eine neue Identitaet. +- Auf manchen Remote-/DFS-Szenarien kann der Dateiserver diese Metadaten nicht verlaesslich liefern. +#>