Knowledge Base

Find answers to common questions about Cloudmersive products and services.



Analyze SMB File Share Workload for Virus Scanning
9/2/2026 - Cloudmersive Support


To assist customers in analyzing SMB File Share Workloads for Virus Scanning, customers can run the below script:

& .\EnumerateWorkload.ps1 -Path "\\AzVsOnPremTest2\smbshare5"

Customers can save the below script as EnumerateWorkload.ps1 which will produce an output file file-histograms.csv with the result data:

param(
    [Parameter(Mandatory = $true)]
    [string]$Path,

    [Parameter(Mandatory = $false)]
    [string]$OutputCsv = ".\file-histograms.csv"
)


# ============================================================
# FILE HISTOGRAM GENERATOR
#
# Compatible with:
#   C:\Data
#   D:\Files\SomeFolder
#   \\server\share
#   \\server\share\folder
#
# Generates histograms for:
#   - File extension
#   - File size
#   - Folder nesting depth
#   - Modification hour (0-23)
#   - Files per folder
#
# Folder depth:
#   0 = file is directly in the specified root folder
#   1 = one folder below the root
#   2 = two folders below the root
#   etc.
#
# Modification hour is based on LastWriteTime.
# ============================================================


# ============================================================
# VALIDATE INPUT PATH
# ============================================================

try {
    $rootItem = Get-Item -LiteralPath $Path -ErrorAction Stop
}
catch {
    throw "Unable to access path '$Path'. Error: $($_.Exception.Message)"
}

if (-not $rootItem.PSIsContainer) {
    throw "The specified path must be a directory: $Path"
}

$rootFullPath = $rootItem.FullName

# Normalize the root so it always has exactly one trailing backslash
# for relative-path/depth comparisons. This also works correctly for
# a drive root such as C:\ as well as UNC paths.
$rootPrefix = $rootFullPath.TrimEnd('\') + '\'


# ============================================================
# PREPARE OUTPUT PATH
# ============================================================

$OutputCsvFullPath = [System.IO.Path]::GetFullPath($OutputCsv)

$outputDirectory = [System.IO.Path]::GetDirectoryName($OutputCsvFullPath)

if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) {
    if (-not (Test-Path -LiteralPath $outputDirectory)) {
        New-Item `
            -ItemType Directory `
            -Path $outputDirectory `
            -Force | Out-Null
    }
}


# ============================================================
# HISTOGRAM STORAGE
# ============================================================

$extensionCounts = @{}
$depthCounts = @{}

# Tracks the number of files directly contained in every folder.
# The specified root folder itself is included.
$filesByFolder = @{}
$filesByFolder[$rootFullPath] = [long]0

# Initialize every hour so that hours with zero files still appear.
$hourCounts = @{}

for ($hour = 0; $hour -le 23; $hour++) {
    $hourCounts[$hour] = [long]0
}


# ============================================================
# FILE SIZE HISTOGRAM
#
# Bucket indexes:
#
# 0 = 0 bytes
# 1 = 1 B - <1 KB
# 2 = 1 KB - <10 KB
# 3 = 10 KB - <100 KB
# 4 = 100 KB - <1 MB
# 5 = 1 MB - <10 MB
# 6 = 10 MB - <100 MB
# 7 = 100 MB - <1 GB
# 8 = 1 GB - <10 GB
# 9 = >=10 GB
# ============================================================

$sizeBucketLabels = @(
    "0 B",
    "1 B - <1 KB",
    "1 KB - <10 KB",
    "10 KB - <100 KB",
    "100 KB - <1 MB",
    "1 MB - <10 MB",
    "10 MB - <100 MB",
    "100 MB - <1 GB",
    "1 GB - <10 GB",
    ">=10 GB"
)

$sizeCounts = New-Object 'long[]' $sizeBucketLabels.Count


function Get-SizeBucketIndex {

    param(
        [long]$Size
    )

    if ($Size -eq 0) {
        return 0
    }

    if ($Size -lt 1KB) {
        return 1
    }

    if ($Size -lt 10KB) {
        return 2
    }

    if ($Size -lt 100KB) {
        return 3
    }

    if ($Size -lt 1MB) {
        return 4
    }

    if ($Size -lt 10MB) {
        return 5
    }

    if ($Size -lt 100MB) {
        return 6
    }

    if ($Size -lt 1GB) {
        return 7
    }

    if ($Size -lt 10GB) {
        return 8
    }

    return 9
}


# ============================================================
# FILES PER FOLDER HISTOGRAM
#
# Count = number of folders whose DIRECT file count falls into
# the bucket. Subfolders do not contribute to their parent's count.
# The root folder itself is included.
# ============================================================

$folderFileBucketLabels = @(
    "0 - 10 files",
    "11 - 100 files",
    "101 - 1,000 files",
    "1,001 - 2,500 files",
    "2,501 - 5,000 files",
    "5,001 - 10,000 files",
    "10,001 - 25,000 files",
    "25,001 - 50,000 files",
    "50,001 - 100,000 files",
    "100,001 - 250,000 files",
    "250,001 - 500,000 files",
    "500,001 - 1,000,000 files",
    ">1,000,000 files"
)

$folderFileBucketCounts = New-Object 'long[]' $folderFileBucketLabels.Count

function Get-FolderFileBucketIndex {

    param(
        [long]$Count
    )

    if ($Count -le 10)      { return 0 }
    if ($Count -le 100)     { return 1 }
    if ($Count -le 1000)    { return 2 }
    if ($Count -le 2500)    { return 3 }
    if ($Count -le 5000)    { return 4 }
    if ($Count -le 10000)   { return 5 }
    if ($Count -le 25000)   { return 6 }
    if ($Count -le 50000)   { return 7 }
    if ($Count -le 100000)  { return 8 }
    if ($Count -le 250000)  { return 9 }
    if ($Count -le 500000)  { return 10 }
    if ($Count -le 1000000) { return 11 }

    return 12
}


# ============================================================
# ENUMERATE FOLDERS
#
# This pass makes sure empty folders are included in the histogram.
# ============================================================

$folderEnumerationErrors = @()

Get-ChildItem `
    -LiteralPath $rootFullPath `
    -Directory `
    -Recurse `
    -Force `
    -ErrorAction SilentlyContinue `
    -ErrorVariable +folderEnumerationErrors |
ForEach-Object {

    $folderFullPath = $_.FullName

    if (-not $filesByFolder.ContainsKey($folderFullPath)) {
        $filesByFolder[$folderFullPath] = [long]0
    }
}


# ============================================================
# ENUMERATE FILES
#
# This processes files as they are enumerated rather than
# loading the complete file list into memory.
# ============================================================

$totalFiles = [long]0

$enumerationErrors = @()

Write-Host ""
Write-Host "Scanning:"
Write-Host "  $rootFullPath"
Write-Host ""

Get-ChildItem `
    -LiteralPath $rootFullPath `
    -File `
    -Recurse `
    -Force `
    -ErrorAction SilentlyContinue `
    -ErrorVariable +enumerationErrors |
ForEach-Object {

    $file = $_

    # If an existing output CSV happens to be inside the scanned
    # directory, do not include it in the statistics.
    if ($file.FullName -ieq $OutputCsvFullPath) {
        return
    }

    $totalFiles++


    # ----
    # FILE COUNT IN CONTAINING FOLDER
    # ----

    $containingFolder = $file.DirectoryName

    if ($filesByFolder.ContainsKey($containingFolder)) {
        $filesByFolder[$containingFolder]++
    }
    else {
        # Defensive fallback if a folder could not be returned by
        # the directory enumeration but its file was still visible.
        $filesByFolder[$containingFolder] = [long]1
    }


    # ----
    # EXTENSION
    # ----

    $extension = $file.Extension

    if ([string]::IsNullOrWhiteSpace($extension)) {
        $extension = "[no extension]"
    }
    else {
        $extension = $extension.ToLowerInvariant()
    }

    if ($extensionCounts.ContainsKey($extension)) {
        $extensionCounts[$extension]++
    }
    else {
        $extensionCounts[$extension] = [long]1
    }


    # ----
    # FILE SIZE
    # ----

    $sizeBucket = Get-SizeBucketIndex -Size $file.Length

    $sizeCounts[$sizeBucket]++


    # ----
    # FOLDER NESTING DEPTH
    #
    # Root:
    #   \\server\share\data
    #
    # File:
    #   \\server\share\data\customer\2026\file.pdf
    #
    # Depth = 2
    # ----

    $directoryPath = $file.DirectoryName

    # Normalize the file's containing directory to the same trailing-
    # backslash form as $rootPrefix. This avoids the C:\ -> C: bug
    # caused by blindly trimming a drive-root backslash.
    $directoryPrefix = $directoryPath.TrimEnd('\') + '\'

    if ($directoryPrefix -ieq $rootPrefix) {

        $depth = 0

    }
    elseif ($directoryPrefix.StartsWith(
        $rootPrefix,
        [System.StringComparison]::OrdinalIgnoreCase
    )) {

        $relativeDirectory = $directoryPrefix.Substring($rootPrefix.Length).TrimEnd('\')

        if ([string]::IsNullOrWhiteSpace($relativeDirectory)) {
            $depth = 0
        }
        else {
            $depth = ($relativeDirectory -split '\\').Count
        }

    }
    else {

        # This normally should not happen, but fall back to
        # parent traversal if path normalization behaves oddly
        # on a particular filesystem.
        $depth = 0
        $currentDirectory = $file.Directory

        while ($null -ne $currentDirectory) {

            $currentPrefix = $currentDirectory.FullName.TrimEnd('\') + '\'

            if ($currentPrefix -ieq $rootPrefix) {
                break
            }

            $depth++
            $currentDirectory = $currentDirectory.Parent
        }
    }

    if ($depthCounts.ContainsKey($depth)) {
        $depthCounts[$depth]++
    }
    else {
        $depthCounts[$depth] = [long]1
    }


    # ----
    # MODIFICATION HOUR
    # ----

    $modificationHour = $file.LastWriteTime.Hour

    $hourCounts[$modificationHour]++
}


# ============================================================
# BUILD FILES-PER-FOLDER COUNTS
# ============================================================

[long]$totalFolders = $filesByFolder.Count

foreach ($folderFileCount in $filesByFolder.Values) {

    $folderBucket = Get-FolderFileBucketIndex -Count ([long]$folderFileCount)

    $folderFileBucketCounts[$folderBucket]++
}


# ============================================================
# HELPER FOR PERCENTAGE
# ============================================================

function Get-Percent {

    param(
        [long]$Count
    )

    if ($totalFiles -eq 0) {
        return 0
    }

    return [Math]::Round(
        (($Count / [double]$totalFiles) * 100),
        4
    )
}


function Get-FolderPercent {

    param(
        [long]$Count
    )

    if ($totalFolders -eq 0) {
        return 0
    }

    return [Math]::Round(
        (($Count / [double]$totalFolders) * 100),
        4
    )
}


# ============================================================
# BUILD CSV ROWS
# ============================================================

$rows = New-Object System.Collections.Generic.List[object]


# --------
# EXTENSION HISTOGRAM
#
# Sorted largest count first.
# --------

$extensionCounts.GetEnumerator() |
    Sort-Object -Property @{ Expression = { $_.Value }; Descending = $true }, @{ Expression = { $_.Key }; Descending = $false } |
    ForEach-Object {

        $count = [long]$_.Value

        $rows.Add(
            [PSCustomObject]@{
                Histogram = "FileExtension"
                Bucket    = $_.Key
                Count     = $count
                Percent   = Get-Percent -Count $count
            }
        )
    }


# --------
# FILE SIZE HISTOGRAM
# --------

for ($i = 0; $i -lt $sizeBucketLabels.Count; $i++) {

    $count = [long]$sizeCounts[$i]

    $rows.Add(
        [PSCustomObject]@{
            Histogram = "FileSize"
            Bucket    = $sizeBucketLabels[$i]
            Count     = $count
            Percent   = Get-Percent -Count $count
        }
    )
}


# --------
# FOLDER DEPTH HISTOGRAM
#
# Fill in missing depths with zero counts so the histogram
# remains contiguous.
# --------

if ($depthCounts.Count -gt 0) {

    $maxDepth = ($depthCounts.Keys | Measure-Object -Maximum).Maximum

    for ($depth = 0; $depth -le $maxDepth; $depth++) {

        if ($depthCounts.ContainsKey($depth)) {
            $count = [long]$depthCounts[$depth]
        }
        else {
            $count = [long]0
        }

        $rows.Add(
            [PSCustomObject]@{
                Histogram = "FolderDepth"
                Bucket    = $depth
                Count     = $count
                Percent   = Get-Percent -Count $count
            }
        )
    }
}


# --------
# MODIFICATION HOUR HISTOGRAM
#
# Hours are 0-23.
# For example:
#
# 0  = midnight through 00:59
# 13 = 1:00 PM through 1:59 PM
# 23 = 11:00 PM through 11:59 PM
# --------

for ($hour = 0; $hour -le 23; $hour++) {

    $count = [long]$hourCounts[$hour]

    $rows.Add(
        [PSCustomObject]@{
            Histogram = "ModificationHour"
            Bucket    = $hour
            Count     = $count
            Percent   = Get-Percent -Count $count
        }
    )
}


# --------
# FILES PER FOLDER HISTOGRAM
#
# This is intentionally the final histogram in the CSV.
# Count   = number of folders in the bucket.
# Percent = percentage of all folders in the bucket.
# --------

for ($i = 0; $i -lt $folderFileBucketLabels.Count; $i++) {

    $count = [long]$folderFileBucketCounts[$i]

    $rows.Add(
        [PSCustomObject]@{
            Histogram = "FilesPerFolder"
            Bucket    = $folderFileBucketLabels[$i]
            Count     = $count
            Percent   = Get-FolderPercent -Count $count
        }
    )
}


# ============================================================
# WRITE CSV
# ============================================================

$rows |
    Export-Csv `
        -LiteralPath $OutputCsvFullPath `
        -NoTypeInformation `
        -Encoding UTF8


# ============================================================
# SUMMARY / CONSOLE OUTPUT
# ============================================================

# Everything below is written through Console.Out in strict sequence.
# This avoids mixing Write-Host, the success pipeline, and Console.Out.
[Console]::Out.WriteLine("")
[Console]::Out.WriteLine("Finished.")
[Console]::Out.WriteLine("")
[Console]::Out.WriteLine("Files scanned: {0}" -f $totalFiles)
[Console]::Out.WriteLine("Folders scanned: {0}" -f $totalFolders)
[Console]::Out.WriteLine("FilesPerFolder buckets written: {0}" -f $folderFileBucketLabels.Count)

if ($folderEnumerationErrors.Count -gt 0) {
    [Console]::Out.WriteLine("")
    [Console]::Out.WriteLine("WARNING: {0} folder enumeration errors occurred." -f $folderEnumerationErrors.Count)
    [Console]::Out.WriteLine("")
    [Console]::Out.WriteLine("Examples:")

    $folderEnumerationErrors |
        Select-Object -First 10 |
        ForEach-Object {
            [Console]::Out.WriteLine("  {0}" -f $_.Exception.Message)
        }
}

if ($enumerationErrors.Count -gt 0) {
    [Console]::Out.WriteLine("")
    [Console]::Out.WriteLine("WARNING: {0} enumeration errors occurred." -f $enumerationErrors.Count)
    [Console]::Out.WriteLine("")
    [Console]::Out.WriteLine("Examples:")

    $enumerationErrors |
        Select-Object -First 10 |
        ForEach-Object {
            [Console]::Out.WriteLine("  {0}" -f $_.Exception.Message)
        }
}

[Console]::Out.WriteLine("")
[Console]::Out.WriteLine("CSV contents:")
[Console]::Out.WriteLine("")

foreach ($csvLine in [System.IO.File]::ReadLines($OutputCsvFullPath)) {
    [Console]::Out.WriteLine($csvLine)
}

[Console]::Out.WriteLine("")

# DO NOT PUT ANY OUTPUT AFTER THIS LINE.
# The absolute CSV path is intentionally the final line emitted by the script.
[Console]::Out.WriteLine($OutputCsvFullPath)

600 free API calls/month, with no expiration

Sign Up Now or Sign in with Google    Sign in with Microsoft

Questions? We'll be your guide.

Contact Sales