古い同期クライアントの不具合や運用ミスで、OneDrive 上に意味のない空フォルダーが何千件も残ってしまうことがあります。Purview 監査ログから URL 一覧は取れたものの、クライアントやブラウザーから手動削除するのは現実的ではありません。本記事では、その CSV を活用して、PnP PowerShell と Microsoft Graph で空フォルダーを安全かつ効率的に一括削除する具体的な手順とベストプラクティスを詳しく解説します。
OneDrive に空フォルダーが数千件できてしまったときの整理
よくある発生原因
次のような理由で「同一日に同一ユーザーが作成した空フォルダー」が大量発生するケースがよく見られます。
- 非常に古い OneDrive 同期クライアントのバグにより、失敗した同期フォルダーだけが空で残ってしまう
- サードパーティ製ツールや旧バッチ処理が誤ってフォルダーだけを大量作成してしまった
- 移行ツールの設定ミスで、フォルダーだけ先に作られ、中身のデータが別ライブラリへ移動済み
結果として、エクスプローラー表示や OneDrive Web UI が空フォルダーだらけになり、ユーザー体験や運用管理に支障をきたします。
今回想定する前提条件
- 対象は OneDrive for Business(=SharePoint Online 上の個人用サイト)
- Purview 監査ログから「作成されたフォルダーの SharePoint URL 一覧(CSV)」は取得済み
- 空フォルダー数が数千件規模で、クライアント/ブラウザーからの手作業削除は非現実的
- OneDrive 専用の PowerShell モジュールは存在しないため、SharePoint として扱うか Microsoft Graph を直接叩く必要がある
OneDrive は「SharePoint サイト」として扱う
OneDrive for Business の実体は、https://<tenant>-my.sharepoint.com/personal/<user>_<domain>_com のような SharePoint サイト コレクションです。そのため、以下の二つのアプローチが取れます。
- PnP PowerShell で SharePoint サイトとして接続し、フォルダー削除機能を使う
- Microsoft Graph の
driveItem削除 API を使い、REST もしくは Graph PowerShell SDK から削除する
PnP PowerShell の Remove-PnPFolder コマンドレットは、親フォルダーとフォルダー名を指定して対象のフォルダーを削除できます。-Recycle を付けると SharePoint/OneDrive のごみ箱に移動し、付けない場合は完全削除になります。
Microsoft Graph の driveItem 削除 API は、ID もしくはパスでファイル/フォルダーを指定して削除することができ、標準ではごみ箱に移動されます。
解決方針の全体像(PnP PowerShell vs Microsoft Graph)
| 方式 | 技術 | 特徴 | 向いているケース |
|---|---|---|---|
| 方式A | PnP PowerShell | SharePoint/OneDrive 向けの高機能 PowerShell モジュール。 CSV の URL をサイト相対パスに変換し、 Remove-PnPFolder で削除。コードが読みやすくデバッグしやすい。 | 特定のユーザー OneDrive だけ/数サイト程度の削除。 まずは安全に少量から試したい場合。 |
| 方式B | Microsoft Graph(REST / Graph PowerShell) | driveItem API を利用して、ID またはパスで削除。 バッチ機能で多サイト・多ユーザーにもスケール可能。 スロットリング(429)対策が必須。 | 多数ユーザーの OneDrive を横断して削除したい場合。 既存の Graph ベース自動化に組み込みたい場合。 |
実務では、まず PnP PowerShell で「仕組みと CSV の中身が正しいか」を確認し、その後必要に応じて Graph によるバッチ処理へ発展させる、というステップをおすすめします。
事前準備:権限・環境・CSV の整理
必要な権限
| 項目 | PnP PowerShell 方式 | Microsoft Graph 方式 |
|---|---|---|
| SharePoint / OneDrive 権限 | 対象 OneDrive サイトに対する サイト コレクション管理者(推奨) もしくは対象フォルダーの削除権限 | OneDrive/サイト側の削除権限(通常はサイト管理者 or 対象ユーザーの OneDrive 管理権限)。 + 後述の Graph アプリ権限 or 委任権限。 |
| PowerShell 側 | PnP.PowerShell モジュール。Install-Module PnP.PowerShell で導入。 | Microsoft.Graph モジュール。Install-Module Microsoft.Graph など。 |
| Graph 権限(代表例) | 不要 | driveItem 削除には、Files.ReadWrite.All や Sites.ReadWrite.All などのスコープが必要です。 |
CSV(監査ログ)の前処理
Purview 監査ログからエクスポートした CSV は、そのままでは次のような問題を含んでいることがあります。
- 同一 URL の重複行
- 既に削除済みのフォルダーの URL
- フォルダーではなくファイルの URL
最低限、以下のような PowerShell で重複排除と簡単なフィルタリングを行っておくと安全です。
$rows = Import-Csv .\folders_raw.csv
$clean = $rows |
Where-Object { $_.Url -and $_.Url.StartsWith("https://<tenant>-my.sharepoint.com/") } |
Select-Object Url -Unique
$clean | Export-Csv .\folders.csv -NoTypeInformation -Encoding UTF8
ここでは、Url 列のみを残したシンプルな folders.csv を作成しています。以降のサンプルは、この列名を前提にしています。
方式A:PnP PowerShell で OneDrive の空フォルダーを CSV から一括削除(推奨)
PnP PowerShell を使うメリット
- SharePoint / OneDrive 専用のコマンドレット群が豊富で、スクリプトがシンプルになる
Remove-PnPFolderでフォルダー削除が正式サポートされている- Connect-PnPOnline の対話ログインにより、アカウント切り替えが容易
URL から「親フォルダー」と「フォルダー名」を取り出す考え方
PnP の Remove-PnPFolder は、次のように「親フォルダー」と「削除対象フォルダー名」を分けて指定します。
Remove-PnPFolder -Name <フォルダー名> -Folder <親フォルダー(サイト相対)> [-Recycle] [-Force]
一方、CSV 内の URL は通常「絶対 URL」です。そこで、URL を次のように段階的に分解します。
| 項目 | 例 | 説明 |
|---|---|---|
| Absolute URL | https://tenant-my.sharepoint.com/personal/user_domain_com/Documents/AAA/BBB | CSV に格納されている URL |
| ServerRelativeUrl | /personal/user_domain_com/Documents/AAA/BBB | [Uri]::new(Url).AbsolutePath の結果 |
| SiteRoot | /personal/user_domain_com | Get-PnPWeb の ServerRelativeUrl |
| SiteRelative | /Documents/AAA/BBB | ServerRelativeUrl から SiteRoot を取り除いたもの |
| 親フォルダー | Documents/AAA | SiteRelative のディレクトリ部分 |
| フォルダー名 | BBB | SiteRelative の末尾名 |
この「親フォルダー」と「フォルダー名」を Remove-PnPFolder に渡すことで、任意のフォルダーを削除できます。
ステップ1:PnP PowerShell のインストールと接続
# 1) PnP PowerShell のインストール(初回のみ)
Install-Module PnP.PowerShell -Scope CurrentUser
# 2) 対象ユーザーの OneDrive サイト URL
$siteUrl = "https://<tenant>-my.sharepoint.com/personal/<user>_<domain>_com"
# 3) 対話ログインで接続
Connect-PnPOnline -Url $siteUrl -Interactive
接続後、サイトのサーバー相対 URL(例:/personal/user_domain_com)を取得しておきます。
$web = Get-PnPWeb
$root = $web.ServerRelativeUrl.TrimEnd('/')
ステップ2:CSV の URL をもとにフォルダーを一括削除するスクリプト
もっともシンプルな「単一の OneDrive サイトに対して一括削除する」例です。
# 接続済みを前提($siteUrl, Connect-PnPOnline は前述)
$web = Get-PnPWeb
$root = $web.ServerRelativeUrl.TrimEnd('/')
# CSV を読み込み(列名 Url を想定)
$rows = Import-Csv .\folders.csv
foreach ($r in $rows) {
if (-not $r.Url) { continue }
# URL からパス部分を取得
$u = [Uri]$r.Url
$srv = $u.AbsolutePath # 例: /personal/.../Documents/AAA/BBB
# サイト相対パスに変換
$siteRel = if ($srv.StartsWith($root)) {
$srv.Substring($root.Length)
} else {
$srv
}
# 親フォルダーと末端フォルダー名に分割
$parent = [System.IO.Path]::GetDirectoryName($siteRel).TrimStart('\').Replace('\','/')
$name = [System.IO.Path]::GetFileName($siteRel)
# 日本語名・スペースを URL デコード
$parent = [System.Web.HttpUtility]::UrlDecode($parent)
$name = [System.Web.HttpUtility]::UrlDecode($name)
if ([string]::IsNullOrWhiteSpace($name)) { continue }
Write-Host "Deleting: '$parent/$name'" -ForegroundColor Yellow
# ごみ箱へ移動(安全策)。完全削除したい場合は -Recycle を外す
Remove-PnPFolder -Name $name -Folder $parent -Recycle -Force
}
最初は CSV の先頭 10~20 行だけを Select-Object -First 20 で試すなど、少量で動作確認することを強くおすすめします。
複数サイト・複数ユーザーの URL が混在する場合
監査ログの CSV には、チームサイトや複数ユーザーの OneDrive の URL が混在していることがあります。この場合、以下のように「サイト URL ごとにグルーピングして接続 → 削除」を繰り返す構成が安全です。
$rows = Import-Csv .\folders.csv | Where-Object { $_.Url }
# URL からサイト URL を推定してグループ化
$grouped = $rows | Group-Object {
$u = [Uri]$_.Url
$parts = $u.AbsolutePath.Trim('/') -split '/'
# OneDrive(/personal/...)もチームサイト(/sites/...)も
# 「スキーム + ホスト + 最初の2セグメント」をサイト URL として扱う簡易ロジック
"$($u.Scheme)://$($u.Host)/$($parts[0])/$($parts[1])"
}
foreach ($g in $grouped) {
$siteUrl = $g.Name
Write-Host "=== Site: $siteUrl ===" -ForegroundColor Cyan
Connect-PnPOnline -Url $siteUrl -Interactive
$web = Get-PnPWeb
$root = $web.ServerRelativeUrl.TrimEnd('/')
foreach ($r in $g.Group) {
$u = [Uri]$r.Url
$srv = $u.AbsolutePath
$siteRel = if ($srv.StartsWith($root)) {
$srv.Substring($root.Length)
} else {
$srv
}
$parent = [System.IO.Path]::GetDirectoryName($siteRel).TrimStart('\').Replace('\','/')
$name = [System.IO.Path]::GetFileName($siteRel)
$parent = [System.Web.HttpUtility]::UrlDecode($parent)
$name = [System.Web.HttpUtility]::UrlDecode($name)
if ([string]::IsNullOrWhiteSpace($name)) { continue }
Write-Host " Deleting: '$parent/$name'" -ForegroundColor Yellow
Remove-PnPFolder -Name $name -Folder $parent -Recycle -Force
}
}
環境によってはルートサイト(/)や /teams/ パスなども存在するため、実際には自社テナントの URL パターンに合わせて条件分岐を追加することをおすすめします。
「空フォルダーだけ」を消したい場合の追加チェック(PnP)
フォルダーが本当に空かどうかを削除前に確認したい場合、PnP PowerShell では「フォルダーの子アイテム数」を見て判断することができます。PnP 公式サンプルにも、ドキュメントライブラリ内の空フォルダーを特定して削除するスクリプトが紹介されています。
一例として、削除前に Get-PnPFolder を呼び出して子数を確認するロジックを入れると、安全性が高まります。
# $parent, $name を求めたあとで追加する例
$folderUrl = ($parent.TrimEnd('/') + "/" + $name).TrimStart('/')
# 必要に応じて -Includes でプロパティを指定
$folder = Get-PnPFolder -Url $folderUrl -Includes ListItemAllFields
# プロパティは環境により異なるため、まずは中身を確認する
# $folder.ListItemAllFields | Format-List *
# ここでは例として「子アイテム数が 0 なら空」とみなす
if ($folder.ListItemAllFields["ItemChildCount"] -eq 0 -and
$folder.ListItemAllFields["FolderChildCount"] -eq 0) {
Write-Host " Empty folder, deleting: $folderUrl" -ForegroundColor Yellow
Remove-PnPFolder -Name $name -Folder $parent -Recycle -Force
}
else {
Write-Host " Not empty, skip: $folderUrl" -ForegroundColor DarkGray
}
プロパティ名や取得方法はサイト テンプレートやバージョンにより多少異なるため、実際には自環境で Format-List * などで確認しながら調整してください。
方式B:Microsoft Graph(REST / Graph PowerShell)で一括削除
Graph を使うメリットと注意点
Microsoft Graph は、OneDrive / SharePoint のファイルやフォルダーを統一的に扱える REST API です。driveItem を ID またはパスで指定して削除することができ、標準ではごみ箱に移動されます。
さらに、JSON バッチ機能を使用すると、最大 20 件のリクエストを 1 回の HTTP 呼び出しにまとめられます。 ただし、後述のスロットリング(429)対策が必須です。
driveItem 削除 API の概要
| 操作 | HTTP | 説明 |
|---|---|---|
| ごみ箱に移動 | DELETE /drives/{drive-id}/items/{item-id}または DELETE /drives/{drive-id}/root:/{item-path} | フォルダー/ファイルを削除し、ごみ箱に移動する。復元可能。 |
| 完全削除 | POST /drives/{drive-id}/items/{item-id}/permanentDelete | ごみ箱を経由せず完全削除する API。復元不可のため、本番利用時は慎重に。 |
安全のため、本記事では基本的に「削除=ごみ箱に移動」する DELETE を前提とします。
準備:Graph PowerShell SDK への接続
Install-Module Microsoft.Graph -Scope CurrentUser
# 必要なスコープは環境に合わせて追加
Connect-MgGraph -Scopes "Files.ReadWrite.All","Sites.ReadWrite.All"
Select-MgProfile -Name "v1.0"
アプリ権限を使う場合は、Azure AD / Entra ID でアプリ登録し、同等のスコープを「アプリケーションのアクセス許可」として付与します。
OneDrive(特定ユーザー)のフォルダーを URL から削除する例
Purview 監査ログの URL が、特定ユーザーの OneDrive に紐づいている場合、次のように「ユーザーの UPN とパス」を使って driveItem を特定し、削除できます。
$upn = "[email protected]"
$rows = Import-Csv .\folders.csv
foreach ($r in $rows) {
if (-not $r.Url) { continue }
$u = [Uri]$r.Url
# OneDrive 以外(チームサイトなど)は別処理に回す
if ($u.Host -notlike "*-my.sharepoint.com") {
Write-Host "Skip (not OneDrive): $($r.Url)" -ForegroundColor DarkGray
continue
}
# /Documents/ より後ろを OneDrive 内のパスとして扱う
$pathAfterDocuments = ($u.AbsolutePath -split "/Documents/")[1]
if (-not $pathAfterDocuments) { continue }
$pathDecoded = [System.Web.HttpUtility]::UrlDecode($pathAfterDocuments)
# パス → driveItem(ID)に解決
$item = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$upn/drive/root:/$pathDecoded"
if (-not $item) {
Write-Host "Not found: $pathDecoded" -ForegroundColor DarkGray
continue
}
# 空フォルダー確認(任意)
if ($item.folder -and $item.folder.childCount -ne 0) {
Write-Host "Not empty, skip: $pathDecoded" -ForegroundColor DarkGray
continue
}
Write-Host "Deleting: $pathDecoded" -ForegroundColor Yellow
Invoke-MgGraphRequest -Method DELETE -Uri "https://graph.microsoft.com/v1.0/users/$upn/drive/items/$($item.id)"
}
ここでは REST 形式をそのまま Invoke-MgGraphRequest で呼び出していますが、Graph PowerShell SDK の Remove-MgDriveItem を使う形に置き換えることも可能です。
SharePoint サイト配下の URL を削除する例
監査ログにチームサイトの URL(https://contoso.sharepoint.com/sites/TeamA/Shared%20Documents/... など)が含まれている場合、次のように /sites/{hostname}:{site-path} 経由で drive を特定し、パスから driveItem を取得して削除します。
$rows = Import-Csv .\folders.csv
foreach ($r in $rows) {
if (-not $r.Url) { continue }
$u = [Uri]$r.Url
# OneDrive は別処理に
if ($u.Host -like "*-my.sharepoint.com") { continue }
# 例: https://contoso.sharepoint.com/sites/TeamA/Shared%20Documents/AAA/BBB
$host = $u.Host
$parts = $u.AbsolutePath.Trim('/') -split '/'
# /sites/TeamA までをサイトパスとみなす簡易例
$sitePath = "/$($parts[0])/$($parts[1])"
# サイト ID を取得
$site = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/sites/$host:$sitePath"
# サイト内のドキュメント ライブラリ(通常は 'Documents' あるいは 'Shared Documents')を取得
$drives = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/sites/$($site.id)/drives"
$docDrive = $drives.value | Where-Object { $_.name -eq "Documents" -or $_.name -eq "Shared Documents" } | Select-Object -First 1
if (-not $docDrive) {
Write-Host "No document library found for $($r.Url)" -ForegroundColor DarkGray
continue
}
# サイトパスより後ろの部分をライブラリ内のパスとして扱う
$afterSite = ($u.AbsolutePath -split "/$($parts[0])/$($parts[1])/")[1]
$afterLib = $afterSite -replace "^Shared%20Documents/",""
$pathDecoded = [System.Web.HttpUtility]::UrlDecode($afterLib)
# パス → driveItem
$item = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/drives/$($docDrive.id)/root:/$pathDecoded"
if (-not $item) {
Write-Host "Not found: $pathDecoded" -ForegroundColor DarkGray
continue
}
Write-Host "Deleting: $pathDecoded" -ForegroundColor Yellow
Invoke-MgGraphRequest -Method DELETE -Uri "https://graph.microsoft.com/v1.0/drives/$($docDrive.id)/items/$($item.id)"
}
ライブラリ表示名が「ドキュメント」などの場合は、$_ .name の条件を自環境に合わせて変更してください。
Graph での「空フォルダー判定」
driveItem の folder ファセットには childCount プロパティが含まれ、子アイテム数を取得できます。 先ほどの例のように ?select=id,name,folder を付与して取得し、folder.childCount -eq 0 だけ削除する、といったロジックを入れると安全性が高まります。
大量削除時のスロットリング(429)対策
Graph のスロットリングの基本
Microsoft Graph は、サービスの安定性を保つために、一定以上のリクエストを送ると 429 Too Many Requests を返してリクエストを抑制します。
また、JSON バッチの仕様として、1 バッチあたりに含められるサブリクエストは最大 20 件に制限されています。
実装時のベストプラクティス
- 1 バッチあたり最大 20 件を厳守する
- DELETE だけのバッチでも、連続で大量に投げない
- 429 が返ってきた場合は、レスポンスの
Retry-Afterヘッダーを読み取り、その秒数+アルファ待ってから再試行する - 指数バックオフ(1秒 → 2秒 → 4秒 → …)を採用し、一定回数で諦める
- サブリクエストの一部だけが 429 になるケースもあるため、「失敗分だけ再送」できる構造にする
バッチ API 自体は便利ですが、「速く終わらせるためにバッチ数を増やす → スロットリングでかえって遅くなる」という本末転倒になりがちです。実際の削除件数とテナント全体の負荷を考慮して、慎重に調整してください。
運用上の注意点とチェックリスト
ごみ箱と完全削除の使い分け
| 項目 | ごみ箱へ移動 | 完全削除 |
|---|---|---|
| PnP PowerShell | Remove-PnPFolder -Recycle でサイトのごみ箱へ移動 | -Recycle を付けない場合は完全削除(復元不可) |
| Graph | DELETE /drives/{id}/items/{id} でごみ箱へ移動 | POST /drives/{id}/items/{id}/permanentDelete で完全削除。慎重に利用。 |
誤削除リスクを考えると、まずは「ごみ箱行き」の方法で 1~2 週間様子を見る運用をおすすめします。その後、問題がなければごみ箱を空にする、という二段階構成にしておくと安心です。
本番実行前に確認したいこと
- 少数(10~20 件程度)の URL でテスト済みか
- 実行アカウントに必要な権限(サイト管理者、Graph スコープなど)が付与されているか
- フォルダー名に日本語やスペース、記号が含まれていても、URL デコード処理で正しく扱えているか
- ログ(どの URL を削除したか)を残すようにしているか
- バックアップ(保持ポリシーやごみ箱の保持期間等)を事前に把握しているか
ログ出力の簡易サンプル
どのフォルダーを削除したかを後から追跡できるよう、簡易的なログ CSV を出力しておくと便利です。
$log = @()
# 削除ループの中で(PnP 例)
$log += [pscustomobject]@{
Url = $r.Url
Parent = $parent
Name = $name
DeletedAt = (Get-Date)
Method = "PnP"
}
# 最後にログを書き出し
$log | Export-Csv .\deleted-folders-log.csv -NoTypeInformation -Encoding UTF8
実務でのおすすめ手順(まとめ)
- 監査ログ CSV の整理
重複 URL や明らかに不要な行を削除し、Url列のみのシンプルな CSV に整形します。 - PnP PowerShell で少数の検証を実施
対象 OneDrive サイトに接続し、本記事の PnP サンプルを「10~20 件 +-Recycle」で試運転します。 - 空フォルダー判定が必要ならロジックを追加
PnP のGet-PnPFolderや Graph のfolder.childCountを利用し、「本当に空か」を確認してから削除するようにします。 - 本番対象全体を PnP で削除
数サイト程度であれば、PnP のループ処理で十分に対応可能です。実行ログも必ず残しましょう。 - 多ユーザー・多サイトに広げる場合は Graph を検討
既に Graph ベースの自動化環境がある場合や、数万件単位で削除する必要があれば、Graph のバッチ+スロットリング対策を組み合わせたスクリプトに発展させます。 - ごみ箱の確認と最終クリーンアップ
数日~数週間モニタリングし、問題がないことを確認したうえでごみ箱をクリーンアップします。完全削除 API や-Recycle無し実行は、この段階で慎重に検討してください。
参考リンク
- Remove-PnPFolder | PnP PowerShell
- Delete a file or folder – Microsoft Graph v1.0
- Permanently delete a file or folder – Microsoft Graph v1.0
- Combine multiple HTTP requests using JSON batching
- Microsoft Graph throttling guidance
- Delete Empty Folders in SharePoint Document Library (PnP サンプル)
OneDrive の空フォルダー問題は、一度発生するとユーザー体験を大きく損ねるだけでなく、運用チームにとっても頭の痛い課題です。しかし、監査ログから取得した URL を上手に活用し、PnP PowerShell や Microsoft Graph を組み合わせれば、安全に、そして短時間でクリーンな状態に戻すことができます。この記事をベースに、自社テナントに合わせたスクリプトに発展させていってください。

コメント