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.
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)
Options you can set
| Parameter | What it does |
|---|---|
-Path | Required. The folder or UNC path to scan, for example \\fs01\Finance or D:\Shares\Finance. |
-TargetPrefix | Optional. 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. |
-MaxPathLength | Optional. Path length to flag. Default: 400, the SharePoint and OneDrive limit for the full decoded path including the file name. |
-MaxFileSizeGB | Optional. File size to flag, in GB. Default: 250, the SharePoint and OneDrive per-file upload limit. |
-OutputPath | Optional. Where to save the CSV. Default: PathIssues_yyyyMMdd_HHmm.csv in the current folder. |
Step by step
- 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
Step 2
Save the script as
Find-CMSharePointPathIssues.ps1, for example inC:\Scripts. - 3
Step 3
Open PowerShell 7 (or Windows PowerShell) from the Start menu and run
cd C:\Scripts. - 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
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.
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)"
}Commands documented here
Every command in the script is documented on these vendor pages. Test in a non-production tenant or environment first.