PowerShell create a duplicate folder with zero-size files - powershell

I want to create a 0-filesize mirror image of a set of folder, but while robocopy is really good, it doesn't save all of the information that I would like:
robocopy D:\documents E:\backups\documents_$(Get-Date -format "yyyyMMdd_HHmm")\ /mir /create
The /create switch makes each file in the duplicate folder have zero-size, and that is good, but I would like each file in the duplicate folder to have [size] appended to the end of the name with the size in KB or MB or GB, and the create / last modified time on every file to exactly match the original file. This way, I will have a zero-size duplicate of the folder that I can archive, but which contains all of the relevant information for the files in that directory, showing the size of each and the exact create / last modified times.
Are there good / simple ways to iterate through a tree in PowerShell, and for each item create a zero size file with all relevant information like this?

This would be one way to implement the copy command using the approach I mentioned in the comments. This should give you something to pull ideas from. I didn't intend to spend as much time on it as I did, but I ran it on several directories and found some problems and debugged each problem I encountered. This is a pretty solid example at this point.
function Copy-FolderZeroSizeFiles {
[CmdletBinding()]
param( [Parameter(Mandatory)] [string] $FolderPath,
[Parameter(Mandatory)] [string] $DestinationPath )
$dest = New-Item $DestinationPath -Type Directory -Force
Push-Location -LiteralPath $FolderPath
try {
foreach ($item in Get-ChildItem '.' -Recurse) {
$relPath = Resolve-Path -LiteralPath $item -Relative
$type = if ($item.Attributes -match 'Directory')
{ 'Directory' }
else { 'File' }
$destItem = New-Item "$dest\$relPath" -Type $type -Force
$destItem.Attributes = $item.Attributes
$destItem.LastWriteTime = $item.LastWriteTime
}
} finally {
Pop-Location
}
}
Note: the above implementation is simplistic and represents anything that isn't a directory as a file. That means symbolic links, et al. will be files with no information what they would be linked to.
Here's a function to get the conversion from number of bytes to N.N B/K/M/G format. To get more decimal places, just add 0's to the end of the format strings.
function ConvertTo-FriendlySize($NumBytes) {
switch ($NumBytes) {
{$_ -lt 1024} { "{0,7:0.0}B" -f ($NumBytes) ; break }
{$_ -lt 1048576} { "{0,7:0.0}K" -f ($NumBytes / 1024) ; break }
{$_ -lt 1073741824} { "{0,7:0.0}M" -f ($NumBytes / 1048576) ; break }
default { "{0,7:0.0}G" -f ($NumBytes / 1073741824); break }
}
}
Often, people get these conversions wrong. For instance, it's a common error to use 1024 * 1000 to get Megabytes (which is mixing the base10 value for 1K with the base2 value for 1K) and follow that same logic to get GB and TB.

Here is what I came up with with the additional parts in the question, change $src / $dst as required (D:\VMs is where I keep a lot of Virtual Machines). I have included setting all of CreationTime, LastWriteTime, LastAccessTime so that the backup location with zero-size files is a perfect representation of the source. As I want to use this for archival purposes, I have finally zipped things up and included a date-time stamp in the zipfile name.
# Copy-FolderZeroSizeFiles
$src = "D:\VMs"
$dst = "D:\VMs-Backup"
function ConvertTo-FriendlySize($NumBytes) {
switch ($NumBytes) {
{$_ -lt 1024} { "{0:0.0}B" -f ($NumBytes) ; break } # Change {0: to {0,7: to align to 7 characters
{$_ -lt 1048576} { "{0:0.0}K" -f ($NumBytes / 1024) ; break }
{$_ -lt 1073741824} { "{0:0.0}M" -f ($NumBytes / 1048576) ; break }
default { "{0:0.0}G" -f ($NumBytes / 1073741824); break }
}
}
function Copy-FolderZeroSizeFiles($FolderPath, $DestinationPath) {
Push-Location $FolderPath
if (!(Test-Path $DestinationPath)) { New-Item $DestinationPath -Type Directory }
foreach ($item in Get-ChildItem $FolderPath -Recurse -Force) {
$relPath = Resolve-Path $item.FullName -Relative
if ($item.Attributes -match 'Directory') {
$new = New-Item "$DestinationPath\$relPath" -ItemType Directory -Force -EA Silent
}
else {
$fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($item.Name)
$fileExt = [System.IO.Path]::GetExtension($item.Name)
$fileSize = ConvertTo-FriendlySize($item.Length)
$new = New-Item "$DestinationPath\$(Split-Path $relPath)\$fileBaseName ($fileSize)$fileExt" -ItemType File
}
"$($new.Name) : creation $($item.CreationTime), lastwrite $($item.CreationTime), lastaccess $($item.LastAccessTime)"
$new.CreationTime = $item.CreationTime
$new.LastWriteTime = $item.LastWriteTime
$new.LastAccessTime = $item.LastAccessTime
$new.Attributes = $item.Attributes # Must this after setting creation/write/access times or get error on Read-Only files
}
Pop-Location
}
Copy-FolderZeroSizeFiles $src $dst
$dateTime = Get-Date -Format "yyyyMMdd_HHmm"
$zipName = "$([System.IO.Path]::GetPathRoot($dst))\$([System.IO.Path]::GetFileName($dst))_$dateTime.zip"
Add-Type -AssemblyName System.IO.Compression.FileSystem
[IO.Compression.ZipFile]::CreateFromDirectory($dst, $zipName)

Related

Move multiple files from folder A to folder B

We need to move .csv files from folder where the are stored down to external server using powershell.
this is what i've tried so far but for some reason i only get message not copying and name of the files:
$DestinationFolder = "C:\d1\"
$SourceFolder = "C:\s1\"
If (-not (Test-Path $DestinationFolder) ) {
New-Item -ItemType Directory -Force -Path $DestinationFolder
}
$EarliestModifiedTime = (Get-Date).AddMinutes(200).Date # get's current time + adds 30 min
$LatestModifiedTime = (Get-Date).Date
echo($DestinationFolder); # will check time
Get-ChildItem "C:\s1\*.*" |
ForEach-Object {
if ( ($_.CreationTime -ge $EarliestModifiedTime) -and ($_.CreationTime -lt $LatestModifiedTime) ){ # i.e., all day yesterday
Copy-Item $_ -Destination $DestinationFolder -Force
Write-Host "Copied $_" }
else {
Write-Host "Not copying $_"
}
}
does it work if you simplify it and just try for a test (e.g. pick one file rather than trying to run for a delta of every 30 mins / last day)? Just thinking first you need to see if the problem is with accessing (or the formatting) of your source / destination directories or the delta logic itself. Perhaps some more error conditions would help....

How to compare Last.Write.Time using Powershell

I wanted to write script which will merge files to one, IF they were modified later then the one which should be a destination. My script looks like this:
Function UnifyConfigs { param ( $destination = "C:\temp\all.txt", [Parameter()] $files )
foreach ($config in $files) {
If((Get-ChildItem $config ).LastWriteTime -gt (Get-Item $destination).LastWriteTime)
{
Clear-Content -path $destination
Set-Content -path $destination -value (Get-Content $config)
}
else {
break
}
}
}
My main problem is that $destination file is modified ALWAYS . As far as I understand it should be changed only if modification date of $config is greater than modification date of $destination. But now, it is overwritten each time I run script. What is wrong?
$destination as defined in your param block is a string - you need to resolve the corresponding item in the file system provider to get to the LastWriteTime value - here using Get-Item:
if((Get-Item $config).LastWriteTime -gt (Get-Item $destination).LastWriteTime)
{
# ...
}

Looping through File Groups such as FileGroup159, FileGroup160, etc. in Powershell

So I got the code to work how I like it for individual files. Based on some of the suggestions below, I was able to come up with this:
$Path = "C:\Users\User\Documents\PowerShell\"
$Num = 160
$ZipFile = "FileGroup0000000$Num.zip"
$File = "*$Num*.txt"
$n = dir -Path $Path$File | Measure
if($n.count -gt 0){
Remove-Item $Path$ZipFile
Compress-Archive -Path $Path$File -DestinationPath $Path
Rename-Item $Path'.zip' $Path'FileGroup0000000'$Num'.zip'
Remove-Item $Path$File
}
else {
Write-Output "No Files to Move for FileGroup$File"
}
The only thing I need to do now is have $Num increment after the program finishes each time. Therefore the program will run, and then move $Num to 160, 161, etc. and I will not have to re initiate the code manually. Thanks for the help so far.
Your filename formatting should go inside the loop and you should use the Format operator -f to get the preceding zeros, like:
159..1250 | ForEach-Object {
$UnzippedFile = 'FileGroup{0:0000000000}' -f $_
$ZipFile = "$UnzippedFile.zip"
Write-Host "Unzipping: $ZipFile"
# Do your thing here
}

Regex for dirpaths named with letters

I have a large amount of folders that are named like this:
C:\Folders\A
C:\Folders\A\AB\ABC
C:\Folders\E\EA\EAB\EABA
C:\Folders\A\AZ\AZA
C:\Folders\B\BA\BAE
And in a PowerShell script I want to move some files (or create directory if it doesn't exist) to the right place. E.g FileABC.txt to C:\Folders\A\AB\ABC and FileBA.txt to C:\Folders\B\BA.
What I have now is this:
$naming just contains all the letter codes from a CSV.
if ($naming -match '^A$') {
$folderA = C:\Folders
New-Item -Name $naming -Path $folderA -Type Directory
}
Which creates C:\Folders\A. So I do the same for all the other top level folders (there are only 6 of them).
To match the 2 letter paths I do:
if ($naming -match '^A[A-Z]{1}$'){
$folderA2 = "C:\Folders\A\"
New-Item -Name $naming -Path $folderA2 -Type Directory
}
Which creates C:\Folders\A\AA, C:\Folders\A\AB etc.
The problem arises when I want to create the 3 or 4 letter directories:
I tried this:
if ($naming -match '^A[A-Z]{2}$') {
$folderA3 = 'C:\Folders\A\$($naming)\'
New-Item -Name $naming -Path $folderA3 -Type Directory
}
and:
if ($naming -match '^A[A-Z]{3}$') {
$folderA3 = 'C:\Folders\A\$($naming)\$($naming)\'
New-Item -Name $naming -Path $folderA3 -Type Directory
}
But files are not placed correctly. E.g FileABC.txt is moved to random places like C:\Folders\A\AK\ABC.
#edit
I also notice that folders are not created in the right place. Folders with 3 or 4 letter combos are placed at which seems random:
C:\Folders\E\EA\EBC
C:\Folders\A\AB\AZA
C:\Folders\E\EC\EFG\ECXA
I could do:
if ($naming -match '^AB[A-Z]{2}$')
if ($naming -match '^AC[A-Z]{2}$')
But then i would have to make one for each letter A-Z, which i feel should not be necessary.
Cannot totally discern the algorithm because your examples do not map to the same things, but perhaps with an approach like this, you can further refine it do to what you want. It does not use regex, but does do the mapping I believe to what you want, or close to it.
# E.g FileABC.txt to C:\Folders\A\AB\ABC and FileBA.txt to C:\Folders\B\BA.
function FileToPath($file)
{
$filePrefix = 'File'
$destPrefix = 'c:\Folders'
$fileParts = [System.IO.Path]::GetFileNameWithoutExtension($file) -split $filePrefix
$destPath = $destPrefix
$len = $fileParts[1].Length
for ($i = 0; $i -lt $len; $i++)
{
$destPath = Join-Path $destPath $fileParts[1].Substring(0,$i+1)
# or maybe the below?
#$destPath = Join-Path $destPath $fileParts[1][$i]
}
return (Join-Path $destPath $file)
}
'=== 1 ==='
$f = FileToPath 'FileABC.txt'
$f
Split-Path -Path $f -Parent
Split-Path -Path $f -Leaf
'=== 2 ==='
FileToPath 'FileBA.txt'
See also Test-Path, New-Item, Move-Item, etc.

Using Powershell to recursively search directory for files that only contain zeros

I have a directory that contains millions of files in binary format. Some of these files were written to the disk wrong (no idea how). The files are not empty, but they only contain zeros. Heres an example http://pastebin.com/5b7jHjgr
I need to search this directory, find the files that are all zeros and write their path out to a file.
I've been experimenting with format-hex and get-content, but my limited powershell experience is tripping me up. Format-Hex reads the entire file, when I only need the first few bytes, and Get-Content expects text files.
Use IO.BinaryReader:
Get-ChildItem r:\1\ -Recurse -File | Where {
$bin = [IO.BinaryReader][IO.File]::OpenRead($_.FullName)
foreach ($byte in $bin.ReadBytes(16)) {
if ($byte) { $bin.Close(); return $false }
}
$bin.Close()
$true
}
In the old PowerShell 2.0 instead of -File parameter you'll need to filter it manually:
Get-ChildItem r:\1\ -Recurse | Where { $_ -is [IO.FileInfo] } | Where { ..... }
You can use a System.IO.FileStream object to read the first n bytes of each file.
The following code reads the first ten bytes of each file:
Get-ChildItem -Path C:\Temp -File -Recurse | ForEach-Object -Process {
# Open file for reading
$file = [System.IO.FileStream]([System.IO.File]::OpenRead($_.FullName))
# Go through the first ten bytes of the file
$containsTenZeros = $true
for( $i = 0; $i -lt $file.Length -and $i -lt 10; $i++ )
{
if( $file.ReadByte() -ne 0 )
{
$containsTenZeros = $false
}
}
# If the file contains ten zeros then add its full path to List.txt
if( $containsTenZeros )
{
Add-Content -Path List.txt -Value $_.FullName
}
}