From f977fec525ce6bde0749ccfed3db6e8bc43fe1bf Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Thu, 8 Oct 2026 09:50:14 +0000 Subject: [PATCH] fix: detect destination folders and name a missing destination folder in Copy-Item2 and Move-Item2 The check for an existing destination looked for a file only. For a folder whose name existed at the destination, the cmdlets failed in the middle with a CopyError or a MoveError, and Copy-Item2 could copy a part of the folder first. They now write DestinationFileAlreadyExists, as for a file. When the folder that is to contain the new item didn't exist, AlphaFS reported a DirectoryNotFoundException that named the source item, which reproduces the symptom of #21. The cmdlets now write an error that names the missing folder, with the destination as the target. Copy-Item2 no longer creates the missing folders for a folder: the workaround that creates the destination folder for AlphaFS created its parents as well, which only the prereleases of 5.0.0 did. The pages also say that -Force merges a folder into an existing folder of the same name, and that a folder can't move to another volume. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 8 ++ Docs/Cmdlets/Copy-Item2.md | 6 +- Docs/Cmdlets/Move-Item2.md | 8 +- NTFSSecurity/BaseCmdlets.cs | 23 ++++++ NTFSSecurity/ItemCmdlets/CopyItem2.cs | 8 +- NTFSSecurity/ItemCmdlets/MoveItem2.cs | 8 +- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 18 ++--- Tests/ItemCmdlets.Tests.ps1 | 83 ++++++++++++++++++++ 8 files changed, 144 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 871d0ed..53bc6d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -287,5 +287,13 @@ The format is based on grants only `ReadData`, and which left out the parent folder of a relative path with a single folder name, although `-IncludeRootFolder` is on by default +- Fix `Copy-Item2` and `Move-Item2`, which didn't detect a folder at the + destination, so that a copy failed in the middle after it had copied a + part of the folder, and which reported a missing destination folder with + an error that named the source item + ([#21](https://github.com/raandree/NTFSSecurity/issues/21)); they now + write `DestinationFileAlreadyExists` and an error that names the missing + folder. `Copy-Item2` no longer creates the missing folders of the + destination when it copies a folder, which the prereleases of 5.0.0 did [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Cmdlets/Copy-Item2.md b/Docs/Cmdlets/Copy-Item2.md index fd7fcaa..e272ff4 100644 --- a/Docs/Cmdlets/Copy-Item2.md +++ b/Docs/Cmdlets/Copy-Item2.md @@ -24,7 +24,7 @@ The `Copy-Item2` cmdlet copies the items in `-Path` to the location in `-Destina How `-Destination` is interpreted depends on what is already there. If the value names an existing folder, the cmdlet keeps the name of the source item and copies it into that folder. In every other case the value is the full path of the new item, which lets you copy and rename in one step. `-Destination` is resolved against the current location once, when the cmdlet starts. -Without `-Force`, the cmdlet checks whether the destination file already exists and writes a `DestinationFileAlreadyExists` error instead of overwriting it. With `-WhatIf`, it names an existing destination file in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, an existing file is replaced. Relative paths and the `.` and `..` notations in `-Path` are resolved against the current location, and wildcard characters are not supported. +Without `-Force`, the cmdlet checks whether a file or folder already exists at the destination and writes a `DestinationFileAlreadyExists` error instead of overwriting it or merging into it. With `-WhatIf`, it names an existing destination in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, an existing file is replaced, and a folder is copied into an existing folder of the same name, replacing the files that exist in both. The folder that is to contain the new item must exist; otherwise the cmdlet writes an error that names that folder. Relative paths and the `.` and `..` notations in `-Path` are resolved against the current location, and wildcard characters are not supported. The cmdlet supports `-WhatIf` and `-Confirm`, and it writes nothing to the pipeline unless you specify `-PassThru $true`. @@ -98,7 +98,7 @@ Accept wildcard characters: False ### -Force -Indicates that the cmdlet overwrites an existing destination file. Without `-Force`, an existing file causes the error `DestinationFileAlreadyExists` and the item is not copied. +Indicates that the cmdlet overwrites an existing destination file, and copies a folder into an existing folder of the same name. Without `-Force`, an existing file or folder at the destination causes the error `DestinationFileAlreadyExists` and the item is not copied. ```yaml Type: SwitchParameter @@ -192,7 +192,7 @@ Before 5.0.0, `-PassThru` also wrote the item when `-WhatIf` or a declined confi Before 5.0.0, copying a folder that contained files failed with a `CopyError` that reported a `DirectoryNotFoundException` for the first file in the folder. -If a path in `-Path` does not exist or the destination file exists and `-Force` is missing, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. +If a path in `-Path` does not exist, a file or folder exists at the destination and `-Force` is missing, or the folder that is to contain the copy does not exist, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. Before 5.0.0, it also didn't detect an existing destination folder, so that the copy failed in the middle with a `CopyError` after it had copied a part of the folder; it reported a missing destination folder as a `DirectoryNotFoundException` that named the source item; and, in the prereleases of 5.0.0, it created the missing folders of the destination for a folder. ## RELATED LINKS diff --git a/Docs/Cmdlets/Move-Item2.md b/Docs/Cmdlets/Move-Item2.md index 82549dd..a71a067 100644 --- a/Docs/Cmdlets/Move-Item2.md +++ b/Docs/Cmdlets/Move-Item2.md @@ -24,7 +24,7 @@ The `Move-Item2` cmdlet moves the items in `-Path` to the location in `-Destinat How `-Destination` is interpreted depends on what is already there. If the value names an existing folder, the cmdlet keeps the name of the source item and moves it into that folder. In every other case the value is the full path of the new item, which lets you move and rename in one step, or rename an item in place. `-Destination` is resolved against the current location once, when the cmdlet starts. -Without `-Force`, the cmdlet checks whether the destination file already exists and writes a `DestinationFileAlreadyExists` error instead of overwriting it; the move itself then runs with the `CopyAllowed` option, which allows a file to move to a different volume. With `-WhatIf`, the cmdlet names an existing destination file in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, the move runs with the `ReplaceExisting` option and overwrites an existing destination item. +Without `-Force`, the cmdlet checks whether a file or folder already exists at the destination and writes a `DestinationFileAlreadyExists` error instead of overwriting it; the move itself then runs with the `CopyAllowed` option, which allows a file to move to a different volume. With `-WhatIf`, the cmdlet names an existing destination in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, the move runs with the `ReplaceExisting` option and overwrites an existing destination item. The folder that is to contain the moved item must exist; otherwise the cmdlet writes an error that names that folder. The cmdlet supports `-WhatIf` and `-Confirm`, and it writes nothing to the pipeline unless you specify `-PassThru $true`. @@ -98,7 +98,7 @@ Accept wildcard characters: False ### -Force -Indicates that the cmdlet replaces an existing destination item. Without `-Force`, an existing destination file causes the error `DestinationFileAlreadyExists` and the item is not moved. +Indicates that the cmdlet replaces an existing destination item. Without `-Force`, an existing file or folder at the destination causes the error `DestinationFileAlreadyExists` and the item is not moved. ```yaml Type: SwitchParameter @@ -190,9 +190,9 @@ With `-PassThru $true` the cmdlet returns a folder object for each folder that i Before 5.0.0, `-PassThru` also wrote the item when `-WhatIf` or a declined confirmation skipped the operation. -The cmdlet chooses between two mutually exclusive move options. Without `-Force` it moves with `CopyAllowed`, which permits a file to cross volume boundaries because Windows then copies and deletes it. With `-Force` it moves with `ReplaceExisting`, which overwrites the destination but does not request `CopyAllowed`, so a move across volumes can fail when `-Force` is specified. +The cmdlet chooses between two mutually exclusive move options. Without `-Force` it moves with `CopyAllowed`, which permits a file to cross volume boundaries because Windows then copies and deletes it. With `-Force` it moves with `ReplaceExisting`, which overwrites the destination but does not request `CopyAllowed`, so a move across volumes can fail when `-Force` is specified. A folder can't move to another volume: the cmdlet writes a `MoveError` and leaves the folder in place, so copy it with `Copy-Item2` and remove it with `Remove-Item2` instead. -If a path in `-Path` does not exist or the destination file exists and `-Force` is missing, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. +If a path in `-Path` does not exist, a file or folder exists at the destination and `-Force` is missing, or the folder that is to contain the moved item does not exist, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. Before 5.0.0, it also reported an existing destination folder as a `MoveError`, and a missing destination folder as a `DirectoryNotFoundException` that named the source item ([#21](https://github.com/raandree/NTFSSecurity/issues/21)). ## RELATED LINKS diff --git a/NTFSSecurity/BaseCmdlets.cs b/NTFSSecurity/BaseCmdlets.cs index e2f2263..d5a3646 100644 --- a/NTFSSecurity/BaseCmdlets.cs +++ b/NTFSSecurity/BaseCmdlets.cs @@ -149,6 +149,29 @@ namespace NTFSSecurity } #endregion + #region WriteMissingDestinationFolderError + /// + /// Writes an error that names the folder of a destination path when that folder doesn't exist. Before + /// 5.0.0-rc6, AlphaFS reported such a destination as the source path that could not be found (#21), and + /// Copy-Item2 created the missing folders for a folder. + /// + /// The full path of the item that the operation would create. + /// The error ID of the cmdlet for a failed operation. + /// Whether the folder is missing and the error was written. + protected bool WriteMissingDestinationFolderError(string destinationPath, string errorId) + { + var folder = Alphaleonis.Win32.Filesystem.Path.GetDirectoryName(destinationPath.TrimEnd('\\')); + if (string.IsNullOrEmpty(folder) || Alphaleonis.Win32.Filesystem.Directory.Exists(folder)) + { + return false; + } + + var exception = new System.IO.DirectoryNotFoundException(string.Format("The destination folder '{0}' does not exist.", folder)); + WriteError(new ErrorRecord(exception, errorId, ErrorCategory.ObjectNotFound, destinationPath)); + return true; + } + #endregion + #region InvokeAsOwner /// /// Takes ownership of the item, runs the action, and restores the previous owner on every exit path. diff --git a/NTFSSecurity/ItemCmdlets/CopyItem2.cs b/NTFSSecurity/ItemCmdlets/CopyItem2.cs index 322f2b7..fdbe5bb 100644 --- a/NTFSSecurity/ItemCmdlets/CopyItem2.cs +++ b/NTFSSecurity/ItemCmdlets/CopyItem2.cs @@ -88,7 +88,8 @@ namespace NTFSSecurity actualDestination = destination; } - var destinationExists = !force && File.Exists(actualDestination); + // A folder at the destination counts as well; before 5.0.0-rc6, only a file did. + var destinationExists = !force && (File.Exists(actualDestination) || Directory.Exists(actualDestination)); // Report a conflict only for an operation that runs; -WhatIf names it in a verbose message (#108). if (!ShouldProcess(resolvedPath, item is FileInfo ? "Copy File" : "Copy Directory")) @@ -107,6 +108,11 @@ namespace NTFSSecurity continue; } + if (WriteMissingDestinationFolderError(actualDestination, "CopyError")) + { + continue; + } + try { FileSystemInfo copy = null; diff --git a/NTFSSecurity/ItemCmdlets/MoveItem2.cs b/NTFSSecurity/ItemCmdlets/MoveItem2.cs index e941e37..52ea1d6 100644 --- a/NTFSSecurity/ItemCmdlets/MoveItem2.cs +++ b/NTFSSecurity/ItemCmdlets/MoveItem2.cs @@ -88,7 +88,8 @@ namespace NTFSSecurity actualDestination = destination; } - var destinationExists = !force && File.Exists(actualDestination); + // A folder at the destination counts as well; before 5.0.0-rc6, only a file did. + var destinationExists = !force && (File.Exists(actualDestination) || Directory.Exists(actualDestination)); // Report a conflict only for an operation that runs; -WhatIf names it in a verbose message (#108). if (!ShouldProcess(resolvedPath, item is FileInfo ? "Move File" : "Move Directory")) @@ -107,6 +108,11 @@ namespace NTFSSecurity continue; } + if (WriteMissingDestinationFolderError(actualDestination, "MoveError")) + { + continue; + } + try { if (item is FileInfo) diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 8dcd09f..5276fae 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -1997,7 +1997,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd The `Copy-Item2` cmdlet copies the items in `-Path` to the location in `-Destination`. It is the long-path counterpart of the built-in `Copy-Item` cmdlet: it works through the AlphaFS library (`Alphaleonis.Win32.Filesystem`), so source and destination may be longer than the 260-character `MAX_PATH` limit. How `-Destination` is interpreted depends on what is already there. If the value names an existing folder, the cmdlet keeps the name of the source item and copies it into that folder. In every other case the value is the full path of the new item, which lets you copy and rename in one step. `-Destination` is resolved against the current location once, when the cmdlet starts. - Without `-Force`, the cmdlet checks whether the destination file already exists and writes a `DestinationFileAlreadyExists` error instead of overwriting it. With `-WhatIf`, it names an existing destination file in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, an existing file is replaced. Relative paths and the `.` and `..` notations in `-Path` are resolved against the current location, and wildcard characters are not supported. + Without `-Force`, the cmdlet checks whether a file or folder already exists at the destination and writes a `DestinationFileAlreadyExists` error instead of overwriting it or merging into it. With `-WhatIf`, it names an existing destination in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, an existing file is replaced, and a folder is copied into an existing folder of the same name, replacing the files that exist in both. The folder that is to contain the new item must exist; otherwise the cmdlet writes an error that names that folder. Relative paths and the `.` and `..` notations in `-Path` are resolved against the current location, and wildcard characters are not supported. The cmdlet supports `-WhatIf` and `-Confirm`, and it writes nothing to the pipeline unless you specify `-PassThru $true`. @@ -2041,7 +2041,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd Force - Indicates that the cmdlet overwrites an existing destination file. Without `-Force`, an existing file causes the error `DestinationFileAlreadyExists` and the item is not copied. + Indicates that the cmdlet overwrites an existing destination file, and copies a folder into an existing folder of the same name. Without `-Force`, an existing file or folder at the destination causes the error `DestinationFileAlreadyExists` and the item is not copied. SwitchParameter @@ -2102,7 +2102,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd Force - Indicates that the cmdlet overwrites an existing destination file. Without `-Force`, an existing file causes the error `DestinationFileAlreadyExists` and the item is not copied. + Indicates that the cmdlet overwrites an existing destination file, and copies a folder into an existing folder of the same name. Without `-Force`, an existing file or folder at the destination causes the error `DestinationFileAlreadyExists` and the item is not copied. SwitchParameter @@ -2189,7 +2189,7 @@ PS C:\> Set-NTFSSecurityDescriptor -SecurityDescriptor $sd `Copy-Item2` copies through the AlphaFS library (`Alphaleonis.Win32.Filesystem`), which is why it handles source and destination paths that exceed the 260-character `MAX_PATH` limit of the built-in `Copy-Item` cmdlet. Before 5.0.0, `-PassThru` also wrote the item when `-WhatIf` or a declined confirmation skipped the operation. Before 5.0.0, copying a folder that contained files failed with a `CopyError` that reported a `DirectoryNotFoundException` for the first file in the folder. - If a path in `-Path` does not exist or the destination file exists and `-Force` is missing, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. + If a path in `-Path` does not exist, a file or folder exists at the destination and `-Force` is missing, or the folder that is to contain the copy does not exist, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. Before 5.0.0, it also didn't detect an existing destination folder, so that the copy failed in the middle with a `CopyError` after it had copied a part of the folder; it reported a missing destination folder as a `DirectoryNotFoundException` that named the source item; and, in the prereleases of 5.0.0, it created the missing folders of the destination for a folder. @@ -6784,7 +6784,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor The `Move-Item2` cmdlet moves the items in `-Path` to the location in `-Destination`. It is the long-path counterpart of the built-in `Move-Item` cmdlet: it works through the AlphaFS library (`Alphaleonis.Win32.Filesystem`), so source and destination may be longer than the 260-character `MAX_PATH` limit. Files and folders can both be moved, and a folder is moved with everything it contains. How `-Destination` is interpreted depends on what is already there. If the value names an existing folder, the cmdlet keeps the name of the source item and moves it into that folder. In every other case the value is the full path of the new item, which lets you move and rename in one step, or rename an item in place. `-Destination` is resolved against the current location once, when the cmdlet starts. - Without `-Force`, the cmdlet checks whether the destination file already exists and writes a `DestinationFileAlreadyExists` error instead of overwriting it; the move itself then runs with the `CopyAllowed` option, which allows a file to move to a different volume. With `-WhatIf`, the cmdlet names an existing destination file in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, the move runs with the `ReplaceExisting` option and overwrites an existing destination item. + Without `-Force`, the cmdlet checks whether a file or folder already exists at the destination and writes a `DestinationFileAlreadyExists` error instead of overwriting it; the move itself then runs with the `CopyAllowed` option, which allows a file to move to a different volume. With `-WhatIf`, the cmdlet names an existing destination in a verbose message instead; before 5.0.0, it wrote the error also with `-WhatIf`. With `-Force`, the move runs with the `ReplaceExisting` option and overwrites an existing destination item. The folder that is to contain the moved item must exist; otherwise the cmdlet writes an error that names that folder. The cmdlet supports `-WhatIf` and `-Confirm`, and it writes nothing to the pipeline unless you specify `-PassThru $true`. @@ -6828,7 +6828,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor Force - Indicates that the cmdlet replaces an existing destination item. Without `-Force`, an existing destination file causes the error `DestinationFileAlreadyExists` and the item is not moved. + Indicates that the cmdlet replaces an existing destination item. Without `-Force`, an existing file or folder at the destination causes the error `DestinationFileAlreadyExists` and the item is not moved. SwitchParameter @@ -6889,7 +6889,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor Force - Indicates that the cmdlet replaces an existing destination item. Without `-Force`, an existing destination file causes the error `DestinationFileAlreadyExists` and the item is not moved. + Indicates that the cmdlet replaces an existing destination item. Without `-Force`, an existing file or folder at the destination causes the error `DestinationFileAlreadyExists` and the item is not moved. SwitchParameter @@ -6975,8 +6975,8 @@ PS C:\Data> Get-NTFSSecurityDescriptor `Move-Item2` moves through the AlphaFS library (`Alphaleonis.Win32.Filesystem`), which is why it handles source and destination paths that exceed the 260-character `MAX_PATH` limit of the built-in `Move-Item` cmdlet. Before 5.0.0, `-PassThru` also wrote the item when `-WhatIf` or a declined confirmation skipped the operation. - The cmdlet chooses between two mutually exclusive move options. Without `-Force` it moves with `CopyAllowed`, which permits a file to cross volume boundaries because Windows then copies and deletes it. With `-Force` it moves with `ReplaceExisting`, which overwrites the destination but does not request `CopyAllowed`, so a move across volumes can fail when `-Force` is specified. - If a path in `-Path` does not exist or the destination file exists and `-Force` is missing, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. + The cmdlet chooses between two mutually exclusive move options. Without `-Force` it moves with `CopyAllowed`, which permits a file to cross volume boundaries because Windows then copies and deletes it. With `-Force` it moves with `ReplaceExisting`, which overwrites the destination but does not request `CopyAllowed`, so a move across volumes can fail when `-Force` is specified. A folder can't move to another volume: the cmdlet writes a `MoveError` and leaves the folder in place, so copy it with `Copy-Item2` and remove it with `Remove-Item2` instead. + If a path in `-Path` does not exist, a file or folder exists at the destination and `-Force` is missing, or the folder that is to contain the moved item does not exist, the cmdlet writes a non-terminating error and continues with the next path. Before 5.0.0, it skipped the remaining paths that were passed in the same call. Before 5.0.0, it also reported an existing destination folder as a `MoveError`, and a missing destination folder as a `DirectoryNotFoundException` that named the source item ( #21 (https://github.com/raandree/NTFSSecurity/issues/21)). diff --git a/Tests/ItemCmdlets.Tests.ps1 b/Tests/ItemCmdlets.Tests.ps1 index ae29a04..835badb 100644 --- a/Tests/ItemCmdlets.Tests.ps1 +++ b/Tests/ItemCmdlets.Tests.ps1 @@ -185,6 +185,89 @@ Describe 'Copy-Item2, Move-Item2, and Remove-Item2 with several paths' { $result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'First.txt') } + + It 'Move-Item2 -PassThru should return a folder at its new location' { + $sourceFolder = Join-Path -Path $folder -ChildPath 'MovedFolder' + Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder + New-Item -ItemType Directory -Path $sourceFolder | Out-Null + + $result = Move-Item2 -Path $sourceFolder -Destination $destination -PassThru $true + + $result | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo] + $result.FullName | Should -Be (Join-Path -Path $destination -ChildPath 'MovedFolder') + $sourceFolder | Should -Not -Exist + } + + # Before 5.0.0-rc6, the check for an existing destination looked for a file only. For a folder whose name existed + # in the destination, the cmdlets failed in the middle with a CopyError or a MoveError, and Copy-Item2 could copy + # a part of the folder. + It '<_> should write DestinationFileAlreadyExists for a folder that exists at the destination and change nothing' -ForEach @('Copy-Item2', 'Move-Item2') { + $sourceFolder = Join-Path -Path $folder -ChildPath 'Conflict' + $existingFolder = Join-Path -Path $destination -ChildPath 'Conflict' + Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder, $existingFolder + New-Item -ItemType Directory -Path $sourceFolder, $existingFolder | Out-Null + Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'A.txt') -Value 'New' + Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'B.txt') -Value 'New' + Set-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'A.txt') -Value 'Existing' + + & $_ -Path $sourceFolder -Destination $destination -ErrorVariable itemErrors -ErrorAction SilentlyContinue + + $itemErrors | Should -HaveCount 1 + $itemErrors[0].FullyQualifiedErrorId | Should -BeLike 'DestinationFileAlreadyExists,*' + $itemErrors[0].TargetObject | Should -Be $existingFolder + Get-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'A.txt') | Should -Be 'Existing' + Join-Path -Path $existingFolder -ChildPath 'B.txt' | Should -Not -Exist + Join-Path -Path $sourceFolder -ChildPath 'A.txt' | Should -Exist + } + + It 'Copy-Item2 -Force should copy a folder into an existing folder of the same name and replace the files in both' { + $sourceFolder = Join-Path -Path $folder -ChildPath 'Merge' + $existingFolder = Join-Path -Path $destination -ChildPath 'Merge' + Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceFolder, $existingFolder + New-Item -ItemType Directory -Path $sourceFolder, $existingFolder | Out-Null + Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'A.txt') -Value 'New' + Set-Content -LiteralPath (Join-Path -Path $sourceFolder -ChildPath 'B.txt') -Value 'New' + Set-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'A.txt') -Value 'Existing' + Set-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'C.txt') -Value 'Existing' + + Copy-Item2 -Path $sourceFolder -Destination $destination -Force -ErrorVariable itemErrors -ErrorAction SilentlyContinue + + $itemErrors | Should -BeNullOrEmpty + Get-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'A.txt') | Should -Be 'New' + Get-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'B.txt') | Should -Be 'New' + Get-Content -LiteralPath (Join-Path -Path $existingFolder -ChildPath 'C.txt') | Should -Be 'Existing' + } + + # Before 5.0.0-rc6, the error named the source item as the path that wasn't found, also when the folder of the + # destination was missing (#21). + It ' should name the missing folder of the destination for a and change nothing' -ForEach @( + @{ Command = 'Copy-Item2'; Kind = 'file'; ErrorId = 'CopyError' } + @{ Command = 'Copy-Item2'; Kind = 'folder'; ErrorId = 'CopyError' } + @{ Command = 'Move-Item2'; Kind = 'file'; ErrorId = 'MoveError' } + @{ Command = 'Move-Item2'; Kind = 'folder'; ErrorId = 'MoveError' } + ) { + $source = $first + if ($Kind -eq 'folder') { + $source = Join-Path -Path $folder -ChildPath 'SourceFolder' + Assert-TestSandboxPath -Sandbox $sandbox -Path $source + New-Item -ItemType Directory -Path $source | Out-Null + Set-Content -LiteralPath (Join-Path -Path $source -ChildPath 'Inner.txt') -Value 'Inner' + } + + $missingFolder = Join-Path -Path $folder -ChildPath 'MissingFolder' + $target = Join-Path -Path $missingFolder -ChildPath 'Item' + Assert-TestSandboxPath -Sandbox $sandbox -Path $missingFolder, $target + + & $Command -Path $source -Destination $target -ErrorVariable itemErrors -ErrorAction SilentlyContinue + + $itemErrors | Should -HaveCount 1 + $itemErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*" + $itemErrors[0].Exception.Message | Should -BeLike "*'$missingFolder'*" + $itemErrors[0].TargetObject | Should -Be $target + $source | Should -Exist + $missingFolder | Should -Not -Exist + } + # Before 5.0.0, -PassThru wrote the item also when -WhatIf skipped the operation. It '<_> should write nothing with -PassThru and -WhatIf' -ForEach @('Copy-Item2', 'Move-Item2', 'Remove-Item2') { $parameters = @{ Path = $first; PassThru = $true; WhatIf = $true }