Extract a single file from a ZIP archive - powershell

I'm using the next code to download some zip archive:
$client = new-object System.Net.WebClient
$client.DownloadFile("https://chromedriver.storage.googleapis.com/$LatestChromeRelease/chromedriver_win32.zip","D:\MyFolder.zip")
As the result I get the ZIP archive "MyFolder.zip" that contains a required file (lets imagine 'test.txt').
How I can extract this particular file from the ZIP archive into a given folder?

PowerShell 4+ has an Expand-Archive command but as of PS 7.2.3 it can only extract the archive completely. So extract it to a temporary directory and copy the file you are interested in.
If you have PS 5.1+ available, scroll down for a more efficient solution that uses .NET classes.
$archivePath = 'D:\MyFolder.zip'
$destinationDir = 'D:\MyFolder'
# Relative path of file in ZIP to extract.
$fileToExtract = 'test.txt'
# Create destination dir if not exist.
$null = New-Item $destinationDir -ItemType Directory -Force
# Create a unique temporary directory
$tempDir = Join-Path ([IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString('n'))
$null = New-Item $tempDir -ItemType Directory
try {
# Extract archive to temp dir
Expand-Archive -LiteralPath $archivePath -DestinationPath $tempDir
# Copy the file we are interested in
$tempFilePath = Join-Path $tempDir $fileToExtract
Copy-Item $tempFilePath $destinationDir
}
finally {
# Remove the temp dir
if( Test-Path $tempDir ) {
Remove-Item $tempDir -Recurse -Force -EA Continue
}
}
With PS 5.1+ you can use .NET classes to directly extract a single file (without having to extract the whole archive):
# Load required .NET assemblies. Not necessary on PS Core 7+.
Add-Type -Assembly System.IO.Compression.FileSystem
$archivePath = 'D:\MyFolder.zip'
$destinationDir = 'D:\MyFolder'
# Relative path of file in ZIP to extract.
# Use FORWARD slashes as directory separator, e. g. 'subdir/test.txt'
$fileToExtract = 'test.txt'
# Create destination dir if not exist.
$null = New-Item $destinationDir -ItemType Directory -Force
# Convert (possibly relative) paths for safe use with .NET APIs
$resolvedArchivePath = Convert-Path -LiteralPath $archivePath
$resolvedDestinationDir = Convert-Path -LiteralPath $destinationDir
$archive = [IO.Compression.ZipFile]::OpenRead( $resolvedArchivePath )
try {
# Locate the desired file in the ZIP archive.
# Replace $_.Fullname by $_.Name if file shall be found in any sub directory.
if( $foundFile = $archive.Entries.Where({ $_.FullName -eq $fileToExtract }, 'First') ) {
# Combine destination dir path and name of file in ZIP
$destinationFile = Join-Path $resolvedDestinationDir $foundFile.Name
# Extract the file.
[IO.Compression.ZipFileExtensions]::ExtractToFile( $foundFile[ 0 ], $destinationFile )
}
else {
Write-Error "File not found in ZIP: $fileToExtract"
}
}
finally {
# Close the archive so the file will be unlocked again.
if( $archive ) {
$archive.Close()
$archive.Dispose()
}
}
Notes:
Convert-Path should be used when passing PowerShell paths that might be relative paths, to .NET APIs. The .NET framework has its own current directory, which doesn't necessarily match PowerShell's. Using Convert-Path we convert to absolute paths so the current directory of .NET is no longer relevant.
.Where and .ForEach are PowerShell intrinsic methods that are available on all objects. They are similar to the Where-Object and ForEach-Object commands but more efficient. Passing 'First' as the 2nd argument to .Where stops searching as soon as we have found the file.
Note that .Where always outputs a collection, even if only a single element matches. This is contrary to Where-Object which returns a single object if only a single element matches. So we have to write $foundFile[ 0 ] when passing it to function ExtractToFile, instead of just $foundFile which would be an array.

Related

Powershell script to copy files to folders by matching file to folder names

I have a directory with empty folders, let's call it D1. And another directory (let's call it D2) with files which are named such that the file name contains the folder names from D1. I want to match the file names from D2 to folder names from D1 and copy (not move) the files to the corresponding folder in D1. Can this be done using Powershell? As an example,
D1 looks like:
FolderName1
FolderName2
FolderName3
And files in D2 look like:
abcd.FolderName1.txt
xyz.FolderName2.txt
qwerty.FolderName3.txt
EDIT: the folder names above are just an example. The actual folder/file names could vary e.g. to Folder#Name, My_Folder, abcd.1234.Folder_Name etc.
I pulled the folder names from the filename with regex.
There's a little bit of error handling in there, but you might want to instead create the folder if it doesn't exist.
$FromPath = 'C:\D2'
$ToPath = 'C:\D1'
$FilesToMove = Get-ChildItem -Path $FromPath -File
$RegexPattern = '^[^.]+\.(?<NewFolder>[^.]+)\..+$'
foreach ($File in $FilesToMove) {
If($File.Name -match $RegexPattern){
$OutFolder = Join-Path -Path $ToPath -ChildPath $Matches.NewFolder
if (Test-Path -Path $OutFolder) {
Copy-Item -Path $File -Destination $OutFolder
}
else {
Write-Error -Message "Invalid or Inaccesible path $OutFolder"
}
}
else {
Write-Error -Message "Filename $($File.Name) Is Incorrectly Formatted"
}
}

PowerShell: How to copy only ".exe" file from ZIP folder to another folder

I have a "ZIP" file and when we extract this, we have 1 "EXE" file within 4-5 sub folder depth level.
I would like to grab that "EXE" file and copy into another folder. How to do it using PowerShell?
I tried below, but it will copy all the ZIP content,
$shell = New-Object -ComObject shell.application
$zip = $shell.NameSpace("Source Path")
foreach ($item in $zip.items()) {
$shell.Namespace("Destination Path").CopyHere($item)
}
Simple snippet should get your job done
#Sets the variable to the Source folder, recurse drills down to folders within
$Source = get-childitem "C:\Users" -recurse #"C:\Users" an example
#Filters by extension .exe
$List = $Source | where {$_.extension -eq ".exe"}
#Copies all the items to the specified destination
$List | Copy-Item -Destination "C:\Scripts" #"C:\Scripts" an example
The module above scans for every single .EXE files within C:\Users* and copies them to C:\Scripts
As it stands, Clint's answer did not work for me, but something based on Extract Specific Files from ZIP Archive does, with a variation to target a specifically named file. Will need a further tweak to handle multiple files sharing the same name.
Code:
# Set source zip path, target output directory and file name filter
$ZipPath = 'C:\temp\Test.zip'
$OutDir = 'C:\temp'
$Filter = 'MyExe.exe'
# Load compression methods
Add-Type -AssemblyName System.IO.Compression.FileSystem
# Open zip file for reading
$Zip = [System.IO.Compression.ZipFile]::OpenRead($Path)
# Copy selected items to the target directory
$Zip.Entries |
Where-Object { $_.FullName -eq $Filter } |
ForEach-Object {
# Extract the selected items from the zip archive
# and copy them to the out folder
$FileName = $_.Name
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, "$OutDir\$FileName", $true)
}
# Close zip file
$Zip.Dispose()

Script help rename - copy - move

I am trying to write a PowerShell script to do the following.
Rename files in source (FTP folders) directories with it's "current_name_datetime.csv" as per a source file "Source_list.csv" this file has the directories "source,destination" I want this script to look into.
Copy newly renamed files to backup directories as per destination in Source_list.csv this file has the directories "source,destination" I want this script to look into.
Move newly renamed files to final destination directory which is not in my current script.
Source_list.csv contents
cscenter,Costume_Supercenter
fkimports,FKImports
My Script:
$sdfiles = Get-Content c:\!tony\Source_list.csv
$sourceDir = "c:\test\"
$destinationDir = "c:\testing\"
Get-ChildItem $sourceDir -Recurse -Include $sdfiles "*.csv"|
ForEach-Object{
$newname= "{0}{1}_{2}.csv" -f $destinationDir, $_.BaseName, [datetime]::Now.ToString('MM-dd-yyyy-hh-mm-ss')
$_|Copy-Item -Include ,$sdfiles -Destination $newname -whatif }
Error:
What if: Performing operation "Copy Directory" on Target "Item: C:\test\cscenter Destination: C:\testing\cscenter_10-01-2015-12-22-24.csv".
I see in the error that it is trying to copy the directory not the single file in each directory and creating a new folder using the original folder name and renaming the folder and appending the date/time stamp.
Confused. The -Include parameter should only be accepting a single array of strings, throwing "*.csv" on to the end of it won't work AFAIK. Additionally It will be interpreting the whole line of the CSV, ie searching for the file "cscenter,Costume_Supercenter" so shouldn't be returning anything. At least that's what I see when I replicate this on my computer.
Lastly you've tried to filter the files, piped that to Copy-Item and tried to filter it again?
I'd take a more straightforward approach:
$sdfiles = Import-CSV c:\!tony\Source_list.csv -Header #("File", "Folder")
$sourcedir = "c:\test\"
$destinationdir = "c:\testing\"
$sdfiles | ForEach-Object {
$path = $sourcedir + $_.File + ".csv"
$folder = $destinationdir + $_.Folder + '\'
if (!(Test-Path $folder)) { New-Item -Type Directory -Path $folder }
if (Test-Path ($path))
{
$newname = "{0}{1}_{2}.csv" -f $folder, $_.File, (Get-Date).ToString('MM-dd-yyyy-hh-mm-ss')
Copy-Item -Path $path -Destination $newname -whatif
}
else { Write-Error "File $($_.File) not found" }
}
It's a bit chunkier but much easier to read and tweak to your liking. Note that Import-CSV does require PowerShell v3. Let me know if you've got v2 and need help tweaking it for a two-dimensional array.
I also recommend looking into Microsoft's MVA courses on PowerShell, they are excellent resources for starting out.

Passing a collection instead of directory as a parameter to CreateFromDirectory

I have a statement in my power shell script that archives all files in a directory to a given path:
[System.IO.Compression.ZipFile]::CreateFromDirectory($sourcePath,$destPath, $compressionLevel, $false);
What I want to is to instead of archiving whole directory just archive items that I selected through this statement:
Get-ChildItem $sourcePath -include *.txt,*.log | Where-Object { $_.CreationTime -le $archiveTillDate }
So how is it possible to pass a specific set of files as a parameter to CreateFromDirectory instead of path to whole directory?
Add selected files to a new or existing zip archive
Add-Type -AssemblyName System.IO.Compression.FileSystem
$CompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
$SourceFiles = Get-Childitem C:\TestLogs2\*.csv
$ZipFile = 'C:\TestFiles\testzip1.zip'
$Zip = [System.IO.Compression.ZipFile]::Open($ZipFile,'Create')
ForEach ($SourceFile in $SourceFiles)
{
$SourcePath = $SourceFile.Fullname
$SourceName = $SourceFile.Name
$null = [System.IO.Compression.ZipFileExtensions]::
CreateEntryFromFile($Zip,$SourcePath,$SourceName,$CompressionLevel)
}
$Zip.Dispose()
The 'Update' method will open an existing zip file for update,
or create a new file if the specified file doesn't exit.
There is also an 'Create' method that will create an new zip file
but will throw an error if the file already exists.

Create directory if it does not exist

I am writing a PowerShell script to create several directories if they do not exist.
The filesystem looks similar to this
D:\
D:\TopDirec\SubDirec\Project1\Revision1\Reports\
D:\TopDirec\SubDirec\Project2\Revision1\
D:\TopDirec\SubDirec\Project3\Revision1\
Each project folder has multiple revisions.
Each revision folder needs a Reports folder.
Some of the "revisions" folders already contain a Reports folder; however, most do not.
I need to write a script that runs daily to create these folders for each directory.
I am able to write the script to create a folder, but creating several folders is problematic.
Try the -Force parameter:
New-Item -ItemType Directory -Force -Path C:\Path\That\May\Or\May\Not\Exist
You can use Test-Path -PathType Container to check first.
See the New-Item MSDN help article for more details.
$path = "C:\temp\NewFolder"
If(!(test-path -PathType container $path))
{
New-Item -ItemType Directory -Path $path
}
Test-Path -PathType container checks to see if the path exists and is a directory. When it does not, it will create a new directory. If the path exists but is a file, New-Item will raise an error (you can overwrite the file by using the -force argument if you are risky).
[System.IO.Directory]::CreateDirectory('full path to directory')
This internally checks for directory existence, and creates one, if there is no directory. Just one line and native .NET method working perfectly.
Use:
$path = "C:\temp\"
If (!(test-path $path))
{
md $path
}
The first line creates a variable named $path and assigns it the string value of "C:\temp"
The second line is an If statement which relies on the Test-Path cmdlet to check if the variable $path does not exist. The not exists is qualified using the ! symbol.
Third line: If the path stored in the string above is not found, the code between the curly brackets will be run.
md is the short version of typing out: New-Item -ItemType Directory -Path $path
Note: I have not tested using the -Force parameter with the below to see if there is undesirable behavior if the path already exists.
New-Item -ItemType Directory -Path $path
The following code snippet helps you to create a complete path.
Function GenerateFolder($path) {
$global:foldPath = $null
foreach($foldername in $path.split("\")) {
$global:foldPath += ($foldername+"\")
if (!(Test-Path $global:foldPath)){
New-Item -ItemType Directory -Path $global:foldPath
# Write-Host "$global:foldPath Folder Created Successfully"
}
}
}
The above function split the path you passed to the function and will check each folder whether it exists or not. If it does not exist it will create the respective folder until the target/final folder created.
To call the function, use below statement:
GenerateFolder "H:\Desktop\Nithesh\SrcFolder"
I had the exact same problem. You can use something like this:
$local = Get-Location;
$final_local = "C:\Processing";
if(!$local.Equals("C:\"))
{
cd "C:\";
if((Test-Path $final_local) -eq 0)
{
mkdir $final_local;
cd $final_local;
liga;
}
## If path already exists
## DB Connect
elseif ((Test-Path $final_local) -eq 1)
{
cd $final_local;
echo $final_local;
liga; (function created by you TODO something)
}
}
When you specify the -Force flag, PowerShell will not complain if the folder already exists.
One-liner:
Get-ChildItem D:\TopDirec\SubDirec\Project* | `
%{ Get-ChildItem $_.FullName -Filter Revision* } | `
%{ New-Item -ItemType Directory -Force -Path (Join-Path $_.FullName "Reports") }
BTW, for scheduling the task please check out this link: Scheduling Background Jobs.
There are three ways I know to create a directory using PowerShell:
Method 1: PS C:\> New-Item -ItemType Directory -path "C:\livingston"
Method 2: PS C:\> [system.io.directory]::CreateDirectory("C:\livingston")
Method 3: PS C:\> md "C:\livingston"
From your situation it sounds like you need to create a "Revision#" folder once a day with a "Reports" folder in there. If that's the case, you just need to know what the next revision number is. Write a function that gets the next revision number, Get-NextRevisionNumber. Or you could do something like this:
foreach($Project in (Get-ChildItem "D:\TopDirec" -Directory)){
# Select all the Revision folders from the project folder.
$Revisions = Get-ChildItem "$($Project.Fullname)\Revision*" -Directory
# The next revision number is just going to be one more than the highest number.
# You need to cast the string in the first pipeline to an int so Sort-Object works.
# If you sort it descending the first number will be the biggest so you select that one.
# Once you have the highest revision number you just add one to it.
$NextRevision = ($Revisions.Name | Foreach-Object {[int]$_.Replace('Revision','')} | Sort-Object -Descending | Select-Object -First 1)+1
# Now in this we kill two birds with one stone.
# It will create the "Reports" folder but it also creates "Revision#" folder too.
New-Item -Path "$($Project.Fullname)\Revision$NextRevision\Reports" -Type Directory
# Move on to the next project folder.
# This untested example loop requires PowerShell version 3.0.
}
PowerShell 3.0 installation.
Here's a simple one that worked for me. It checks whether the path exists, and if it doesn't, it will create not only the root path, but all sub-directories also:
$rptpath = "C:\temp\reports\exchange"
if (!(test-path -path $rptpath)) {new-item -path $rptpath -itemtype directory}
I wanted to be able to easily let users create a default profile for PowerShell to override some settings, and ended up with the following one-liner (multiple statements yes, but can be pasted into PowerShell and executed at once, which was the main goal):
cls; [string]$filePath = $profile; [string]$fileContents = '<our standard settings>'; if(!(Test-Path $filePath)){md -Force ([System.IO.Path]::GetDirectoryName($filePath)) | Out-Null; $fileContents | sc $filePath; Write-Host 'File created!'; } else { Write-Warning 'File already exists!' };
For readability, here's how I would do it in a .ps1 file instead:
cls; # Clear console to better notice the results
[string]$filePath = $profile; # Declared as string, to allow the use of texts without plings and still not fail.
[string]$fileContents = '<our standard settings>'; # Statements can now be written on individual lines, instead of semicolon separated.
if(!(Test-Path $filePath)) {
New-Item -Force ([System.IO.Path]::GetDirectoryName($filePath)) | Out-Null; # Ignore output of creating directory
$fileContents | Set-Content $filePath; # Creates a new file with the input
Write-Host 'File created!';
}
else {
Write-Warning "File already exists! To remove the file, run the command: Remove-Item $filePath";
};
$mWarningColor = 'Red'
<#
.SYNOPSIS
Creates a new directory.
.DESCRIPTION
Creates a new directory. If the directory already exists, the directory will
not be overwritten. Instead a warning message that the directory already
exists will be output.
.OUTPUT
If the directory already exists, the directory will not be overwritten.
Instead a warning message that the directory already exists will be output.
.EXAMPLE
Sal-New-Directory -DirectoryPath '.\output'
#>
function Sal-New-Directory {
param(
[parameter(mandatory=$true)]
[String]
$DirectoryPath
)
$ErrorActionPreference = "Stop"
try {
if (!(Test-Path -Path $DirectoryPath -PathType Container)) {
# Sal-New-Directory is not designed to take multiple
# directories. However, we use foreach to supress the native output
# and substitute with a custom message.
New-Item -Path $DirectoryPath -ItemType Container | `
foreach {'Created ' + $_.FullName}
} else {
Write-Host "$DirectoryPath already exists and" `
"so will not be (re)created." `
-ForegroundColor $mWarningColor
}
} finally {
$ErrorActionPreference = "Continue"
}
}
"Sal" is just an arbitrary prefix for my own library. You could remove it or replace it with your own.
Another example (place here because it otherwise ruins stackoverflow syntax highlighting):
Sal-New-Directory -DirectoryPath ($mCARootDir + "private\")
Example, create a 'Reports' folder inside of the script's folder.
$ReportsDir = $PSScriptRoot + '\Reports'
$CreateReportsDir = [System.IO.Directory]::CreateDirectory($ReportsDir)