I am trying to create a little patcher in PowerShell which first backs up the files before replacing them. But I can't get it to cooperate when the file is in a subdirectory. To simplify:
First, it creates the backup directory:
$Null = New-Item -Path 'C:\Backup' -ItemType 'Directory'
Next, I check for the file:
[System.IO.FileInfo]$FileInfo = Get-Item -LiteralPath 'C:\App\bin\executable.exe'
Now, I want this file to end up in C:\Backup\bin\executable.exe
The first thing I tried was to get the FullName and replace C:\App with C:\Backup resulting in $FileInfo | Copy-Item -Destination 'C:\Backup\bin\executable.exe'
But it keeps throwing an exception because C:\Backup\bin does not exist. I tried combinations of the -Recurse and -Container switches.
I assume the issue is that I'm setting the full path as my destination, resulting in no relative paths being created. But I don't see another way to get that target path set. I can't tell it to copy to C:\Backup\bin because there's no logical way for me to know about bin without extracting it. At which point I might as well create it in the backup directory.
But I kind of want this to be automagic (for the C:\App\multi\level\path\file.dll at a later stage). This makes the script more flexible if I change the file locations later on.
I'm probably missing something really obvious here. Any suggestions?
This is what I'm doing now, compensating for having to make the parent directories myself. I'm open to ways to optimize and of course still looking for a way to not have to explicitly create the parent directories.
Function Backup-Item {
Param (
[Parameter(Mandatory = $True)] [System.String]$Source,
[Parameter(Mandatory = $True)] [System.String]$SourceRoot,
[Parameter(Mandatory = $False)][System.Management.Automation.SwitchParameter]$Backup = $True,
[Parameter(Mandatory = $False)][System.String]$BackupDirectory
) # Param
Begin {
If (Test-Path -LiteralPath $Source) { [System.IO.FileInfo]$SourceFile = Get-Item -LiteralPath $Source }
Else { } # TODO: Break
If ($Backup) {
If (Test-Path -LiteralPath $BackupDirectory) { } # TODO: Break
Else { $Null = New-Item -Path $BackupDirectory -ItemType 'Directory' }
[System.String]$Destination = $SourceFile.FullName.Replace($SourceRoot,$BackupDirectory)
} # If
} # Begin
Process {
If ($Backup) {
[System.String]$TargetDirectory = $SourceFile.DirectoryName.Replace($SourceRoot,$BackupDirectory)
If (-not (Test-Path -LiteralPath $TargetDirectory)) { $Null = New-Item -Path $TargetDirectory -ItemType 'Directory' }
$SourceFile | Copy-Item -Destination $Destination
} # If
} # Process
End {}
} # Function Backup-Item
I don't like having to provide the SourceRoot but it's the only way to deduce the relative paths I could think of.
I will also need to make BackupDirectory mandatory but only if Backup is True (which is the default and a bit dirty since a switch should be off by default, but I wanted the ability to do -Backup:$False to override).
I would use this as a helper script in the larger patcher-script.
Why not use robocopy to copy the entire C:\Apps folder to the C:\Backup folder, like with
robocopy C:\Apps C:\Backup /MIR
If you want pure PowerShell, you can do
$sourcePath = 'C:\Apps'
$backupPath = 'C:\Backup'
Get-ChildItem $sourcePath -File -Recurse | ForEach-Object {
# create the destination folder, even if it is empty
$targetFolder = Join-Path -Path $backupPath -ChildPath $_.DirectoryName.Substring($sourcePath.Length)
$null = New-Item -ItemType Directory -Path $targetFolder -Force
# next copy the file
$_ | Copy-Item -Destination $targetFolder
}
Related
In other words, does PowerShell's Expand-Archive have an equivalent to unzip's -j command-line argument? If not, are there alternatives on Windows?
I have tried Expand-Archive -Path thing.zip -DestinationPath "somepath" -Force, which just puts the directory structure in another folder called somepath.
This function will do what you want, obviously handling of possible file collision is not implemented, up to you how you want to implement that. Currently, if a file already exists with the same name it will give you an error and skip it. The function is a simplified version of the one from this answer which does actually keep the folder structure.
If no argument is passed to the -DestinationPath parameter, the zip entries will be extracted to the current location.
using namespace System.IO
using namespace System.IO.Compression
function Expand-ZipArchive {
[CmdletBinding(DefaultParameterSetName = 'Path')]
param(
[Parameter(ParameterSetName = 'Path', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string] $Path,
[Parameter(ParameterSetName = 'LiteralPath', Mandatory, ValueFromPipelineByPropertyName)]
[Alias('PSPath')]
[string] $LiteralPath,
[Parameter()]
[string] $DestinationPath
)
begin {
Add-Type -AssemblyName System.IO.Compression
$DestinationPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath)
}
process {
$arguments = switch($PSCmdlet.ParameterSetName) {
Path { $Path, $false, $false }
LiteralPath { $LiteralPath, $false, $true }
}
$null = [Directory]::CreateDirectory($DestinationPath)
foreach($item in $ExecutionContext.InvokeProvider.Item.Get.Invoke($arguments)) {
try {
$fileStream = $item.Open([FileMode]::Open)
$zipArchive = [ZipArchive]::new($fileStream, [ZipArchiveMode]::Read)
foreach($entry in $zipArchive.Entries) {
try {
# if it's a folder, exclude it
if(-not $entry.Name) {
continue
}
$path = [Path]::Combine($DestinationPath, $entry.Name)
# will throw if a file with same name exists, intended
# error handling should be implemented in `catch` block
$fs = [FileStream]::new($path, [FileMode]::CreateNew)
$wrappedStream = $entry.Open()
$wrappedStream.CopyTo($fs)
}
catch {
$PSCmdlet.WriteError($_)
}
finally {
$fs, $wrappedStream | ForEach-Object Dispose
}
}
}
catch {
$PSCmdlet.WriteError($_)
}
finally {
$zipArchive, $fileStream | ForEach-Object Dispose
}
}
}
}
Expand-ZipArchive .\myZip.zip
As a PowerShell-only way you could extract the archive to a temporary directory and then move the files to the final location, discarding directory structure.
$archiveName = 'test.zip'
$destination = 'test'
# Create temp path as a sub directory of actual destination path, so the files don't
# need to be moved (potentially) across drives.
$destinationTemp = Join-Path $destination "~$((New-Guid).ToString('n'))"
# Create temp directory
$null = New-Item $destinationTemp -ItemType Directory
# Extract to temp dir
Expand-Archive $archiveName -DestinationPath $destinationTemp
# Move files from temp dir to actual destination, discarding directory structure
Get-ChildItem $destinationTemp -File -Recurse | Move-Item -Destination $destination
# Remove temp dir
Remove-Item $destinationTemp -Recurse -Force
With PowerShell 7+, you could even move each file immediately after extraction, using the new -PassThru switch of Expand-Archive:
$archiveName = 'test.zip'
$destination = 'test'
# Create temp path as a sub directory of actual destination path, so the files don't
# need to be moved (potentially) across drives.
$destinationTemp = Join-Path $destination "~$((New-Guid).ToString('n'))"
# Create temp directory
$null = New-Item $destinationTemp -ItemType Directory
# Expand to temp dir and move to final destination, discarding directory structure
Expand-Archive $archiveName -DestinationPath $destinationTemp -PassThru |
Where-Object -not PSIsContainer | Move-Item -Destination $destination
# Remove temp dir
Remove-Item $destinationTemp -Recurse -Force
I need help to create a script that can save my life.
My backup software got it wrong because I misdesigned my backup plan and I have lots of named files; filename or folder name (2) (encoding conflict).
I would like to recursively search my network share to find folders and files with "(encode conflict)" in their name and first export them for verification.
Then, if all goes well, I would like to move them to another place while keeping the folder's hierarchy and that's where I stuck.
Get-ChildItem -LiteralPath '\\?\E:\Network Shares\Commun' -Recurse -Filter "*(encode*" #| move-item -Destination 'C:\Users\Desktop\Conflits\'
For the export I found the script here :
https://stackoverflow.com/a/15261816/19493679
Thanks to OP
The files from my test are moving without folder's and file's hierarchy ...
Can you help me please ? :)
Move-Item doesn't know about the directory structure, it just gets paths fed one by one from Get-ChildItem. For preserving directory structure, Move-Item would have to know a common base path, but there is currently no way to specify one.
So I've created a helper function New-DestinationPath that can be chained in between Get-ChildItem and Move-Item (or Copy-Item). The function creates the destination directory structure and outputs the source path and the fully resolved destination path.
Function New-DestinationPath {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('Fullname', 'PSPath')]
[String] $Path,
[Parameter(Mandatory, Position=0)]
[String] $CommonPath,
[Parameter(Mandatory, Position=1)]
[String] $Destination
)
process {
# Temporarily change current directory -> base directory for Resolve-Path -Relative
Push-Location $CommonPath
try {
# Resolve input path relative to $CommonPath (current directory)
$relativePath = Resolve-Path $Path -Relative
}
finally {
Pop-Location # Restore current directory
}
# Resolve full target file path and directory
$targetPath = Join-Path $Destination $relativePath
$targetDir = Split-Path $targetPath -Parent
# Create target dir if not already exists (-Force).
$null = New-Item $targetDir -ItemType Directory -Force
# Output the full source and destination paths (for use with Copy-Item or Move-Item)
[PSCustomObject]#{
Path = $Path
Destination = $targetPath
}
}
}
Usage:
$searchPath = '\\?\E:\Network Shares\Commun'
Get-ChildItem -LiteralPath $searchPath -Recurse -File |
Where-Object Fullname -like '*(encode*' |
New-DestinationPath -CommonPath $searchPath -Destination 'C:\Users\Desktop\Conflits' -WhatIf |
Move-Item -LiteralPath { $_.Path } -Destination { $_.Destination } -WhatIf
By using the common parameter -WhatIf the functions only output what they would do. Remove both -WhatIf arguments when you have confirmed that the right paths are used.
In the Move-Item call we are using delay-bind scriptblocks to avoid a ForEach-Object command. Using the automatic variable $_ we can directly refer to the output of the previous command in the pipeline (New-DestinationPath).
I've finally have given up googling and come here out of desperation. Go easy on me I'm fairly new to Powershell.
So, the objective of the code below was to first look through the source folder, then read through each .zip file and move to the directory specified by the value in the hashtable. Unfortunately, this is not how they want it to work anymore.
Now I need to retain the parent folder from source: for example "DAL" and then create the proceeding folders based on the file names and finally move each .zip to its file specified folder. Also, it needs to go through each folder under source which will be at least 20 other folders with a unique 3 character names.
$srcRoot = "C:\Cloud\source\dal"
$dstRoot = "C:\Cloud\Destination"
##$map = #{}; dir -recurse | ? { !$_.psiscontainer} | % { ##$map.add($_.name,$_.PSChildName) }
# DAT and DEV will have to be excluded from folder creation
$map = {
#AEODDAT_201901 = "AEOD\2019\01"
#AEOMDEV_201902 = "AEOM\2019\01"
#AEOYDAT_201902 = "AEOY\2019\01"
}
$fileList = Get-ChildItem -Path $srcRoot -Filter "*.zip*" -File -Force -Recurse
foreach ($file in $fileList)
{
#Go through each file up to mapped string
$key = $file.BaseName.Substring(0,14)
if ($key -in $map.Keys)
{
$fileName = $file.Name
$dstDir = Join-Path -Path $dstRoot -ChildPath $map[$key]
#create direcotory if not in path
if (-not (Test-Path -Path $dstDir))
{
mkdir -Path $dstDir
}
Write-Verbose "Moving $($file.FullName)"
if (Test-Path -Path (Join-Path -Path $dstDir -ChildPath $fileName))
{
#Write error if name exists
Write-Error -Message "File $fileName already exists at $dstDir"
#move path
} else {
Move-Item -Path $($file.FullName) -Destination $dstDir
}
}
}
So C:\Cloud\source\DAL\AEODDAT20190101.zip should create folders in C:\Cloud\Destination\DAL\AEOD\2019\01\AEODDAT20190101.zip would be my desired output.
Welcome, Matt! (no pun intended) One of the habits I have in similar situations with destination folders is to Set-Location $dstRoot and create folders from the relative path. You can execute New-Item with the relative path and the syntax is simpler. For example, your If statement could look like this and it would work the same way (with a slightly different error message):
if ($key -in $map.Keys){
Set-Location $dstRoot
New-Item -ItemType Directory $map[$key] -ErrorAction Ignore #won't raise an error if it exists
Write-Verbose "Moving $($file.FullName)"
#this will raise an error if the file already exists, unless you specify -Force
Move-Item "$($file.FullName)" $map[$key]
}
EDIT: Found 2 issues.
$map is a Hashtable literal that should be preceded with #:
$map = #{
AEODDAT20190101 = "AEOD\2019\01"
You were missing the last character of the base file name by taking only the first 14 characters. AEODDAT2019010 didn't match AEODDAT20190101. This should fix it:
$key = $file.BaseName.Substring(0,15)
the paths are like this and none of these directory exist:
"D:\temp\test\abc.txt"
"D:\temp2\test2\abc2.txt"
"D:\temp1\abc.txt"
I am trying to split the path and create directories only.
I am trying below:
New-Item -Path "D:\temp\testing\abc.txt" -ItemType file
Split-Path -Path "D:\temp\testing\abc.txt" -Resolve –IsAbsolute
[edit - didn't see the comment by Tuttu. [*blush*] i will leave this here, but that one is the 1st answer.]
i think what you are looking for is the Split-Path cmdlet. [grin] something like this ...
$PathList = #(
'c:\temp\test1\abc.txt'
'c:\temp\test2\subtest2-1\abc2.txt'
'c:\temp\test3\subtest3-1\subtest3-1-1\abc.txt'
)
foreach ($PL_Item in $PathList)
{
$NewDir = Split-Path -Path $PL_Item -Parent
if (-not (Test-Path -LiteralPath $NewDir))
{
$Null = New-Item -Path $NewDir -ItemType Directory -Force
}
}
that made 3 new directories, two of them with sub-directories.
note that this does NOT take into account any input path that has no terminating file ... you will always get the parent path.
I have a pretty basic powershell copy script that copies items from a source folder to a destination folder. However this is moving way too much data, and I'd like to check if the filename already exists so that file can be ignored. I don't need this as complex as verifying created date/checksum/etc.
Currently it's along the lines of:
Copy-Item source destination -recurse
Copy-Item source2 destination2 -recurse
I'd imagine I need to add the Test-Path cmdlet, but I'm uncertain how to implement it.
You could always call ROBOCOPY from PowerShell for this.
Use the /xc (exclude changed) /xn (exclude newer) and /xo (exclude older) flags:
robocopy /xc /xn /xo source destination
This will ONLY copy those files that are not in the destination folder.
For more option type robocopy /?
$exclude = Get-ChildItem -recurse $dest
Copy-Item -Recurse $file $dest -Verbose -Exclude $exclude
While I agree that Robocopy is the best tool for something like this, I'm all for giving the customer what they asked for and it was an interesting PowerShell exercise.
This script should do just what you asked for: copy a file from Source to Destination only if it does not already exist in the Destination with a minimum of frills. Since you had the -recurse option in your example, that made for a bit more coding than just simply testing for the filename in the Destination folder.
$Source = "C:\SourceFolder"
$Destination = "C:\DestinationFolder"
Get-ChildItem $Source -Recurse | ForEach {
$ModifiedDestination = $($_.FullName).Replace("$Source","$Destination")
If ((Test-Path $ModifiedDestination) -eq $False) {
Copy-Item $_.FullName $ModifiedDestination
}
}
Building off of Wai Ha Lee's post, here's an example that worked for me:
$Source = "<your path here>"
$Dest = "<your path here>"
$Exclude = Get-ChildItem -recurse $Dest
Get-ChildItem $Source -Recurse -Filter "*.pdf" | Copy-Item -Destination $Dest -Verbose -Exclude $Exclude
This builds a list to exclude, then copies any pdf in the source directory and sub-directories to the destination in a single folder...excluding the existing files. Again, this is an example from my needs, but similar to yours. Should be easy enough to tweak to your hearts content.
Function Copy-IfNotPresent will accept one file at a time but it's easy to loop for all files you want to copy. Here's an example:
gci c:\temp\1\*.* -Recurse -File | % { Copy-IfNotPresent -FilePath $_ -Destination "C:\temp\2\$(Resolve-Path $_ -relative)" -Verbose }
Here's the function. It will generate the folder tree if necessary. Here's the gists link: https://gist.github.com/pollusb/cd47b4afeda8edbf8943a8808c880eb8
Function Copy-IfNotPresent {
<#
Copy file only if not present at destination.
This is a one file at a time call. It's not meant to replace complex call like ROBOCOPY.
Destination can be a file or folder. If it's a folder, you can use -Container to force Folder creation when not exists
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory)]
$FilePath,
[Parameter(Mandatory)]
[string]$Destination,
[switch]$Container,
[switch]$WhatIf
)
#region validations
if ($FilePath -isnot [System.IO.FileInfo]){
$File = Get-ChildItem $FilePath -File
} else {
$File = $FilePath
}
if (!$File.Count){
Write-Warning "$FilePath no file found."
return
} elseif ($File.Count -gt 1) {
Write-Warning "$FilePath must resolve to one file only."
return
}
#endregion
# Destination is a folder
if ($Container -or (Test-Path -Path $Destination -PathType Container)) {
if (!(Test-Path $Destination)) {
New-Item -Path $Destination -ItemType Container | Out-Null
}
$Destination += "\$($File.Name)"
}
# Destination is a file
if (!(Test-Path $Destination)) {
if ($WhatIf) {
Write-Host "WhatIf:Copy-IfNotPresent $FilePath -> $Destination"
} else {
# Force creation of parent folder
$Parent = Split-Path $Destination -Parent
if (!(Test-Path $Parent)) {
New-Item $Parent -ItemType Container | Out-Null
}
Copy-Item -Path $FilePath -Destination $Destination
Write-Verbose "Copy-IfNotPresent $FilePath -> $Destination (is absent) copying"
}
} else {
Write-Verbose "Copy-IfNotPresent $Destination (is present) not copying"
}
}
$source = "c:\source"
$destination = "c:\destination"
Create a list of files to exclude, i.e. files already existing in the destination.
$exclude = Get-Childitem -Recurse $destination | ForEach-Object { $_.FullName -replace [Regex]::Escape($destination ), "" }
Recursively copy all contents from the source to the destination excluding the previously collected files.
Copy-Item -Recurse -Path (Join-Path $source "*") -Destination $destination -Exclude $exclude -Force -Verbose
(Join-Path $source "*") add a wildcard at end ensuring that you get the children of the source folder instead of the source folder itself.
Force is used because I don't mind that there are already existing folders (results in error messages). Use with caution.
ForEach-Object { $_.FullName -replace [Regex]::Escape($destination ), "" } transforms the existing file full names into values which can be used as Exclude parameter
Here is a recursive script that syncronizes 2 folders ignoring existing files:
function Copy-FilesAndFolders([string]$folderFrom, [string]$folderTo) {
$itensFrom = Get-ChildItem $folderFrom
foreach ($i in $itensFrom)
{
if ($i.PSIsContainer)
{
$subFolderFrom = $folderFrom + "\" + $i.BaseName
$subFolderTo = $folderTo + "\" + $i.BaseName
Copy-FilesAndFolders $subFolderFrom $subFolderTo | Out-Null
}
else
{
$from = $folderFrom + "\" + $i.Name
$to = $folderTo + "\" + $i.Name
if (!(Test-Path $from)) # only copies non-existing files
{
if (!(Test-Path $folderTo)) # if folder doesn't exist, creates it
{
New-Item -ItemType "directory" -Path $folderTo
}
Copy-Item $from $folderTo
}
}
}
}
To call it:
Copy-FilesAndFolders "C:\FromFolder" "C:\ToFolder"
Lots of great answers in here, here's my contribution as it relates to keeping an mp3 player in sync with a music library.
#Tom Hubbard, 10-19-2021
#Copy only new music to mp3 player, saves time by only copying items that don't exist on the destination.
#Leaving the hardcoded directories and paths in here, sometimes too much variable substitution is confusing for newer PS users.
#Gets all of the albums in the source directory such as your music library
$albumsInLibrary = gci -Directory -path "C:\users\tom\OneDrive\Music" | select -ExpandProperty Name
#Gets all of the albums of your destination folder, such as your mp3 player
$albumsOnPlayer = gci -Directory -Path "e:\" | select -ExpandProperty name
#For illustration, it will list the differences between the music library and the music player.
Compare-Object -DifferenceObject $albumsInLibrary -ReferenceObject $albumsOnPlayer
#Loop through each album in the library
foreach ($album in $albumsInLibrary)
{
#Check to see if the music player contains this directory from the music library
if ($albumsOnPlayer -notcontains $album)
{
#If the album doesn't exist on the music player, copy it and it's child items from the library to the player
write-host "$album is not on music player, copying to music player" -ForegroundColor Cyan
Copy-Item -path "C:\users\Tom\OneDrive\music\$album" -Recurse -Destination e:\$album
}
}