PowerShellでファイルサーバーを棚卸しする ── 容量調査とアクセス権(ACL)監査

· 更新日: · · PowerShell, Windows, ファイルサーバー, ACL, アクセス権, 運用改善, セキュリティ, スクリプト

更新履歴(7件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
隔離スクリプトの台帳を2点直しました。1つは固定名`quarantine-log.csv`への書き出しで、隔離は部門ごと・四半期ごとに繰り返すため、2回目のバッチを流した瞬間に1回目の台帳が消えます。移したファイルは隔離先にあるのに、どこから来たかの記録がどこにも無くなり復旧できません。実行ごとに`-$runId`を付けた別ファイルにしました。もう1つは書き出しの位置で、ループを抜けてからまとめて出す作りだと、途中でセッションが切れた場合に移動済みのファイルが1件も記録されません。`-Append`で1件ずつ追記する形に変更しました。あわせて、権限を絞れなかったぶんも1件ごとに別CSVへ追記するようにしています。
隔離スクリプトが、移動元・移動先の範囲を確かめずに動かしていたのを直しました。承認済みCSVは人が編集するファイルで、`$root`の外を指す行が紛れ込み得ます。`$src.Substring($root.Length)`は文字数を数えて先頭を切り落とすだけなので例外を出さず、まったく無関係なファイルの移動先を組み立てます。同じ理由で、`..`を含む行は`Join-Path`が解決しないため隔離先の外を指し得ます。`Resolve-Path`で実体パスに直して`$root`の内側を確認し、移動先も`GetFullPath`で畳んでから`$quarantine`の内側かを確認するようにしました。比較用パスの末尾に`\`を足して、`D:\share2`が`D:\share`の内側と判定されるのも防いでいます。
隔離スクリプトの記録を2点直しました。1つは試走で、`$dryRun`のときも本実行と同じ`$moved`に積んで同じCSVへ書き出していたため、本実行のあとに試走しただけで、有効な台帳が「実在しない移動先」の一覧で上書きされる状態でした。試走は別のファイルに分けています。もう1つは、移動には成功したが`icacls /reset`に失敗した場合で、これを失敗として扱うと記録上は元の場所にあることになり、再実行しても元パスが無いので拾われません。隔離先に広い権限のまま、どの台帳にも載らずに残ります。移動が済んだものは必ず台帳に載せ、権限を絞れたかを列で持ち、絞れなかった分は別のCSVと警告で出すようにしました。
隔離の手順を2点直しました。1つは権限で、勧めている「同じボリュームへの移動」では元の権限がそのまま付いてくるため、隔離フォルダーを絞っていても中のファイルは元のまま読めます。移動の直後に`icacls /reset`で継承既定へ戻す形にしました。もう1つは台帳で、`Move-Item`のエラーは既定で非終了のため、ロックやアクセス拒否で失敗しても「移動済み」と書き込んでいました。`-ErrorAction Stop`で捕まえ、成功したものだけを台帳に、失敗は別のCSVに残します。試走も`-WhatIf`ではなくフラグで分け、台帳が試走用と本番用で混ざらないようにしました。
セキュリティ記述子・DACL・ACE・SID・SDDLの関係図と説明を追加しました。成果物4点の列構成の表、消す前に隔離する手順(別ボリュームへ移動するとACLが移動先の継承に置き換わる注意を含む)、エラー1219の概要、所要時間を代表フォルダーで実測して外挿する手順を追加しました。実測値は測っていないため、数値ではなく測り方を示しています。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21590160)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「PowerShellでファイルサーバーを棚卸しする ── 容量調査とアクセス権(ACL)監査」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21590160 https://staging.comcomponent.com/blog/powershell-file-server-acl-audit/

DOI(最新版)
10.5281/zenodo.21590160
DOI(この版)
10.5281/zenodo.21733101

「ファイルサーバーの空きが残り1割を切りました」── この警告から始まる仕事は、たいてい憂鬱です。どこが容量を食っているのか分からない。誰も触っていない10年前のフォルダーが消してよいのか判断できない。そもそも誰がどこにアクセスできるのか、退職者の権限が残っていないか、聞かれても即答できない。エクスプローラーでプロパティを開いて回る調査は、数テラバイトの共有相手では終わりません。

この手の依頼で手を動かすたびに思うのは、ファイルサーバーの整理は「削除の技術」ではなく「棚卸しの技術」だということです。数字と一覧がないまま「消してよいですか」と聞いても、部門は絶対に首を縦に振りません。逆に「このフォルダーだけで800GB、うち3年以上更新のないファイルが620GB」という表があれば、話は一気に前へ進みます。

この記事では、中小企業の情シス・運用担当者を対象に、PowerShellでファイルサーバーの容量とアクセス権(ACL)を棚卸しし、CSVレポートに落とすまでの実務手順を整理します。方針は一貫して「まず読み取り、変更は最後に、バックアップと-WhatIfつきで」です。

1. まず結論

  • 容量調査はGet-ChildItem -Recurse + Measure-Object -Sumが基本形です。いきなり全体を集計せず、第1階層のフォルダー単位で「太っている場所」の当たりを付けます。12
  • アクセス拒否は握りつぶさず記録します。-ErrorAction SilentlyContinueで走査を止めない代わりに、-ErrorVariableで拒否された場所を必ず控えます。数えられなかった場所は「ゼロ」ではなく「不明」です。3
  • 古いファイルの判定はLastWriteTime基準にします。NTFSの最終アクセス日時は既定で更新が無効化(またはシステム管理)されている環境が多く、判断材料として信頼できません。45
  • 重複ファイルはGet-FileHash(既定SHA256)で「候補」を抽出します。先にサイズで絞ってからハッシュを計算するのがI/O節約の定石です。6
  • ACLの棚卸しはGet-AclのAccessプロパティです。IdentityReference(誰)、FileSystemRights(何を)、IsInherited(継承か直接か)をCSVにし、AreAccessRulesProtectedで継承が切れた場所を検出します。78
  • この記事で棚卸しするのはNTFSのアクセス許可です。共有(SMB)経由の実効アクセスは、共有側のアクセス許可とNTFS ACLの組み合わせで決まります。共有側はGet-SmbShareAccessで別途一覧化し、NTFSの台帳とセットで見てください。9
  • CSVレポートの文字コードは明示します。Windows PowerShell 5.1のExport-Csv既定はASCIIで日本語が壊れ、PowerShell 7の既定はBOMなしUTF-8でExcelの既定動作と相性が悪いことがあります。1011
  • 変更(Set-Acl)は棚卸しと部門確認が済んでから。icacls /saveでバックアップを取り、-WhatIfで対象を確認してから適用します。1213

この記事の手順を一通り流すと、成果物として次の4点が手元に残ります。先に完成形を示しておきます。

成果物 作る章 何が読めるか
フォルダー別の容量ランキング 第2章 Folder / SizeGB / FileCount どのフォルダーが太っているか。「共有全体で◯GB、うち営業部が◯GB」という言い方に必要な数字はここから出ます
old-files.csv 第3章 FullName / LastWriteTime / SizeMB 3年以上更新のないファイルの一覧。SizeMBの降順なので、上から数十行で「効く容量」が見えます
duplicate-candidates.csv 第3章 Hash / Path 内容が同一のファイル群。Hash列でソートすると同じ内容のものが固まって並びます
acl-report.csv 第4章 Path / Identity / Rights / Type / IsInherited / Inheritance / InheritanceBroken 誰がどこに何の権限を持っているかの台帳。ACE1件が1行です

これに加えて、走査できなかった場所を記録するテキスト(denied-paths.txt、old-files-uninspected.txt、hash-uninspected.txt、acl-uninspected.txt)が出ます。これらが空でないなら、上の4点は「穴のあるレポート」です。その旨を添えて渡してください。

冒頭に書いた「このフォルダーだけで800GB、うち3年以上更新のないファイルが620GB」という一文は、1つ目と2つ目を突き合わせれば作れます。後半の数字は、old-files.csvを読み直して合計するだけです。

# old-files.csv から「3年以上更新のないファイルの合計容量」を出す
$old   = Import-Csv .\old-files.csv
$sumMB = ($old | ForEach-Object { [double]$_.SizeMB } | Measure-Object -Sum).Sum
'3年以上更新なし: {0} 件 / {1:N1} GB' -f $old.Count, ($sumMB / 1024)

この記事の知識マップ

ファイルサーバーの棚卸しは、Get-ChildItemとMeasure-Objectによる容量集計から始まり、アクセス拒否やMAX_PATH超過で走査から漏れた場所は「ゼロ」ではなく「不明」として必ず記録します。古いファイルの判定は更新が無効化されがちなLastAccessTimeではなくLastWriteTimeを基準にし、重複候補はGet-FileHashで抽出します。アクセス権はセキュリティ記述子の中のDACL、その中のACEという入れ子構造をGet-Aclで取得して台帳化し、調査にはGet-Acl、定型的な変更にはバックアップと復元ができるicacls、複雑な条件付き変更にはSet-Aclを使い分けます。削除前には隔離フォルダーへ移動しますが、同一ボリューム内の移動ではアクセス許可が継承されず元のまま残る例外があるため、icaclsで継承を戻す手順が欠かせません。

PowerShellのファイルサーバー棚卸し(容量・ACL監査)の知識マップGet-ChildItemとMeasure-Objectによる容量調査、LastWriteTimeを基準とした古いファイル判定、Get-FileHashによる重複候補抽出、セキュリティ記述子・DACL・ACEの構造とGet-Acl・icacls・Set-Aclの使い分け、隔離時のアクセス許可継承の落とし穴の関係を示す図利用する利用するより先に行うべき原因になり得る原因になり得る用いるのは非推奨推奨される対応前提とするで構成できる利用するで確認できるに保存されるに保存されるで構成できるで構成できる推奨される対応推奨される対応推奨される対応前提とする前提とする原因になり得る用いるのは非推奨で確認できるGet-ChildItemGet-AclPowerShellMeasure-Object走査漏れリスクMAX_PATH(パス長260文字)制限LastAccessTime(最終アクセス日時)古いファイル(未使用ファイル)の判定LastWriteTime(最終更新日時)fsutilGet-FileHashセキュリティ記述子DACL(随意アクセス制御リスト)ACE(アクセス制御エントリ)icaclsSet-AclACLの棚卸し・レポート化ACLの変更作業隔離→観察→削除の運用移動時のアクセス許可継承UNCパスエラー1219(ERROR_SESSION_CREDENTIAL_CONFLICT)

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全23件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. 容量調査 ── どこが太っているのかを数字にする

最初にやることは、共有ルート直下のフォルダーごとの合計サイズです。Get-ChildItemで再帰的にファイルを列挙し、Measure-ObjectでLengthを合計します。12

$root = 'D:\share'   # ファイルサーバー上で実行する想定。UNC越しの注意は第6章
$denied = @()

# 第1階層フォルダー単位で集計する ── 全体一括より「当たりを付ける」のが先
# ルート自体の列挙失敗も$deniedに入るよう、外側のGet-ChildItemにも同じ指定を付ける
$report = foreach ($dir in Get-ChildItem -LiteralPath $root -Directory `
    -ErrorAction SilentlyContinue -ErrorVariable +denied) {
    # アクセス拒否で止まらないようSilentlyContinue、ただし拒否は$deniedに蓄積する
    # 列挙はパイプで直接Measure-Objectへ流す(変数に受けるとFileInfo全件をメモリに
    # 保持することになり、ファイル数百万件級のフォルダーで苦しくなる)
    $stats = Get-ChildItem -LiteralPath $dir.FullName -Recurse -File `
        -ErrorAction SilentlyContinue -ErrorVariable +denied |
        Measure-Object -Property Length -Sum
    [PSCustomObject]@{
        Folder    = $dir.Name
        SizeGB    = [math]::Round([double]$stats.Sum / 1GB, 2)
        FileCount = $stats.Count
    }
}
# ルート直下に直接置かれたファイルも1行として数える(フォルダー単位の集計から漏れるため)
$rootStats = Get-ChildItem -LiteralPath $root -File `
    -ErrorAction SilentlyContinue -ErrorVariable +denied |
    Measure-Object -Property Length -Sum
if ($rootStats.Count -gt 0) {
    $report += [PSCustomObject]@{
        Folder    = '(ルート直下)'
        SizeGB    = [math]::Round([double]$rootStats.Sum / 1GB, 2)
        FileCount = $rootStats.Count
    }
}

$report | Sort-Object SizeGB -Descending | Format-Table -AutoSize

# 「数えられなかった場所」を必ず残す ── ここが空でないなら集計は不完全
$denied | ForEach-Object { $_.TargetObject } | Sort-Object -Unique |
    Set-Content -Path .\denied-paths.txt

ポイントは2つです。第一に、-ErrorAction SilentlyContinueは「エラーを見なかったことにする」スイッチではありません。表示を抑えて続行するだけで、エラー自体は発生しています。3 だから-ErrorVariableで受け止めます。変数名の前に+を付けると上書きではなく追記になるため、ループ全体の拒否を1つの変数に集められます。3 アクセス拒否されたフォルダーは集計から漏れており、レポート上は実際より小さく見えます。denied-paths.txtが空でないなら、その旨を必ずレポートに添えてください。エラー処理の設計をもう一段深くやるなら、同時公開の「PowerShellのエラー処理と再実行設計」が参考になります。

第二に、深い階層の問題です。長年運用された共有には、パスが260文字(MAX_PATH)を超える場所がほぼ確実にあり、ツールによってはそこで列挙が失敗します。14 走査結果に不自然な欠けがあるときは、まずパス長を疑ってください。この制限の全体像と対処は「MAX_PATHとWindowsのパス・ファイル名の落とし穴」にまとめています。

3. 古いファイルと重複候補 ── 削除ではなく一覧化から

3.1. 「3年触っていないファイル」を一覧にする

太っている場所が分かったら、次は「消せる候補」の材料集めです。基準に使うのはLastWriteTime(最終更新日時)です。

$cutoff = (Get-Date).AddYears(-3)

# 3年以上更新のないファイルの一覧。削除はしない ── まず部門に見せる材料を作る
# 列挙に失敗した場所も後で開示するため-ErrorVariableで記録する
Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable oldEnumErrors |
    Where-Object LastWriteTime -lt $cutoff |
    Select-Object FullName, LastWriteTime,
        @{ Name = 'SizeMB'; Expression = { [math]::Round($_.Length / 1MB, 2) } } |
    Sort-Object SizeMB -Descending |
    Export-Csv -Path .\old-files.csv -NoTypeInformation -Encoding utf8BOM   # 5.1ならUTF8(どちらもBOM付き)

# 列挙できなかった場所は一覧が「完全に見える」のを防ぐため必ず開示する
$oldEnumErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\old-files-uninspected.txt

「最終アクセス日時(LastAccessTime)を使えば『読まれてもいない』ことまで分かるのでは」と考えたくなりますが、これは罠です。NTFSでは最終アクセス日時の更新が性能に影響するため、fsutil behaviorコマンドとレジストリ(NtfsDisableLastAccessUpdate)で更新の有効/無効が制御されており4、Windows Vista以降は既定で無効(近年のWindows 10以降はシステム管理で、サーバーでは無効)とされています。5 つまり実際には開かれているのに日時が古いままというファイルが普通に存在します。棚卸しの説明資料には「最後に内容が変更された日」であるLastWriteTimeを使い、その意味も明記しておくと、部門との会話がこじれません。

3.2. 重複ファイルの「候補」を挙げる

共有フォルダーには「最終版」「最終版_修正」「コピー 〜」が大量に堆積します。内容が同一かどうかはGet-FileHashで判定できます。既定のアルゴリズムはSHA256で、ハッシュ値が一致すればファイル内容は同一と判断できます。6

# 全ファイルのハッシュ計算はI/Oが重い。先に「同じサイズのファイル」だけに絞る
# 列挙に失敗した場所(アクセス拒否など)も後で開示するため-ErrorVariableで記録する
$candidates = Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable enumErrors |
    Group-Object -Property Length |
    Where-Object { $_.Count -ge 2 -and [long]$_.Name -gt 0 } |   # サイズ0は除外
    ForEach-Object { $_.Group }

# 絞った候補だけハッシュを計算し、一致するグループを重複「候補」として出力
$candidates |
    Get-FileHash -ErrorAction SilentlyContinue -ErrorVariable hashErrors |   # 既定はSHA256
    Group-Object -Property Hash |
    Where-Object Count -ge 2 |
    ForEach-Object { $_.Group } |
    Select-Object Hash, Path |
    Export-Csv -Path .\duplicate-candidates.csv -NoTypeInformation -Encoding utf8BOM   # 5.1ならUTF8

# 列挙できなかったフォルダーと、ロック中・読み取り権限なしなどでハッシュを
# 計算できなかったファイルは「調べられなかった」として必ず一覧に残す
# (黙って落とすと重複ゼロと区別できない)
@($enumErrors) + @($hashErrors) | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\hash-uninspected.txt

あえて「候補」と呼んでいるのは、どちらを残すかの判断が技術ではなく業務の問題だからです。同一内容でも「部門Aの正本」と「部門Bの参照コピー」は意味が違うことがあります。機械的に消さず、一覧を持って関係者と話すところまでがこのスクリプトの守備範囲です。ハッシュという道具自体の性質(何が保証され、何が保証されないか)は「ハッシュ文字列から方式を見分ける実務手順」で整理しています。

3.3. 消す前に隔離する ── 「隔離 → 観察 → 削除」の手順

一覧を部門に確認してもらっても、いきなり削除には進みません。第7章の判断表に書いた「隔離→観察→削除」の中身は、こういう手順です。

  1. 隔離先を決める。同じサーバー上に日付入りのフォルダーを1つ作ります。できれば元と同じボリュームにしてください(理由は後述)。権限は管理者と担当者だけに絞ります。ただし、フォルダーの権限を絞るだけでは中のファイルに効きません。同じボリューム内の移動では元の権限がそのまま付いてくるためで、移した各ファイルを継承既定へ戻す手順まで含めて1セットです(これも後述)。
  2. 元の階層を保ったまま移す。フラットに移すと同名ファイルが衝突し、戻すときにも元の場所が分からなくなります。
  3. 移動の台帳を残す。元パスと移動先パスの対応表がないと、「あのファイルどこへ行った?」に答えられません。台帳は実際に動いたものだけを書きます。試走の結果と、動かせなかったものは別のファイルに分けてください。混ぜると、復旧のときに実在しない場所を見に行くことになります。
  4. 観察期間を決める。「1か月」ではなく「四半期の締めを1回またぐまで」のように、業務の周期に合わせるのが実務的です。問い合わせが来たら台帳を見て戻します。
  5. 期間を過ぎたら削除する。削除の直前に、台帳をもう一度関係部門へ共有します。
# 部門確認が済んだ一覧(old-files.csvから対象行だけ残したもの)を隔離フォルダーへ移す
$root       = 'D:\share'
$quarantine = 'D:\quarantine\2026-07'
$list       = Import-Csv .\old-files-approved.csv   # 列はold-files.csvと同じ
$dryRun     = $true       # まず $true で対象を確認し、問題なければ $false にして本実行する
$planned    = @()   # 試走の結果。本実行の台帳とは別のファイルへ出す
$moved      = @()   # 実際に移動できたもの(件数の集計用。台帳の実体は下のファイル)
$failed     = @()   # 移動していないもの。元の場所にある
$needsAcl   = @()   # 移動できたが権限を絞れなかったもの

# 出力ファイルは実行ごとに別名にする。固定名にすると、次のバッチを流した瞬間に
# 前回の台帳が上書きされ、そのとき移したファイルを戻せなくなる
$runId    = Get-Date -Format 'yyyyMMdd-HHmmss'
$ledger   = ".\quarantine-log-$runId.csv"        # 復旧用の台帳。移動のたびに追記する
$planFile = ".\quarantine-plan-$runId.csv"
$failFile = ".\quarantine-failed-$runId.csv"
$aclFile  = ".\quarantine-needs-acl-$runId.csv"
$enc      = 'utf8BOM'                            # 5.1なら 'UTF8'

# 範囲の判定に使う絶対パス。末尾を \ で揃えておくと、
# D:\share2 が D:\share の内側だと誤判定されるのを防げる
$rootFull       = [System.IO.Path]::GetFullPath($root).TrimEnd('\') + '\'
$quarantineFull = [System.IO.Path]::GetFullPath($quarantine).TrimEnd('\') + '\'

foreach ($row in $list) {
    $src = $row.FullName
    if (-not (Test-Path -LiteralPath $src)) { continue }   # 既に無いものは飛ばす

    # このCSVは人が編集したもので、$root の外を指す行が紛れ込み得る。
    # 下の Substring はその場合も黙って動き、まったく別の場所のファイルを隔離する。
    # 実体パスへ直したうえで、監査対象の内側にあることを確かめてから進む
    $srcFull = (Resolve-Path -LiteralPath $src).ProviderPath
    if (-not $srcFull.StartsWith($rootFull, [StringComparison]::OrdinalIgnoreCase)) {
        $failed += [PSCustomObject]@{ Source = $src; Destination = ''
                                      Error  = "監査対象($root)の外を指しています" }
        continue
    }

    # 元の階層を保ったまま移す(同名ファイルの衝突と「戻せない」を防ぐ)
    $relative = $srcFull.Substring($rootFull.Length)
    # Join-Path は '..' を解決しない。GetFullPath で畳んでから、
    # 隔離先の内側に収まっているかを確かめる
    $dest     = [System.IO.Path]::GetFullPath((Join-Path $quarantine $relative))
    if (-not $dest.StartsWith($quarantineFull, [StringComparison]::OrdinalIgnoreCase)) {
        $failed += [PSCustomObject]@{ Source = $src; Destination = $dest
                                      Error  = "隔離先($quarantine)の外へ出ます" }
        continue
    }

    if ($dryRun) {
        # 試走の行を $moved に混ぜてはいけない。同じ台帳へ書き出すと、
        # 本実行後にもう一度試走しただけで、有効な台帳が
        # 「実在しない移動先」の一覧で上書きされる
        $planned += [PSCustomObject]@{ Source = $src; Destination = $dest }
        continue
    }

    try {
        New-Item -ItemType Directory -Path (Split-Path -Parent $dest) -Force -ErrorAction Stop | Out-Null

        # Move-Item のエラーは既定で非終了。-ErrorAction Stop を付けないと、
        # ロック・アクセス拒否・衝突で失敗しても次の行へ進み、
        # 「元の場所に残っているのに台帳には移動済みと書く」ことになる
        Move-Item -LiteralPath $srcFull -Destination $dest -ErrorAction Stop
    }
    catch {
        # ここで失敗したファイルは動いていない。元の場所にある
        $failed += [PSCustomObject]@{ Source = $src; Destination = $dest; Error = $_.Exception.Message }
        continue
    }

    # ここから先、ファイルは $dest にある。この後で何が失敗しても、
    # 「$dest にある」ことは台帳に必ず残す。ここを failed 側へ入れると、
    # 記録は「元の場所」と読めるのに実物は隔離先、という最悪の食い違いになり、
    # 再実行しても Test-Path $src が偽なので二度と拾われない。
    #
    # そして、ループを抜けてからまとめて書き出してはいけない。途中で電源が落ちれば、
    # 移動済みのファイルが1件も記録されていない状態になる。1件ごとに追記する
    $record = [PSCustomObject]@{ Source = $srcFull; Destination = $dest; MovedAt = Get-Date }
    $record | Export-Csv -Path $ledger -NoTypeInformation -Encoding $enc -Append
    $moved += $record

    # 同じボリューム内の移動では元の権限がそのまま付いてくる(後述)。
    # 継承既定へ戻して、隔離フォルダーの権限だけが効く状態にする
    try {
        & icacls $dest /reset /q
        if ($LASTEXITCODE -ne 0) { throw "icacls /reset に失敗しました (exit $LASTEXITCODE)" }
    }
    catch {
        # 移動はできたが権限を絞れていない。隔離先に元の広い権限のまま置かれている。
        # これも1件ごとに追記する
        $aclRow = [PSCustomObject]@{ Destination = $dest; Error = $_.Exception.Message }
        $aclRow | Export-Csv -Path $aclFile -NoTypeInformation -Encoding $enc -Append
        $needsAcl += $aclRow
    }
}

if ($dryRun) {
    $planned | Export-Csv -Path $planFile -NoTypeInformation -Encoding $enc
    Write-Host "試走: $($planned.Count) 件が対象です。移動はしていません ($planFile)"
    return
}

$failed | Export-Csv -Path $failFile -NoTypeInformation -Encoding $enc

# 権限を絞れなかったぶんは黙って流さない。ここが0になるまで観察期間を始めない
if ($needsAcl.Count -gt 0) {
    Write-Warning "権限を絞れていない隔離ファイルが $($needsAcl.Count) 件あります。$aclFile を見て対処してください"
}
Write-Host "移動 $($moved.Count) 件 / 失敗 $($failed.Count) 件 / 権限未処理 $($needsAcl.Count) 件"
Write-Host "台帳: $ledger"

移す前に、そのパスが本当に監査対象の内側かを確かめてください。このスクリプトが読むのは old-files-approved.csv ── 人が開いて行を削り、ときには手で書き足すファイルです。ここに $root の外を指す FullName が1行紛れ込んでも、$src.Substring($root.Length) は例外を出しません。文字数を数えて先頭を切り落とすだけなので、まったく無関係なファイルの移動先を平然と組み立てます。古いCSVを使い回した、パスをコピーし損ねた、対象を「ついでに」書き足した ── どれも起きます。同じ理由で、Join-Path.. を解決しないので、相対区切りを含む行は隔離フォルダーの外を指す $dest になり得ます。

そこで、Resolve-Path で実体パスに直してから $root の内側にあることを確かめ、$destGetFullPath で畳んでから $quarantine の内側に収まっているかを見ています。比較用のパスの末尾に \ を足しているのは、D:\share2D:\share の内側だと判定されるのを防ぐためです。範囲外の行は移動せず $failed に落として、CSV側を直してもらいます。この2つの確認は、隔離作業が「対象を決めてから動かす」ものであることを、コードの側でも担保するためのものです。

スクリプトの中で icacls /reset を挟んでいるのは、権限の都合です。公式ドキュメントに明記されているとおり、オブジェクトは既定で移動先の親から権限を継承しますが、唯一の例外が「同じボリューム内の別フォルダーへの移動」で、この場合だけ元の権限が保持されます。15

つまり、手順1で勧めた「元と同じボリュームへ移す」形は、隔離フォルダーの権限が中のファイルに効かない形でもあります。部門の共有グループに開いていたファイルは、隔離フォルダーを管理者だけに絞っていても、移した後もそのグループから読めたままです。「消してはいないが、隔離したことになっていない」という、この作業で一番まずい失敗がここで起きます。icacls <path> /reset既定の継承ACLでACLを置き換えるので、これを移動の直後に通せば、隔離フォルダーの権限だけが効く状態になります。13

別ボリューム(別ドライブ・別サーバー)へ移した場合は、その時点で移動先フォルダーの権限を継承します。15 この場合は /reset は要りませんが、隔離フォルダー自体の権限を先に絞っておくことが前提になります。どちらの経路でも、移動後に数件をGet-Aclで抜き取り、想定どおりの権限になっているか確認しておくと確実です。

移動できたのに /reset に失敗した分の扱いにも注意してください。このとき、ファイルは既に隔離先へ移っていて、権限だけが元の広いままです。これを「失敗」として台帳から外すと、記録上は元の場所にあることになり、しかも再実行しても元パスが存在しないので拾われません。隔離先に、広い権限のまま、誰の台帳にも載らずに残るという一番まずい形です。上のスクリプトが、移動が済んだ時点で台帳に載せ、権限を絞れなかったぶんを別のCSVへ出しているのはこのためです。件数が0でないなら、そこを片付けるまで観察期間を始めないでください。

台帳のファイル名を固定しないでください。隔離は一度で終わりません。部門ごと、四半期ごとに何度も回します。固定名の quarantine-log.csv へ書き出す作りだと、2回目のバッチを流した瞬間に1回目の台帳が消えます。そのとき移したファイルは隔離先にあるのに、どこから来たかを知る記録がどこにも無い ── 復旧できません。上のスクリプトが実行ごとに -$runId を付けているのはこのためです。戻すときは Import-Csv .\quarantine-log-*.csv で全部読みます。

台帳は1件ごとに追記します。ループを抜けてからまとめて Export-Csv する作りだと、途中でセッションが切れた・サーバーが再起動した場合に、移動済みのファイルが1件も記録されていない状態になります。移動は実際に起きているので、これは「作業前」ではなく「記録だけ無い」状態です。-Append で1件ずつ書けば、どこで止まってもそこまでは戻せます。ファイルを開き直すぶん遅くなりますが、台帳は速度より確実さが要るところです。

スクリプトが出すファイルは、役割ごとに分かれています($runId20260718-143052 のような実行時刻)。

ファイル 中身 いつ出るか
quarantine-plan-$runId.csv これから動かす予定の一覧 試走($dryRun = $true)のとき
quarantine-log-$runId.csv 実際に動いたもの。戻すときに使う台帳。1件ずつ追記される 本実行のとき
quarantine-failed-$runId.csv 動かせなかったもの。元の場所にある 本実行のとき
quarantine-needs-acl-$runId.csv 動いたが権限を絞れなかったもの 該当があるときだけ

4. アクセス権(ACL)の棚卸し ── 誰が何をできるかを表にする

容量と並ぶもう一つの棚卸し対象が権限です。ここから用語が一気に増えるので、先に関係を整理しておきます。

同じ中身の別表現セキュリティ記述子ファイル/フォルダーごとに付いているGet-Aclが取ってくるのはこれ所有者SIDで記録されるDACL誰に何を許可/拒否するかの一覧Get-AclのAccessプロパティで見えるのがここSACL監査の設定。この記事では扱わないACE ── DACLの1行IdentityReference: 誰が(SIDまたは解決した名前)FileSystemRights: 何ができるかAccessControlType: 許可か拒否かIsInherited: 継承か直接付与かACEACE ...SDDLこの記述子ぜんぶを1本の文字列で表した形式

言葉にすると、セキュリティ記述子の中にDACLがあり、DACLの中にACEが並んでいて、ACEが指す相手はSID、という入れ子です。実務で「ACL」と呼んでいるものは、ほぼこのDACLを指しています。この後に出てくる表もCSVも、一番内側のACEを1行ずつ書き出したものだと思って読んでください。SDDLは同じ内容の文字列表現で、棚卸しの段階では読む必要がありません。

その前提で本題です。Get-Aclはファイルやフォルダーのセキュリティ記述子を取得し、AccessプロパティからDACLのアクセス制御エントリ(ACE)一覧を読み取れます。7

# 共有ルートから2階層分のフォルダーのACLを棚卸しする
# 権限設計の「幹」は浅い階層に集中しているので、まずそこを確認する
$aclErrors = @()   # +付き-ErrorVariableは追記のため、前回実行分を持ち越さないよう毎回初期化する
$targets = @(Get-Item -LiteralPath $root `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors) +
           @(Get-ChildItem -LiteralPath $root -Directory -Recurse -Depth 1 `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors)

$aclReport = foreach ($t in $targets) {
    # 取得失敗(読み取り不可・走査中の削除など)も後で開示できるよう記録する
    $acl = Get-Acl -LiteralPath $t.FullName -ErrorAction SilentlyContinue -ErrorVariable +aclErrors
    if (-not $acl) { continue }
    if (@($acl.Access).Count -eq 0) {
        # DACLが空(明示エントリゼロ)のフォルダーも行として残す。継承切れの情報が台帳から消えないように
        [PSCustomObject]@{
            Path = $t.FullName; Identity = '(エントリなし)'; Rights = $null; Type = $null
            IsInherited = $null; Inheritance = $null
            InheritanceBroken = $acl.AreAccessRulesProtected
        }
        continue
    }
    foreach ($ace in $acl.Access) {
        [PSCustomObject]@{
            Path        = $t.FullName
            Identity    = $ace.IdentityReference    # 誰(ユーザー/グループ)
            Rights      = $ace.FileSystemRights     # 何ができるか
            Type        = $ace.AccessControlType    # Allow / Deny
            IsInherited = $ace.IsInherited          # 親からの継承か、直接付与か
            # 同じ権限でも「このフォルダーだけ」か「配下のファイル/フォルダーにも及ぶ」かを区別する
            Inheritance = "$($ace.InheritanceFlags)/$($ace.PropagationFlags)"
            InheritanceBroken = $acl.AreAccessRulesProtected  # このフォルダーで継承を切っているか
        }
    }
}
$aclReport | Export-Csv -Path .\acl-report.csv -NoTypeInformation -Encoding utf8BOM   # 5.1ならUTF8

# ACLを取得できなかった対象も台帳の穴として必ず開示する(他の調査と同じ流儀)
$aclErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\acl-uninspected.txt

このCSVを開いたら、次の観点でフィルタして異常を探します。

見るべき列 異常のサイン 典型的な背景
Identity 個人ユーザー名が直接載っている 「とりあえずこの人に権限を」の積み重ね。異動・退職で腐る
Identity S-1-5-21-...のようなSIDのまま表示される 削除済みアカウントの権限が残っている(掃除の第一候補)
IsInherited = False 深い階層に直接付与のACEが点在する その場しのぎの個別付与。設計から外れた例外
InheritanceBroken = True 継承が切られたフォルダー 過去の「このフォルダーだけ見せたくない」対応。棚卸しの重点対象
Type = Deny 拒否ACEがある 同じ条件なら拒否が許可に優先する。ただし明示ACEは継承ACEより先に評価されるため、明示の許可が継承の拒否に勝つ並びもある。影響はACEの由来まで見て、最終的には実効アクセスで確認する

IsInheritedがFalseのエントリは「そのフォルダーで誰かが手作業で足した権限」であり、AreAccessRulesProtectedがTrueのフォルダーは「親の権限変更が届かない島」です。78 権限トラブルの多くはこの2つに集中するので、まずここから台帳化します。ひとつ限界も明記しておきます。この例は-Depth 1の浅い走査なので、それより深い階層で継承を切ったフォルダーは対象に入らず、エラーも出ません。継承切れを全数調査したくなったら、時間はかかりますが-Depthの制限を外して同じスクリプトを流すか、疑いのある部門フォルダーをルートに指定して再実行してください。なおGet-AclはSDDLという文字列形式でもセキュリティ記述子を表示できますが7、棚卸し目的ならAccessプロパティの表で十分です。SDDLの読解に踏み込むのは、この表で説明できない現象に出会ってからで遅くありません。

ひとつ実務的な注意を。CSVの文字コードは必ず明示してください。Windows PowerShell 5.1のExport-Csvは既定でASCII出力のため日本語が壊れます。10 PowerShell 7系の既定はutf8NoBOM(BOMなしUTF-8)で11、Excelでそのまま開くと文字化けする環境があります。部門に渡すファイルなら-Encoding UTF8BOM(5.1では-Encoding UTF8)のようにBOM付きUTF-8へ寄せるのが無難です。

5. 変更はどうやるか ── icaclsとSet-Aclの使い分け、バックアップと-WhatIf

棚卸しと部門確認が終わって、初めて変更のフェーズです。ここで道具が2系統あります。

用途 道具 理由
調査・レポート Get-Acl 結果がオブジェクトなのでフィルタ・CSV化が容易7
変更前バックアップ icacls /save ACLをファイルに保存し、/restoreでそのまま戻せる13
定型的な権限付与・削除 icacls /grant, /remove 1行で完結し、/tで再帰適用、継承の再有効化(/inheritancelevel)も可能13
複雑な条件付き変更 Set-Acl ルールをコードで組み立てられる。-WhatIf対応12
壊れたACLの立て直し icacls /reset 既定の継承ACLに置き換える(影響大。最後の手段)13

どちらを使うにしても、順序は固定です。バックアップ → -WhatIf(または対象確認) → 適用 → 事後確認。

# 1. 変更前にACLをファイルへ保存する(/tで配下すべて、/cでエラーがあっても継続)
icacls "D:\share\sales" /save "C:\aclbackup\sales-acl.txt" /t /c
# バックアップが取れていないのに変更へ進まない。icaclsの成否は終了コードで確認する
if ($LASTEXITCODE -ne 0) {
    throw "ACLバックアップに失敗しました (icacls ExitCode=$LASTEXITCODE)。変更作業を中止します"
}

# 2. Set-Aclで変更する場合: 取得 → ルール編集 → 適用 の3段階
$path = 'D:\share\sales\estimate'
$acl  = Get-Acl -LiteralPath $path

# 営業グループに変更権限を付与するルール(サブフォルダー・ファイルへ継承)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    'CONTOSO\SalesTeam', 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
# 追加付与はAddAccessRuleで行う。SetAccessRuleは同じユーザー/グループの既存Allowルールを
# 置き換えるため、そのグループに設定済みだった細かい権限が黙って消えることがある
$acl.AddAccessRule($rule)

# 3. まず-WhatIfで「どこに適用されるか」を確認してから、本実行に進む
Set-Acl -LiteralPath $path -AclObject $acl -WhatIf
# 問題なければ: Set-Acl -LiteralPath $path -AclObject $acl

Set-Aclは「Get-Aclで取ったセキュリティ記述子をモデルとして適用する」動きをするため12、取得と適用の間で対象を間違えると、意図しないACLの丸ごと置き換えになります。上のように取得パスと適用パスを同じ変数で持つ、適用前に-WhatIfを挟む、という地味な習慣が事故を防ぎます。継承を切る操作(SetAccessRuleProtection)も同じ枠組みでできますが12、第4章で見たとおり継承切れは将来の管理コストになるので、新設は本当に必要な場所に限定してください。

この一連の作業を定期実行のスクリプトに育てるときの設計(SupportsShouldProcess、証跡の残し方、タスクスケジューラ登録)は「PowerShellスクリプト応用 ── ログ調査・アーカイブ・レポート化を安全に自動化する」で扱った型がそのまま使えます。

6. どこで実行するか ── UNC越しの走査と実行アカウント

最後に、意外と結果を左右する「実行場所と実行者」の話です。

  • 大規模走査はサーバー上で実行するのが原則です。UNCパス(\\fs01\share)越しの再帰列挙は、ファイル1つごとのメタデータ取得がネットワーク往復になるため、数十万ファイル規模では所要時間が桁で変わります。管理端末からやりたい場合は、PowerShell Remotingでスクリプトをサーバー側で動かし、結果のCSVだけ回収する構成が実務的です。やり方は同時公開の「PowerShell Remoting(WinRM)入門」を参照してください。
  • 結果は実行アカウントの権限のスナップショットです。アクセスできない場所は列挙からもGet-Aclからも漏れます。調査は管理者権限のアカウントで行い、それでも拒否された場所(第2章のdenied-paths.txt)を「調査できていない場所」として報告に含めます。
  • UNC越しに実行する場合は、資格情報とセッションの罠にも注意が必要です。ドライブ文字はログオンセッション単位なので、対話ログオン中に割り当てたZ:はタスクスケジューラのジョブからは見えません。またエラー1219(ERROR_SESSION_CREDENTIAL_CONFLICT)は、同じサーバー(または共有)に対して、同じユーザーが複数のユーザー名を使って接続することは許可されないという仕様上のエラーです。16 「調査用の共有だけ管理者アカウントで、ほかは自分のアカウントで」と同じファイルサーバーに2種類の資格情報でつなごうとすると、2つ目がこれで失敗します。UNC越しに調査するなら、走らせる前に既存の接続を1つの資格情報へ揃えておいてください。落とし穴の全体像は「ネットワークドライブとUNCパスの落とし穴」にまとめています。
  • 所要時間は「よその数字」ではなく自分の環境で測ります。ディスクの種類、ファイル数、ウイルス対策のリアルタイムスキャン、階層の深さで結果が大きく変わるため、全体を流す前に代表的な部門フォルダーを1つ選んで実測し、共有全体のファイル数へ外挿するのが確実です。同じ測定をUNC越しとサーバー上の両方でやれば、「サーバーで流すべきか」も数字で示せます。
# 代表的な部門フォルダーを1つ選び、走査速度を実測してから全体の見積もりを立てる
# (ファイルが1件もないフォルダーを選ぶと最後の割り算で失敗するので、実データのある場所を選ぶ)
$sample = 'D:\share\sales'
$start  = Get-Date
$count  = (Get-ChildItem -LiteralPath $sample -Recurse -File `
    -ErrorAction SilentlyContinue | Measure-Object).Count
$sec    = ((Get-Date) - $start).TotalSeconds
'{0} ファイルを {1:N1} 秒で列挙 → 1万ファイルあたり {2:N1} 秒' -f `
    $count, $sec, ($sec / $count * 10000)

この見積もりが効くのは列挙が中心の処理(第2章の容量集計、第3章の古いファイル一覧、第4章のACL取得)です。第3章のハッシュ計算だけはファイル数ではなく総バイト数で時間が決まるので、別枠で見積もってください。

7. 実務の定石(判断表)

論点 選択肢 判断の目安
容量調査の範囲 全体一括 / 第1階層ごと まず浅い階層で当たりを付け、太い場所だけ深掘りする12
エラーの扱い 停止 / 無視 / 記録して継続 SilentlyContinue + ErrorVariableで「継続しつつ全記録」。拒否地帯はレポートに明記3
古いファイルの基準 LastAccessTime / LastWriteTime アクセス日時は更新無効の環境が多く不採用。更新日時+期間で部門と合意45
重複検出 全ファイルハッシュ / サイズで絞ってからハッシュ ハッシュはI/Oが重い。同一サイズのグループだけ計算する6
ACL調査の深さ 全フォルダー / 浅い階層+継承切れの場所 継承が生きていれば親を見れば分かる。IsInherited=FalseとProtected=Trueを重点に78
権限変更の道具 icacls / Set-Acl 定型変更とバックアップはicacls。条件分岐を伴う一括処理はSet-Acl+-WhatIf1312
削除の実行 即削除 / 隔離→観察→削除 一覧を部門確認後、まず隔離フォルダーへ移動し、一定期間問題がなければ削除

8. まとめ

  • ファイルサーバー整理は削除ではなく棚卸しから。第1階層の容量ランキング、古いファイル一覧、重複候補、ACL台帳の4点セットをCSVで作り、それを材料に部門と話します。
  • 走査はSilentlyContinueで止めない代わりに、ErrorVariableで拒否された場所を必ず記録します。数えられなかった場所は「ゼロ」ではなく「不明」です。
  • 古さの判定はLastWriteTime基準。NTFSの最終アクセス日時は更新が無効な環境が多く、信頼できません。
  • 重複はサイズで絞ってからGet-FileHash(既定SHA256)。一致は「候補」であり、残す判断は業務側と行います。
  • ACLはGet-AclのAccessをCSV化し、個人直付け・SIDのみのエントリ・継承切れ(AreAccessRulesProtected)を重点確認します。SDDLには深入り不要です。
  • 変更はicacls /saveでバックアップ → -WhatIfや対象確認 → 適用 → 事後確認の順。大規模走査はサーバー上(またはRemoting経由)で、管理者権限で実行します。

関連記事

関連する相談領域

合同会社小村ソフトでは、ファイルサーバーの容量・権限の棚卸しスクリプトの作成支援、権限設計の見直しに伴う調査、定期レポートの自動化(タスクスケジューラ運用まで)を扱っています。「まず現状を数字にしたい」という段階からのご相談も歓迎です。

参考リンク

  1. Microsoft Learn, Get-ChildItem. Get-ChildItemによる項目列挙、-Recurseによる再帰と-Depthによる深さ制限、-Recurse使用時はワイルドカード解釈を避けるため-LiteralPathで対象を指定するのが推奨されることについて。  2 3

  2. Microsoft Learn, Measure-Object. Measure-Objectが-Property Length -Sumなどでファイルサイズの合計・最大・最小・平均を計算できること、Get-ChildItemと組み合わせたディレクトリ内ファイルの集計例について。  2 3

  3. Microsoft Learn, about_CommonParameters. -ErrorAction SilentlyContinueがエラー表示を抑止して実行を継続すること、-ErrorVariableで指定変数にエラーレコードが格納されること、変数名の前に+を付けると上書きではなく追記になることについて。  2 3 4

  4. Microsoft Learn, fsutil behavior. NTFSの最終アクセス日時(Last Access Time)更新の有効/無効がfsutil behaviorのdisablelastaccessパラメーターとNtfsDisableLastAccessUpdateレジストリ値で制御されること、更新の無効化がファイル/ディレクトリアクセスの速度向上のために用意されていることについて。  2 3

  5. Microsoft Learn, [MS-FSA]: Appendix A: Product Behavior. Windows Vista以降でNTFS/ReFSの最終アクセス日時更新が既定で無効であること、Windows 10 v1803以降はシステム管理となり、サーバーシステムでは最終アクセス日時の更新が常に無効化されることについて。  2 3

  6. Microsoft Learn, Get-FileHash. Get-FileHashの既定アルゴリズムがSHA256であること、ハッシュ値が一致する2つのファイルは内容も同一と判断できること、ファイル名や拡張子を変えてもハッシュ値は変わらないことについて。  2 3

  7. Microsoft Learn, Get-Acl. Get-Aclがファイルやリソースのセキュリティ記述子を取得すること、既定でDACLのアクセス制御エントリ一覧(Access)を表示すること、SDDL形式(Sddlプロパティ)でも取得できることについて。  2 3 4 5 6

  8. Microsoft Learn, ObjectSecurity.AreAccessRulesProtected Property. AreAccessRulesProtectedプロパティが、セキュリティ記述子のDACLが保護されている(親からの継承を受けない)かどうかを返すことについて。  2 3

  9. Microsoft Learn, Get-SmbShareAccess. Get-SmbShareAccessがSMB共有のACL(共有へのアクセス権を付与されたセキュリティプリンシパルと許可/拒否・権利)を取得するコマンドレットであることについて。 

  10. Microsoft Learn, about_Character_Encoding. Windows PowerShell(5.1)ではcmdletごとの既定エンコーディングが不統一で、Export-CsvはASCIIでファイルを作成すること、PowerShell 6以降は既定がutf8NoBOMに統一されたことについて。  2

  11. Microsoft Learn, Export-Csv. Export-Csvがオブジェクトの各プロパティを列としたCSVファイルを作ること、PowerShell 7系での-Encodingの既定値がUTF8NoBOMであり、UTF8BOMなどを明示指定できることについて。  2

  12. Microsoft Learn, Set-Acl. Set-AclがAclObjectで渡したセキュリティ記述子をモデルとして対象のACLを変更すること、-WhatIf/-Confirmに対応すること、FileSystemAccessRuleを作成してSetAccessRule()で追加する手順、SetAccessRuleProtection()による継承の無効化(既存の継承ルール保持の選択つき)の例について。  2 3 4 5

  13. Microsoft Learn, icacls. /reset が「対象ファイルのACLを既定の継承ACLで置き換える」こと、/q が成功メッセージを抑止することについて。  2 3 4 5 6

  14. Microsoft Learn, Maximum Path Length Limitation. Windows APIのパス長上限MAX_PATHが260文字であること、レジストリのLongPathsEnabledとアプリ側のlongPathAware宣言の両方が揃った場合のみ多くのWin32関数で制限が緩和されることについて。 

  15. Microsoft Learn, Permissions on copying and moving files and folders. オブジェクトが既定で移動先・作成先の親からアクセス許可を継承すること、唯一の例外が同じボリューム内の別フォルダーへの移動であり、その場合は元のアクセス許可が保持されること、別のボリュームへコピーまたは移動した場合は移動先フォルダーのアクセス許可を継承することについて。  2

  16. Microsoft Learn, System Error Codes (1000-1299). エラー1219(ERROR_SESSION_CREDENTIAL_CONFLICT)が「同一ユーザーが複数のユーザー名を使って、同じサーバーまたは共有リソースへ複数の接続を行うことは許可されない」というエラーであることについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

PowerShellでフォルダーごとの合計サイズを調べるにはどうしますか?
Get-ChildItemで-Recurse付きでファイルを列挙し、Measure-Objectの-Property Length -Sumで合計します。全体を一気に集計するのではなく、まず共有ルート直下の第1階層フォルダー単位で集計して「太っている場所」の当たりを付けるのが実務的です。その際-ErrorAction SilentlyContinueと-ErrorVariableを併用し、アクセス拒否で数えられなかった場所を必ず記録してください。エラーを黙って握りつぶすと、集計から漏れた場所が「サイズゼロ」に見えてしまいます。
古いファイルの判定にLastAccessTime(最終アクセス日時)を使ってよいですか?
推奨しません。NTFSでは性能上の理由から最終アクセス日時の更新が既定で無効化(またはシステム管理)されている環境が多く、実際にはファイルが読まれていても日時が更新されないことがあるためです。棚卸しの基準にはLastWriteTime(最終更新日時)を使い、「最後に内容が変更されてからの経過期間」として部門に提示するのが安全です。アーカイブ判断は機械的に行わず、一覧を関係者に確認してもらってから進めてください。
Get-Aclで何が分かりますか?SDDLは読む必要がありますか?
Get-Aclはファイルやフォルダーのセキュリティ記述子を取得し、Accessプロパティから「誰が(IdentityReference)」「何を(FileSystemRights)」「許可か拒否か(AccessControlType)」「継承か直接付与か(IsInherited)」を読み取れます。AreAccessRulesProtectedを見れば、そのフォルダーで継承が切られているかも分かります。SDDLという文字列形式もありますが、棚卸し目的ならAccessプロパティをCSVに出す方が関係者と共有しやすく、SDDLに深入りする必要はありません。
ACLの変更はPowerShellのSet-Aclとicaclsのどちらでやるべきですか?
調査・レポートはオブジェクトとして扱えるGet-Acl、変更の実務はicaclsを第一候補にするのが使い分けの目安です。icaclsは/saveでACLをファイルに保存し/restoreでそのまま戻せるため、変更前のバックアップと切り戻しが簡単です。Set-Aclを使う場合はGet-Aclで取得→ルール編集→適用の3段階になり、-WhatIfで対象を確認してから実行します。いずれの場合も、棚卸しと関係部門への確認が済んでから変更に進むのが原則です。
ファイルサーバーの調査はUNCパス越しに実行してよいですか?
少量なら問題ありませんが、数十万ファイル規模の全走査はネットワーク越しだと大幅に遅くなります。可能ならファイルサーバー上で直接(またはPowerShell Remotingで)実行し、結果のCSVだけを持ち帰る構成が効率的です。また、走査結果は実行アカウントの権限に依存します。アクセスできないフォルダーは列挙から漏れるため、管理者アカウントで実行したうえで、それでも拒否された場所を記録して「調査できていない場所」として報告に含めてください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る