Skip to content

Commit

Permalink
Stream uploading
Browse files Browse the repository at this point in the history
  • Loading branch information
coryrwest committed Apr 21, 2020
1 parent 629ccba commit 07d3f74
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 2 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

B2.NET is a C# client for the [Backblaze B2 Cloud Storage](https://secure.backblaze.com/b2/) service.

B2.NET is still in Beta, so use it in production at your own risk.
While the core of B2.NET is mature you should still consider this library in Beta, so use it in production at your own risk.

[B2 Documentation](https://www.backblaze.com/b2/docs/)

## Features

* Full implementation of the B2 REST API (except Keys management)
* Suport for uploading Streams
* Support for file FriendlyURL's (this functionality is not part of the supported B2 api and may break at any time)
* UFT-8 and Url Encoding support
* Fully Async
Expand Down Expand Up @@ -226,6 +227,23 @@ var file = await client.Files.Upload("FILEDATABYTES", "FILENAME", "CONTENTTYPE",
// FileInfo: Dictionary<string,string> }
```

#### Upload a file via Stream
Please note that there are ceratin limitations when using Streams for upload. Firstly, If you want to use SHA1 hash verification on your uploads
you will have to append the SHA1 to the end of your data stream. The library will not do this for you. It is up to you to decide how to get the SHA1
based on the type of stream you have and how you are handling it. Secondly, you may disable SHA1 verification on the upload by setting the `dontSHA`
flag to true.
```csharp
var client = new B2Client("KEYID", "APPLICATIONKEY");
var uploadUrl = await client.Files.GetUploadUrl("BUCKETID");
var file = await client.Files.Upload("FILESTREAM", "FILENAME", "CONTENTTYPE", uploadUrl, "AUTORETRY", dontSHA, "BUCKETID", "FILEINFOATTRS");
// { FileId: "",
// FileName: "",
// ContentLength: "",
// ContentSHA1: "",
// ContentType: "",
// FileInfo: Dictionary<string,string> }
```

#### Download a file by id
```csharp
var client = new B2Client("KEYID", "APPLICATIONKEY");
Expand Down Expand Up @@ -349,6 +367,7 @@ should retry the request if you are so inclined.

## Release Notes

* 0.7.5 Stream uploading.
* 0.7.4 Content-Type setting on Upload.
* 0.7.3 Thread-safeish HttpClient.
* 0.7.2 Async the authorize method, Added initialize path that does not call the API until told.
Expand Down
36 changes: 35 additions & 1 deletion src/Files.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using B2Net.Models;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
Expand Down Expand Up @@ -204,7 +205,40 @@ public Files(B2Options options) {
response.StatusCode == HttpStatusCode.RequestTimeout ||
response.StatusCode == HttpStatusCode.ServiceUnavailable)) {
Task.Delay(1000, cancelToken).Wait(cancelToken);
response = await _client.SendAsync(requestMessage, cancelToken);
var retryMessage = FileUploadRequestGenerators.Upload(_options, uploadUrl.UploadUrl, fileData, fileName, fileInfo, contentType);
response = await _client.SendAsync(retryMessage, cancelToken);
}

return await ResponseParser.ParseResponse<B2File>(response, _api);
}

/// <summary>
/// Uploads one file to B2 using a stream, returning its unique file ID. Filename will be URL Encoded. If auto retry
/// is set true it will retry a failed upload once after 1 second. If you don't want to use a SHA1 for the stream set dontSHA.
/// </summary>
/// <param name="fileDataWithSHA"></param>
/// <param name="fileName"></param>
/// <param name="uploadUrl"></param>
/// <param name="contentType"></param>
/// <param name="autoRetry"></param>
/// <param name="bucketId"></param>
/// <param name="fileInfo"></param>
/// <param name="dontSHA"></param>
/// <param name="cancelToken"></param>
/// <returns></returns>
public async Task<B2File> Upload(Stream fileDataWithSHA, string fileName, B2UploadUrl uploadUrl, string contentType, bool autoRetry, string bucketId = "", Dictionary<string, string> fileInfo = null, bool dontSHA = false, CancellationToken cancelToken = default(CancellationToken)) {
// Now we can upload the file
var requestMessage = FileUploadRequestGenerators.Upload(_options, uploadUrl.UploadUrl, fileDataWithSHA, fileName, fileInfo, contentType, dontSHA);

var response = await _client.SendAsync(requestMessage, cancelToken);
// Auto retry
if (autoRetry && (
response.StatusCode == (HttpStatusCode)429 ||
response.StatusCode == HttpStatusCode.RequestTimeout ||
response.StatusCode == HttpStatusCode.ServiceUnavailable)) {
Task.Delay(1000, cancelToken).Wait(cancelToken);
var retryMessage = FileUploadRequestGenerators.Upload(_options, uploadUrl.UploadUrl, fileDataWithSHA, fileName, fileInfo, contentType, dontSHA);
response = await _client.SendAsync(retryMessage, cancelToken);
}

return await ResponseParser.ParseResponse<B2File>(response, _api);
Expand Down
40 changes: 40 additions & 0 deletions src/Http/RequestGenerators/FileUploadRequestGenerators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using B2Net.Http.RequestGenerators;
using B2Net.Models;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Newtonsoft.Json;

namespace B2Net.Http {
Expand Down Expand Up @@ -52,6 +54,44 @@ public static HttpRequestMessage Upload(B2Options options, string uploadUrl, byt
return request;
}

/// <summary>
/// Upload a file to B2 using a stream. NOTE: You MUST provide the SHA1 at the end of your stream. This method will NOT do it for you.
/// </summary>
/// <param name="options"></param>
/// <param name="uploadUrl"></param>
/// <param name="fileData"></param>
/// <param name="fileName"></param>
/// <param name="fileInfo"></param>
/// <returns></returns>
public static HttpRequestMessage Upload(B2Options options, string uploadUrl, Stream fileDataWithSHA, string fileName, Dictionary<string, string> fileInfo, string contentType = "", bool dontSHA = false) {
var uri = new Uri(uploadUrl);
var request = new HttpRequestMessage() {
Method = HttpMethod.Post,
RequestUri = uri,
Content = new StreamContent(fileDataWithSHA)
};

// Add headers
request.Headers.TryAddWithoutValidation("Authorization", options.UploadAuthorizationToken);
request.Headers.Add("X-Bz-File-Name", fileName.b2UrlEncode());
// Stream puts the SHA1 at the end of the content
request.Headers.Add("X-Bz-Content-Sha1", dontSHA ? "do_not_verify" : "hex_digits_at_end");
// File Info headers
if (fileInfo != null && fileInfo.Count > 0) {
foreach (var info in fileInfo.Take(10)) {
request.Headers.Add($"X-Bz-Info-{info.Key}", info.Value);
}
}
// TODO last modified
//request.Headers.Add("X-Bz-src_last_modified_millis", hash);

request.Content.Headers.ContentType = new MediaTypeHeaderValue(string.IsNullOrWhiteSpace(contentType) ? "b2/x-auto" : contentType);
// SHA will be in Stream already
request.Content.Headers.ContentLength = fileDataWithSHA.Length;

return request;
}

public static HttpRequestMessage GetUploadUrl(B2Options options, string bucketId) {
var json = JsonConvert.SerializeObject(new { bucketId });
return BaseRequestGenerator.PostRequest(Endpoints.GetUploadUrl, json, options);
Expand Down
2 changes: 2 additions & 0 deletions src/IFiles.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using B2Net.Models;
Expand All @@ -23,6 +24,7 @@ public interface IFiles {
Task<B2File> Upload(byte[] fileData, string fileName, B2UploadUrl uploadUrl, string bucketId = "", Dictionary<string, string> fileInfo = null, CancellationToken cancelToken = default(CancellationToken));
Task<B2File> Upload(byte[] fileData, string fileName, B2UploadUrl uploadUrl, bool autoRetry, string bucketId = "", Dictionary<string, string> fileInfo = null, CancellationToken cancelToken = default(CancellationToken));
Task<B2File> Upload(byte[] fileData, string fileName, B2UploadUrl uploadUrl, string contentType, bool autoRetry, string bucketId = "", Dictionary<string, string> fileInfo = null, CancellationToken cancelToken = default(CancellationToken));
Task<B2File> Upload(Stream fileDataWithSHA, string fileName, B2UploadUrl uploadUrl, string contentType, bool autoRetry, string bucketId = "", Dictionary<string, string> fileInfo = null, bool dontSHA = false, CancellationToken cancelToken = default(CancellationToken));
Task<B2File> Copy(string sourceFileId, string newFileName, B2MetadataDirective metadataDirective = B2MetadataDirective.COPY, string contentType = "", Dictionary<string, string> fileInfo = null, string range = "", string destinationBucketId = "", CancellationToken cancelToken = default(CancellationToken));
}
}
38 changes: 38 additions & 0 deletions tests/FileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace B2Net.Tests {
Expand Down Expand Up @@ -192,6 +193,43 @@ public void FileUploadWithInfoTest() {
Assert.AreEqual(hash, file.ContentSHA1, "File hashes did not match.");
Assert.AreEqual(1, file.FileInfo.Count, "File info count was off.");
}

[TestMethod]
public void FileUploadStreamTest() {
var fileName = "B2Test.txt";
var bytes = File.ReadAllBytes(Path.Combine(FilePath, fileName));

string hash = Utilities.GetSHA1Hash(bytes);
var hashBytes = Encoding.UTF8.GetBytes(hash);

var fileData = new MemoryStream(bytes.Concat(hashBytes).ToArray());

var uploadUrl = Client.Files.GetUploadUrl(TestBucket.BucketId).Result;

var file = Client.Files.Upload(fileData, fileName, uploadUrl, "", false, TestBucket.BucketId).Result;

// Clean up.
FilesToDelete.Add(file);

Assert.AreEqual(hash, file.ContentSHA1, "File hashes did not match.");
}

[TestMethod]
public void FileUploadStreamNoSHATest() {
var fileName = "B2Test.txt";
var bytes = File.ReadAllBytes(Path.Combine(FilePath, fileName));

var fileData = new MemoryStream(bytes);

var uploadUrl = Client.Files.GetUploadUrl(TestBucket.BucketId).Result;

var file = Client.Files.Upload(fileData, fileName, uploadUrl, "", false, TestBucket.BucketId, null, true).Result;

// Clean up.
FilesToDelete.Add(file);

Assert.IsTrue(file.ContentSHA1.StartsWith("unverified"), $"File was verified when it should not have been: {file.ContentSHA1}.");
}

[TestMethod]
public void FileDownloadNameTest() {
Expand Down

0 comments on commit 07d3f74

Please sign in to comment.