in Common/WorkItemTrackingHelpers.cs [114:183]
public async static Task<AttachmentReference> CreateAttachmentChunkedAsync(WorkItemTrackingHttpClient client, VssConnection connection, MemoryStream uploadStream, int chunkSizeInBytes)
{
// it's possible for the attachment to be empty, if so we can't used the chunked upload and need
// to fallback to the normal upload path.
if (uploadStream.Length == 0)
{
return await CreateAttachmentAsync(client, uploadStream);
}
var requestSettings = new VssHttpRequestSettings
{
SendTimeout = TimeSpan.FromMinutes(5)
};
var httpClient = new HttpClient(new VssHttpMessageHandler(connection.Credentials, requestSettings));
// first create the attachment reference.
// can't use the WorkItemTrackingHttpClient since it expects either a file or a stream.
var attachmentReference = await RetryHelper.RetryAsync(async () =>
{
var request = new HttpRequestMessage(HttpMethod.Post, $"{connection.Uri}/_apis/wit/attachments?uploadType=chunked&api-version=3.2");
var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsAsync<AttachmentReference>();
}
else
{
var exceptionResponse = await response.Content.ReadAsAsync<ExceptionResponse>();
throw new Exception(exceptionResponse.Message);
}
}, 5);
// now send up each chunk
var totalNumberOfBytes = uploadStream.Length;
// if number of chunks divides evenly, no need to add an extra chunk
var numberOfChunks = ClientHelpers.GetBatchCount(totalNumberOfBytes, chunkSizeInBytes);
for (var i = 0; i < numberOfChunks; i++)
{
var chunkBytes = new byte[chunkSizeInBytes];
// offset is always 0 since read moves position forward
var chunkLength = uploadStream.Read(chunkBytes, 0, chunkSizeInBytes);
var result = await RetryHelper.RetryAsync(async () =>
{
// manually create the request since the WorkItemTrackingHttpClient does not support chunking
var content = new ByteArrayContent(chunkBytes, 0, chunkLength);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Headers.ContentLength = chunkLength;
content.Headers.ContentRange = new ContentRangeHeaderValue(i * chunkSizeInBytes, i * chunkSizeInBytes + chunkLength - 1, totalNumberOfBytes);
var chunkRequest = new HttpRequestMessage(HttpMethod.Put, $"{connection.Uri}/_apis/wit/attachments/" + attachmentReference.Id + "?uploadType=chunked&api-version=3.2") { Content = content };
var chunkResponse = await httpClient.SendAsync(chunkRequest);
if (!chunkResponse.IsSuccessStatusCode)
{
// there are two formats for the exception, so detect both.
var responseContentAsString = await chunkResponse.Content.ReadAsStringAsync();
var criticalException = JsonConvert.DeserializeObject<CriticalExceptionResponse>(responseContentAsString);
var exception = JsonConvert.DeserializeObject<ExceptionResponse>(responseContentAsString);
throw new Exception(criticalException.Value?.Message ?? exception.Message);
}
return chunkResponse.StatusCode;
}, 5);
}
return attachmentReference;
}