Author Posts

May 1, 2018 at 11:01 pm

Good afternoon all!

Running the following one liner:
$Results = Get-ChildItem -Path 'I:\*.*' -Recurse -Force | Where-Object {$_.length/1GB -gt 15} | Select-Object Name,Extension,Fullname,CreationTime,LastAccessTime,@{n=”Size MB”;e={$_.length/1GB}}

Getting lots of the following error messages:
Get-ChildItem : The specified path, file name, or both are too long. The fully qualified file name must be less than 260

Is there any way to get around these errors? I've been researching at the university of google, tried -LiteralPath '\\?\I:\' nothing that I've found has worked.

Any and all input would be greatly appreciated, this is over my head.

Norm

May 1, 2018 at 11:42 pm

PowerShell leverages .NET which has the 256 path character limit. We can use something that leverages COM such as robocopy.exe

This will not copy anything, just list the full file names under the given $FolderPath recursively..

$FolderPath = 'D:\Sandbox'

robocopy $FolderPath NULL /L /E /MT:128 /FP /NP /TEE /NJH /NJS /NC /NDL /R:0 /W:0 /XJ | foreach {
    $PSItem.Substring($PSItem.IndexOf(':') - 1 , $PSItem.Length - $PSItem.IndexOf(':') + 1)
}

May 1, 2018 at 11:47 pm

Sam;
Thank you for your input. Question how I tie together your code with my $Results = Get-ChildItem -Path 'I:\*.*' -Recurse -Force | Where-Object {$_.length/1GB -gt 15} | Select-Object Name,Extension,Fullname,CreationTime,LastAccessTime,@{n=”Size MB”;e={$_.length/1GB}}

Norm

May 1, 2018 at 11:55 pm

I'll tell on one condition..
Explain to me the 4 lines I posted above.

May 2, 2018 at 4:50 pm

Sam;
Not sure I can pass your test I'm happier in the linux world with bash scripts but here goes:
1. Your using robocopy with applicable options (no knowledge of what each option is doing but I guess I can look them up) to the contents of $FolderPath
2. In the for each loop it appears you are using a substring method to return values in this case the path of every file contained in $FolderPath. Honestly not sure what your doing with the $PSItem.Length – $PSItem.IndexOf(':') + 1). Powershell complained a bit when I tested the code: Exception calling "Substring" with "2" argument(s): "StartIndex cannot be less than zero.
Parameter name: startIndex"
3. Ran the following in order to drill down into the object typename in order to attempt to figure out what you were doing
$FolderPath | Get-Member
$FolderPath.Substring | Get-Member

Do I pass?

Norm

May 2, 2018 at 5:10 pm

THere is an alternative. If you grab the NTFSSecurity module from the PSGallery:

Install-Module NTFSSecurity -Scope CurrentUser

Then you can use the Get-ChildItem2 cmdlet, which leverages a third-party .dll to access the filesystem and correctly handle longer paths. The syntax is identical to Get-ChildItem, and it supports all the same parameters, it just works with a different library to manage the long paths:

Get-ChildItem2 -Path $TargetPath_Long -Recurse -Force 

May 2, 2018 at 6:17 pm

Hi Joel, apparently the third-party dll does note like a PSDrive mount volume: New-PSDrive -Name "I" -PSProvider "FileSystem" -Root "\\jesse\users"

When I run the following:
$Results = Get-ChildItem2 -Path I:\ -Recurse | Where-Object {$_.length/1GB -gt 15} | Select-Object Name,Extension,Fullname,CreationTime,LastAccessTime,@{n=”Size GB”;e={$_.length/1GB}}

The following error messages occur
Get-ChildItem2 : Unable to find the specified file.
At line:1 char:12
+ $Results = Get-ChildItem2 -Path I:\ -Recurse | Where-Object {$_.lengt ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (I:\:String) [Get-ChildItem2], FileNotFoundException
+ FullyQualifiedErrorId : FileNotFound,NTFSSecurity.GetChildItem2

Don't get these messages with Get-ChildItem ran against the created psdrive.

Any thoughts

Thanks for input
Norm

May 2, 2018 at 6:45 pm

Yes, the whole point is to further your understanding of PowerShell.

The use of the line

robocopy $FolderPath NULL /L /E /MT:128 /FP /NP /TEE /NJH /NJS /NC /NDL /R:0 /W:0 /XJ /BYTES 

outputs the file size in bytes, and the full file name

This output is then piped to one or more lines to parse that text output of robocopy.exe to extract the Name,Extension,Fullname,CreationTime,LastAccessTime,FileSize properties you want.

What's missing in the robocopy.exe output is the creationtime and lastaccesstime perperties.

I admit that the expression

$PSItem.Substring($PSItem.IndexOf(':') - 1 , $PSItem.Length - $PSItem.IndexOf(':') + 1)

is complex and not optimal for learning. But the idea is simple. Robocopy returns a line of text for each file that looks like:

            12345     c:\bla1\bla 2\bla.3\bla4.txt

You need to parse out file name, extension, and full name. You have to account for possible multiple spaces and multiple dots (but a single colon)
if

$a = '            12345     c:\bla1\bla 2\bla.3\bla4.txt'

then

$a.Split(':')

will return

            12345     c
\bla1\bla 2\bla.3\bla4.txt

The colon is an attractive marker for parsing here because you can be assured that there can be only one colon in a file name
The .Split() method will provide an array of 2 elements.
The second one is the file full name less the colon and the drive letter which is the last letter of the first split string.
So,

$a.Split(':')[1]

returns the 2nd element of that .Split() array of text (array elements are counted starting from 0)

\bla1\bla 2\bla.3\bla4.txt

The last letter of the first .Split() array element can be referenced as

$a.Split(':')[0][-1]

This may be a little complicated, but simply put

$a.Split(':')[0]

returns the first string of characters being the first .Split() array element

            12345     c

which is in itself an array of characters. The last array element can be refereed to by [-1]
So, to stitch this expression together, the file full name can be parsed as:

"$($a.Split(':')[0][-1]):$($a.Split(':')[1])"

What this expression does is to concatenate the last character before the colon + the colon + the rest of the characters after the colon
Once we have file full name as in

$FileFullName = "$($a.Split(':')[0][-1]):$($a.Split(':')[1])"

We can get the file name by using:

$FileName = Split-Path $FileFullName -Leaf

Using similar parsing techniques, we can demo parsing a line like:

$a = '            12345     c:\bla1\bla 2\bla.3\bla4.txt'

$FileFullName = "$($a.Split(':')[0][-1]):$($a.Split(':')[1])"

$FileName = Split-Path $FileFullName -Leaf

$Extension = $FileName.Substring($FileName.LastIndexOf('.') , $FileName.Length - $FileName.LastIndexOf('.'))

[Int]$FileSize = ($a.Split(':')[0][0..($a.Split(':')[0].Length -2)] -join '').Trim()

"If the string is '$a'"
"FileFullName is  '$FileFullName'"
"FileName is      '$FileName'"
"Extension is     '$Extension'"
"Size (bytes) is  '$FileSize' bytes"

In the original expression $PSItem or $_ refers to the text line being passed to the pipeline from robocopy.exe

Putting the parsed output in a more manageable PS object:

$FolderPath = 'D:\Sandbox'

$Results = robocopy $FolderPath NULL /L /E /MT:128 /FP /NP /TEE /NJH /NJS /NC /NDL /R:0 /W:0 /XJ /BYTES | foreach {
    $FileFullName = "$($PSItem.Split(':')[0][-1]):$($PSItem.Split(':')[1])"
    $FileName = Split-Path $FileFullName -Leaf
    [Int]$FileSize = ($PSItem.Split(':')[0][0..($PSItem.Split(':')[0].Length -2)] -join '').Trim()
    [PSCustomObject][Ordered]@{
        Name       = $FileName
        Extension  = $(
            if ($FileName -match '\.') { # this if statement accounts for files with no extension
                $FileName.Substring($FileName.LastIndexOf('.') , $FileName.Length - $FileName.LastIndexOf('.'))
            } else {
                'None' 
            }
        )
        FullName   = $FileFullName
        'Size(GB)' = [Math]::Round($FileSize / 1GB , 2)
    }
}

$Results | FL

What's missing here is the CreationTime and LastAccessTime, can you lookup robocopy documentation to see if there are switches to show those?

May 2, 2018 at 7:02 pm

Strange. You may be able to bypass the issue by using the -Persist parameter of New-PSDrive, as that causes it to make a new network drive much like windows explorer does, although you'd have to manually remove it when you're done.

It's very possible that the third party DLL wasn't designed for lengthy network paths, I'm not sure. I know there's at least one other module that has a cmdlet for this purpose of a similar design, I think the cmdlet was called Get-LongChildItem if that helps you find the module at all. Perhaps that one works a bit differently.

May 2, 2018 at 7:40 pm

Here's a version with CreationTime and LastAccessTime and the file size filter

$FolderPath     = 'D:\Sandbox'
$FilterFileSize = 15GB # return files larger than this value

$Results = robocopy $FolderPath NULL /L /E /MT:128 /FP /NP /TEE /NJH /NJS /NC /NDL /R:0 /W:0 /XJ /BYTES | foreach {
    if ($PSItem -match ':') {
        $FileFullName = "$($PSItem.Split(':')[0][-1]):$($PSItem.Split(':')[1])"
        $FileName = Split-Path $FileFullName -Leaf
        if ($FileName -match '\.') { # this if statement accounts for files with no extension
            $Extension = $FileName.Substring($FileName.LastIndexOf('.') , $FileName.Length - $FileName.LastIndexOf('.'))
        } else {
            $Extension = 'None' 
        }
        [Int]$FileSize = ($PSItem.Split(':')[0][0..($PSItem.Split(':')[0].Length -2)] -join '').Trim()
        if ($FileSize -ge $FilterFileSize) {
            [PSCustomObject][Ordered]@{
                FullName       = $FileFullName
                Name           = $FileName
                Extension      = $Extension
                'Size(GB)'     = [Math]::Round($FileSize / 1GB , 2)
                CreationTime   = [IO.File]::GetCreationTime($FileFullName)
                LastAccessTime = [IO.File]::GetLastAccessTime($FileFullName)
            }
        }
    }
}

$Results | FL

May 2, 2018 at 8:57 pm

Hello Sam;
Thank you for the explanation and pushing me to do some digging. In running your final script came across the following error message:
Cannot convert value "4268605440" to type "System.Int32". Error: "Value was either too large or too small for an Int32."
At line:13 char:9
+ [Int32]$FileSize = ($PSItem.Split(':')[0][0..($PSItem.Split(' ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : MetadataError: (:) [], ArgumentTransformationMetadataException
+ FullyQualifiedErrorId : RuntimeException

Did some digging and poking around and looking and looking at the error message came to the realization of the need to change the Int definition to Int64. Still chewing on your script explanation.

Norm

May 3, 2018 at 9:43 am

Haven't checked but depending on your .Net version and OS version you may only need to change a setting in the registry.
Just do a google/bing/duckduckgo on "windows maxpath" and you should find articles about the registry key and .net version.

You could also try third party .dll's.
I created a blog post about using quickio.net some time ago.
QuickIO.Net and Powershell

QuickIO don't seem to be developed further so YMMV.
But in my opinion it's a "nicer" approach to get the results as objects directly rather than going through robocopy and then regex/split the output.

May 3, 2018 at 12:50 pm

Get-ChildItem2 uses AlphaFS, I'm pretty sure, and it doesn't seem to handle UNC long paths. 🙁

May 3, 2018 at 4:08 pm

Joel,
Wanted to let you know the PSDrive -Persist Parameter allowed get-childitem2 to work with no directory length issues!

Many thanks for your input

Norm

May 3, 2018 at 7:45 pm

Interesting. I might have to go submit an issue on the github for NTFSSecurity if I can find it — seems like it's just an issue it not checking the PSDrives directly.

May 3, 2018 at 8:07 pm

hmm thats interesting Joel, i use alpha directly on an almost daily basis to delete users home drives, and enumerating/deleting always works on the long file-paths.