Powershell script to copy old files and create shortcuts - powershell

I am working on creating a stubbing script using powershell. My intentions for this script is to copy any data that hasn't been written to in the past x time period to a new location, check that the file copied, create a shortcut to the "archived" file in it's original location, and eventually delete the old file (I haven't written this section yet). Below is what I have so far. The issue I am having now is that a shortcut is created however all of the shortcuts are saved to the C:\Temp directory and not in any subfolders; if that is where the original file was stored. I think my issue is with $link and I need to split the path's and join them but I am not sure. Any assistance is greatly appreciated!
# Variables
$Original = "C:\Temp"
$Archive = "\\data\users\Temp"
Get-ChildItem -Path $Original -Recurse |
Where-Object {
$_.LastWriteTime -lt [datetime]::Now.AddMinutes(-1)
} | Copy-Item -Destination $Archive -Recurse -force
$sourceFiles = Get-ChildItem $Original -Recurse | Where-Object { $_.LastWriteTime -lt [datetime]::Now.AddMinutes(-1) } | Select Name, Fullname
$destFiles = Get-ChildItem $Archive -Recurse | Select Name, Fullname
$Comparison = Compare-Object $sourceFiles.name $destFiles.name
If ($comparison.sideindicator -ne "<=") {
get-childitem $Archive -Recurse | where { $_.PsIsContainer -eq $false } | ForEach-Object {
$path = '"' + $_.FullName + '"'
$link = $Original + '\' + $_.Basename + '.lnk'
$wshell = New-Object -ComObject WScript.Shell
$shortcut = $wshell.CreateShortcut($link)
$shortcut.TargetPath = $path
$shortcut.Save()
}
}
If ($comparison.sideindicator -eq "<=") {
$comparison.inputobject, $sourceFiles.fullname | Out-File 'C:\ScriptLogs\stubbing.csv' -Force
}

This is the problematic code:
$link = $Original + '\' + $_.Basename + '.lnk'
Basename is only the filename portion without the path, so the .lnk files end up directly in the top-level directory.
You have to use the relative path like this:
$RelativePath = [IO.Path]::GetRelativePath( $Archive, $_.FullName )
$link = [IO.Path]::ChangeExtension( "$Original\$RelativePath", 'lnk' )
This requires .NET 5 for GetRelativePath. To support older .NET versions you can use:
Push-Location $Archive
try { $RelativePath = Resolve-Path -Relative $_.FullName }
finally { Pop-Location }
Resolve-Path -Relative uses the current location as the base base. So we use Push-Location to temporarily change the current location. The try / finally is used to ensure restoring of the original location even in case Resolve-Path throws an exception (e. g. when you have set $ErrorActionPreference = 'Stop').
Although the code might work like this, I think it could be refactored like this:
A single Get-ChildItem call to store the files to be moved into an array (your current $sourceFiles = ... line)
A foreach loop over this array to move each file and create the shortcut.
I think the Compare-Object isn't even necessary, but maybe I'm missing something.
Currently you are iterating over the same directories multiple times, which isn't very efficient and a lot of duplicate code.

Related

Can't Move-Item Modified file

I was trying to follow the solution given by #StephenP in this post:
Renaming and Moving Files Powershell
I am trying to Move a renamed file to the Output Folder, but it didn't move.
What could go wrong?
Here's my code:
$Files = GCI "$ParentFolder" | ?{$_.Extension -Match "png?"}
$Date = Get-Date -Format "yyyymmddhhmmss"
$Dest = ".\Output"
$Files | ForEach-Object {
# Get the File BaseName and Select the Screen Title only
$FileName = $_.BaseName
$NameCount = $FileName.length
$ScreenTitle = $FileName.substring(0,$NameCount -21)
# Set the New File Name as Variable
$NewFileName = "$($Date)_[$($ScreenTitle)]"
# Start Renaming
$GetName = $_.FullName -replace "$FileName","$NewFileName"
Rename-Item $_ $GetName
# Move the renamed file
Move-Item $GetName -Destination $Dest
}
Thank you for helping :)
First of all, you don't need to rename the file first and then move, because you can do this using Move-Item at the same time.
Use -Filter '*.png' instead of a Where-Object afterwards. The Filter is much more efficient.
Your code does not check if the length of the file BaseName is actually more than 21 characters long, so this $FileName.Substring(0, $NameCount -21) can throw exceptions. However, since you didn't provide any filename examples, I left that in.
Try
$Files = Get-ChildItem -Path $ParentFolder -Filter '*.png' -File
$Date = Get-Date -Format "yyyymmddhhmmss"
$Dest = ".\Output"
$Files | ForEach-Object {
# Get the File BaseName and Select the Screen Title only
$FileName = $_.BaseName
$NameCount = $FileName.Length
# very tricky this.. could throw error if $FileName is less than 21 characters..
$ScreenTitle = $FileName.Substring(0, $NameCount -21)
# Set the New File Name as Variable
$NewFileName = '{0}_[{1}]{2}' -f $Date, $ScreenTitle, $_.Extension
# Move the file with a new name to the destination
$_ | Move-Item -Destination (Join-Path -Path $Dest -ChildPath $NewFileName)
}
As aside, using square brackets in filenames could cause you problems and to do more PowerShell on these files, you need to always remember to use -LiteralPath instead of -Path on cmdlets that support it like Get-ChildItem

Fix script to allow a scanning of all fields for every row to find if that field contains a target folder name

This is an deepening extension to solve a previously question
Since the input csv file is inconsistent, I cannot use that in this way.
The folder to move to is not always in the same column, so any code trying to use that as input will hit the problem of the value not corresponding to the folder I want to move to. It simply reads garbage ("spam") where it expects the folder name.
The only way to do it is by examining all fields for every row to find if that field contains a target folder name you can use. That means a LOT of Test-Path lines
This is the incriminated code part
Foreach ($fileName in $_.Group.FileName) {
$ValidFileName = $filename -replace $invalid
$targetFile = Join-Path -Path $TargetFolder -ChildPath $fileName
Error is explicitly telling it to move those folders when I iterate through the FileName column. Foreach ($fileName in $_.Group.FileName) {.. this is reading: (1959 10) Showcase Presents n 22,(1959 12) Showcase Presents n 23,alfa, da definire.
My request: Since that examination of all fields for every row is required so I ask for a code editing of this script. Also if this means a LOT of Test-Path lines I suppose that there is no alternative.
However this script below don't create folders and move anything so my request is to try to fix it
$csvpath = 'C:\temp\temp.csv'
$invalid = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join ''))
$sourcePath = 'C:\temp\'
Import-Csv C:\temp\temp.csv -Header Title,FileName,Link -Delimiter ';' |
Group-Object Title |
Foreach {
# I prefer to add a trailing slash to folder names
$TargetFolder = Join-Path -Path $sourcePath -ChildPath (($_.Name -replace $invalid)+'\')
# We don't have to create the new folders, because -Force will create them for us
Foreach ($fileName in $_.Group.FileName) {
$ValidFileName = $filename -replace $invalid
$targetFile = Join-Path -Path $TargetFolder -ChildPath $fileName
# Write your values to the console - Make sure the folder is what it should be
Write-Output "Moving '$targetFile' to '$TargetFolder'"
Move-Item $targetFile $TargetFolder -Force -WhatIf
}
}
I was hesitant to even comment on this since you're just not taking any of our advise on all the previoous posts, and just repeating what we say in a new question. We are here to help with your code, and not write it for you but, learning isn't a crime so I will just assume you're just unfamiliar with powershell in general.
$csvpath = 'C:\tomp\temp.csv'
#$invalid = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join ''))
$sourcePath = 'C:\tomp\'
Import-Csv $csvpath -Header Title,FileName,Link -Delimiter ',' |
Group-Object Title |
ForEach-Object `
-Begin {
$FoldersLocation = Get-ChildItem -Path C:\tomp -Directory
$Source_Folder = ($FoldersLocation.Parent.FullName | Select-Object -Unique)
#Create array list to hold both columns
[System.Collections.ArrayList]$CombinedRows = #()
} `
-Process {
# Create folder if it doesn't exist
$Destination = Join-Path -Path $Source_Folder -ChildPath $_.Name
if ((Test-Path -Path $Destination) -eq $false) {
"Created: $Destination"
#$null = New-Item -Path ($FoldersLocation.Parent.FullName | Select -Unique) -Name $_.Name -ItemType Directory
}
#region Combine two columns
Foreach ($fileName in $_.Group.FileName) {
$null = $CombinedRows.Add($fileName)
}
Foreach ($link in $_.Group.Link) {
$null = $CombinedRows.Add($link)
}
#endregion
} `
-End {
Foreach ($name in $FoldersLocation) {
if ($name.Name -in $CombinedRows) {
if ($name.Name -match "Showcase"){
# No need for output when supplying the -verbose switch
"Moving: '$($Name.FullName)' to 'C:\tomp\Lanterna Verde - Le Prime Storie'"
# Move-Item -Path $Name.FullName -Destination "C:\tomp\Lanterna Verde - Le Prime Storie"
}
else {
# No need for output when supplying the -verbose switch
"Moving: '$($Name.FullName)]' to 'C:\tomp\Batman DC'"
# Move-Item -Path $Name.FullName -Destination "C:\tomp\Batman DC"
}
}
}
}
All it comes down to is control of flow logic. If a certain condition is met, then do this, or do something else if its not. Added very few inline comments as most of it pretty self explanatory.
I recommend reading up on some powershell instead of hoping others could do everything for you.
Powershell

How to unzip all files in folder? (Not .zip extension)

Currently, I am writing a script that moves PDF files that are wrongfully zipped to a certain folder. I have achieved that. The next thing I need to get to work, is that the zipped .pdf files get unzipped into a different folder.
This is my whole script. Everything except for the last 2 lines is dedicated to finding the PDF files that are zipped and moving them.
In the first parts, the script checks the first few bytes of every pdf file in the folder. If they start with "PK*", they are zip files and get moved to the zipped folder.
For every PDF/zip file, there is one associated HL7 file in the folder next to it.
These also need to get moved to the same folder. From there the zip files need to be unzipped and relocated to "unzipped"
The last 2 lines are for unzipping.
$pdfDirectory = 'Z:\Documents\16_Med._App\Auftraege\PDFPrzemek\struktur_id_1225\ext_dok'
$newLocation = 'Z:\Documents\16_Med._App\Auftraege\PDFPrzemek\Zip'
Get-ChildItem "$pdfDirectory" -Filter "*.pdf" | foreach {
if ((Get-Content $_.FullName | select -First 1 ) -like "PK*") {
$HL7 = $_.FullName.Replace("ext_dok","MDM")
$HL7 = $HL7.Replace(".pdf",".hl7")
move $_.FullName $newLocation;
move $HL7 $newLocation
}
}
Get-ChildItem 'Z:\Documents\16_Med._App\Auftraege\PDFPrzemek\Zip' |
Expand-Archive -DestinationPath 'Z:\Documents\16_Med._App\Auftraege\PDFPrzemek\Zip\unzipped' -Force
This, sadly, doesn't work.
I suspect that it's because these files dont have the .zip extension. The only Filter that works for Expand-Archive is .zip.
So I need to find a way to get this function to unzip the files, even though they dont have the fitting extension...
Like #Ansgar said this would be the way to go:
Param (
$SourcePath = 'C:\Users\xxx\Downloads\PDF',
$ZipFilesPath = 'C:\Users\xxx\Downloads\ZIP',
$UnzippedFilesPath = 'C:\Users\xxx\Downloads\Unzipped'
)
$VerbosePreference = 'Continue'
#region Test folders
#($SourcePath, $ZipFilesPath, $UnzippedFilesPath) | Where-Object {
-not (Test-Path -LiteralPath $_)
} | ForEach-Object {
throw "Path '$_' not found. Make sure that the folders exist before running the script."
}
#endregion
#region Get all files with extension .pdf
$Params = #{
Path = Join-Path -Path $SourcePath -ChildPath 'ext_dok'
Filter = '*.pdf'
}
$PDFfiles = Get-ChildItem #Params
Write-Verbose "Got $($PDFfiles.count) files with extension '.pdf' from '$($Params.Path)'"
#endregion
#region Move PDF and HL7 files
$MDMpath = Join-Path -Path $SourcePath -ChildPath 'MDM'
foreach ($PDFfile in ($PDFfiles | Where-Object {
(Get-Content $_.FullName | Select-Object -First 1) -like 'PK*'})
) {
$MoveParams = #{
Path = $PDFfile.FullName
Destination = Join-Path -Path $ZipFilesPath -ChildPath ($PDFfile.BaseName + '.zip')
}
Move-Item #MoveParams
Write-Verbose "Moved file '$($MoveParams.Path)' to '$($MoveParams.Destination)'"
$GetParams = #{
Path = Join-Path -Path $MDMpath -ChildPath ($PDFfile.BaseName + '.hl7')
ErrorAction = 'Ignore'
}
if ($HL7file = Get-Item #GetParams) {
$MoveParams = #{
Path = $HL7file
Destination = $ZipFilesPath
}
Move-Item #MoveParams
Write-Verbose "Moved file '$($MoveParams.Path)' to '$($MoveParams.Destination)$($HL7file.Name)'"
}
}
#endregion
#region Unzip files
$ZipFiles = Get-ChildItem -Path $ZipFilesPath -Filter '*.zip' -File
foreach ($ZipFile in $ZipFiles) {
$ZipFile | Expand-Archive -DestinationPath $UnzippedFilesPath -Force
Write-Verbose "Unzipped file '$($ZipFile.Name)' in folder '$UnzippedFilesPath'"
}
#endregion
Some tips:
Add a Param () clause at the beginning of the script to contain all your variables that can change.
Try to use the full parameter name to clearly indicate what is what. Use Get-ChildItem -Path xxx instead of Get-ChildItem xxx.
Use hash tables for long parameters. This makes the code more compact in width and more easily to read.
Use #region and #endregion to group your code.

Using Powershell how can I bulk rename files recursively and include a counter in the new name?

I have a project where I need to search for multiple pdfs within multiple subfolders which contain a specific string and then rename them using a counter variable. I'm a pretty novice powershell user but I think I've got it pretty close. The only thing that is not working is the counter. The counter variable never changes. This is what I have so far for code:
$prefix = "sample"
$id = 1
Get-ChildItem -Filter "*combined*" -Recurse | Rename-Item -NewName ($prefix + ($id.tostring("000")) + '_regular_' + ($id.tostring("000")) + '.pdf' -f $id++ )
What am I missing? Should I be using a 'foreach' loop?
Yes you need to use a ForEach loop. This will go through and do the renames, ensuring that any files found in subdirectories are preserved in those directories after renaming
Get-ChildItem -Filter "*combined*" -Recurse | %{
$fullId = $id.toString("000")
$curPath = Split-Path $_.FullName
Rename-Item $_.fullname -NewName "$curPath\$($prefix)$($fullId)_regular_$($fullId).pdf" )
$id += 1
}
Here is my edited version of the above code:
$id = 1
Get-ChildItem -Filter "*combined*" -Recurse | %{
$fullId = $id.toString("000")
$curPath = Split-Path $_.FullName
Rename-Item $_.fullname -NewName "$curPath\$($prefix)_$($fullId)_regular_$($fullId).pdf"
$id++
}

Access a PowerShell Variable Immediately After Mutation

I have a simple PowerShell script that bulk renames files and I'd like to have a table output that shows 'Old File Name' and 'New File Name' to show the changes. How do I access/reference the new file name while still inside the 'ForEach-Object' loop, assuming it can be done? Can I refresh that particular value of $_? I don't want to just use my variable $newname because I created that and since this is supposed to be output that shows the file names were actually changed, I'd like to access the file's new name from the system as determined by the script.
Write-Host "Old Name `t`t`t`t`t`t New Name"
Write-Host "-------- `t`t`t`t`t`t --------"
$files = Get-ChildItem -Path C:\Users\<user>\Desktop\Config
$files | % ({
if ((!$_.PsIsContainer) -and ($_.Extension -eq '')) {
$oldname = $_.FullName
$newname = "$($_.FullName).cfg"
Rename-Item $oldname $newname
# $_.Name still refers to the old file name without the extension
# How do I immediately access its new name (including extension)?
Write-Host $_.Name `t`t`t`t`t`t $newname
}
})
You can do a Get-Item on $newname:
(Get-Item $newname).Name
I have taken the liberty to rewrite your Script a little, using a Hash Table and New-Object.
To explain what I'm doing here, I will split it a bit up for you.
First I declar some Variables, I'm gonna use in my foreach loop, with this:
Note: -File parameter, is so it will only take file names, if you like to rename everything, files and folder, then remove it.
$files = Get-ChildItem -Path "C:\Users\dha\Desktop\PS test folder" -File
$OldFiles = $files
[int]$i = "0"
Then I'm taking each file one by one, and creating the new name in:
$Newname = ("Blop" + "$i" + ".txt")
And then take the file info for one file, and pipe it to rename-item here
$File | Rename-Item -NewName $Newname
The $ixx is just a plusser so increase the file number by one for each time the foreach loop runs.
Then I'm writing a Hash Table with $Old_And_New variable.
Last, I'm creating a Object with New-object and outputting it.
I Hope this help's and my explanation is understandable.
To script assembled:
$files = Get-ChildItem -Path "C:\Users\dha\Desktop\PS test folder" -File
$OldFiles = $files
[int]$i = "0"
foreach ($File in $files)
{
$Newname = ("Blop" + "$i" + ".txt")
$File | Rename-Item -NewName $Newname
$i++
$Old_And_New = #{
'Old File Name' = "$OldFiles";
'New File Name' = "$Newname"
}
$obj = New-Object -TypeName PSObject -Property $Old_And_New
Write-Output $obj
}