Expand-Archive without Importing and Exporting files - powershell

How do I stop exporting then importing files in and out of Powershell when working with .zip files (Expand-Archive)?
I am currently using a temporary folder to extract the .zip file.
Is there a variable or something I missed that would work better than the solution below?
$filename = 'foobar'
$Zip_in_Bytes | Set-Content -Encoding Byte -Path "C:\temp\filename.zip"
Expand-Archive -Path "C:\temp\filename.zip" -DestinationPath "C:\temp\" -Force
[xml]$xml = Get-Content -Path "C:\temp\filename.xml"
Remove-Item "C:\temp\filename.zip"
Remove-Item "C:\temp\filename.xml"
Expand-Archive only support paths parameters, not objects
Is there a better way to handle .zip files?

Using System.IO.Compression you can work with byte arrays and streams rather than temporary files, but it's a bit more work than Expand-Archive.
EDIT: Added Get-ZipEntryContent and Add-ZipEntry sample calls, and tweaked parameters making $ZipFilePath optional.
#( 'System.IO.Compression','System.IO.Compression.FileSystem') | % { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
function Get-ZipEntryContent(#returns the bytes of the first matching entry
[string] $ZipFilePath, #optional - specify a ZipStream or path
[IO.Stream] $ZipStream = (New-Object IO.FileStream($ZipFilePath, [IO.FileMode]::Open)),
[string] $EntryPath){
$ZipArchive = New-Object IO.Compression.ZipArchive($ZipStream, [IO.Compression.ZipArchiveMode]::Read)
$buf = New-Object byte[] (0) #return an empty byte array if not found
$ZipArchive.GetEntry($EntryPath) | ?{$_} | %{ #GetEntry returns first matching entry or null if there is no match
$buf = New-Object byte[] ($_.Length)
Write-Verbose " reading: $($_.Name)"
$_.Open().Read($buf,0,$buf.Length)
}
$ZipArchive.Dispose()
$ZipStream.Close()
$ZipStream.Dispose()
return $buf
}
function Add-ZipEntry(#Adds an entry to the $ZipStream. Sample call: Add-ZipEntry -ZipFilePath "$PSScriptRoot\temp.zip" -EntryPath Test.xml -Content ([text.encoding]::UTF8.GetBytes("Testing"))
[string] $ZipFilePath, #optional - specify a ZipStream or path
[IO.Stream] $ZipStream = (New-Object IO.FileStream($ZipFilePath, [IO.FileMode]::OpenOrCreate)),
[string] $EntryPath,
[byte[]] $Content,
[switch] $OverWrite, #if specified, will not create a second copy of an existing entry
[switch] $PassThru ){#return a copy of $ZipStream
$ZipArchive = New-Object IO.Compression.ZipArchive($ZipStream, [IO.Compression.ZipArchiveMode]::Update, $true)
$ExistingEntry = $ZipArchive.GetEntry($EntryPath) | ?{$_}
If($OverWrite -and $ExistingEntry){
Write-Verbose " deleting existing $($ExistingEntry.FullName)"
$ExistingEntry.Delete()
}
$Entry = $ZipArchive.CreateEntry($EntryPath)
$WriteStream = New-Object System.IO.StreamWriter($Entry.Open())
$WriteStream.Write($Content,0,$Content.Length)
$WriteStream.Flush()
$WriteStream.Dispose()
$ZipArchive.Dispose()
If($PassThru){
$OutStream = New-Object System.IO.MemoryStream
$ZipStream.Seek(0, 'Begin') | Out-Null
$ZipStream.CopyTo($OutStream)
}
$ZipStream.Close()
$ZipStream.Dispose()
If($PassThru){$OutStream}
}
Here's an example of how you would call Add-ZipEntry and Get-ZipEntryContent functions completely in memory:
$NewZipStream = Add-ZipEntry -ZipStream (New-Object IO.MemoryStream) -EntryPath Test.xml -Content ([text.encoding]::UTF8.GetBytes("<xml><test>1</test>")) -PassThru
$bytes = Get-ZipEntryContent -ZipStream $NewZipStream -EntryPath 'Test.xml'
[text.encoding]::UTF8.GetString($bytes)

Although Expand-Archive doesn't accept objects, you can provide it the string property of the objects. E.G. $File.FullName will be a String.
Get-ChildItem C:\temp\ -Filter "*.zip" |
ForEach-Object {
Expand-Archive -Path $_.FullName -DestinationPath "C:\Temp\Extracted\$($_.BaseName)\"
}

Related

Unzipping an archive with 7zip using a predefined lis of passwords

I am trying since hourse to unpack an passwort-protected rar-archive. The passwort is unknown and should be determined based on the contents of a json file.
$content = Get-Content "C:\JDownloader
v2.0\cfg\org.jdownloader.extensions.extraction.ExtractionExtension.passwordlist.json"
$passwords = ConvertFrom-Json
$content $7ZipPath = '"C:\Program
Files\7-Zip\7z.exe"'
$zipFile = Get-Clipboard $output = Split-Path
$zipFile
Write-Host(Get-Clipboard);
foreach ($password in $passwords) {
7zip x -o$output -p$password $zipFile
}
}
If i instead use instead of the variable $password the plain-text-password, everyting is working as expected.

How can I update a changing value in Powershell in a web.config file?

I am trying to update text inside a web.config file with powershell, but I am having a very, very difficult time.
The problem is this line
<add key="Models.Catalog.Repository" value=Server23 />
You see where the Server23 is? this value sometimes can be empty, which will be represented as "" or sometimes it can have a different name like "\Server25\path"
How do I tell PowerShell to replace/insert data if the field is empty/has some text in it?
here is the full code
$GP_AP= "Server1"
$NewMD = "\\JohnWick"
Invoke-Command -ComputerName $GP_AP -ScriptBlock {
$date= Get-Date -Format "dd_MM_yyyy_HHMM_ss"
$webfile = "C:\Program Files\TEST\IIS\Web.config"
(Copy-Item -Path $webfile -Destination $webfile_Test_$Date)
#[xml]$webconfigfile = Get-Content "C:\Program Files\TEST\IIS\Web.config"
#[xml]$2 = Get-Content "\\Server1\C$\Program Files\TEST\IIS\Web.config"
$olddata = "*"
#(Get-Content $webfile -raw).Replace("<add key=`"Models.Catalog.Repository`" value=$olddata />","<add key=`"Models.Catalog.Repository`" value=$Using:NewMD />") `
#| Set-Content "C:\Program Files\TEST\IIS\Web.config" -Force -Encoding UTF8 -ErrorAction Stop
(Get-Content $webfile -raw).Replace('<add key="Models.Catalog.Repository" value=Server23 />',"<add key=`"Models.Catalog.Repository`" value=$Using:NewMD />") `
| Set-Content "C:\Program Files\TEST\IIS\Web.config" -Force -Encoding UTF8 -ErrorAction Stop
} # end of script block
The script below will allow you to set values in a web config's <appSettings> based on key names. All the nodes must exist. This script will not create them. You'll obviously need to edit the script's default params.
param(
[string] $pathToWebConfig = "$PSScriptRoot\web.config"
[System.Collections.Hashtable] $appSettings = #{
someKey = 'somevalue';
someOtherKey = 'someOtherValue'
}
)
function UpdateXmlConfigAppSettings(
[Parameter(mandatory=$true)]
[string] $configFilePath,
[Parameter(mandatory=$true)]
[System.Collections.Hashtable] $appSettingsDict
) {
[xml] $webConfig = Get-Content $configFilePath
foreach($key in $appSettingsDict.Keys) {
$node = $webConfig.SelectSingleNode("//appSettings/add[#key = '$key']")
if ($null -ne $node) {
$node.SetAttribute('value', $appSettingsDict[$key])
}
else {
throw "$appSettingsDict[$key] is not a valid key in AppSettings"
}
}
$webConfig.Save($configFilePath)
}
UpdateXmlConfigAppSettings -configFilePath $pathToWebConfig -appSettingsDict $appSettings
$path = '\\server\c$\Program Files\TEST\IIS\web.config'
[xml]$config = Get-Content -Path $path -Raw
$stuffIwant = "\\Blah-244"
$config.SelectNodes("//add[#key='Models.Catalog.Repository']") | `
ForEach-Object { $_.SetAttribute("value", $stuffIwant) }
$config.Save($path)

Dynamic Parameters - with Dynamic ValidateSet

I have a script that I've been working on to provide parsing of SCCM log files. This script takes a computername and a location on disk to build a dynamic parameter list and then present it to the user to choose the log file they want to parse. Trouble is I cannot seem to get the ValidateSet portion of the dynamic parameter to provide values to the user. In addition the script won't display the -log dynamic parameter when attempting to call the function.
When you run it for the first time you are not presented with the dynamic parameter Log as I mentioned above. If you then use -log and then hit tab you’ll get the command completer for the files in the directory you are in. Not what you’d expect; you'd expect that it would present you the Logfile names that were gathered during the dynamic parameter execution.
PSVersion 5.1.14409.1012
So the question is how do I get PowerShell to present the proper Validate set items to the user?
If you issue one of the items in the error log you get the proper behavior:
Here are the two functions that i use to make this possible:
function Get-CCMLog
{
[CmdletBinding()]
param([Parameter(Mandatory=$true,Position=0)]$ComputerName = '$env:computername', [Parameter(Mandatory=$true,Position=1)]$path = 'c:\windows\ccm\logs')
DynamicParam
{
$ParameterName = 'Log'
if($path.ToCharArray() -contains ':')
{
$FilePath = "\\$ComputerName\$($path -replace ':','$')"
if(test-path $FilePath)
{
$logs = gci "$FilePath\*.log"
$LogNames = $logs.basename
$logAttribute = New-Object System.Management.Automation.ParameterAttribute
$logAttribute.Position = 2
$logAttribute.Mandatory = $true
$logAttribute.HelpMessage = 'Pick A log to parse'
$logCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$logCollection.add($logAttribute)
$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)
$logCollection.add($logValidateSet)
$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)
$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$logDictionary.Add($ParameterName,$logParam)
return $logDictionary
}
}
}
begin {
# Bind the parameter to a friendly variable
$Log = $PsBoundParameters[$ParameterName]
}
process {
# Your code goes here
#dir -Path $Path
$sb2 = "$((Get-ChildItem function:get-cmlog).scriptblock)`r`n"
$sb1 = [scriptblock]::Create($sb2)
$results = Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb1 -ArgumentList "$path\$log.log"
[PSCustomObject]#{"$($log)Log"=$results}
}
}
function Get-CMLog
{
param(
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipelineByPropertyName=$true)]
[Alias("FullName")]
$Path,
$tail =10
)
PROCESS
{
if(($Path -isnot [array]) -and (test-path $Path -PathType Container) )
{
$Path = Get-ChildItem "$path\*.log"
}
foreach ($File in $Path)
{
if(!( test-path $file))
{
$Path +=(Get-ChildItem "$file*.log").fullname
}
$FileName = Split-Path -Path $File -Leaf
if($tail)
{
$lines = Get-Content -Path $File -tail $tail
}
else {
$lines = get-cotnet -path $file
}
ForEach($l in $lines ){
$l -match '\<\!\[LOG\[(?<Message>.*)?\]LOG\]\!\>\<time=\"(?<Time>.+)(?<TZAdjust>[+|-])(?<TZOffset>\d{2,3})\"\s+date=\"(?<Date>.+)?\"\s+component=\"(?<Component>.+)?\"\s+context="(?<Context>.*)?\"\s+type=\"(?<Type>\d)?\"\s+thread=\"(?<TID>\d+)?\"\s+file=\"(?<Reference>.+)?\"\>' | Out-Null
if($matches)
{
$UTCTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)$($matches.TZAdjust)$($matches.TZOffset/60)"),"MM-dd-yyyy HH:mm:ss.fffz", $null, "AdjustToUniversal")
$LocalTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)"),"MM-dd-yyyy HH:mm:ss.fff", $null)
}
[pscustomobject]#{
UTCTime = $UTCTime
LocalTime = $LocalTime
FileName = $FileName
Component = $matches.component
Context = $matches.context
Type = $matches.type
TID = $matches.TI
Reference = $matches.reference
Message = $matches.message
}
}
}
}
}
The problem is that you have all the dynamic logic inside scriptblock in the if statement, and it handles the parameter addition only if the path provided contains a semicolon (':').
You could change it to something like:
if($path.ToCharArray() -contains ':') {
$FilePath = "\\$ComputerName\$($path -replace ':','$')"
} else {
$FilePath = $path
}
and continue your code from there
PS 6 can do a dynamic [ValidateSet] with a class:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-6#dynamic-validateset-values

How to extract metadata using a specific filename (get-childitem) rather than looping through ComObject namespace items

I have found multiple code snippets to scroll through a folder and display the metadata of each item in the folder, like this:
function funLine($strIN)
{
$strLine = "=" * $strIn.length
Write-Host -ForegroundColor Yellow "`n$strIN"
Write-Host -ForegroundColor Cyan $strLine
}
$sfolder = "S:\Temp"
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.namespace($sFolder)
foreach ($strFileName in $objFolder.items())
{funline "$($strFileName.name)"
for ($a ; $a -le 266; $a++)
{
$a
if($objFolder.getDetailsOf($strFileName, $a))
{
$hash += #{ $($objFolder.getDetailsOf($objFolder.items, $a)) = $a.tostring() + $($objFolder.getDetailsOf($strFileName, $a)) }
$hash | out-file c:\temp\output.txt -Append
$hash.clear()
}
}
$a=0
}
But in my script, I would like to loop through the folder(s) using Get-ChildItem and for selected files, I would like to use the getDetailsOf() to extract the authors of MS Office documents.
So, knowing the filename (example: $strFileName, can I skip the looping through each $strFileName in $objFolder.items() and just access the metadata details (where $a = 20) for the authors of $sFileName?
I have seen it done using "New-Object -ComObject word.application" but I believe that opens the document, so on a large file system with many files locked by users, this could be slow and painful.
Can I just jump to the index of $objFolder.items() for my selected filename?
Here, I was curious how it'd be done too so I looked it up and made a function that'll add that property to your [FileInfo] object (what's normally passed for a file by the Get-ChildItem cmdlet).
Function Get-CreatedBy{
[cmdletbinding()]
Param(
[Parameter(ValueFromPipelineByPropertyName=$true)]
[Alias("Path")]
[string[]]$FullName
)
Begin{
$Shell = New-Object -ComObject Shell.Application
}
Process{
ForEach($FilePath in $FullName){
$NameSpace = $Shell.NameSpace((Split-Path $FilePath))
$File = $NameSpace.ParseName((Split-Path $FilePath -Leaf))
$CreatedBy = $NameSpace.GetDetailsOf($File,20)
[System.IO.FileInfo]$FilePath|Add-Member 'CreatedBy' $CreatedBy -PassThru
}
}
}
Then you can just pipe things to that, or specify a path directly like:
Get-ChildItem *.docx | Get-CreatedBy | FT Name,CreatedBy
or
Get-CreatedBy 'C:\Temp\File.docx' | Select -Expand CreatedBy
Edit: Fixed for arrays of files! Sorry about the previous error.
Thanks Matt! Although that question was different, it had the one piece I was looking for - how to reference $objFolder.items().item($_.Name)
So this makes a quick little snippet to display the Authors (or any other metadata field):
$FullName = "S:\Temp\filename.xlsx"
$Folder = Split-Path $FullName
$File = Split-Path $FullName -Leaf
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.namespace($Folder)
$Item = $objFolder.items().item($File)
$Author = $objFolder.getDetailsOf($Item, 20)
Write-Host "$FullName is owned by $Author"
Where Author is the 20th metadata item.

Powershell to get metadata of files

I am looking to get metadata of a specified file (or directory of files). I am specifically looking for "Program Description" on .WTV files.
I found code, but it doesn't list that attribute. Some of that code looks like this:
foreach($sFolder in $folder)
{
$a = 0
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.namespace($sFolder)
foreach ($strFileName in $objFolder.items())
{ FunLine( "$($strFileName.name)")
for ($a ; $a -le 266; $a++)
{
if($objFolder.getDetailsOf($strFileName, $a))
{
$hash += #{ `
$($objFolder.getDetailsOf($objFolder.items, $a)) =`
$($objFolder.getDetailsOf($strFileName, $a))
} #end hash
$hash
$hash.clear()
I can see that attribute in File explorer.
The code you had #user1921849 has almost got it, but to address the original question more clearly you should use the Windows Shell Property System named properties, listed in the Windows devloper documentation for WTV files, and used in line 4 below.
$shell = new-object -com shell.application
$folder = $shell.namespace("\\MEDIA\Recorded Tv\")
$item = $folder.Items().Item('Person of Interest_WBBMDT_2013_11_26_21_01_00.wtv')
write-output $item.ExtendedProperty('System.RecordedTV.ProgramDescription')
Updated URL to docs
General properties list https://learn.microsoft.com/en-us/windows/desktop/properties/props
WTV properties list https://learn.microsoft.com/en-us/windows/desktop/properties/recordedtv-bumper
Grab TagLib# from Nuget or various other places. Then check out this blog post showing how to use TagLib# to edit MP3 tags. Hopefully, it can retrieve the WTV tag you're looking for.
$shell = new-object -comobject shell.application
$ShFolder=$shell.namespace("\\MEDIA\Recorded Tv\")
$ShFile =$ShFolder.parsename("Person of Interest_WBBMDT_2013_11_26_21_01_00.wtv")
$count = 0
while ($count -le 294)
{
$ShRating = $ShFolder.getdetailsof($ShFile,$count)
$count
$ShRating
$count = $count+1
}
Program Description is item 272.
I have done a sample code which will check all file in a folder and export a csv file with all the metadata details. Please find the following powershell script.
Step 1. Create a file Fileproperty.ps1
Import-Module ".\Module\AddModule.psm1" -Force
$commands = {
$source = read-host "Enter folder path "
if ([string]::IsNullOrWhitespace($source)){
Write-host "Invalid file path, re-enter."
$source = $null
&$commands
}else{
$output = read-host "Enter output folder path "
if ([string]::IsNullOrWhitespace($output)){
Write-host "Invalid output path, re-enter."
$output = $null
&$commands
}else{
$outputfilename = read-host "Enter output file name "
if ([string]::IsNullOrWhitespace($outputfilename)){
Write-host "Invalid file name, re-enter."
$outputfilename = $null
&$commands
}else{
Get-FileMetaData -folder $source | Export-Csv -Path $output\$outputfilename.csv -Encoding ascii -NoTypeInformation
Write-host "Process has been done..."
}
}
}
}
&$commands
Step 2. Create a folder Module
Step 3. create another file Module/AddModule.psm1
$FunctionFiles = #(Get-ChildItem -Path $PSScriptRoot\*.ps1 -ErrorAction SilentlyContinue)
Foreach($fileItem in #($FunctionFiles))
{
Try
{
. $fileItem.fullname
}
Catch
{
Write-Error -Message "Vsts module -> Unable to import a function in file $($fileItem.fullname): $_"
}
}
Export-ModuleMember -Function $FunctionFiles.Basename
Step 4. create another file Module/Get-FileMetaData.ps1
Function Get-FileMetaData
{
Param([string[]]$folder)
$OutputList = New-Object 'System.Collections.generic.List[psobject]'
foreach($sFolder in $folder) {
$a = 0
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.namespace($sFolder)
foreach ($File in $objFolder.items())
{
$FileMetaData = New-Object PSOBJECT
for ($a ; $a -le 266; $a++)
{
if($objFolder.getDetailsOf($File, $a))
{
$hash += #{$($objFolder.getDetailsOf($objFolder.items, $a)) =
$($objFolder.getDetailsOf($File, $a)) }
$FileMetaData | Add-Member $hash
$hash.clear()
} #end if
} #end for
$a=0
$OutputList.Add($FileMetaData)
} #end foreach $file
} #end foreach $sfolder
return $OutputList
} #end Get-FileMetaData
Hope this will work for you.