Microsoft Graph Files API(DriveItem: PUT /content)で SharePoint(サイト/Teams チャネル)へ .csv / .xlsx をアップロードしたのに、XLSX が破損して開けない/CSV が1行に潰れる――そんな現象は PowerShell の「読み込み方」が原因で起きがちです。この記事では、壊れないアップロードの基本と、実務で再発しないためのチェックポイントをまとめます。
起きている現象:XLSX が破損、CSV が 1 行になる
SharePoint(Teams のチャネルを含む)に対して、Microsoft Graph の Files API を使い、次のようなエンドポイントへ PUT ...:/filename:/content でアップロードするケースを想定します。
PUT https://graph.microsoft.com/v1.0/sites/{site-id}/drive/items/{parent-item-id}:/Test.xlsx:/content
PUT https://graph.microsoft.com/v1.0/sites/{site-id}/drive/items/{parent-item-id}:/Test.csv:/content
PowerShell で「ファイルの中身を読んで Body に載せる」実装をしたとき、よくある症状は次の2つです。
| ファイル種別 | 症状 | アップロード結果 | 現場での見え方 |
|---|---|---|---|
| .xlsx | 破損して開けない | Graph API は 2xx で成功することも多い | Excel で「ファイルが破損しています」「修復しますか?」など |
| .csv | 全データが 1 行になる | 開けはする | 改行が消え、ヘッダー+全レコードが 1 行に連結されたように見える |
ポイントは、「API 側が失敗している」のではなく、送信した Body の時点でファイルの中身が変質していることが多い点です。
なぜ Get-Content で送ると壊れるのか
結論から言うと、Graph の PUT .../content は “ファイルのバイト列そのもの” を期待するのに対し、Get-Content は既定動作でテキストとして読み込み、加工されやすい形(文字列/行配列)を返すためです。
Get-Content の既定動作が “行単位の配列” になりやすい
Get-Content は(パラメータ指定がない場合)多くのケースで「1行=1要素」の文字列配列を返します。ここが CSV の “1行化” につながりやすい典型ポイントです。
- 改行コード自体は要素から取り除かれやすい(行の区切りは「配列の区切り」になる)
- その配列を
-Bodyに渡すと、HTTP 送信時に配列が連結される(連結のされ方次第で改行が消える/スペース区切りになる) - 結果として、CSV の改行が失われ「1行」に見える
XLSX は “テキストとして読んだ時点で致命傷” になりやすい
.xlsx は見た目こそ 1 ファイルですが、実体は ZIP 形式のバイナリコンテナです。これを Get-Content で文字列として扱うと、
- バイト列が文字コード変換される(不正なバイトは置換され得る)
- 行分割/結合など、テキスト前提の処理が介在する
- 結果、ZIP の構造が壊れて Excel が開けない
つまり「アップロードできたのに壊れている」は、アップロード前に壊して送っている可能性が高い、ということです。
“テキストとして加工される” を整理すると
| 処理 | テキスト向けの挙動 | CSV への影響 | XLSX への影響 |
|---|---|---|---|
| 文字コードの解釈 | バイト→文字列にデコード | 文字化けの原因になり得る | バイナリを文字列化した時点で破損しやすい |
| 改行の扱い | 行単位で配列化、改行自体は保持されないことが多い | 改行が消えて 1 行になる | ZIP 内のバイナリが変質しやすい |
| 配列の Body 送信 | 送信時に連結(区切りが不定) | 意図しない連結でレコード境界が消える | 構造破壊 |
最短の解決策:ReadAllBytes で “生のバイト列” を送る
対策はシンプルです。アップロード時の Body は “ファイルのバイト列(生データ)” をそのまま送る。これだけで、XLSX 破損と CSV 1行化の多くは止まります。
代表的で分かりやすい方法が、.NET の [System.IO.File]::ReadAllBytes() を使うやり方です。
$Filepath = "C:\Test\Test.xlsx"
$Content = [System.IO.File]::ReadAllBytes($Filepath)
$Headers = @{
"Authorization" = "Bearer $Token"
"Content-Type" = "application/octet-stream"
}
$uri = "https://graph.microsoft.com/v1.0/sites/{site-id}/drive/items/{parent-item-id}:/Test.xlsx:/content"
Invoke-RestMethod -Uri $uri -Headers $Headers -Method PUT -Body $Content
ポイントは「Body が byte[] になっていること」です。これで Graph に渡るのは加工されていないバイト列になり、ファイルの整合性が保たれます。
パス指定でアップロードする例(アイテム ID を用意しにくい場合)
フォルダの item-id を先に解決する設計もありますが、「パスで置きたい」場面は多いはずです。Graph はパスベースも扱えます。
$Filepath = "C:\Test\Test.csv"
$Content = [System.IO.File]::ReadAllBytes($Filepath)
$Headers = @{
"Authorization" = "Bearer $Token"
"Content-Type" = "application/octet-stream"
}
# 例:ドキュメント ライブラリ直下のフォルダへ配置
$uri = "https://graph.microsoft.com/v1.0/sites/{site-id}/drive/root:/Reports/Test.csv:/content"
Invoke-RestMethod -Uri $uri -Headers $Headers -Method PUT -Body $Content
パスにスペースや記号が含まれる場合は URL エンコードが絡むため、運用では「英数字+ハイフン」などに寄せると事故が減ります。
PowerShell だけで完結させる別解:-InFile で送る
実装を短くしたい場合、Invoke-RestMethod の -InFile を使うと「ファイルをそのまま送る」形に寄せられます(環境差があるため、動作確認は必須ですが、方針としては安全側です)。
$Filepath = "C:\Test\Test.xlsx"
$Headers = @{
"Authorization" = "Bearer $Token"
}
$uri = "https://graph.microsoft.com/v1.0/sites/{site-id}/drive/items/{parent-item-id}:/Test.xlsx:/content"
Invoke-RestMethod -Uri $uri -Headers $Headers -Method PUT -InFile $Filepath -ContentType "application/octet-stream"
運用上は、「byte[] を送っている」または「InFile でそのまま送っている」のどちらかに揃えておくと、後から見返したときに原因切り分けが速くなります。
CSV が 1 行になる問題を確実に潰す:改行コードと“読み方”のチェック
今回の主因は “Get-Content による加工” ですが、CSV はもうひとつ落とし穴があります。ファイル自体の改行コード(LF/CRLF)や文字コード(UTF-8/Shift-JIS など)です。
特に Excel で直接開く運用だと、次の条件が重なると「1行に見える」「列が崩れる」「文字化けする」が連鎖します。
| 項目 | ズレやすいパターン | 起きがちな症状 | 対策の方向性 |
|---|---|---|---|
| 改行コード | LF のみ(Unix 形式) | Excel で 1 行に見えることがある | CRLF に統一して出力する |
| 文字コード | UTF-8(BOM なし)を Excel が想定していない | 文字化け、区切り誤認 | Excel 想定に合わせる(UTF-8 with BOM / Shift-JIS など) |
| 区切り文字 | カンマではなくセミコロン、またはロケール依存 | 列が分割されない | 開き方(データ取り込み)を統一する |
ここで重要なのは、Graph は「受け取ったバイト列をそのまま保存する」という点です。アップロード方式を正しても、元ファイルが LF だけで作られていれば、そのまま LF のまま SharePoint に置かれます。
CSV を CRLF に揃える例(アップロード前に整形する場合)
CSV の生成工程が自分のスクリプト内にあるなら、書き出し時点で Windows(CRLF)に揃えるのが安全です。既存 CSV を変換したい場合は、次のように “テキストとして扱う” のではなく、用途を限定して行うのがポイントです(XLSX には絶対にやらない)。
$csvPath = "C:\Test\source.csv"
$outPath = "C:\Test\fixed.csv"
# CSV に限定して「生テキスト」として読み、改行だけ CRLF に揃える例
# ※文字コードは運用に合わせて調整(UTF-8 / Shift-JIS など)
$text = Get-Content -Path $csvPath -Raw
$text = $text -replace "`r?`n", "`r`n"
# PowerShell 7+ なら utf8BOM が使える環境もある(環境に合わせて選択)
Set-Content -Path $outPath -Value $text -Encoding utf8
ただし、アップロード不具合の話と混同しないために、運用ルールとしては「CSV 生成工程で改行と文字コードを決める」「アップロードは常に byte[]」に分離しておくのがおすすめです。
アップロード方式の選び方:小さいファイルは PUT /content、大きいファイルは Upload session
PUT .../content は手軽ですが、ファイルサイズが大きくなると アップロードセッション(Upload session) を使う必要が出ます。これは Graph 側の仕様(上限やタイムアウト、再送性)に関わるため、実務では最初から分岐設計にしておくと安定します。
| 方式 | 向いているケース | メリット | 注意点 |
|---|---|---|---|
| PUT …:/content(シンプルアップロード) | 小さめのファイル | 実装が短い/呼び出し回数が少ない | サイズ上限がある/ネットワーク断に弱い |
| Upload session(分割アップロード) | 大きいファイル/安定性優先 | 分割送信・再送がしやすい/長時間でも完走しやすい | 実装が長い/チャンク管理が必要 |
Upload session の流れ(概要)
POST ...:/filename:/createUploadSessionでアップロード URL(uploadUrl)を取得- 返ってきた uploadUrl に対し、チャンク単位で
PUT(Content-Rangeを付与) - 最後まで送ると driveItem 情報が返り、アップロード完了
Upload session の PowerShell 例(骨組み)
ここでは理解しやすさを優先し、エラーハンドリングやリトライは簡略化しています。実運用では、タイムアウト/再送/ログ出力(どの Range を送ったか)まで含めて実装してください。
$Filepath = "C:\Test\Big.xlsx"
$filename = [System.IO.Path]::GetFileName($Filepath)
$filesize = (Get-Item $Filepath).Length
# 1) セッション作成
$sessionUri = "https://graph.microsoft.com/v1.0/sites/{site-id}/drive/items/{parent-item-id}:/$filename:/createUploadSession"
$headers = @{
Authorization = "Bearer $Token"
"Content-Type" = "application/json"
}
$body = @{
item = @{
"@microsoft.graph.conflictBehavior" = "replace"
name = $filename
}
} | ConvertTo-Json -Depth 5
$session = Invoke-RestMethod -Uri $sessionUri -Headers $headers -Method POST -Body $body
$uploadUrl = $session.uploadUrl
# 2) チャンク送信(例:10MB)
$chunkSize = 10MB
$stream = [System.IO.File]::OpenRead($Filepath)
try {
$buffer = New-Object byte[] $chunkSize
$offset = 0
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$start = $offset
$end = $offset + $read - 1
# 読んだ分だけの配列に切り出す(最後のチャンク対策)
$chunk = if ($read -eq $buffer.Length) { $buffer } else { $buffer[0..($read-1)] }
$chunkHeaders = @{
"Content-Length" = $read
"Content-Range" = "bytes $start-$end/$filesize"
}
Invoke-RestMethod -Uri $uploadUrl -Method PUT -Headers $chunkHeaders -Body $chunk
$offset += $read
}
}
finally {
$stream.Dispose()
}
Upload session の場合も本質は同じで、送るのは常に “バイト列”です。ここで Get-Content を挟むと、再び破損要因を作ってしまいます。
Content-Type は何を指定すべきか
PUT .../content の場合、動作上は text/plain でも通ってしまうケースがありますが、実務では次の理由で application/octet-stream を推奨します。
- 「バイナリを送る」意図が明確になり、レビューで事故が減る
- .xlsx のようなバイナリで
text/*を指定しても意味がない - PowerShell 側の暗黙変換を避ける設計に寄せやすい
CSV だけに限っては text/csv を使いたい場面もありますが、Graph の保存動作自体はバイト列なので、運用を単純化するなら application/octet-stream に統一しても問題が起きにくいです(閲覧側のアプリや取り込み方法が別途重要になります)。
壊れていないことを証明する:アップロード前後でハッシュを比較する
「開ける/開けない」は主観が入りやすいので、再発防止には ハッシュ比較 が効果的です。
- アップロード前:ローカルファイルの SHA256 を取得
- アップロード後:Graph でダウンロード(GET /content)して SHA256 を取得
- 一致すれば “バイト単位で同一”=破損していない
# アップロード前のハッシュ
$localHash = (Get-FileHash "C:\Test\Test.xlsx" -Algorithm SHA256).Hash
# アップロード後にダウンロードして比較(例:一時保存)
$downloadUri = "https://graph.microsoft.com/v1.0/sites/{site-id}/drive/items/{item-id}/content"
Invoke-RestMethod -Uri $downloadUri -Headers @{Authorization="Bearer $Token"} -OutFile "C:\Test\Downloaded.xlsx"
$remoteHash = (Get-FileHash "C:\Test\Downloaded.xlsx" -Algorithm SHA256).Hash
$localHash
$remoteHash
これを CI 的に回せると、「たまたま開けた」ではなく「確実に同一」を担保できます。
実務での“あるある”と対処の整理
同じテーマで詰まりやすいポイントを、原因→対策の形でまとめます。
| つまずきポイント | 原因 | 対処 |
|---|---|---|
| Get-Content をそのまま -Body に渡している | テキスト化・行配列化で改行やバイナリが壊れる | ReadAllBytes / -InFile で byte を送る |
| CSV が 1 行になる | 改行が落ちる/LF のみ/配列連結 | byte[] アップロード+CSV の改行コードを統一(可能なら CRLF) |
| 文字化けする | CSV の文字コードと Excel の期待が一致していない | 生成工程で文字コードを固定(UTF-8 BOM / Shift-JIS など運用で統一) |
| 大きいファイルで失敗する | 方式の上限、タイムアウト、ネットワーク断 | Upload session に切り替える |
| 同名ファイルの扱いが不安定 | 置換/リネームの挙動が期待と違う | 競合時の動作を設計に入れる(置換、リネーム、失敗など) |
そのまま使える:壊れない PUT /content アップロード関数(PowerShell)
チーム内で“書き方がバラバラ”になると再発しやすいので、関数化して Get-Content を使わない形に固定するのが効果的です。以下はシンプルアップロード(PUT /content)用の例です。
function Upload-GraphDriveItemContent {
param(
[Parameter(Mandatory=$true)][string]$Token,
[Parameter(Mandatory=$true)][string]$Uri, # ...:/filename:/content まで含めた完全なURI
[Parameter(Mandatory=$true)][string]$FilePath
)
if (-not (Test-Path -LiteralPath $FilePath)) {
throw "File not found: $FilePath"
}
$bytes = [System.IO.File]::ReadAllBytes($FilePath)
$headers = @{
Authorization = "Bearer $Token"
"Content-Type" = "application/octet-stream"
}
try {
return Invoke-RestMethod -Uri $Uri -Headers $headers -Method PUT -Body $bytes
}
catch {
# 失敗時は原因が追えるように、最低限の情報を投げる
throw ("Upload failed. Uri={0} File={1} Error={2}" -f $Uri, $FilePath, $_.Exception.Message)
}
}
# 使い方例
$uri = "https://graph.microsoft.com/v1.0/sites/{site-id}/drive/items/{parent-item-id}:/Test.xlsx:/content"
Upload-GraphDriveItemContent -Token $Token -Uri $uri -FilePath "C:\Test\Test.xlsx"
この形にしておけば、レビュー観点が「URI が正しいか」「権限が足りているか」「サイズ的に upload session にすべきか」に集約され、“読み込み方” が原因の破損はほぼ排除できます。
よくある質問
Get-Content -Raw なら CSV の 1 行化は防げますか?
CSV に限っては改善することがあります。-Raw は「全体を 1 つの文字列として読む」ため、行配列→連結による改行消失は避けられます。ただし、文字コード変換や改行コードの差し替えなど “テキストとしての加工” は起き得るため、Graph へのアップロードは byte[] を基本にするのが安全です。
XLSX も -Raw でいけますか?
おすすめしません。.xlsx はバイナリ(ZIP)なので、テキスト読み込みを挟む時点で破損リスクが高くなります。ReadAllBytes / -InFile のいずれかで byte を送るのが前提です。
Content-Type を text/csv にすべきですか?
保存という観点では、Graph は受け取ったバイト列を格納するため、application/octet-stream でも問題になりにくいです。閲覧・取り込み(Excel で開く、Power BI で読む等)で困るなら、まずは 改行コード・文字コード・区切り文字 を運用として固定するのが効果的です。
成功したのに壊れているかどうかを確実に判定したいです
ハッシュ比較(SHA256 など)を導入してください。アップロード前後で一致すれば、少なくとも “転送と保存でバイトは変わっていない” と判断できます。Excel が開けない問題は、ほぼこの段階で原因を絞り込めます。
Teams のチャネルにアップロードしているつもりなのに、別の場所に入ります
Teams のファイルは SharePoint のドキュメント ライブラリ上のフォルダにマッピングされます。チャネルごとに格納先フォルダが異なるため、Graph で参照している site-id / drive / パス(または parent item-id)が意図したチャネルに紐づいているかを確認してください。ここがズレていると「成功はするが想定外の場所に置かれる」現象になります。
まとめ:壊れない条件は “byte[] を送る” に尽きる
- XLSX が破損するのは、Get-Content 等でバイナリをテキスト化してしまうのが主因
- CSV が 1 行になるのは、行配列化→連結で改行が消える/改行コードが合わないのが主因
- 対策は、ReadAllBytes / -InFile で生のバイト列を PUT /content に送る
- サイズが大きい場合は、Upload session を使う設計にしておくと安定する

コメント