Free script · PowerShell

File share scan: path length, blocked names and large files

Scan a file share before you move it to SharePoint or OneDrive and get a CSV of every path that is too long, every file or folder name Microsoft blocks, and every file over the upload size limit, so you can fix them before the migration fails on them.

PowerShellRead-onlyUpdated
Read-only: makes no changes
Before you run it

What you need

Modules and tools

  • None. Uses built-in PowerShell cmdlets only.

Admin roles

  • Read access (NTFS and share) to every folder you scan. Run it as an account that can read the whole share, or folders it cannot read are listed as AccessError.

Runs on

  • PowerShell 7 on a Windows server or PC with access to the share (Windows PowerShell 5.1 also runs it)
Parameters

Options you can set

ParameterWhat it does
-PathRequired. The folder or UNC path to scan, for example \\fs01\Finance or D:\Shares\Finance.
-TargetPrefixOptional. The part of the SharePoint path that will sit in front of the migrated files, for example sites/Finance/Shared Documents/Archive. Its length is added to every path. Default: empty.
-MaxPathLengthOptional. Path length to flag. Default: 400, the SharePoint and OneDrive limit for the full decoded path including the file name.
-MaxFileSizeGBOptional. File size to flag, in GB. Default: 250, the SharePoint and OneDrive per-file upload limit.
-OutputPathOptional. Where to save the CSV. Default: PathIssues_yyyyMMdd_HHmm.csv in the current folder.
How to run it

Step by step

  1. 1

    Step 1

    Sign in to a Windows server or PC that can reach the share, with an account that can read every folder in it.

  2. 2

    Step 2

    Save the script as Find-CMSharePointPathIssues.ps1, for example in C:\Scripts.

  3. 3

    Step 3

    Open PowerShell 7 (or Windows PowerShell) from the Start menu and run cd C:\Scripts.

  4. 4

    Step 4

    Run .\Find-CMSharePointPathIssues.ps1 -Path \\fs01\Finance -TargetPrefix "sites/Finance/Shared Documents". Set -TargetPrefix to the site and library path you plan to migrate into.

  5. 5

    Step 5

    Open the CSV. Filter the Issue column, fix or rename the items (or plan a shorter target path), then run the scan again until it is clean.

What you get: A UTF-8 CSV with one row per problem: Issue (PathTooLong, InvalidCharacter, BlockedName, LeadingOrTrailingSpace, FileTooLarge or AccessError), ItemType (File or Folder), FullName, Name, ProjectedPathLength, SizeGB, LastWriteTime and Detail. One item can appear on several rows if it has several problems.

The script

Find-CMSharePointPathIssues.ps1

<#
.SYNOPSIS
    Find-CMSharePointPathIssues.ps1

.DESCRIPTION
    Scans a file share for items that SharePoint in Microsoft 365 and OneDrive will reject:
      - Paths longer than the limit (default 400 characters). Microsoft states the entire
        decoded file path, including the file name, can't contain more than 400 characters.
        The script measures the path relative to -Path and adds the length of -TargetPrefix.
      - Characters not allowed in names:  " * : < > ? / \ |
      - Names not allowed: .lock, CON, PRN, AUX, NUL, COM0-COM9, LPT0-LPT9, desktop.ini,
        names starting with ~$, _vti_ anywhere in a name, "forms" at the root of a library,
        and folder names starting with the characters U+309B or U+1027.
      - Leading or trailing spaces in names.
      - Files over the upload limit (default 250 GB).
    Read-only: it only reads folder listings and file sizes. It changes nothing.

    Limits are from Microsoft's "SharePoint limits" and "Restrictions and limitations in
    OneDrive and SharePoint" pages. Check them before you rely on the defaults.

    Provided by cloud-migrations.au as is; test before use.
    Read the script before you run it and try it on a small folder first.

.NOTES
    Requirements
      - PowerShell 7 (recommended) or Windows PowerShell 5.1. No modules.
      - Read access to every folder under -Path
    Commands used (built-in, documented by Microsoft):
      Get-ChildItem, Test-Path, Write-Progress, Export-Csv

.EXAMPLE
    .\Find-CMSharePointPathIssues.ps1 -Path \\fs01\Finance -TargetPrefix "sites/Finance/Shared Documents"

.EXAMPLE
    .\Find-CMSharePointPathIssues.ps1 -Path D:\Shares\Projects -MaxPathLength 300 -OutputPath C:\Reports\projects.csv
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)][string]$Path,
    [string]$TargetPrefix = '',
    [int]$MaxPathLength = 400,
    [double]$MaxFileSizeGB = 250,
    [string]$OutputPath = (Join-Path -Path (Get-Location) -ChildPath ('PathIssues_{0}.csv' -f (Get-Date -Format 'yyyyMMdd_HHmm')))
)

if (-not (Test-Path -LiteralPath $Path)) {
    Write-Error "Path not found or not readable: $Path"
    return
}

$root          = (Get-Item -LiteralPath $Path).FullName.TrimEnd('\', '/')
$prefix        = $TargetPrefix.Trim('/', '\')
$prefixLength  = if ($prefix) { $prefix.Length + 1 } else { 0 }   # +1 for the slash between prefix and path
$maxBytes      = [int64]($MaxFileSizeGB * 1GB)
$invalidChars  = [char[]]@('"', '*', ':', '<', '>', '?', '/', '\', '|')
$blockedNames  = @('.lock', 'CON', 'PRN', 'AUX', 'NUL', 'desktop.ini') +
                 (0..9 | ForEach-Object { "COM$_" }) + (0..9 | ForEach-Object { "LPT$_" })
$badFolderStart = @([char]0x309B, [char]0x1027)

$issues = New-Object System.Collections.Generic.List[object]
function Add-CMIssue {
    param($Issue, $Item, $Length, $Detail)
    $isFolder = $Item.PSIsContainer
    $issues.Add([pscustomobject][ordered]@{
        Issue               = $Issue
        ItemType            = $(if ($isFolder) { 'Folder' } else { 'File' })
        FullName            = $Item.FullName
        Name                = $Item.Name
        ProjectedPathLength = $Length
        SizeGB              = $(if ($isFolder) { '' } else { [math]::Round($Item.Length / 1GB, 3) })
        LastWriteTime       = $Item.LastWriteTime
        Detail              = $Detail
    })
}

Write-Host "Scanning $root ..."
$scanErrors = $null
$count = 0

Get-ChildItem -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue -ErrorVariable +scanErrors |
ForEach-Object {
    $item = $_
    $count++
    if ($count % 500 -eq 0) {
        Write-Progress -Activity "Scanning $root" -Status ("{0} items checked, {1} issues found" -f $count, $issues.Count) -CurrentOperation $item.FullName
    }

    # Path relative to the scan root, with forward slashes, plus the target prefix
    $relative  = $item.FullName.Substring($root.Length).TrimStart('\', '/') -replace '\\', '/'
    $projected = $prefixLength + $relative.Length
    $name      = $item.Name
    $baseName  = [System.IO.Path]::GetFileNameWithoutExtension($name)

    if ($projected -gt $MaxPathLength) {
        Add-CMIssue 'PathTooLong' $item $projected ("Over the {0} character limit by {1}" -f $MaxPathLength, ($projected - $MaxPathLength))
    }
    if ($name.IndexOfAny($invalidChars) -ge 0) {
        Add-CMIssue 'InvalidCharacter' $item $projected 'Name contains one of " * : < > ? / \ |'
    }
    if ($blockedNames -contains $name -or $blockedNames -contains $baseName) {
        Add-CMIssue 'BlockedName' $item $projected "Reserved name: $name"
    }
    if ($name.StartsWith('~$')) {
        Add-CMIssue 'BlockedName' $item $projected 'Names starting with ~$ are not allowed'
    }
    if ($name -like '*_vti_*') {
        Add-CMIssue 'BlockedName' $item $projected '_vti_ is not allowed anywhere in a name'
    }
    if ($name -eq 'forms' -and -not $relative.Contains('/')) {
        Add-CMIssue 'BlockedName' $item $projected '"forms" is not allowed at the root of a library'
    }
    if ($item.PSIsContainer -and $name.Length -gt 0 -and $badFolderStart -contains $name[0]) {
        Add-CMIssue 'BlockedName' $item $projected 'Folder name starts with a character Microsoft does not allow'
    }
    if ($name -ne $name.Trim(' ')) {
        Add-CMIssue 'LeadingOrTrailingSpace' $item $projected 'Leading or trailing space in the name'
    }
    if (-not $item.PSIsContainer -and $item.Length -gt $maxBytes) {
        Add-CMIssue 'FileTooLarge' $item $projected ("Larger than {0} GB" -f $MaxFileSizeGB)
    }
}
Write-Progress -Activity "Scanning $root" -Completed

# Folders the scan could not read
foreach ($e in @($scanErrors)) {
    if ($null -eq $e) { continue }
    $issues.Add([pscustomobject][ordered]@{
        Issue = 'AccessError'; ItemType = ''; FullName = "$($e.TargetObject)"; Name = ''
        ProjectedPathLength = ''; SizeGB = ''; LastWriteTime = ''; Detail = $e.Exception.Message
    })
}

try {
    $issues | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Host ("Done. {0} items checked, {1} issue rows written." -f $count, $issues.Count)
    $issues | Group-Object -Property Issue | Sort-Object -Property Count -Descending |
        ForEach-Object { Write-Host ("  {0}: {1}" -f $_.Name, $_.Count) }
    Write-Host "Report saved to: $((Resolve-Path -Path $OutputPath).Path)"
}
catch {
    Write-Error "Could not write the CSV to $OutputPath : $($_.Exception.Message)"
}
Sources

Commands documented here

Every command in the script is documented on these vendor pages. Test in a non-production tenant or environment first.