SharePoint Online/On-Premises 環境に対してプログラミングで大きなファイルをアップロードする

こんにちは SharePoint サポートの森 健吾 (kenmori) です。今回の投稿では、SharePoint Online / On-Premises 環境において、ファイルをアップロードするプログラム開発で、よくあるご質問とその回答を記載させていただきます。

 

1. CSOM (または REST) で 1.6 MB 以上のファイルをアップロードできない。

質問事項

CSOM を利用し、Office365 上 SharePoint Onlineへ 1.6 MB 程度のファイルをアップロードすると 「要求メッセージのサイズが大きすぎます。2097152 バイトを超えるメッセージはサーバーで許可されません。」とエラーが出力されます。

なぜ、CSOM だと約 2 MBの制限が発生するのでしょうか。ドキュメント ライブラリのページからのアップロードは成功します。また、エラーが発生するファイルも、約1.6MBとエラーメッセージに記載されているバイト数よりも少ない容量となります。

説明

本現象は、クライアント サイド オブジェクト モデル (以下 CSOM) のサーバー側サービスとして公開されている client.svc において、サービス メッセージの最大サイズとして指定されたサイズ (*1 SPClientRequestServiceSettings.MaxParseMessageSize) を超えたメッセージを受信したためエラーが発生したことに起因します。

CSOM ではなく、手動でファイルをアップロード際には、この client.svc を使用しないため本制限値の影響を受けません。

コードと現象

--- サンプルコード : ここから ---

Dim ctx As SP.ClientContext = New SP.ClientContext("https://kenmori.sharepoint.com")
Dim pwd As System.Security.SecureString = New Security.SecureString()
For Each cpwd As Char In "password".ToArray()
    pwd.AppendChar(cpwd)
Next
ctx.Credentials = New SP.SharePointOnlineCredentials("user", pwd)

Dim fileName As String = TextBox1.Text
Dim loadFilePath As String = "C:\Shared\" & fileName
Dim url As String = "https://kenmori.sharepoint.com/Shared%20Documents/"

'アップロードファイルの読み込み
Dim bFile As Byte() = System.IO.File.ReadAllBytes(loadFilePath)
Dim fci As New SP.FileCreationInformation()
fci.Content = bFile
fci.Overwrite = True
fci.Url = fileName

'フォルダのURLから、Folderクラスを取得
Dim oWebsite As SP.Web = ctx.Web
Dim spfolder As SP.Folder = oWebsite.GetFolderByServerRelativeUrl(url)
ctx.Load(spfolder)
ctx.ExecuteQuery()

'FolderへFileをアップロード
Dim newFile As SP.File = spfolder.Files.Add(fci)
ctx.Load(newFile)
ctx.ExecuteQuery() 'ここでエラー!

 --- サンプルコード : ここまで ---

上記のアップロードは、もちろん小さなファイルのアップロードでは問題なく動作します。
この処理で SharePoint 2013 On-Premises 環境に対して CSOM でアップロードしても同じくエラーとなります。その場で診断ログを見ると以下のようなエラーが出力されています。

 08/22/2013 11:42:23.24 w3wp.exe (0x2A74) 0x0CD8 SharePoint Foundation CSOM afxv1 High Microsoft.SharePoint.Client.InvalidClientQueryException: The request message is too big. The server does not allow messages larger than 2097152 bytes. at Microsoft.SharePoint.Client.MaxSizeRequestStream.CheckTotalReadCount(Int64 totalReadCount) at Microsoft.SharePoint.Utilities.ReadonlyWrapStream.Read(Byte[] buffer, Int32 offset, Int32 count) at System.IO.StreamReader.ReadBuffer(Char[] userBuffer, Int32 userOffset, Int32 desiredChars, Boolean& readToUserBuffer) at System.IO.StreamReader.Read(Char[] buffer, Int32 index, Int32 count) at Microsoft.SharePoint.Client.ServerRuntime.TextPeekReader.Read(Char[] buffer, Int32 offset, Int32 count) at System.Xml.XmlTextReaderImpl.ReadData() at System.Xml.XmlTextReaderImpl.ParseText(Int32& startPos, Int32& endPos, Int32& out... f75a299c-6165-708a-27e3-1dd2ccc65d66

2015/05/07 更新

REST でも同様のエラーが発生する旨を記載しておりましたが、REST は本制約の影響受けないことを確認したため訂正します。
REST API を使用する場合、CSOM で /ProcessQuery メソッドのような XML メソッドのやりとりを介さず、直接コンテンツを HTTP 本文に記述してアップロードすることが可能です。
要求サイズのチェックは XML メッセージのサイズにて実施されていますので問題 1 の現象を回避できます。

対処方法

On-Premises 環境では、以下の方法で、メッセージの最大サイズを変更することは可能です。
ただし、根本対処策としては、HttpWebRequest クラスを使用してアップロードする方法となります。その方法は本投稿 "3. 大きなファイルをアップロードするための確実な方法"でまとめてご説明します。

 (手順)
 1. SharePoint 管理シェルを起動します。
 2. 以下のコマンド レットを実行します。
  [Microsoft.SharePoint.Administration.SPWebService]::ContentService
  $ws = [Microsoft.SharePoint.Administration.SPWebService]::ContentService
  $ws.ClientRequestServiceSettings.MaxParseMessageSize = 2147483647
  $ws.Update()

補足

このSPClientRequestServiceSettings.MaxParseMessageSize の既定値は2097152 Byte です。ファイルは 1.5MB 程度なのになぜ SOAP メッセージは 2.0 MB を超えてしまうのでしょうか。

その理由は Base64Binary (Base 64 エンコード) にあります。client.svc は HTTP 要求応答をベースとしたサービスとして公開されています。Web メソッドを実行する際には、XML などのテキスト ベースの情報で SOAP を構成し、メソッド名が引数などの情報を引き渡します。

この SOAP 上にバイナリ データを記載する際に、バイナリをそのまま渡した場合プロトコルが正常機能しないため XML Web サービスの一般的な方法として Base64 エンコードを実施して Base64Binary 型のデータを引数として転送します。 

 - 例
 <Property Name="Content" Type="Base64Binary">YQ==</Property>

このエンコードが実施された場合、バイナリ データを SOAP に影響しない範囲で定義された文字コードとして変換するため、データ サイズは大きくなります。このため、2 MB に満たないファイルを転送した場合においても、HTTP 要求上では 2097152 バイトを超えるデータ サイズとなったため、エラーが発生します。
SharePoint 2010 REST サービスである ListData.svc を使用してファイルをアップロードしても、10485760 バイトを超えると同じエラーです。
チャンク形式 (TransferEncodingChunked) として一度のトラフィックを減らしたとしても、全体のメッセージ サイズには影響しません。

上記のため、大きなサイズのファイルを SOAP 経由でアップロードすることはトラフィックの増大につながるためお勧めできません。 

2. 大きなファイルをアップロードしようとすると、クライアント環境で OutOfMemoryException が発生する。

 

質問事項

CSOM、REST API、WebClient クラスなどを利用し、SharePoint へ数百 MB の大きなファイルをアップロードするとOutOfMemoryException 例外が検出されます。
小さなメモリを搭載している 32 ビットのクライアント PC なので、メモリが足らなくなったことは理解できるのですが、このようなマシンからもファイルをアップロードすることは可能でしょうか。

説明

HTTP を送信する際にバッファリング機能を無効化することで、ご要望を実現できる可能性があります。

.NET Framework のライブラリには様々な Web クライアント機能を持ったクラスが存在します。Web 参照で生成される ASMX Web サービス プロキシ、WCF サービス 参照で生成される Web サービス クライアント、WebClient クラスなどがあります。
これらのクラスは内部的に HttpWebRequest を使用してサーバーに要求を送信しています。

以下のサポート技術情報にある通り、HttpWebRequest.AllowWriteStreamBuffering プロパティが true の場合、メモリ上にアップロードするファイルをバッファリングした上で、サーバーに送信する動作となります。この値は既定値が true であり、このクラスを Web 送信に内部利用する多くのクラスは AllowWriteStreamBuffering を false に設定するためのプロパティを持たないため、常にバッファリングが有効 (true) として動作します。この動作によりメモリを消費して OutOfMemoryException を生成する状況が発生します。

HttpWebRequest を直接呼出し、AllowWriteStreamBuffering プロパティを false にすることで、このバッファリングを無効にし、クライアント端末のメモリを節約する方法があります。根本対処策は、1 の現象に対する対処策と併せて以下に記載いたします。

タイトル : .NET Framework を実行しているコンピュータで HttpWebRequest クラスを使用して大量のデータを送信する場合に POST 要求または PUT 要求が失敗することがある
アドレス : https://support.microsoft.com/kb/908573

3. 大きなファイルをアップロードするための確実な方法

上記の理由により、大きなファイルをアップロードする最も確実な方法はHttpWebRequest クラスを使用して、SharePoint Online / On-Premises 環境に PUT 要求を送信する方法となります。

--- サンプルコード : ここから ---

HttpWebRequest webReq;
HttpWebResponse res;
string user = "user@kenmori.onmicrosoft.com";
string pwd = "password";
string siteUrl = "https://kenmori.sharepoint.com/";

//-----------------------------------
// SPO 認証 Cookie の作成
// SharePointOnlineCredentials から認証 Cookie を取得
// -----------------------------------
CredentialCache myCredCache = new CredentialCache();
System.Security.SecureString secpas = new System.Security.SecureString();
foreach(char c in pwd.ToCharArray())
{
    secpas.AppendChar(c);
}

// ClientContext であれば SharePointOnlineCredentials だけで良いが、HttpWebRequest に指定するために変換
SharePointOnlineCredentials userCredentials = new Microsoft.SharePoint.Client.SharePointOnlineCredentials(user,secpas);
CookieContainer cookiecontainer = new CookieContainer();
Cookie authCookie = new Cookie("SPOIDCRL", userCredentials.GetAuthenticationCookie(new Uri(siteUrl)).Replace("SPOIDCRL=", String.Empty));
cookiecontainer.Add(new Uri(siteUrl), authCookie);

//-----------------------------------
// HEAD 要求を送信して接続を事前認証
// (KB 908573 Windows 認証 & AllowWriteStreamBufferint=false)
// SPO では不要なためコメントアウトしておきます。
//-----------------------------------
/*
webReq = (HttpWebRequest)WebRequest.Create(siteUrl);
webReq.PreAuthenticate = false;
webReq.Method = "HEAD";
CredentialCache myCredCache = new CredentialCache();
myCredCache.Add(URL,"Negotiate",(NetworkCredential) CredentialCache.DefaultCredentials);          
webReq.Credentials = myCredCache;
webReq.PreAuthenticate = true;
res = (HttpWebResponse)webReq.GetResponse();
res.Close();
*/

// -----------------------------------
// PUT 要求を送信
//-----------------------------------
// アップロードするファイル URL を指定
webReq = (HttpWebRequest)WebRequest.Create(siteUrl + "/Shared%20Documents/bigdata.txt");
webReq.CookieContainer = cookiecontainer;
//HTTP 要求するデータを一度メモリにバッファリングしない
webReq.UnsafeAuthenticatedConnectionSharing = true; // 接続共有を true に設定
webReq.AllowWriteStreamBuffering = false; // バッファリングを false に設定
webReq.Method = WebRequestMethods.File.UploadFile;
webReq.Timeout = int.MaxValue;

// アップロードするファイルをオープンし、HttpWebRequestにサイズを設定
FileStream inStrm = new FileStream(@"C:\bigdata.txt", FileMode.Open);
webReq.ContentLength = inStrm.Length;

// ファイルをバッファに読み込み・サーバーへ書き込み
using (Stream outStrm = webReq.GetRequestStream())
{
    int bufSize = 2048; // 2KBバッファ               
    byte[] buffer = new byte[bufSize];
    int bytesRead = 0;
    while ((bytesRead = inStrm.Read(buffer, 0, bufSize)) > 0)
        outStrm.Write(buffer, 0, bytesRead);
    inStrm.Close();
    outStrm.Flush();
    outStrm.Close();
}

// 結果取得
res = (HttpWebResponse)webReq.GetResponse();
res.Close();

--- サンプルコード : ここから ---

2015/02/09 更新
FedAuth Cookie はブラウザー認証のみで使用されるよう制限され、クライアント アプリケーションからは FedAuth Cookie が処理されないように動作変更されたようです。
CSOM ライブラリから送信されているものと同じように FedAuth から SPOIDCRL に変更をお願いします。

いかがでしたでしょうか。今回の投稿は以上になります。