Skip to content

Commit a8ce79e

Browse files
committed
new-script-spo-time-based-file-reports
1 parent 5512b7f commit a8ce79e

2 files changed

Lines changed: 296 additions & 3 deletions

File tree

scripts/spo-time-based-file-reports/README.md

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,279 @@ Now that the scripts are setup, you just need to run them. All these steps are t
8080

8181
## 1. OneDrive Scan
8282

83+
# [CLI for Microsoft 365](#tab/cli-m365-ps)
84+
85+
```powershell
86+
87+
[CmdletBinding(SupportsShouldProcess)]
88+
param(
89+
[Parameter(Mandatory = $false, HelpMessage = "SharePoint admin center URL (e.g., https://contoso-admin.sharepoint.com)")]
90+
[ValidatePattern('^https://')]
91+
[string]$TenantAdminUrl,
92+
93+
[Parameter(Mandatory = $false, HelpMessage = "SharePoint site URL to scan (e.g., https://contoso.sharepoint.com/sites/project)")]
94+
[ValidatePattern('^https://')]
95+
[string]$SiteUrl,
96+
97+
[Parameter(Mandatory = $false, HelpMessage = "Title of a specific document library to scan")]
98+
[string]$LibraryName,
99+
100+
[Parameter(Mandatory = $false, HelpMessage = "Server-relative URL of a specific folder to scan")]
101+
[string]$FolderUrl,
102+
103+
[Parameter(Mandatory = $false, HelpMessage = "Number of days to use as age threshold (default: 1460 = 4 years)")]
104+
[int]$DaysOld = 1460,
105+
106+
[Parameter(Mandatory = $false, HelpMessage = "Full path for the CSV report file")]
107+
[string]$OutputPath,
108+
109+
[Parameter(Mandatory = $false, HelpMessage = "Include OneDrive personal sites in scan (requires TenantAdminUrl)")]
110+
[switch]$IncludeOneDrive,
111+
112+
[Parameter(Mandatory = $false, HelpMessage = "Scan subfolders recursively")]
113+
[switch]$Recursive
114+
)
115+
116+
begin {
117+
if (-not $TenantAdminUrl -and -not $SiteUrl) {
118+
throw "You must specify either -TenantAdminUrl or -SiteUrl parameter."
119+
}
120+
121+
if ($TenantAdminUrl -and $SiteUrl) {
122+
throw "Cannot specify both -TenantAdminUrl and -SiteUrl. Choose one scan mode."
123+
}
124+
125+
if ($IncludeOneDrive -and -not $TenantAdminUrl) {
126+
throw "-IncludeOneDrive requires -TenantAdminUrl parameter."
127+
}
128+
129+
if ($LibraryName -and -not $SiteUrl) {
130+
throw "-LibraryName requires -SiteUrl parameter."
131+
}
132+
133+
if ($FolderUrl -and -not $SiteUrl) {
134+
throw "-FolderUrl requires -SiteUrl parameter."
135+
}
136+
137+
if (-not $OutputPath) {
138+
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
139+
$OutputPath = Join-Path (Get-Location) "OldFilesReport-$timestamp.csv"
140+
} else {
141+
$parentFolder = Split-Path -Path $OutputPath -Parent
142+
if (-not (Test-Path -Path $parentFolder)) {
143+
throw "Output folder does not exist: $parentFolder"
144+
}
145+
}
146+
147+
$transcriptPath = $OutputPath -replace '\.csv$', '-Transcript.log'
148+
Start-Transcript -Path $transcriptPath
149+
150+
Write-Host "Starting old file report generation..." -ForegroundColor Cyan
151+
Write-Host "Age threshold: $DaysOld days" -ForegroundColor Cyan
152+
Write-Host "Output path: $OutputPath" -ForegroundColor Cyan
153+
154+
Write-Verbose "Ensuring CLI for Microsoft 365 login..."
155+
m365 login --ensure
156+
if ($LASTEXITCODE -ne 0) {
157+
Stop-Transcript
158+
throw "Failed to authenticate with CLI for Microsoft 365."
159+
}
160+
161+
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
162+
$cutoffDateString = $cutoffDate.ToString("yyyy-MM-ddTHH:mm:ssZ")
163+
Write-Verbose "Cutoff date: $cutoffDateString"
164+
165+
$script:ReportCollection = [System.Collections.Generic.List[object]]::new()
166+
$script:Summary = @{
167+
SitesProcessed = 0
168+
LibrariesProcessed = 0
169+
OldFilesFound = 0
170+
Failures = 0
171+
}
172+
}
173+
174+
process {
175+
try {
176+
$sitesToProcess = @()
177+
178+
if ($TenantAdminUrl) {
179+
Write-Host "Retrieving sites from tenant..." -ForegroundColor Cyan
180+
Write-Verbose "Building site list command..."
181+
182+
$siteListArgs = @('spo', 'site', 'list', '--output', 'json')
183+
if ($IncludeOneDrive) {
184+
Write-Verbose "Including OneDrive sites"
185+
$siteListArgs += '--withOneDriveSites'
186+
} else {
187+
Write-Verbose "Filtering for SharePoint sites only"
188+
$siteListArgs += '--filter'
189+
$siteListArgs += "Url -like '/sites/'"
190+
}
191+
192+
$sitesJson = m365 @siteListArgs 2>&1
193+
if ($LASTEXITCODE -ne 0) {
194+
Write-Warning "Failed to retrieve sites. CLI: $sitesJson"
195+
$script:Summary.Failures++
196+
return
197+
}
198+
199+
$sitesToProcess = @($sitesJson | ConvertFrom-Json)
200+
Write-Host "Found $($sitesToProcess.Count) sites to scan" -ForegroundColor Green
201+
} else {
202+
Write-Verbose "Using single site: $SiteUrl"
203+
$sitesToProcess = @(@{ Url = $SiteUrl; Title = "" })
204+
}
205+
206+
$siteCounter = 0
207+
foreach ($site in $sitesToProcess) {
208+
$siteCounter++
209+
$siteUrl = $site.Url
210+
$siteTitle = if ($site.Title) { $site.Title } else { $siteUrl }
211+
212+
Write-Progress -Activity "Processing sites" -Status "Site $siteCounter of $($sitesToProcess.Count): $siteTitle" -PercentComplete (($siteCounter / $sitesToProcess.Count) * 100)
213+
Write-Verbose "Processing site: $siteUrl"
214+
215+
try {
216+
$librariesToProcess = @()
217+
218+
if ($LibraryName) {
219+
Write-Verbose "Using specific library: $LibraryName"
220+
$libraryListJson = m365 spo list list --webUrl $siteUrl --filter "Title eq '$LibraryName' and BaseTemplate eq 101 and Hidden eq false" --properties "Title,RootFolder/ServerRelativeUrl" --output json 2>&1
221+
if ($LASTEXITCODE -ne 0) {
222+
Write-Warning "Failed to retrieve library '$LibraryName' from site '$siteUrl'. CLI: $libraryListJson"
223+
$script:Summary.Failures++
224+
continue
225+
}
226+
$librariesToProcess = @($libraryListJson | ConvertFrom-Json)
227+
} else {
228+
Write-Verbose "Retrieving all document libraries..."
229+
$libraryListJson = m365 spo list list --webUrl $siteUrl --filter "BaseTemplate eq 101 and Hidden eq false" --properties "Title,RootFolder/ServerRelativeUrl" --output json 2>&1
230+
if ($LASTEXITCODE -ne 0) {
231+
Write-Warning "Failed to retrieve libraries from site '$siteUrl'. CLI: $libraryListJson"
232+
$script:Summary.Failures++
233+
continue
234+
}
235+
$librariesToProcess = @($libraryListJson | ConvertFrom-Json)
236+
}
237+
238+
if ($librariesToProcess.Count -eq 0) {
239+
Write-Verbose "No document libraries found in site '$siteUrl'"
240+
continue
241+
}
242+
243+
$script:Summary.SitesProcessed++
244+
245+
foreach ($library in $librariesToProcess) {
246+
$libraryTitle = $library.Title
247+
$folderPath = if ($FolderUrl) { $FolderUrl } else { $library.RootFolder.ServerRelativeUrl }
248+
249+
Write-Verbose "Scanning library: $libraryTitle (Folder: $folderPath)"
250+
251+
try {
252+
$fileListArgs = @(
253+
'spo', 'file', 'list',
254+
'--webUrl', $siteUrl,
255+
'--folderUrl', $folderPath,
256+
'--fields', 'Name,ServerRelativeUrl,TimeLastModified,TimeCreated,Length,ListItemAllFields/Author,ListItemAllFields/Editor',
257+
'--filter', "TimeLastModified lt datetime'$cutoffDateString'",
258+
'--output', 'json'
259+
)
260+
if ($Recursive) {
261+
$fileListArgs += '--recursive'
262+
}
263+
264+
$filesJson = m365 @fileListArgs 2>&1
265+
if ($LASTEXITCODE -ne 0) {
266+
Write-Warning "Failed to retrieve files from library '$libraryTitle' in site '$siteUrl'. CLI: $filesJson"
267+
$script:Summary.Failures++
268+
continue
269+
}
270+
271+
$files = @($filesJson | ConvertFrom-Json)
272+
Write-Verbose "Found $($files.Count) old files in library '$libraryTitle'"
273+
274+
foreach ($file in $files) {
275+
$lastModified = [DateTime]::Parse($file.TimeLastModified)
276+
$daysOldValue = [Math]::Round((Get-Date).Subtract($lastModified).TotalDays)
277+
278+
$script:ReportCollection.Add([PSCustomObject]@{
279+
SiteUrl = $siteUrl
280+
SiteTitle = $siteTitle
281+
LibraryTitle = $libraryTitle
282+
FileName = $file.Name
283+
FilePath = $file.ServerRelativeUrl
284+
LastModified = $file.TimeLastModified
285+
Created = $file.TimeCreated
286+
SizeBytes = $file.Length
287+
Author = if ($file.ListItemAllFields.Author) { $file.ListItemAllFields.Author.LookupValue } else { "N/A" }
288+
Editor = if ($file.ListItemAllFields.Editor) { $file.ListItemAllFields.Editor.LookupValue } else { "N/A" }
289+
DaysOld = $daysOldValue
290+
})
291+
$script:Summary.OldFilesFound++
292+
}
293+
294+
$script:Summary.LibrariesProcessed++
295+
} catch {
296+
Write-Warning "Error scanning library '$libraryTitle' in site '$siteUrl': $_"
297+
$script:Summary.Failures++
298+
continue
299+
}
300+
}
301+
} catch {
302+
Write-Warning "Error processing site '$siteUrl': $_"
303+
$script:Summary.Failures++
304+
continue
305+
}
306+
}
307+
} catch {
308+
Write-Warning "Unexpected error during processing: $_"
309+
$script:Summary.Failures++
310+
}
311+
}
312+
313+
end {
314+
Write-Progress -Activity "Processing sites" -Completed
315+
316+
if ($script:ReportCollection.Count -gt 0) {
317+
Write-Host "Exporting report to CSV..." -ForegroundColor Cyan
318+
$script:ReportCollection | Sort-Object SiteUrl, LibraryTitle, LastModified | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
319+
Write-Host "Report exported: $OutputPath" -ForegroundColor Green
320+
} else {
321+
Write-Host "No old files found matching criteria." -ForegroundColor Yellow
322+
}
323+
324+
Write-Host "`n===== Summary =====" -ForegroundColor Cyan
325+
Write-Host "Sites processed: $($script:Summary.SitesProcessed)" -ForegroundColor White
326+
Write-Host "Libraries processed: $($script:Summary.LibrariesProcessed)" -ForegroundColor White
327+
Write-Host "Old files found: $($script:Summary.OldFilesFound)" -ForegroundColor White
328+
if ($script:Summary.Failures -gt 0) {
329+
Write-Host "Failures: $($script:Summary.Failures)" -ForegroundColor Red
330+
} else {
331+
Write-Host "Failures: 0" -ForegroundColor Green
332+
}
333+
Write-Host "==================`n" -ForegroundColor Cyan
334+
335+
Stop-Transcript
336+
}
337+
338+
# Scans all SharePoint sites for files older than 3 years
339+
# .\Report-OldFiles.ps1 -TenantAdminUrl "https://contoso-admin.sharepoint.com" -DaysOld 1095
340+
341+
# Scans all SharePoint AND OneDrive sites for files older than 4 years
342+
# .\Report-OldFiles.ps1 -TenantAdminUrl "https://contoso-admin.sharepoint.com" -IncludeOneDrive
343+
344+
# Scans all libraries in a specific site recursively
345+
# .\Report-OldFiles.ps1 -SiteUrl "https://contoso.sharepoint.com/sites/project" -Recursive
346+
347+
# Scans a specific folder in a specific library with verbose output
348+
# .\Report-OldFiles.ps1 -SiteUrl "https://contoso.sharepoint.com/sites/project" -LibraryName "Documents" -FolderUrl "/sites/project/Documents/Archive" -DaysOld 730 -Recursive -Verbose
349+
350+
```
351+
352+
353+
[!INCLUDE [More about CLI for Microsoft 365](../../docfx/includes/MORE-CLIM365.md)]
354+
***
355+
83356
# [PnP PowerShell](#tab/pnpps)
84357
```powershell
85358
@@ -339,6 +612,7 @@ Stop-Transcript
339612
| Author(s) |
340613
|-----------|
341614
| Nick Brattoli|
615+
| Adam Wójcik|
342616

343617

344618
[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)]

scripts/spo-time-based-file-reports/assets/sample.json

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@
99
""
1010
],
1111
"creationDateTime": "2024-03-25",
12-
"updateDateTime": "2024-03-25",
12+
"updateDateTime": "2026-01-05",
1313
"products": [
1414
"SharePoint"
1515
],
1616
"metadata": [
1717
{
1818
"key": "PNP-POWERSHELL",
1919
"value": "2.4.0"
20+
},
21+
{
22+
"key": "CLI-FOR-MICROSOFT365",
23+
"value": "11.3.0"
2024
}
2125
],
2226
"categories": [
@@ -26,7 +30,11 @@
2630
"Add-PnPFile",
2731
"Connect-PnPOnline",
2832
"Get-PnPList",
29-
"Get-PnPListItem"
33+
"Get-PnPListItem",
34+
"m365 login",
35+
"m365 spo site list",
36+
"m365 spo list list",
37+
"m365 spo file list"
3038
],
3139
"thumbnails": [
3240
{
@@ -42,14 +50,25 @@
4250
"company": "",
4351
"pictureUrl": "https://github.com/nbrattoli.png",
4452
"name": "Nick Brattoli"
53+
},
54+
{
55+
"gitHubAccount": "Adam-it",
56+
"company": "",
57+
"pictureUrl": "https://avatars.githubusercontent.com/u/58668583?v=4",
58+
"name": "Adam Wójcik"
4559
}
4660
],
4761
"references": [
4862
{
4963
"name": "Want to learn more about PnP PowerShell and the cmdlets",
5064
"description": "Check out the PnP PowerShell site to get started and for the reference to the cmdlets.",
5165
"url": "https://aka.ms/pnp/powershell"
66+
},
67+
{
68+
"name": "Want to learn more about CLI for Microsoft 365 and the commands",
69+
"description": "Check out the CLI for Microsoft 365 site to get started and for the reference to the commands.",
70+
"url": "https://aka.ms/cli-m365"
5271
}
5372
]
5473
}
55-
]
74+
]

0 commit comments

Comments
 (0)