Copying Folder Items With GUI Variation - user-interface

This code will use a Shell.Application COM object and use the native Windows copy dialogue to copy an item to a specified destination. The only problem is that for each immediate child folder within the source it will create separate copy dialogues.
Is there any way for me to only get 1 copy dialogue displayed so that the user can see accurate info such as overall progress, time remaining, etc.
The easiest thing I can think of so far is to either zip up the files then decompress them at the source (please, no) or to just copy the entire parent item then move the child items into place after, although I feel like that would limit the capabilities of the function.
Can anyone think of a good solution for this?
function Copy-ItemGUI {
Param(
# TODO: Allow only folder paths (Can we test these here and loop if
# path is invalid?)
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
$Source,
[Parameter(Mandatory=$true, Position=1)]
[string]$Destination
)
#If destination does not exist, break
#TODO: Create folder if destination does not exist
if (!(Test-Path $Destination)) {
break
}
$src = gci $Source
$objShell = New-Object -ComObject "Shell.Application"
$objFolder = $objShell.NameSpace($Destination)
$counter = ($src.Length) - 1
foreach ($file in $src) {
Write-Host -ForegroundColor Cyan "Copying file '"$file.name"' to ' $Destination '"
try {
#Info regarding options for displayed info during shell copy - https://technet.microsoft.com/en-us/library/ee176633.aspx
$objFolder.CopyHere("$source\$file", "&H0&")
} catch {
Write-Error $_
}
Write-Host -ForegroundColor Green "Copy complete - Number of items remaining: $counter`n"
$counter--
}
}

Don't enumerate the contents of $source and copy each file separately. Use a wildcard for specifying the items to copy. Change your function to this:
function Copy-ItemGUI {
Param(
[Parameter(
Mandatory=$true,
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[ValidateScript({Test-Path -LiteralPath $_})]
[string[]]$Source,
[Parameter(Mandatory=$true, Position=1)]
[string]$Destination
)
Begin {
if (-not (Test-Path -LiteralPath $Destination)) {
New-Item -Type Directory $Destination | Out-Null
}
$objShell = New-Object -ComObject 'Shell.Application'
$objFolder = $objShell.NameSpace($Destination)
}
Process {
$objFolder.CopyHere("$source\*", '&H0&')
}
}

Related

How can I move an array of files dynamically with Powershell and RoboCopy to individual subfolders of a fixed size?

I am creating a script that splits a target folder's files into subfolders of n length, where n is a number specified dynamically.
So basically, if Folder A has 9000 files, and I limit the number of files to 1000 per folder, the script would create nine sub-directories inside of Folder A with 1000 files each.
Here is working code:
param (
[Parameter(Mandatory,Position=0)]
[String]
$FileList,
[Parameter(Mandatory=$false,ValueFromPipelineByPropertyName)]
[Int32]
$NumFilesPerFolder = 1000,
[Parameter(Mandatory=$false,ValueFromPipelineByPropertyName)]
[Int32]
$FolderNumberPadding = 2
)
$Folders = Get-Content $FileList
Set-Location -LiteralPath ([IO.Path]::GetTempPath())
function Move-Files {
[CmdletBinding()]
param (
[Parameter(Mandatory,Position=0)]
[System.Collections.ArrayList]
$List,
[Parameter(Mandatory)]
[Int32]
$Index
)
$BaseFolder = [System.IO.Path]::GetDirectoryName($List[0])
$DestFolderName = $Index.ToString().PadLeft($FolderNumberPadding, '0')
$DestFolder = New-Item -Path (Join-Path $BaseFolder $DestFolderName) -Type Directory -Force
Move-Item $List -Destination $DestFolder -Force
}
foreach ($Folder in $Folders) {
$Files = Get-ChildItem -LiteralPath $Folder -File -Force
$filesidx = 1
$totalidx = $null
$groupidx = 0
$FilesToMove = [System.Collections.ArrayList]#()
foreach ($File in $Files) {
if($null -eq $totalidx){
$totalidx = $Files.Length
}
if($filesidx -eq 1){
$groupidx++
}
$FilesToMove.Add($File)
if($filesidx -eq $NumFilesPerFolder){
Move-Files -List $FilesToMove -Index $groupidx
$FilesToMove.Clear()
$filesidx = 1
}elseif($totalidx -eq 1){
Move-Files -List $FilesToMove -Index $groupidx
$FilesToMove.Clear()
break
}else{
$filesidx++
}
$totalidx--
}
}
Remove-Item $FileList -Force
$app = New-Object -ComObject Shell.Application
$appwin = $app.Windows()
foreach ($window in $appwin) {
if($window.Name -eq "File Explorer"){
$window.Refresh()
}
}
Invoke-VBMessageBox "Operation Complete" -Title "Operation Complete" -Icon Information -BoxType OKOnly
This code runs reasonably well, but it heavily bottlenecks when actually moving the files with Move-Item. I'd like to try and use RoboCopy here, but I am perplexed as to how I can implement it.
What I'm having trouble with is that the items I need to move are stored in a list (see the Move-Files function), and every item that needs to be moved are all in the same sub-directory. So I can't just do RoboCopy.exe C:\Source C:\Destination /mov.
How can I integrate RoboCopy here to accomplish my goal? I really need multi-threaded performance as this function will be responsible for moving thousands of files around in production on a frequent basis.
Any help would be greatly appreciated - please let me know if I can provide more information to further clarify my objective.
Thanks for any help at all!

Powershell - Move Images in Subdirectories + Maintain Directory Structure

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)"
}
}

in powershell, copy-item and test-path are both failing silently.. is it my code, or something else...?

I frequently have to copy a single file to multiple destinations, so i'm trying to write a script to make that go faster. it seems to work fine when i'm dealing with local files, but fails without any errors when running on a file that is on a mapped network drive.
at first I was using copy-item, and I couldn't make that work, so i used robocopy. that does the trick, but if the file already exists, i have an if statement using test-path which is supposed to skip to a user input that asks if you want to overwrite.. this is not working. i should say the one that checks the folder exists is working, but the one that checks for the file name always comes back true. for now, i have it just forcing an overwrite with robocopy because most of the time that's what i'll want to do anyway.
here's what i have right now.. "K:" is the mapped network drive i'm copying to, and i'm usually copying files from another mapped network drive "T:". I also should mention i have this set up to run from the context menu in windows (7) explorer, and it passes the file path to the script via %L and $args.
any advice is appreciated. (i apologize in advance, i know it's rather rough.. This is somewhat new to me.)
$Folders = #("K:\OKKHM 800" , "K:\OKKHM 1000" , "K:\OKKHM 1002" , "K:\OKKHM 1003" , "K:\OKKHM 1004", "K:\OKKHM 1250")
$source = $args[0]
$Filename = Split-Path -Path $source -Leaf
$sourcefolder= split-path -path $source -parent
$COUNTER = 0
$successful=0
$CONFIRMATION=0
foreach($Folder in $Folders){
$newpath = $folder + "\" + $filename
WRITE-HOST $NEWPATH
if(-not(test-path -path $newpath)) {
if((test-path -path $folder)) {
WRITE-HOST 'TEST 2'
robocopy $sourcefolder $folder $filename -is -it
$successful=1
}
else{
write-host 'folder does not exist'
}
}
else {
$title = 'Existing File Will Be Overwritten'
$question = 'Are you sure you want to proceed?'
$choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)
if ($decision -eq 0) {
Write-Host 'confirmed'
$CONFIRMATION=1
}
else {
Write-Host 'cancelled'
$CONFIRMATION=0
}
IF ($CONFIRMATION -EQ 1) {
try {
robocopy $sourcefolder $folder $filename
$successful=1
}
catch {
throw "NO GOOD"
}
}
}
$COUNTER++
}
if ($successful -eq 1) {
WRITE-HOST 'SUMMARY: ' $COUNTER ' FILES COPIED SUCCESSFULLY.'
}
Start-Sleep 5

Powershell issues with parameters: 'Cannot convert value "" to type "System.Boolean"'

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"

Move contents of folder (Download) into recycle.bin folder - Powershell [duplicate]

When using the rm command to delete files in Powershell, they are permanently deleted.
Instead of this, I would like to have the deleted item go to the recycle bin, like what happens when files are deleted through the UI.
How can you do this in PowerShell?
2017 answer: use the Recycle module
Install-Module -Name Recycle
Then run:
Remove-ItemSafely file
I like to make an alias called trash for this.
If you don't want to always see the confirmation prompt, use the following:
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('d:\foo.txt','OnlyErrorDialogs','SendToRecycleBin')
(solution courtesy of Shay Levy)
It works in PowerShell pretty much the same way as Chris Ballance's solution in JScript:
$shell = new-object -comobject "Shell.Application"
$folder = $shell.Namespace("<path to file>")
$item = $folder.ParseName("<name of file>")
$item.InvokeVerb("delete")
Here is a shorter version that reduces a bit of work
$path = "<path to file>"
$shell = new-object -comobject "Shell.Application"
$item = $shell.Namespace(0).ParseName("$path")
$item.InvokeVerb("delete")
Here's an improved function that supports directories as well as files as input:
Add-Type -AssemblyName Microsoft.VisualBasic
function Remove-Item-ToRecycleBin($Path) {
$item = Get-Item -Path $Path -ErrorAction SilentlyContinue
if ($item -eq $null)
{
Write-Error("'{0}' not found" -f $Path)
}
else
{
$fullpath=$item.FullName
Write-Verbose ("Moving '{0}' to the Recycle Bin" -f $fullpath)
if (Test-Path -Path $fullpath -PathType Container)
{
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
else
{
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
}
}
Remove file to RecycleBin:
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('e:\test\test.txt','OnlyErrorDialogs','SendToRecycleBin')
Remove folder to RecycleBin:
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::Deletedirectory('e:\test\testfolder','OnlyErrorDialogs','SendToRecycleBin')
Here's slight mod to sba923s' great answer.
I've changed a few things like the parameter passing and added a -WhatIf to test the deletion for the file or directory.
function Remove-ItemToRecycleBin {
Param
(
[Parameter(Mandatory = $true, HelpMessage = 'Directory path of file path for deletion.')]
[String]$LiteralPath,
[Parameter(Mandatory = $false, HelpMessage = 'Switch for allowing the user to test the deletion first.')]
[Switch]$WhatIf
)
Add-Type -AssemblyName Microsoft.VisualBasic
$item = Get-Item -LiteralPath $LiteralPath -ErrorAction SilentlyContinue
if ($item -eq $null) {
Write-Error("'{0}' not found" -f $LiteralPath)
}
else {
$fullpath = $item.FullName
if (Test-Path -LiteralPath $fullpath -PathType Container) {
if (!$WhatIf) {
Write-Verbose ("Moving '{0}' folder to the Recycle Bin" -f $fullpath)
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
else {
Write-Host "Testing deletion of folder: $fullpath"
}
}
else {
if (!$WhatIf) {
Write-Verbose ("Moving '{0}' file to the Recycle Bin" -f $fullpath)
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
else {
Write-Host "Testing deletion of file: $fullpath"
}
}
}
}
$tempFile = [Environment]::GetFolderPath("Desktop") + "\deletion test.txt"
"stuff" | Out-File -FilePath $tempFile
$fileToDelete = $tempFile
Start-Sleep -Seconds 2 # Just here for you to see the file getting created before deletion.
# Tests the deletion of the folder or directory.
Remove-ItemToRecycleBin -WhatIf -LiteralPath $fileToDelete
# PS> Testing deletion of file: C:\Users\username\Desktop\deletion test.txt
# Actually deletes the file or directory.
# Remove-ItemToRecycleBin -LiteralPath $fileToDelete
Here is a complete solution that can be added to your user profile to make 'rm' send files to the Recycle Bin. In my limited testing, it handles relative paths better than the previous solutions.
Add-Type -AssemblyName Microsoft.VisualBasic
function Remove-Item-toRecycle($item) {
Get-Item -Path $item | %{ $fullpath = $_.FullName}
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
Set-Alias rm Remove-Item-toRecycle -Option AllScope

Resources