Skip to content

Commit c63db4d

Browse files
authored
Merge pull request #77 from iricigor/v.1.2
V.1.2
2 parents 55d5cc4 + 81b06cd commit c63db4d

File tree

5 files changed

+123
-36
lines changed

5 files changed

+123
-36
lines changed

Get-FolderAge.Tests.ps1

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ Describe "Function $CommandName Definition" {
3838
$CmdDef.Parameters.Keys | Should -Contain $P1
3939
}
4040
}
41+
42+
It 'Command should have alias' {
43+
Get-Alias -Definition $CommandName -ea 0 | Should -Not -Be $null
44+
}
45+
4146
}
4247

4348

@@ -152,6 +157,17 @@ Describe "Proper $CommandName Functionality" {
152157
$Result0.TotalFiles -eq $Result1.TotalFiles | Should -Be $true
153158
}
154159

160+
It 'Runs Threads with the same output file' {
161+
$File1 = Join-Path 'TestFolder' 'NoThreads.csv'
162+
$File2 = Join-Path 'TestFolder' 'WithThreads.csv'
163+
$Result0 = Get-FolderAge -FolderName 'TestFolder' -TestSubFolders -OutputFile $File1
164+
$Result1 = Get-FolderAge -FolderName 'TestFolder' -TestSubFolders -OutputFile $File2 -Threads 2 -ea 0
165+
$Count1 = (Get-Content $File1).Count
166+
$Count2 = (Get-Content $File2).Count
167+
$Count2 | Should -Be $Count1 -Because "NoThreads and WithThreads should be the same"
168+
$Count2 | Should -Be ($Result1.Count + 1) -Because "Pipeline and File should differ by 1 (header line)"
169+
}
170+
155171
}
156172

157173
Describe "V2 Compatibility check for $CommandName" {

Get-FolderAge.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,11 @@ Accept wildcard characters: False
105105
```
106106
107107
### -OutputFile
108-
A string specifying file name which will be used for output.
109-
If not specified, there will be no file output generated.
110-
This is especially useful for long running commands.
111-
Each folder as soon as processed will be stored in the file.
108+
A string specifying file name which will be used for output in addition to screen (or pipeline) output.
109+
This is especially useful for long running commands. Each folder as soon as processed will be stored in the file.
110+
This can be also used for restarting the script, if it gets interrupted before it finishes all folders.
111+
Just specify the same input and output files, and script will skip already processed folders!
112+
If this parameter is not specified, there will be no file output generated.
112113
113114
```yaml
114115
Type: String

Get-FolderAge.ps1

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<#PSScriptInfo
22
3-
.VERSION 1.1
3+
.VERSION 1.2
44
.GUID c9788cc2-d4af-4219-bf7d-8dd8fa89584f
55
.AUTHOR Igor Iric, iricigor@gmail.com, https://github.com/iricigor
66
.COMPANYNAME
@@ -12,7 +12,7 @@
1212
.EXTERNALMODULEDEPENDENCIES
1313
.REQUIREDSCRIPTS
1414
.EXTERNALSCRIPTDEPENDENCIES
15-
.RELEASENOTES Added -Exclude and -Threads parameters, for more info see https://github.com/iricigor/GetFolderAge/blob/master/ReleaseNotes.md
15+
.RELEASENOTES Added restartable script functionality and alias, bug fixes for append and threads issues, for more info see https://github.com/iricigor/GetFolderAge/blob/master/ReleaseNotes.md
1616
.DESCRIPTION Get-FolderAge returns `LastModifiedDate` for a specified folder(s) and if folders were modified after a specified cut-off date.
1717
1818
#>
@@ -103,8 +103,11 @@ function Global:Get-FolderAge {
103103
If both CutOffTime and CutOffDays specified, the script will throw a warning.
104104
105105
.PARAMETER OutputFile
106-
A string specifying file name which will be used for output. If not specified, there will be no file output generated.
106+
A string specifying file name which will be used for output in addition to screen (or pipeline) output.
107107
This is especially useful for long running commands. Each folder as soon as processed will be stored in the file.
108+
This can be also used for restarting the script, if it gets interrupted before it finishes all folders.
109+
Just specify the same input and output files, and script will skip already processed folders!
110+
If this parameter is not specified, there will be no file output generated.
108111
109112
.PARAMETER Exclude
110113
Specifies, as a string array, an folder names that this cmdlet excludes in the search operation.
@@ -163,7 +166,8 @@ function Global:Get-FolderAge {
163166

164167
[parameter(Mandatory=$false)] [string[]]$Exclude,
165168

166-
[parameter(Mandatory=$false)] [int]$Threads = 0,
169+
[parameter(Mandatory=$false)]
170+
[ValidateRange(0,50)] [int]$Threads = 0,
167171

168172
#
169173
# Switches
@@ -185,10 +189,10 @@ function Global:Get-FolderAge {
185189

186190
# process $InputFile
187191
if ($InputFile) {
188-
if (!(Test-Path $InputFile)) {
192+
if (!(Test-Path -LiteralPath $InputFile)) {
189193
throw "$FunctionName cannot find input file $InputFile"
190194
}
191-
$FolderName = Get-Content -Path $InputFile -ErrorAction SilentlyContinue
195+
$FolderName = Get-Content -LiteralPath $InputFile -ErrorAction SilentlyContinue
192196
if ($FolderName) {
193197
Write-Verbose -Message "$(Get-Date -f T) successfully read $InputFile with $(@($FolderName).Count) entries"
194198
} else {
@@ -209,7 +213,6 @@ function Global:Get-FolderAge {
209213
}
210214
}
211215

212-
$First = $true # Used if there is output to file, only first line drops header
213216
$Separator = [IO.Path]::DirectorySeparatorChar
214217
$UC = '\\?\'
215218

@@ -240,6 +243,34 @@ function Global:Get-FolderAge {
240243
}
241244

242245
}
246+
247+
# Restartable script setup
248+
if ($OutputFile) {
249+
if (Test-Path -LiteralPath $OutputFile) {
250+
$RP = Resolve-Path -LiteralPath $OutputFile
251+
try {
252+
$FoldersToSkip = Import-Csv -LiteralPath $OutputFile | Select -Expand Path
253+
Write-Verbose -Message "$(Get-Date -f T) Script continues writing to $OutputFile, with skipping $(@($FoldersToSkip).Count) processed folders"
254+
} catch {
255+
throw "$FunctionName found existing file $OutputFile in unrecognized format, cannot continue."
256+
}
257+
} else {
258+
$FoldersToSkip = $null
259+
try {
260+
New-Item $OutputFile -ItemType File -ea Stop | Out-Null
261+
$RP = Resolve-Path -LiteralPath $OutputFile
262+
Remove-Item $OutputFile -Force
263+
} catch {
264+
throw "$FunctionName cannot write output file $OutputFile`: $_"
265+
}
266+
}
267+
if ($RP.Provider.Name -ne 'FileSystem') {
268+
throw "$FunctionName provided output file $OutputFile is not on the FileSystem"
269+
} elseif ($OutputFile -ne $RP.ProviderPath) {
270+
Write-Verbose -Message "$(Get-Date -f T) Expanding $OutputFile to $($RP.Path) via $($RP.Provider.Name) call"
271+
$OutputFile = $RP.ProviderPath
272+
}
273+
}
243274
}
244275

245276

@@ -253,7 +284,7 @@ function Global:Get-FolderAge {
253284
Write-Error "$FunctionName cannot find folder $FolderEntry"
254285
continue
255286
}
256-
$RP = Resolve-Path $FolderEntry
287+
$RP = Resolve-Path -LiteralPath $FolderEntry
257288
if ($RP.Provider.Name -ne 'FileSystem') {
258289
Write-Error "$FunctionName provided path $FolderEntry is not on the FileSystem"
259290
continue
@@ -263,7 +294,7 @@ function Global:Get-FolderAge {
263294
}
264295

265296
if ($TestSubFolders) {
266-
$FolderList = @(Get-ChildItem $FolderEntry -Directory -ea SilentlyContinue | Select -Expand FullName)
297+
$FolderList = @(Get-ChildItem -LiteralPath $FolderEntry -Directory -ea SilentlyContinue | Select -Expand FullName)
267298
if ($FolderList) {
268299
Write-Verbose -Message "$(Get-Date -f T) Processing $($FolderList.Count) subfolders of $FolderEntry"
269300
} else {
@@ -280,16 +311,21 @@ function Global:Get-FolderAge {
280311

281312
# processing single folder $Folder
282313

314+
if ($FoldersToSkip -and ($FoldersToSkip -contains $Folder)) {
315+
Write-Verbose -Message "$(Get-Date -f T) skipping $Folder from processing, because it is present in $OutputFile."
316+
continue
317+
}
318+
283319
if ($Threads -gt 1) {
284320

285321
$JobCode = "$FunctionName '$Folder'"
286322
if ($CutOffTime) {$JobCode += " -CutOffTime '$CutOffString'"}
287323
if ($Exclude) {$Join = "', '"; $JobCode += " -Exclude '$($Exclude -join $Join)'"}
288324
if ($OutputFile) {$JobCode += " -OutputFile '$OutputFile'"}
289-
# TODO: Process other required parameters: CutOffTime, Exclude, OutputFile
290325
Write-Verbose -Message "$(Get-Date -f T) starting background job for '$Folder': $JobCode"
291326
$JobCode = ". $SourceFile`n$JobCode" # first import function
292327
$JobList += Start-ThreadJob -ScriptBlock ([Scriptblock]::Create($JobCode)) -ThrottleLimit $Threads
328+
Start-Sleep -Milliseconds 200 # let the job execute begin block, before starting next one
293329

294330
continue # to next $Folder
295331
}
@@ -298,7 +334,7 @@ function Global:Get-FolderAge {
298334
$StartTime = Get-Date
299335
$i = 0
300336
$queue = @($Folder)
301-
$LastWriteTime = Get-Item -Path $Folder | Select -Expand LastWriteTime
337+
$LastWriteTime = Get-Item -LiteralPath $Folder | Select -Expand LastWriteTime
302338
$TotalFiles = 0
303339
$LastItemName = $Folder
304340
$KeepProcessing = $true
@@ -391,29 +427,34 @@ function Global:Get-FolderAge {
391427
LastWriteTime = $LastWriteTime
392428
Modified = $Modified
393429
Confident = $Confident
430+
# statistical info
394431
TotalFiles = $TotalFiles
395432
TotalFolders = $queue.Count
396433
LastItem = $LastItemName
397434
Depth = ($queue[$i-1].split($Separator)).Count - ($queue[0].split($Separator)).Count + 1
398435
ElapsedSeconds = ($EndTime - $StartTime).TotalSeconds
399436
FinishTime = $EndTime
437+
# error info
400438
Errors = $ErrorsFound
401439
LastError = $LastError
402440
}
441+
442+
#
403443
# File output, if needed
444+
#
445+
404446
if ($OutputFile) {
405-
if ($First) {
447+
if (!(Test-Path $OutputFile)) {
406448
try {
407449
$RetVal | Export-Csv -Path $OutputFile -Encoding Unicode -NoTypeInformation # Export-csv in PS v2 has no -LiteralPath
408450
Write-Verbose -Message "$(Get-Date -f T) created output file $OutputFile"
409-
$First = $false
410451
} catch {
411452
Write-Error "$FunctionName failed while writing to $OutputFile, file output is skipped`n$_"
412453
$OutputFile = $null
413454
}
414455
} else {
415456
try {
416-
$RetVal | ConvertTo-Csv -NoTypeInformation | Select -Skip 1 | Out-File -LiteralPath $OutputFile -Append -Encoding Unicode
457+
$RetVal | ConvertTo-Csv -NoTypeInformation | Select -Skip 1 | Out-File -FilePath $OutputFile -Append -Encoding Unicode
417458
Write-Verbose -Message "$(Get-Date -f T) appended new line to output file $OutputFile"
418459
} catch {
419460
Write-Error "$FunctionName failed to append date to $OutputFile, entry for $Folder will be skipped.`n$_"
@@ -431,11 +472,15 @@ function Global:Get-FolderAge {
431472
# if threads, receive them
432473
if ($Threads -gt 1) {
433474
Write-Verbose -Message "$(Get-Date -f T) $FunctionName waiting for background jobs results."
434-
Receive-Job $JobList -Wait
475+
Receive-Job $JobList -Wait
476+
Remove-Job $JobList
435477
}
436478

437479
# function closing phase
438480
Write-Verbose -Message "$(Get-Date -f T) $FunctionName finished"
439481
}
440482

441483
}
484+
485+
486+
Set-Alias -Name gfa -Value Get-FolderAge

README.md

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
11
# GetFolderAge
22

33
Latest version:
4-
![GitHub release](https://img.shields.io/github/release/iricigor/GetFolderAge.svg)
4+
![GitHub Latest Release](https://img.shields.io/github/release/iricigor/GetFolderAge.svg)
55
![GitHub Release Date](https://img.shields.io/github/release-date/iricigor/GetFolderAge.svg)
6+
![GitHub repo size in bytes](https://img.shields.io/github/repo-size/iricigor/GetFolderAge.svg)
67

7-
PowerShell script which checks for last modified date _(LastWriteTime)_ for large number of folders.
8+
PowerShell script which checks for last modified date _(LastWriteTime)_ for a large number of folders.
89
It checks recursively for all files and folders inside taking into account potential errors (inaccessible files, too long paths, etc.).
910

1011
Running a script itself will just import (i.e. create) new commandlet **`Get-FolderAge`** in your session.
1112
It will not do any checks.
1213
You can afterwards run this commandlet with proper parameters as in examples below.
1314

1415
Running commandlet with specifying only a folder name will return last modification time of that folder.
15-
If you specify `-CutOffDate` (or `-CutoffDays`) script will determine if folder was modified after that time. It will exit folder search as soon as it finds modified file or folder.
16+
If you specify `-CutOffDate` (or `-CutoffDays`) script will determine if the folder was modified after that time. It will exit folder search as soon as it finds a modified file or folder.
1617

17-
Commandlet can be run in un-attended mode also with file output using `-OutputFileName` parameter. Output format is comma-separated value, so file extension should be `.csv`.
18+
Commandlet can be run in unattended mode also with file output using `-OutputFileName` parameter. Output format is comma-separated value, so file extension should be `.csv`.
1819

1920
Technical explanation of LastModifiedDate can be seen in [this archived copy](https://web.archive.org/web/20110604022236/http://support.microsoft.com/kb/299648) of Microsoft knowledge base article.
2021

2122
## Download
2223

23-
You can download this script in couple of ways listed below. Execute a script after downloading it (no admin rights needed) to add commandlet `Get-FolderAge` to your session.
24+
You can download this script in a couple of ways listed below. Execute a script after downloading it (no admin rights needed) to add commandlet `Get-FolderAge` to your session.
2425

2526
- **Download from GitHub:**
2627
You can see online latest script version at this [link](https://github.com/iricigor/GetFolderAge/blob/master/Get-FolderAge.ps1).
27-
Raw PS1 file can be downloaded from [here](https://raw.githubusercontent.com/iricigor/GetFolderAge/master/Get-FolderAge.ps1).
28+
The raw PS1 file can be downloaded from [here](https://raw.githubusercontent.com/iricigor/GetFolderAge/master/Get-FolderAge.ps1).
2829

2930
- **Clone repository:**
30-
If you want to see entire GitHub repository, just clone it
31+
If you want to see the entire GitHub repository, just clone it
3132

3233
`git clone https://github.com/iricigor/GetFolderAge.git`
3334

3435
- **From PowerShell Gallery** _(preferred way)_:
35-
Script can be downloaded from [PS Gallery](https://www.powershellgallery.com/packages/Get-FolderAge) using command
36+
Script can be downloaded from [PS Gallery](https://www.powershellgallery.com/packages/Get-FolderAge) using the command
3637

3738
`Save-Script Get-FolderAge -Repository PSGallery -Path 'MyFolder'`
3839

@@ -52,7 +53,7 @@ Returns last modification date of the specified folder.
5253

5354
* `Get-FolderAge -Folder '\\FileServer01.Contoso.com\Users' -TestSubFolders`
5455

55-
Returns last modification date for each user share on file server.
56+
Returns last modification date for each user share on a file server.
5657

5758
* `Get-FolderAge -InputFile 'ShareList.txt' -OutputFile 'ShareScanResults.csv' -CutoffDays 3`
5859

@@ -63,41 +64,49 @@ Tests if folders listed in specified input file (one folder per line) are modifi
6364
Input can be specified in three ways:
6465

6566
* parameter `-FolderName` _(default parameter, can be omitted)_ followed by string or an array of strings specifying paths to be checked
66-
* via pipeline - the same values as above can be passed via pipeline, see example with `Get-ChildItem`
67+
* via pipeline - the same values as above can be passed via pipeline, see the example with `Get-ChildItem`
6768
* parameter `-InputFile` - a file specifying folders to be processed, one folder per line
6869

6970
![Screenshot 2](img/Screenshot_2.png)
7071

7172
### Cut-off Date explanation
7273

7374
Cut-off date represents the point in time for which we want to know if a folder was modified after.
74-
Usually this is the date when last copy or backup or sync was performed on given folder.
75+
Usually, this is the date when last copy or backup or sync was performed on the given folder.
7576

7677
It can be specified as:
7778

7879
* PowerShell [DateTime] object, i.e. the value returned by Get-Date command
79-
* Integer number representing days since last cut-off date (easier, but less precise)
80+
* An integer number representing days since last cut-off date (easier, but less precise)
8081

8182
### Output format
8283

83-
Commandlet outputs array of FolderAgeResult objects. Each object contain these properties:
84+
Commandlet outputs array of objects. Each object contains these properties:
8485

8586
* [string]`Path` - as specified in input parameters (or obtained subfolder names)
8687
* [datetime]`LastWriteTime` - latest write time for all items inside of the folder
87-
* [bool]`Modified` - if folder was modified since last cut-off date (or null if date not given)
88+
* [bool]`Modified` - if the folder was modified since last cut-off date (or null if date not given)
8889

8990
It also outputs diagnostic/statistics info:
9091

91-
* [bool]`Confident` - if Modified return value is confident result, in case commandlet is called with QuickTest switch, return value for Modified might not be correct.
92+
* [bool]`Confident` - if Modified return value is a confident result; in case commandlet is called with QuickTest switch, return value for Modified might not be correct.
9293
* [int]`TotalFiles` - total number of files and directories scanned
9394
* [int]`TotalFolders` - total number of directories scanned
94-
* [string]`LastItem` - item with latest timestamp found (note that this might not ber really the latest modified file. If this timestamp is newer than CutOffDate, commandlet will not search further.
95+
* [string]`LastItem` - an item with the latest timestamp found (note that this might not be really the latest modified file. If this timestamp is newer than CutOffDate, commandlet will not search further.
9596
* [int]`Depth` - total depth of scanned folders relative to initial folder. If QuickTest, then it will be 1, regardless of real depth. If CutOffDate specified, it might not go to full depth, so this number will be smaller than full depth.
9697
* [decimal]`ElapsedSeconds` - time spent in checking the folder
9798
* [datetime]`FinishTime` - date and time when folder check was completed
98-
* [bool]`Errors` - indicate if command encountered errors during its execution (i.e. Access Denied on part of the files)
99+
* [bool]`Errors` - indicate if command encountered errors during its execution (i.e. Access Denied on some file)
99100
* [string]`LastError` - text of the last encountered error
100101

102+
### Restartable script
103+
104+
Parameter `-OutputFile` specifies where output data is stored on the disk.
105+
If the script is interrupted before finishing, you can restart it without a need to process same folders again.
106+
Just specify the same `-OutputFile` and `-InputFile` and script will skip already processed folders!
107+
108+
This is especially useful for long running scripts.
109+
101110
## Build status
102111

103112
Each commit or PR to master is checked on [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/) [Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/) on two build systems:

ReleaseNotes.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
Release notes for PowerShell script **`Get-FolderAge`** by https://github.com/iricigor
44

5+
## 1.2
6+
7+
Date: Wednesday, October 24, 2018
8+
9+
### New functionality in 1.2
10+
11+
- If interrupted, script can be restarted and it will skip already processed folders
12+
- Added function alias gfa for Get-FolderAge
13+
14+
#### Bug fixes in 1.2
15+
16+
- Threads not working properly together with OutputFile
17+
- PowerShell v2 compatibility issues
18+
19+
Full list of resolved issues available [here](https://github.com/iricigor/GetFolderAge/milestone/4?closed=1)
20+
521
## 1.1
622

723
Date: Wednesday, October 17, 2018

0 commit comments

Comments
 (0)