I am writing a function in PowerShell 7 that flattens a directory.
It's ideally supposed to:
Copy / Move everything to a temp directory (Depending on whether a destination was supplied)
Rename all files that have identical filenames with a _XX numerical suffix (Padding controlled by a parameter)
Move everything back to the root of the original directory, or the destination directory supplied.
Here is the gist.
Here is the relevant code, without documentation to save space as it's a long one:
function Merge-FlattenDirectory {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
[ValidateScript({
if (!(Test-Path -LiteralPath $_)) {
throw [System.ArgumentException] "Path does not exist."
}
if ((Test-IsSensitiveWindowsPath -Path $_ -Strict).IsSensitive) {
throw [System.ArgumentException] "Path supplied is a protected OS directory."
}
return $true
})]
[Alias("source", "input", "i")]
[string]
$SourcePath,
[Parameter(Mandatory = $false, Position = 1, ValueFromPipelineByPropertyName)]
[Alias("destination", "dest", "output", "o")]
[string]
$DestinationPath = $null,
[Parameter(Mandatory=$false)]
[Switch]
$Force,
[Parameter(Mandatory = $false, ValueFromPipelineByPropertyName)]
[ValidateSet(1, 2, 3, 4, 5)]
[int32]
$DuplicatePadding = 2
)
begin {
# Trim trailing backslashes and initialize a new temporary directory.
$SourcePath = $SourcePath.TrimEnd('\')
$DestinationPath = $DestinationPath.TrimEnd('\')
$TempPath = (New-TempDirectory).FullName
New-Item -ItemType Directory -Force -Path $TempPath
# Escape $SourcePath so we can use wildcards.
$Source = [WildcardPattern]::Escape($SourcePath)
# If there is no $DestinationPath supplied, we flatten only the SourcePath.
# Thus, set DestinationPath to be the same as the SourcePath.
if (!$DestinationPath) {
$DestinationPath = $SourcePath
# Since there is no destination supplied, we move everything to a temporary
# directory for further processing.
Move-Item -Path $Source'\*' -Destination $TempPath -Force
}else{
# We need to perform some parameter validation on DestinationPath:
# Make sure the passed Destination is not a file
if(Test-Path -LiteralPath $DestinationPath -PathType Leaf){
throw [System.IO.IOException] "Please provide a valid directory, not a file."
}
# Make sure the passed Destination is a validly formed Windows path.
if(!(Confirm-ValidWindowsPath -Path $DestinationPath -Container)){
throw [System.IO.IOException] "Invalid Destination Path. Please provide a valid directory."
}
# Make sure the passed Destination is not in a protected or sensitive OS location.
if((Test-IsSensitiveWindowsPath -Path $DestinationPath -Strict).IsSensitive){
throw [System.IO.IOException] "The destination path is, or resides in a protected operating system directory."
}
# Since a destination was supplied, we copy everything to a new temp directory
# instead of moving everything. We want the source directory to remain untouched.
# Robocopy seems to be the most performant here.
# Robocopy on Large Dataset: ~789ms - ~810ms
# Copy-Item on Large Dataset: ~1203ms - ~1280ms
#
# Copy-Item -Path $Source'\*' -Destination $TempPath -Force -Recurse
Robocopy $Source $TempPath /COPYALL /B /E /R:0 /W:0 /NFL /NDL /NC /NS /NP /MT:48
# Create the destination directory now, ready for population in the process block.
New-Item -ItemType Directory -Force -Path $DestinationPath
}
# Grab all files as an Array of FileInfo Objects
$AllFiles = [IO.DirectoryInfo]::new($TempPath).GetFiles('*', 'AllDirectories')
# Initialize hashtable to store duplicate files
$Duplicates = #{}
}
process {
##
# $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
#
# Iterate over all files
foreach ($File in $AllFiles) {
# If our $Duplicates hashtable already contains the current filename, we have a duplicate.
if ($Duplicates.Contains($File.Name)) {
# Rename the duplicate file by appending a numerical index to the end of the file.
$PathTemp = Get-ItemProperty -LiteralPath $File
$RenamedFile = Rename-Item -LiteralPath $PathTemp.PSPath -PassThru -NewName ('{0}_{1}{2}' -f #(
$File.BaseName
$Duplicates[$File.Name].ToString().PadLeft($DuplicatePadding, '0')
$File.Extension
))
# Increment the duplicate counter and pass $File down to be moved.
$Duplicates[$File.Name]++
$File = $RenamedFile
} else {
# No duplicates were detected. Add a value of 1 to the duplicates
# hashtable to represent the current file. Pass $File down to be moved.
$PathTemp = Get-ItemProperty -LiteralPath $File
$Duplicates[$File.Name] = 1
$File = $PathTemp
}
# If Force is specified, we don't have to worry about duplicate files,
# as the operation will overwrite every file with a duplicate filename
if($Force){
# Move the file to its appropriate destination. (Force)
Move-Item -LiteralPath $File -Destination $DestinationPath -Force
} else {
try {
# Move the file to its appropriate destination. (Non-Force)
Move-Item -LiteralPath $File -Destination $DestinationPath -ErrorAction Stop
} catch {
# Warn the user that files were skipped because of duplicate filenames.
Write-Warning "File already exists in the destination folder. Skipping this file."
}
}
# Return each file to the pipeline.
# $File
}
# $Stopwatch.Stop()
# Write-Host "`$Stopwatch.Elapsed: " $Stopwatch.Elapsed -ForegroundColor Green
# Write-Host "`$Stopwatch.ElapsedMilliseconds:" $Stopwatch.ElapsedMilliseconds -ForegroundColor Green
# Write-Host "`$Stopwatch.ElapsedTicks: " $Stopwatch.ElapsedTicks -ForegroundColor Green
}
end {
}
}
# Merge-FlattenDirectory "C:\Users\username\Desktop\Testing\Test" "C:\Users\username\Desktop\Testing\TestFlat" -Force
The function works great for the most part, but there is a major problem I didn't anticipate. The code is vulnerable to naming collisions. Here's a problematic directory structure:
(Root directory to be flattened is C:\Users\username\Desktop\Testing\Test)
Directory: C:\Users\username\Desktop\Testing\Test
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 11/4/2021 10:03 PM 1552565 1088_p_01.jpg
-a--- 11/4/2021 10:03 PM 1552565 1088_p_02.jpg
-a--- 11/4/2021 10:03 PM 1552565 1088_p_03.jpg
Directory: C:\Users\username\Desktop\Testing\Test\Folder
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 11/4/2021 10:03 PM 1552565 1088_p_03.jpg
-a--- 11/4/2021 10:03 PM 1552565 1088_p.jpg
Directory: C:\Users\username\Desktop\Testing\Test\Testing
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 11/4/2021 10:03 PM 1552565 1088_p_01.jpg
-a--- 11/4/2021 10:03 PM 1552565 1088_p.jpg
If I run the function to flatten C:\Users\username\Desktop\Testing\Test I get only six files instead of seven in the destination folder. The folder is missing the second 1088_p.jpg. I can verify this by going to my temp directory and looking at what's left:
C:\Users\username\AppData\Local\Temp\DdtElMvSoXbJf\Testing\1088_p.jpg is still in temp.
Anyway, If you're still with me after all this, I thank you generously for reading.
I really need to refactor the function in a way that accounts for this edge-case and I can't figure out how to do it at all gracefully. I could desperately use some help or guidance from someone that can point me in the right direction. I've been working on this function for awhile now and I'd really like to wrap it up.
Many, many thanks.
Edit:
I have a working solution now. I added an additional layer of duplication checks, and moved the actual renaming of the file further down.
Here's the revised code (Only relevant portion included):
# Iterate over all files
foreach ($File in $AllFiles) {
# If our $Duplicates hashtable already contains the current filename, we have a duplicate.
if ($Duplicates.Contains($File.Name)) {
# Create a new name for the file by appending a numerical index to the end of the filename.
$PathTemp = Get-ItemProperty -LiteralPath $File
$NewName = ('{0}_{1}{2}' -f #(
$File.BaseName
$Duplicates[$File.Name].ToString().PadLeft($DuplicatePadding, '0')
$File.Extension
))
# Check if our new name collides with any other filenames in $Duplicates. If so, create
# another new name by appending an additional numeric index to the end of the filename.
$DuplicateCount = 1
while ($Duplicates[$NewName]) {
$NewName = ('{0}_{1}{2}' -f #(
[System.IO.Path]::GetFileNameWithoutExtension($NewName)
$DuplicateCount.ToString().PadLeft($DuplicatePadding, '0')
[System.IO.Path]::GetExtension($NewName)
))
Write-Warning $DuplicateCount.ToString().PadLeft($DuplicatePadding, '0')
$DuplicateCount++
# If we're at a depth of 8, throw. Something is obviously wrong.
if ($DuplicateCount -ge 8) {
throw [System.Exception] "Duplicate count reached limit."
break
}
}
# Finally, rename the file with our new name.
$RenamedFile = Rename-Item -LiteralPath $PathTemp.PSPath -PassThru -NewName $NewName
# Increment the duplicate counters and pass $File down to be moved.
$Duplicates[$File.Name]++
$Duplicates[$NewName]++
$File = $RenamedFile
} else {
# No duplicates were detected. Add a value of 1 to the duplicates
# hashtable to represent the current file. Pass $File down to be moved.
$PathTemp = Get-ItemProperty -LiteralPath $File
$Duplicates[$File.Name] = 1
$File = $PathTemp
}
# If Force is specified, we don't have to worry about duplicate files,
# as the operation will overwrite every file with a duplicate filename
if($Force){
# Move the file to its appropriate destination. (Force)
Move-Item -LiteralPath $File -Destination $DestinationPath -Force
} else {
try {
# Move the file to its appropriate destination. (Non-Force)
Move-Item -LiteralPath $File -Destination $DestinationPath -ErrorAction Stop
} catch {
# Warn the user that files were skipped because of duplicate filenames.
Write-Warning "File already exists in the destination folder. Skipping this file."
}
}
# Return each file to the pipeline.
$File
}
Related
I want to move all images in a directory, including subdirectories, to a new location while maintaining the existing folder structure.
Following the example, here, I put the objects into a variable, like so:
$picMetadata = Get-FileMetaData -folder (Get-childitem K:\myImages -Recurse -Directory).FullName
The move must be based on the results of a logical expression, such as the following for example.
foreach ($test01 in $picMetadata) {
if ($test01.Height -match "^[0-9]?[0-9] ") {
Write-Host "Test01.Height:" $test01.Height
}
}
Still at an early testing phase So far, I'm having no success even testing for the desired files. In the example above, I thought this simple regex test might provide for anything from "1 pixels" to "99 pixels", which would at least slim down my pictures collection (e.g. an expression without the caret, like "[0-9][0-9] " will return "NN pixels" as well as "NNN Pixels", "NNNNNN pixels", etc.)
Once I figure out how to find my desired images based on a logical, image object dimensions test, I will then need to create a script to move the files. Robocopy /MOV would be nice, but i'm probably in over my head already.
I was going to try to base it on this example (which was provided to a User attempting to COPY (not move / copy/delete) *.extension files). Unfortunately, such a simple operation will not benefit me, as I wish to move .jpg,.png,.gif, etc, based on dimensions not file extension:
$sourceDir = 'K:\myImages\'
$targetDir = ' K:\myImages_psMoveTest\'
Get-ChildItem $sourceDir -filter "*" -recurse | `
foreach{
$targetFile = $targetDir + $_.FullName.SubString($sourceDir.Length);
New-Item -ItemType File -Path $targetFile -Force;
Copy-Item $_.FullName -destination $targetFile
}
Perhaps you have a powershell script that could be used for my intended purpose? I'm just trying to move smaller images out of my collection, without having to overwrite same name images, and lose folder structure, etc.
Thank you very much for reading, and any advisory!
(Edit: Never opposed to improving Powershell skill, if you are aware of a freeware software which would perform this operation, please advise.)
If I understand your question correctly, you want to move image files with a pixel height of 1 up to 99 pixels to a new destination folder, while leaving the subfolder structure intact.
If that is true, you can do:
# needed to use System.Drawing.Image
Add-Type -AssemblyName System.Drawing
$sourceDir = 'K:\myImages'
$targetDir = 'K:\myImages_psMoveTest'
Get-ChildItem $sourceDir -File -Recurse | ForEach-Object {
$file = $_.FullName # need this for when we hit the catch block
try {
# Open image file to determine the pixelheight
$img = [System.Drawing.Image]::FromFile($_.FullName)
$height = $img.Height
# dispose of the image to remove the reference to the file
$img.Dispose()
$img = $null
if ($height -ge 1 -and $height -le 99) {
$targetFolder = Join-Path -Path $targetDir -ChildPath $_.DirectoryName.Substring($sourceDir.Length)
# create the target (sub) folder if it does not already exist
$null = New-Item -Path $targetFolder -ItemType Directory -Force
# next move the file
$_ | Move-Item -Destination $targetFolder -ErrorAction Stop
}
}
catch {
Write-Warning "Error moving file '$file': $($_.Exception.Message)"
}
}
I just want to know how to convert an images folder into a CBZ file READABLE for my Ebook (I checked the ebook and she can read this format).
Optimally, I would like to convert it without having to install everything. Just a script.
For those who are fast, I already answered my question... Just sharing it.
GO SEE UPDATE PART
Assuming your OS is Windows, we can do it with Batch or PowerShell.
For this one its process is quite easy, we just need to understand that a CBZ file IS a ZIP file with images in it. So we will just:
zip with 7zip because for some reasons the files converted with WinRAR didn't worked in my Ebook (wasn't even in the library) ;
Change the extension from .zip to .cbz.
I'm only going to show the PowerShell one because the .bat script had known issues.
Architecture
Architecture of the directory
The architecture should be:
My first folder
first image
second image
My second folder
first image
second image
PowerShell
Here's the code from my "#_ImagesFolderToCBZ.ps1"
Clear-Host
# INPUT - Script folder path
$i = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
# 7Zip path
$7zipPath = "$env:ProgramFiles\7-Zip\7z.exe"
# A list of the path of the zipped files
$listOfZippedFiles = New-Object Collections.Generic.List[String]
$listOfZippedNames = New-Object Collections.Generic.List[String]
# Ask the user if we delete the folders after their conversion
$isSdel = Read-Host -Prompt "Do you want to delete the folders after conversion [Y/N]: "
# Get the files inside the INPUT path and forEach children
Get-ChildItem "$i" | ForEach-Object {
# Get the full path of the file
$pathFolder = $_.FullName
# If the path here is a folder
if (Test-Path -Path "$pathFolder" -PathType Container) {
# Set the alias
Set-Alias 7z $7zipPath
# If the user asked for deletion of folders
if ("Y" -eq $isSdel.ToUpper()) {
# Zip the content of the folder
7z a "$pathFolder.zip" "$pathFolder\*" -sdel | FIND "ing archive"
}
else {
# Zip the content of the folder
7z a "$pathFolder.zip" "$pathFolder\*" | FIND "ing archive"
}
# Add the file name into the list
$listOfZippedFiles.Add("$pathFolder.zip")
$listOfZippedNames.Add("$_.zip")
}
# If the user asked for deletion of folders
if ("Y" -eq $isSdel) {
# Remove the now blank folders
if( $_.psiscontainer -eq $true){
if((gci $_.FullName) -eq $null){
$_.FullName | Remove-Item -Force
}
}
}
}
# For each zipped file
foreach ($file in $listOfZippedFiles) {
# Change the extension to CBZ
$dest = [System.IO.Path]::ChangeExtension("$file",".cbz")
Move-Item -Path "$file" -Destination $dest -Force
}
# Write for the user
Write-Host "`r`n`r`nConverted:"
# Displaying the converted files by their names
foreach ($file in $listOfZippedNames) {
$newName = [System.IO.Path]::ChangeExtension("$file",".cbz")
Write-Host "-- $newName"
}
# Blank line
Write-Host ""
# Pause to let us see the result
Pause
Output
output
As we can see, the folder is sucessfully created AND without loops like : I have ZIP files in the root folder of the script and they are also renamed into CBZ ones (I had this loop for my batch script).
I also added the choice to automatically delete the converted folders OR not.
Obviously, there's room for improvements (especially in how we delete the folders). I'll gladly take any advice.
UPDATE
I updated my script and it's much better. Less instructions, a list (in the prompt) that update itself when each folder is really converted. So no more: 1) ZIP all folders 2) rename their extension.
So a code that's more logic and also useful to show a beautiful process in real time.
Here's the updated code :
Clear-Host
# ROOT - Script folder path
$root = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
# 7Zip path
$7zipPath = "$env:ProgramFiles\7-Zip\7z.exe"
# Test if 7zip is installed
if (-not (Test-Path -Path $7zipPath -PathType Leaf)) {
throw "7 zip file '$7zipPath' not found"
}
# Ask the user if we delete the folders after their conversion
$isSdel = Read-Host -Prompt "Do you want to delete the folders after conversion [Y/N]: "
# Write for the user
Write-Host "`r`nConverted:"
# Get the files inside the INPUT path and forEach children
Get-ChildItem "$root" | ForEach-Object {
# Get the full path of the file
$pathFolder = $_.FullName
# If the path here is a folder
if (Test-Path -Path "$pathFolder" -PathType Container) {
# If the user asked for deletion of folders
if ("Y" -eq $isSdel.ToUpper()) {
# Zip the content of the folder while deleting the files zipped
& $7zipPath a "$pathFolder.zip" "$pathFolder\*" -sdel > $null
# Remove the now blank folder
if( $_.psiscontainer -eq $true){
if((gci $_.FullName) -eq $null){
$_.FullName | Remove-Item -Force
}
}
}
else {
# Zip the content of the folder
& $7zipPath a "$pathFolder.zip" "$pathFolder\*" > $null
}
# Change the extension to CBZ
$newName = [System.IO.Path]::ChangeExtension("$pathFolder.zip",".cbz")
Move-Item -Path "$pathFolder.zip" -Destination $newName -Force
# Tells the user this one is finished converting
Write-Host "--" -ForegroundColor DarkGray -NoNewline
Write-Host " $_.cbz"
}
}
# Tells the user it's finished
Write-Host "`r`nFinished`r`n" -ForegroundColor Green
# Pause to let us see the result
Pause
UPDATE 2
I made a GitHub project for this one. The URL is here:
https://github.com/PonyLucky/CBZ-Manga-Creator.
I wrote a PowerShell utility that takes in a couple parameters, and transfers files from a source directory to a destination directory.
Initially, all was done as a single function, and worked well enough.
Before adding some features, I broke repeated logic into its own function.
Then, the ISSUES began.
It appears that the Param() variables are seeded with incorrect values. Running the script yields the following:
PS ...> .\photoTransfer.ps1 E:\DCIM\100OLYMP
Cannot convert value "" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as
$True, $False, 1 or 0.
At C:\Users\SWPhantom\Desktop\admin\photoTransfer.ps1:85 char:3
+ [Parameter(Mandatory=$true, Position = 0, HelpMessage = "The path o ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : MetadataError: (:) [], ArgumentTransformationMetadataException
+ FullyQualifiedErrorId : RuntimeException
I can confirm that something's strange with
Write-Output "src: $source", which spits out src: True. (Expected to be src: E:\DCIM\100OLYMP)
HOWEVER: I can get the value I expect to be passed in with an $args[0].
I expect that the issue is simple, but I can't pick up on it, as this was my first foray into more... mature PowerShell scripting.
I am getting around the immediate problem by using the $args[i] method, but it'd be nice to not get an error message and use the seemingly Nice and orderly Params. (Especially since they seemed to work before I made the separate Transfer function).
Thanks!
Full code:
# Purpose: Transfer all photos from a memory card, to a destination folder, organized by year, month, date.
# Ensure that the Date Modified and Date Created is preserved.
function Transfer {
Param(
[Parameter(Mandatory, Position = 0)]
[string]$src,
[Parameter(Mandatory, Position = 1)]
[string]$dst,
[Parameter(Mandatory, Position = 2)]
[string]$extension
)
# Look at the source directory. Enumerate files to be sent over. (Only copy .ORF/.MOV files)
$files = Get-ChildItem -Path $src -Include $extension -Recurse
$numberOfFiles = $files.Count
if($numberOfFiles -eq 0) {
return "No $extension files found in $src!"
}
# Give user last chance to stop program. Show them number of files and destination folder.
Write-Output "Ensure the action is correct:"
read-host "Copying $numberOfFiles files from $src to $dst ?`nPress Enter to continue"
# Iteration for progress tracking.
$iter = 1
# Foreach file, check the Date Modified field. Make sure the destination folder has the folder structure like:
# Drive/Photos/YYYY/MM/DD/
# Where the YMD matches the Date Modified field of every photo.
foreach ($file in $files) {
$originalCreationTime = $file.LastWriteTime
[string]$year = $file.LastWriteTime.Year
[string]$month = $file.LastWriteTime.Month
[string]$date = $file.LastWriteTime.Day
# Add leading zero, if necessary
if($month.length -lt 2) {
$month = "0" + $month
}
if($date.length -lt 2) {
$date = "0" + $date
}
# Test the path of destinationPath/YYYY/MM/DD/
$path = $dst + "$year\$month\$date\"
if (!(Test-Path -Path $path)) {
if($verb) {
Write-Output " $path"
}
New-Item -ItemType Directory -Path $path
}
# The filepath exists!
if($verb) {
Write-Output " ($iter/$numberOfFiles) $file -> $path"
}
$iter += 1
Copy-Item $file.FullName -Destination $path
# Fix the Creation Time
$(Get-Item -Path "$path$($file.Name)").CreationTime=$originalCreationTime
}
Write-Output "`nCopying done!`n"
# Delete items?
Write-Output "Delete $numberOfItems items?"
$del = read-host "Deleting copied files from $src ?`nY to continue"
if($del -eq "Y") {
foreach ($file in $files) {
Remove-Item $file.FullName
}
}
}
Param(
# Source Folder
[Parameter(Mandatory=$true, Position = 0, HelpMessage = "The path of the source of the media")]
[Alias("s")]
[string]$source,
# Photo Destination
[Parameter(Mandatory=$false, Position = 1, HelpMessage = "The path of the folder you want to move photos to")]
[Alias("pd")]
[string]$photoDestination,
# Video Destination
[Parameter(Mandatory=$false, Position = 2, HelpMessage = "The path of the folder you want to move videos to")]
[Alias("vd")]
[string]$videoDestionation,
# Verbosity
[Parameter(Position = 3, HelpMessage = "Turn extra logging on or off")]
[Alias("v")]
[bool]$verb = $true
)
$usageHelpText = "usage:`n photoTransfer.ps1 <DriveName> <pathToDestinationRootFolder>`nex:`n .\photoTransfer.ps1 C T:\Photos"
#TODO: Solve this conundrum, where passing a via CMD
# Write-Output "Source before treatment: $($args[0])"
# Write-Output "Source before treatment: $($args[1])"
# Write-Output "Source before treatment: $($args[2])"
# Write-Output "Source before treatment: $($args[3])"
$source = $args[0]
$verb = $true
# I expect a drive name. If a ':' is missing, I add it.
if(!$source.Contains(":")) {
$source = $source + ":"
}
# The assumption is that the photos are coming from my Olympus camera, which has the following path to the files.
# $olympusFolderPath = "DCIM\100OLYMP\"
# $source += $olympusFolderPath
# Make sure the destination path has a terminating '\'
# if(!($photoDestination -match "\\$")) {
# $photoDestination = $photoDestination + "\"
# }
$photoDestination = "T:\Photos\"
$videoDestionation = "T:\Footage\"
# Check if the source and destination paths are valid.
if (!(Test-Path -Path $source)) {
Write-Output "Source disk ($source) doesn't exist`n$usageHelpText"
exit 0
}
if (!(Test-Path -Path $photoDestination)) {
Write-Output "Destination path ($photoDestination) doesn't exist`n$usageHelpText"
exit 0
}
if (!(Test-Path -Path $videoDestionation)) {
Write-Output "Destination path ($videoDestionation) doesn't exist`n$usageHelpText"
exit 0
}
Transfer $source $photoDestination "*.ORF"
Transfer $source $videoDestionation "*.MOV"
I have Several zip files that Contain multiple filetypes. The ONLY ones I am interested in are the .txt files. I need to extract the .txt files only and place them in a folder of their own, ignoring all other file types in the zips files.
All the zip files are in the same folder.
Example
-foo.zip
--1.aaa
--2.bbb
--3.ccc
--4.txt
-foo2.zip
--5.aaa
--6.bbb
--7.ccc
--8.txt
I want to have 4.txt and 8.txt extracted to another folder. I can't for the life of my figure it out and spent ages looking, googling and trying. Even managing to delete to zips once in a while :-)
Thanks in advance
Use the ZipArchive type to programmatically inspect the archive before extracting:
Add-Type -AssemblyName System.IO.Compression
$destination = "C:\destination\folder"
# Locate zip file
$zipFile = Get-Item C:\path\to\file.zip
# Open a read-only file stream
$zipFileStream = $zipFile.OpenRead()
# Instantiate ZipArchive
$zipArchive = [System.IO.Compression.ZipArchive]::new($zipFileStream)
# Iterate over all entries and pick the ones you like
foreach($entry in $zipArchive.Entries){
if($entry.Name -like '*.txt'){
# Create new file on disk, open writable stream
$targetFileStream = $(
New-Item -Path $destination -Name $entry.Name -ItemType File
).OpenWrite()
# Open stream to compressed file, copy to new file stream
$entryStream = $entry.Open()
$entryStream.BaseStream.CopyTo($targetFileStream)
# Clean up
$targetFileStream,$entryStream |ForEach-Object Dispose
}
}
# Clean up
$zipArchive,$zipFileStream |ForEach-Object Dispose
Repeat for each zip file.
Note that the code above has very minimal error-handling, and is to be read as an example
Try this:
Set-Location "Extraction path"
#("full path of foo.zip","full path of foo2.zip") | ForEach {
& "Full path of 7z.exe" x '-i!*.txt' $_.FullName
}
Sets location to the path where files will be extracted.
Passes a array of zip files to for loop.
Exexute 7z command to extract only zip files.
Here is one approach:
Go through each .zip file in a folder.
Extract archive into separate folder.
Extract .txt file from folder.
Copy files into destination folder containing all .txt files. This will overwrite files if they already exist in the destination folder.
Cleanup extracted folders once finished.
Demo:
function Copy-ZipArchiveFiles {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateScript({
if (-not(Test-Path $_ -PathType Container))
{
throw "The source path $_ does not exist. Please enter a valid source path."
}
else
{
$true
}
})]
[string]$Path,
[Parameter(Mandatory=$true)]
[ValidateScript({
if ([string]::IsNullOrEmpty($_.Trim()))
{
throw "The Destination path is null or empty. Please enter a valid destination path."
}
else
{
$true
}
})]
[string]$Destination,
[Parameter(Mandatory=$false)]
[AllowNull()]
[AllowEmptyString()]
[AllowEmptyCollection()]
[string[]]$Include
)
# Create destination folder if it doesn't already exist
if (-not(Test-Path -Path $Destination -PathType Container))
{
try
{
New-Item -Path $Destination -ItemType Directory -ErrorAction Stop
}
catch
{
throw "The destination path $Destination is invalid. Please enter a valid destination path."
}
}
# Go through each .zip file
foreach ($zipFile in Get-ChildItem -Path $Path -Filter *.zip)
{
# Get folder name from zip file w/o .zip at the end
$zipFolder = Split-Path $zipFile -LeafBase
# Get full folder path
$folderPath = Join-Path -Path $Path -ChildPath $zipFolder
# Expand .zip file into folder if it doesn't exist
if (-not(Test-Path -Path $folderPath -PathType Container))
{
Expand-Archive -Path $zipFile.FullName -DestinationPath $folderPath
}
# Copy files into destination folder
foreach ($file in Get-ChildItem $folderPath -Include $Include -Recurse)
{
Copy-Item -Path $file.FullName -Destination $Destination
}
# Delete extracted folders
Remove-Item -Path $folderPath -Recurse -Force
}
}
Usage:
Copy-ZipArchiveFiles `
-Path "C:\path\to\zip\files" `
-Destination "C:\path\to\text\files" `
-Include "*.txt"
Note: Could also use this for multiple extension types as well by passing -Include *.txt, *.pdf. I also went a bit overboard in the parameter error checking, but I believe in writing robust code. Good habit to get into when writing your own cmdlets anyways :)
I have a script (pasted below) that's supposed to do the following:
Loops and grabs all files that match a defined pattern
Copies those files into another location
Move the original source file into another location (if copy was successful)
It's doing step #1 and #2 but step #3, Move-Item is not moving any files and I don't know why and I get no indication of an error
Can someone help? Thanks
$source = "C:\Users\Tom\"
$source_move = "C:\Users\Tom\OldLogBackup1\"
$destination ="C:\Users\Tom\Mirror\"
if(-not(Test-Path $destination)){mkdir $destination | out-null}
ForEach ($sourcefile In $(Get-ChildItem $source | Where-Object { $_.Name -match "Daily_Reviews\[\d\d\d\d-\d\d\d\d\].journal" }))
{
#escape filename because there are brackets in the name!
$src = [Management.Automation.WildcardPattern]::Escape($sourcefile)
Copy-Item $src $destination
### Check for a successful file copy
if($?)
{
####
if(-not(Test-Path $source_move)){
echo "Path does not exist!"
} else {
echo "Path exists!"
### Move original source file someplace else
Move-Item $source_move$sourcefile $source_move
if($?)
{
echo "Source file successfully moved"
} else {
echo "Source file was not successfully moved"
}
}
}
Try along with the -force option.
The line Move-Item $source_move$sourcefile $source_move is moving the $sourcefile in the $source_move directory to the $source_move directory, which does nothing. I am guessing you meant to do Move-Item $source$sourcefile $source_move, or Move-Item $src $source_move.
Also, echo is simply an alias for Write-Output, which returns the argument to the caller. You can achieve the same result without the echo (any object on its own will be returned). Or, if you intended to use these as debug statements, you could use Write-Debug or Write-Host.