I'm trying to get a listing of all permissions on some network folders using PowerShell. Unfortunately I'm encountering the dreaded PathTooLongException so I'm attempting to use Robocopy as a work around. However I'm a complete novice with PowerShell so was hoping for a little help. The easiest command I've come up with is
Get-Childitem "S:\StartingDir" -recurse | Get-Acl | Select-Object path,accestostring | Export-Csv "C:\export.csv"
That works and does what I want except the exception I'm getting. How would I insert Robocopy into this statement to bypass the exception? Any thoughts?
First, create a batch file, such as getShortFilename.bat, with the following lines:
#ECHO OFF
echo %~s1
This will return the short filename of the long filename passed to it. The following script will use that to get the short filename when Get-Acl fails due to a long path. It will then use the short path with cacls to return the permissions.
$files = robocopy c:\temp NULL /L /S /NJH /NJS /NDL /NS /NC
remove-item "c:\temp\acls.txt" -ErrorAction SilentlyContinue
foreach($file in $files){
$filename = $file.Trim()
# Skip any blank lines
if($filename -eq ""){ continue }
Try{
Get-Acl "$filename" -ErrorAction Stop| Select-Object path, accesstostring | Export-Csv "C:\temp\acls.txt" -Append
}
Catch{
$shortName = &C:\temp\getShortFilename.bat "$filename"
$acls = &cacls $shortName
$acls = $acls -split '[\r\n]'
#create an object to hold the filename and ACLs so that export-csv will work with it
$outObject = new-object PSObject
[string]$aclString
$firstPass = $true
# Loop through the lines of the cacls.exe output
foreach($acl in $acls){
$trimmedAcl = $acl.Trim()
# Skip any blank lines
if($trimmedAcl -eq "" ){continue}
#The first entry has the filename and an ACL, so requires extra processing
if($firstPass -eq $true){
$firstPass = $false
# Add the long filename to the $exportArray
$outObject | add-member -MemberType NoteProperty -name "path" -Value "$filename"
#$acl
# Add the first ACL to $aclString
$firstSpace = $trimmedAcl.IndexOf(" ") + 1
$aclString = $trimmedAcl.Substring($firstSpace, $trimmedAcl.Length - $firstSpace)
} else {
$aclString += " :: $trimmedAcl"
}
}
$outObject | add-member -MemberType NoteProperty -name "accesstostring" -Value "$aclString"
$outObject | Export-Csv "C:\temp\acls.txt" -Append
}
}
Notes:
The string of ACLs that is created by Get-Acl is different from the on created by cacls, so whether that's an issue or not...
If you want the ACL string to be in the same format for all files, you could just use cacls on all files, not just the ones with long filenames. It wouldn't be very difficult to modify this script accordingly.
You may want to add extra error checking. Get-Acl could of course fail for any number of reasons, and you may or may not want to run the catch block if it fails for some reason other than the path too long.
Related
I tried following Remove leading spaces in Windows file names but it's not working for my use case.
I have a lot of folders and filenames that either have a blank space at the front or at the end. How would I go about removing those spaces in bulk?
This was the command-line command I used after following the linked post:
for /R %A IN ("* ") do #for /F "tokens=*" %B IN ("%~nxA") do #ren "%A" "%B"
But it didn't work out.
Update: thank you to all who replied trying to help. I think there is just a Windows-level glitch in the file system. I ended up just having to manually create new folders without leading and trailing spaces and then dragging all the files over manually then renaming those to non-trailing and leading names as well.
It's unclear whether or not you want a PowerShell solution, but there's a reasonable assumption to be made you might.
If you wanted a PowerShell solution, you could try this:
function Test-LeadingTrailingWhitespace {
param(
[Parameter(Mandatory)]
[String]$String
)
$String[0] -eq ' ' -Or $String[-1] -eq ' '
}
Get-ChildItem -Path "<path_to_folder>" | ForEach-Object {
if ($_.PSIsContainer -And (Test-LeadingTrailingWhitespace -String $_.Name)) {
$Destination = Split-Path -Path $_.FullName -Parent
$NewName = $_.Name.Trim()
Move-Item -Path $_ -Destination (Join-Path -Path $Destination -ChildPath $NewName)
}
elseif (Test-LeadingTrailingWhitespace -String $_.BaseName) {
$Destination = Split-Path -Path $_.FullName -Parent
$NewName = $_.BaseName.Trim() + $_.Extension
Move-Item -Path $_ -Destination (Join-Path -Path $Destination -ChildPath $NewName)
}
}
To be on the safe side, you could add -WhatIf or -Confirm on the Move-Item cmdlet. The former will tell you what would have changed without that parameter without actually making any changes (like a 'dry run'). The latter will prompt you for confirmation before making each change, giving you a chance to validate incrementally and not make changes en masse from the moment you hit enter.
Trim() is a method available for all strings in PowerShell:
Returns a new string in which all leading and trailing occurrences of a set of specified characters from the current string are removed.
You can loop over files and folder and check if they actually have a leading or trailing whitespace before renaming, this would avoid errors like:
Rename-Item: Source and destination path must be different.
We can use the -match matching operator with a simple regex ^\s|\s$ (starts with whitespace or ends with whitespace - regex101 link for a simple example) to see if the file or folder should be renamed:
Get-ChildItem path\to\startingfolder -Recurse | ForEach-Object {
$newName = switch($_) {
# handle folders
{ $_.PSIsContainer -and $_.Name -match '^\s|\s$' } {
$_.Name.Trim()
break
}
# handle files
{ $_.BaseName -match '^\s|\s$' -or $_.Extension -match '^\s|\s$' } {
$_.BaseName.Trim() + $_.Extension.Trim()
break
}
# if none of the above conditions were true, continue with next item
Default {
return
}
}
Rename-Item -LiteralPath $_.FullName -NewName $newName
}
Personally, I'd do this in two steps to rename folders and files separately. This to overcome the problem that when a folder is renamed, the items inside that folder all have a new path.
Using switch -Force allows renaming items such as hidden or read-only files
Using -ErrorAction SilentlyContinue swallows the error when the new name is equal to the existing name
$rootPath = 'X:\thepath'
# first the folders and subfolders (deepest nesting first)
(Get-ChildItem -Path $rootPath -Directory -Recurse | Sort-Object FullName -Descending) |
Rename-Item -NewName {$_.Name.Trim()} -Force -ErrorAction SilentlyContinue
# next the files
(Get-ChildItem -Path $rootPath -File -Recurse) |
Rename-Item -NewName {'{0}{1}' -f $_.BaseName.Trim(), $_.Extension.Trim()} -Force -ErrorAction SilentlyContinue
so I am helping with the migration of the data to another company which bought the first one, for which I have to export Foldernames, User-Names and Permissions in an .csv File.
I have a working Skript but it seems to be taking an awful long amount of time. It worked with a few shares which don´t have too much folders, but now I have run into scripts running for multiple hours(100.000+ Folders).
From the smaller shares I know it gives me the correct information but it is taking to long for the amount of shares left.
$FolderPath = dir -Directory -LiteralPath "\\?\UNC\Server\Share" -Recurse -Force
$Report = #()
Foreach ($Folder in $FolderPath) {
$Acl = Get-Acl -Path $Folder.FullName
foreach ($Access in $acl.Access){
$Properties = [ordered]#{
'FolderName'=$Folder.FullName
'AD Group or User'=$Access.IdentityReference
'Permissions'=$Access.FileSystemRights
'Inherited'=$Access.IsInherited
}
$Report += New-Object -TypeName PSObject -Property $Properties
}
}
$Report | Export-Csv -path "C:\Filepath\export.csv" -Delimiter ";" -Encoding UTF8
Am I missing a simple thing why its taking so long or did I just mess up completely?
I just don´t seem to find it.
Any help would be very much appreciated
Thanks in Advance
Michael
As commented, building the $Report array by adding new items to it with += is a very time and memory consuming way.
It is way quicker to let PowerShell do the collecting like below.
Also, Get-ChildItem (alias dir) can be slow, especially when dealing with many subfolders, so using RoboCopy could be a faster alternative for this.
# get the list of directory full path names
# Get-ChildItem can be slow, so maybe try Robocopy as alternative
# $Folders = (Get-ChildItem -Directory -LiteralPath "\\?\UNC\Server\Share" -Recurse -Force).FullName
$Folders = (robocopy "\\Server\Share" NULL /L /E /NP /NFL /XJ /NC /NJH /NJS /R:0 /W:0) -replace '^\s*\d*\s*'
$Report = foreach ($Folder in $Folders) {
$Acl = Get-Acl -Path $Folder
foreach ($Access in $Acl.Access){
[PsCustomObject]#{
'FolderName' = $Folder
'AD Group or User' = $Access.IdentityReference
'Permissions' = $Access.FileSystemRights.ToString()
'Inherited' = $Access.IsInherited
}
}
}
$Report | Export-Csv -Path "C:\Filepath\export.csv" -Delimiter ";" -Encoding UTF8
I have a file transfer/sync job that is copying files from the main network into a totally secure network using a custom protocol (ie no SMB). The problem is that because I can't look back to see what files exist, the destination is filling up, as the copy doesn't remove any files it hasn't touched (like robocopy MIR does).
Initailly I wrote a script that:
1. Opens the log file and grabs the file paths out (this is quite quick and painless)
2. Does a Get-ChildItem on the destination folder (now using dir /s /b as it's way faster than gci)
3. Compared the two, and then removed the differences.
The problem is that there are more jobs that require this clean up but the log files are 100MB and the folders contain 600,000 files, so it's taking ages and using tons of memory. I actually have yet to see one finish. I'd really like some ideas on how to make this faster (memory/cpu use doesn't bother me too much but speed is essential.
$destinationMatch = "//server/fileshare/folder/"
the log file contains some headers and footers and then 600,000 lines like this one:
"//server/fileshare/folder/dummy/deep/tags/20140826/more_stuff/Deeper/2012-07-02_2_0.dat_v2" 33296B 0B completed
Here's the script:
[CmdletBinding(SupportsShouldProcess=$True)]
param(
[Parameter(Mandatory=$True)]
[String]$logName,
[Parameter(Mandatory=$True)]
[String]$destinationMatch
)
$logPath = [string]("C:\Logs\" + $logName)
$manifestFile = gci -Path $logPath | where {$_.name -match "manifest"} | sort creationtime -descending | select Name -first 1
$manifestFileName = [string]$manifestFile.name
$manifestFullPath = $logPath + "\" + $manifestFileName
$copiedList = #()
(gc $manifestFullPath -ReadCount 0) | where {$_.trim() -match $DestinationMatch} | % {
if ( $_ -cmatch '(?<=")[^"]*(?=")' ){
$copiedList += ($matches[0]).replace("/","\")
}
}
$dest = $destinationMatch.replace("/","\")
$actualPathString = (gci -Path $dest -Recurse | select fullname).fullnameCompare-Object -ReferenceObject $copiedList -DifferenceObject $actualPathString -PassThru | % {
$leaf = Split-Path $_ -leaf
if ($leaf.contains(".")){
$fsoData = gci -Path $_
if (!($fsoData.PSIsContainer)){
Remove-Item $_ -Force
}
}
}
$actualDirectory | where {$_.PSIsContainer -and #(gci -LiteralPath $_.FullName -Recurse -WarningAction SilentlyContinue -ErrorAction SilentlyContinue | where {!$_.PSIsContainer}).Length -eq 0} | remove-item -Recurse -Force
Ok, so let's assume that your file copy preserves the last modified date/time stamp. If you really need to pull a directory listing, and compare it against a log, I think you're doing a decent job of it. The biggest slow down is obviously going to be pulling your directory listing. I'll address that shortly. For right now I would propose the following modification of your code:
[CmdletBinding(SupportsShouldProcess=$True)]
param(
[Parameter(Mandatory=$True)]
[String]$logName,
[Parameter(Mandatory=$True)]
[String]$destinationMatch
)
$logPath = [string]("C:\Logs\" + $logName)
$manifestFile = gci -Path $logPath | where {$_.name -match "manifest"} | sort creationtime -descending | select -first 1
$RegExPattern = [regex]::escape($DestinationMatch)
$FilteredManifest = gc $manifestfile.FullPath | where {$_ -match "`"($RegexPattern[^`"]*)`""} |%{$matches[1] -replace '/','\'}
$dest = $destinationMatch.replace("/","\")
$DestFileList = gci -Path $dest -Recurse | select Fullname,Attributes
$DestFileList | Where{$FilteredManifest -notcontains $_.FullName -and $_.Attributes -notmatch "Directory"}|Remove-Item $_ -Force
$DestFileList | Where{$FilteredManifest -notcontains $_.FullName -and $_.Attributes -match "Directory" -and (gci -LiteralPath $_ -Recurse -WarningAction SilentlyContinue -ErrorAction SilentlyContinue).Length -eq 0}{Remove-Item $_ -Recurse -Force}
This stops you from duplicating efforts. There's no need to get your manifest file, and then assign different variables to different properties of the file object, just reference them directly. Then later when you pull your directory listing of the drive (the slow part here), keep the full name and attributes of the files/folders. That way you can easily filter against Attributes to see what's a directory and what not, so we can deal with files first, then clean up directories later after the files are cleaned up.
That script should be a bit more streamlined version of yours. Now, about pulling that directory listing... Here's the deal, using Get-ChildItem is going to be slower than some alternatives (such as dir /s /b) but it stops you from having to duplicate efforts by later checking what's a file, and what's a directory. I suppose if the actual files/folders that you are concerned with are a small percentage of the total, then the double work may actually be worth the time and effort to pull the list with something like dir /s /b, and then parse against the log, and only pull folder/file info for the specific items you need to address.
I'm working on a script that checks folders in specific directory. For example, I run the script for first time, it generates me a txt file containing folders in the directory.
I need the script to add any new directories that are found to the previously created txt file when the script is run again.
Does anyone have any suggestions how to make that happen?
Here is my code so far:
$LogFolders = Get-ChildItem -Directory mydirectory ;
If (-Not (Test-Path -path "txtfilelocated"))
{
Add-Content txtfilelocated -Value $LogFolders
break;
}else{
$File = Get-Content "txtfilelocatedt"
$File | ForEach-Object {
$_ -match $LogFolders
}
}
$File
something like this?
You can specify what directory to check adding path to get-childitem cmdlet in first line
$a = get-childitem | ? { $_.psiscontainer } | select -expand fullname #for V2.0 and above
$a = get-childitem -Directory | select -expand fullname #for V3.0 and above
if ( test-path .\list.txt )
{
compare-object (gc list.txt) ($a) -PassThru | Add-Content .\list.txt
}
else
{
$a | set-content .\list.txt
}
# ---------------------------------------------------------
# ScriptingGamesBeginnerEvent8_PS1.ps1
# ed wilson, msft 8/21/2009
# PS1 version of HSG-08-19-09 http://bit.ly/1d8Rww
#
# ---------------------------------------------------------
Param(
[string]$path = 'C:\',
[int]$first = 50
)# end param
# *** Function Here ***
function Get-DirSize ($path){
BEGIN {}
PROCESS{
$size = 0
$folders = #()
foreach ($file in (Get-ChildItem $path -Force -ea SilentlyContinue)) {
if ($file.PSIsContainer) {
$subfolders = #(Get-DirSize $file.FullName)
$size += $subfolders[-1].Size
$folders += $subfolders
} else {
$size += $file.Length
}
}
$object = New-Object -TypeName PSObject
$object | Add-Member -MemberType NoteProperty -Name Folder -Value (Get-Item $path).fullname
$object | Add-Member -MemberType NoteProperty -Name Size -Value $size
$folders += $object
Write-Output $folders
}
END {}
} # end function Get-DirSize
Function Get-FormattedNumber($size)
{
IF($size -ge 1GB)
{
"{0:n2}" -f ($size / 1GB) + " GigaBytes"
}
ELSEIF($size -ge 1MB)
{
"{0:n2}" -f ($size / 1MB) + " MegaBytes"
}
ELSE
{
"{0:n2}" -f ($size / 1KB) + " KiloBytes"
}
} #end function Get-FormattedNumber
# *** Entry Point to Script ***
if(-not(Test-Path -Path $path))
{
Write-Host -ForegroundColor red "Unable to locate $path"
Help $MyInvocation.InvocationName -full
exit
}
Get-DirSize -path $path |
Sort-Object -Property size -Descending |
Select-Object -Property folder, size -First $first |
Format-Table -Property Folder,
#{ Label="Size of Folder" ; Expression = {Get-FormattedNumber($_.size)} }
So I have this script which I got from
http://gallery.technet.microsoft.com/scriptcenter/36bf0988-867f-45be-92c0-f9b24bd766fb#content
I've been playing around with it and created a batch file to help handle the log output of this file and such. However, I'm noticing that paths with spaces in them don't get read. For example ..Documents\My Music
Get-Item : Could not find item C:\Users\MyUser\Documents\My Music.
At C:\test.ps1:32 char:80
+ $object | Add-Member -MemberType NoteProperty -Name Folder -Value (Get-It
em <<<< $path).fullname
+ CategoryInfo : ObjectNotFound: (C:\Users\MyUser\Documents\My
Music:String) [Get-Item], IOException
+ FullyQualifiedErrorId : ItemNotFound,Microsoft.PowerShell.Commands.GetIt
emCommand
On the TechNet page for the code, someone brings the issue up but no solution is given. I'm not sure how to fix it here. I've played with the $path argument, surrounding it in " " or ' ' and such.
Here is part of the batch file to execute it:
C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -noe -command "& 'C:\test.ps1' -path "'C:\Users\MyUser\'""
Might be a bit late for answer here, but, as Aaron mentioned, this is not due to spaces in the path.
If you read the documentation for Get-Item cmdlet, there is a -Force switch, which allows the cmdlet to get items that cannot otherwise be accessed, such as hidden items.
Moreover, it seems from your code that you are not expecting to pass a wildcard pattern to the cmdlet, so instead of (Get-Item $path).FullName you should use
(Get-Item -force -LiteralPath $path).FullName
That should resolve this issue.
It's not the spaces in the path. If it was, the error would say path C:\Users\MyUser\Documents\My couldn't be found. Get-ChildItem and Get-Item behave... strangely... with certain files/directories, returning errors like you're seeing. That's why Get-ChildItem has an -ErrorAction SilentlyContinue parameter on it. I would add the same to the call to Get-Item, i.e. change
(Get-Item $path).FullName
to
(Get-Item $path -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
or even forgo the call to Get-Item completely:
$path
As suggested by TheTrowser in a comment above: The problem may be resolved if you replace the double-quotes with single quotes surrounding the file directory with spaces. This is what solved it for me.
Using the command below didn't work for me.
get-item 'some path with two spaces.txt'
Enclosing the filename in double quotes within the single quotes, forces Powershell to use the filename as written.
get-item '"some path with two spaces.txt"'
Note: I'm totally cringing at my origal message (cleaned up a bit above). Below is a better example of what I was seeing.
$exampleA = "c:\temp\weird path\blah.txt"
$exampleB = "c:\temp\normal path\blah.txt"
# Works
get-item '$exampleA'
get-item $exampleB
# Fails
get-item $exampleA