Do you relate to the cover photo? Have your NTFS permissions just bombed out and you can’t bare the idea of waiting hours or days for your new permissions to apply?

Don’t worry, I’m here to help.

It’s no secret that applying NTFS permissions to any directory tree larger than a few thousand files quickly decends in to a painstaking waiting game. The built in UI is garbage, and icacls is decent but single-threaded and slow.

I recently had an issue where a directory containing over 15 million files junked its permissions. Using the GUI would have taken days to reset, as would using ICACLS alone. I was not looking forward to the wait so I wrote a PowerShell script utilsing jobs to apply the permissions in parallel. I also included some logic for reporting on the current state of progress, to avoid the blind-waiting and guessing as to when it might actually finish.

You can adjust the script to your needs, as it stands, fire it at a directory and it will run icalcs /t /c /q /reset on the directory itself and all sub-items (folders and files) within, while keeping you updated every 30 seconds with how it’s doing.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# Report the current state of the script
function Report-ScriptStatus
{
    param(
        $ScriptState
    )

    $currentTime = Get-Date
    $runtime = New-TimeSpan -Start $($ScriptState.startTime) -End $currentTime

    Write-Host "-----"
    Write-Host "Start Time: $($ScriptState.startTime)"
    Write-Host "Current Time: $currentTime"
    Write-Host "Runtime: $runTime"
    
    Write-Host "Completed jobs: $($ScriptState.jobsCompleted.count)"
    foreach ($jobItem in $ScriptState.jobsCompleted) {
        Write-Host " - $($jobItem.Name) | $($jobItem.State) | Started: $($jobItem.PSBeginTime) | Ended: $($jobItem.PsEndTime) | Successes $($jobItem.Successes) | Failures $($jobItem.Failures)"
    }

    $currentlyRunningJobs = get-job | Where-Object {$_.State -eq "Running"}
    Write-Host "Currently running jobs: $($currentlyRunningJobs.Count)"
    $currentlyRunningJobs | foreach-object { Write-Host " - $($_.Name) | $($_.State) | Started: $($_.PSBeginTime)" }
    
    Write-Host "Next Job to Start: $($ScriptState.itemsToProcess[$ScriptState.iterator])"
    Write-Host "Changes Processed - Success:  $($ScriptState.changesProcessedSuccess)"
    Write-Host "Changes Processed - Failed: $($ScriptState.changesProcessedFailed)"



}

# Parse icacls output to get the number of processed files
function Get-IcaclsProcessedFiles {
    param (
        [string]$IcaclsOutput
    )
    <# 
        .SYNOPSIS
        Takes the output of ICACLS and returns the number of successfully processed files.

        .PARAMETER IcaclsOutput
        The string of icacls output. EG Successfully processed 11 files; Failed processing 5 files

        .OUTPUTS
        Two numbers, the first is the number of successfully processed files, the second is the failed processed files.
    #>

    $successfulFiles = ($icaclsOutput.split(";")  -replace "[^0-9]", '')[0]
    $failedFiles = ($icaclsOutput.split(";")  -replace "[^0-9]", '')[1]

    return @($successfulFiles, $failedFiles)
    
}

# Start the job, modify the command to suit your needs
function Start-IcaclsJob {

    param($TargetItemFullName)

    Write-Host "Starting job for item: icacls - $TargetItemFullName"

    $job = {
        param($loc)
        Invoke-Expression -Command "icacls $loc /t /c /q /reset'"  ### ADD YOUR OWN ICACLS command/flags here
    }

    Start-Job -Name "icacls - $targetItemFullName" -ScriptBlock $job -ArgumentList $targetItemFullName
}

function Fix-MyDamnPermissions {
    param(
        [string]$TargetFolder,
        [bool]$ProcessSubfoldersOnly = $false, # Set to True to only process subfolders, not the root folder
        [int]$MaxConcurrentJobs = 20,
        [int]$TimeBetweenReportsSeconds = 30
    )

    $scriptState = [PSCustomObject]@{
        startTime = Get-Date
        lastReportTime = Get-Date -Date "01/01/1970"

        itemsToProcess = @()
        iterator = 0

        jobsCompleted = @()
        changesProcessedSuccess = 0
        changesProcessedFailed = 0
        exitLoop = 0
    }

    if (-not $ProcessSubfoldersOnly) {
        $scriptState.itemsToProcess += Get-Item $TargetFolder 
    }
    
    $scriptState.itemsToProcess += Get-ChildItem $TargetFolder -Depth 0
    
    while ($true) {

        # Check if we have more items to process
        if ($scriptState.iterator -lt $scriptState.itemsToProcess.Count) {

            # Check if we can start new jobs
            $JobsRunning = get-job | Where-Object {$_.State -eq "Running"}
            if ($JobsRunning.Count -lt $MaxConcurrentJobs) {

                $nextItemToProcess = $scriptState.itemsToProcess[$scriptState.iterator]

                Start-IcaclsJob -TargetItemFullName $nextItemToProcess.fullname
                ($scriptState.iterator)++
            }

        }
        else {
            $allJobsQueued = $true
        }

        # Tidy up completed jobs
        $jobsCompletedSinceLastCheck = get-job | Where-Object {$_.State -eq "Completed"}
        
        foreach ($jobItem in $jobsCompletedSinceLastCheck) {
            
            $filesprocessed = Get-IcaclsProcessedFiles -icaclsOutput $jobItem.ChildJobs[0].Output
            $scriptState.changesProcessedSuccess += $filesprocessed[0]
            $scriptState.changesProcessedFailed += $filesprocessed[1]
            
            $jobitem | add-member -type NoteProperty -Name Successes -Value $filesprocessed[0]
            $jobItem | add-member -type NoteProperty -Name Failures -Value $filesprocessed[1]
            $scriptState.jobsCompleted += $jobItem

            Remove-Job $jobItem
        }

        # Check if we need to process the report
        $timeSinceLastReport = New-TimeSpan -start $scriptState.lastReportTime -end (Get-Date)
        if ($timeSinceLastReport.TotalSeconds -gt $TimeBetweenReportsSeconds) {

            Report-ScriptStatus -ScriptState $scriptState
            $scriptState.lastReporttime = Get-Date
        }

        if ($allJobsQueued) {
            $AllJobs = get-job

            if ($AllJobs.Count -eq 0) {
                # All jobs run, report and break
                Report-ScriptStatus -ScriptState $scriptState
                
                # Make sure we capture anything in the final iteration
                ($scriptState.exitLoop)++

                if ($scriptState.exitLoop -gt 1) {

                    break
                }
            }
        }
    }

}

### Example usage
Fix-MyDamnPermissions -TargetFolder "C:\Temp\Permissions"