使用Microsoft Graph SDK从OneDrive下载大文件

时间:2017-06-09 07:18:13

标签: office365 microsoft-graph onedrive

我正在尝试使用以下Microsoft Graph调用从OneDrive下载文件:

using (var strm = await client.Drives[RemoteDriveId].Items[Id].Content.Request().GetAsync())
{
   byte[] byteBuffer = new byte[4096];
   filePath = System.IO.Path.Combine(folderPath, filename);
   using (System.IO.FileStream output = new FileStream(filePath, FileMode.Create))
   {
      int bytesRead = 0;
      do
      {
        bytesRead = contentStream.Read(byteBuffer, 0, byteBuffer.Length);
        if (bytesRead > 0)
        {
           output.Write(byteBuffer, 0, bytesRead);
        }
      }
      while (bytesRead > 0);
   }
}

上述代码的问题是,如果文件大小或网络速度慢,则在完全下载文件之前,SDK中会抛出请求超时异常。我想以块的形式下载文件或增加超时。如何使用Microsoft图形SDK实现此目的?

2 个答案:

答案 0 :(得分:2)

您需要使用Range标头对下载进行分块。

// Based on question by Pavan Tiwari, 11/26/2012, and answer by Simon Mourier
// https://stackoverflow.com/questions/13566302/download-large-file-in-small-chunks-in-c-sharp

const long DefaultChunkSize = 50 * 1024; // 50 KB, TODO: change chunk size to make it realistic for a large file.
long ChunkSize = DefaultChunkSize;
long offset = 0;         // cursor location for updating the Range header.
byte[] bytesInStream;                    // bytes in range returned by chunk download.

// Get the collection of drive items. We'll only use one.
IDriveItemChildrenCollectionPage driveItems = await graphClient.Me.Drive.Root.Children.Request().GetAsync();

foreach (var item in driveItems)
{
    // Let's download the first file we get in the response.
    if (item.File != null)
    {
        // We'll use the file metadata to determine size and the name of the downloaded file
        // and to get the download URL.
        var driveItemInfo = await graphClient.Me.Drive.Items[item.Id].Request().GetAsync();

        // Get the download URL. This URL is preauthenticated and has a short TTL.
        object downloadUrl;
        driveItemInfo.AdditionalData.TryGetValue("@microsoft.graph.downloadUrl", out downloadUrl);

        // Get the number of bytes to download. calculate the number of chunks and determine
        // the last chunk size.
        long size = (long)driveItemInfo.Size;
        int numberOfChunks = Convert.ToInt32(size / DefaultChunkSize); 
        // We are incrementing the offset cursor after writing the response stream to a file after each chunk. 
        // Subtracting one since the size is 1 based, and the range is 0 base. There should be a better way to do
        // this but I haven't spent the time on that.
        int lastChunkSize = Convert.ToInt32(size % DefaultChunkSize) - numberOfChunks - 1; 
        if (lastChunkSize > 0) { numberOfChunks++; }

        // Create a file stream to contain the downloaded file.
        using (FileStream fileStream = System.IO.File.Create((@"C:\Temp\" + driveItemInfo.Name)))
        {
            for (int i = 0; i < numberOfChunks; i++)
            {
                // Setup the last chunk to request. This will be called at the end of this loop.
                if (i == numberOfChunks - 1)
                {
                    ChunkSize = lastChunkSize;
                }

                // Create the request message with the download URL and Range header.
                HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, (string)downloadUrl);
                req.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(offset, ChunkSize + offset);

                // We can use the the client library to send this although it does add an authentication cost.
                // HttpResponseMessage response = await graphClient.HttpProvider.SendAsync(req);
                // Since the download URL is preauthenticated, and we aren't deserializing objects, 
                // we'd be better to make the request with HttpClient.
                var client = new HttpClient();
                HttpResponseMessage response = await client.SendAsync(req);

                using (Stream responseStream = await response.Content.ReadAsStreamAsync())
                {
                    bytesInStream = new byte[ChunkSize];
                    int read;
                    do
                    {
                        read = responseStream.Read(bytesInStream, 0, (int)bytesInStream.Length);
                        if (read > 0)
                            fileStream.Write(bytesInStream, 0, bytesInStream.Length);
                    }
                    while (read > 0);
                }
                offset += ChunkSize + 1; // Move the offset cursor to the next chunk.
            }
        }
        return;
    }
}

答案 1 :(得分:0)

上面提供的代码包含错误

if (read > 0)
   fileStream.Write(bytesInStream, 0, bytesInStream.Length);

哪个不正确,应该是:

if (read > 0)
   fileStream.Write(bytesInStream, 0, read);

并且有点笨拙

int lastChunkSize = Convert.ToInt32(size % DefaultChunkSize) - numberOfChunks - 1;

我重构了一下,决定在这里发表。

更正代码

private int DefaultChunkSize = 5 * 1024 * 1024;//5MB
private int BufferSize = 4096;

...

int chunkSize = DefaultChunkSize;
long offset = 0; // cursor location for updating the Range header.                
byte[] buffer = new byte[BufferSize];
var driveItemInfo = await _api.Drive.Root.ItemWithPath(path).Request().GetAsync();
object downloadUrl;
driveItemInfo.AdditionalData.TryGetValue("@content.downloadUrl", out downloadUrl);
long size = (long) driveItemInfo.Size;

int numberOfChunks = Convert.ToInt32(size / DefaultChunkSize);

// We are incrementing the offset cursor after writing the response stream to a file after each chunk.                 
int lastChunkSize = Convert.ToInt32(size % DefaultChunkSize);
if (lastChunkSize > 0) {
 numberOfChunks++;
}
for (int i = 0; i < numberOfChunks; i++) {
 // Setup the last chunk to request. This will be called at the end of this loop.               if (i == numberOfChunks - 1)
 {
  chunkSize = lastChunkSize;
 }
 //Create the request message with the download URL and Range header.
 HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, (string) downloadUrl);
 //-1 because range is zero based
 request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(offset, chunkSize + offset - 1);
 // We can use the the client library to send this although it does add an authentication cost.
 // HttpResponseMessage response = await graphClient.HttpProvider.SendAsync(req);
 // Since the download URL is preauthenticated, and we aren't deserializing objects, 
 // we'd be better to make the request with HttpClient.
 var client = new HttpClient();
 HttpResponseMessage response = await client.SendAsync(request);
 int totalRead = 0;
 using(Stream responseStream = await response.Content.ReadAsStreamAsync()) {
  int read;
  while ((read = await responseStream.ReadAsync(buffer: buffer, offset: 0, count: buffer.Length)) > 0) {
   stream.Write(buffer, 0, read);
   totalRead += read;
  }
 }
 offset += totalRead; // Move the offset cursor to the next chunk.
}