Microsoft Graph Files API(PUT /content)でSharePointにCSV/XLSXをアップロードすると破損する原因と対策【PowerShell】

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 を使う設計にしておくと安定する

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次