/// <summary> /// Given a <see cref="B2UploadUrl"/> returned from the API, attempts to upload a file. /// </summary> /// <param name="b2UploadUrl">Information returned by the <c>b2_get_upload_url</c> API.</param> /// <param name="destinationPath">The remote path to upload to.</param> /// <param name="file">The file to upload.</param> /// <returns> /// A B2UploadResult(HTTP status, B2Error, B2Upload) that can be decomposed as follows: /// /// <ul> /// <li><b>If successful:</b> <c>(200, null, B2Upload)</c></li> /// <li><b>If unsuccessful:</b> <c>(HTTP status, B2Error, null)</c></li> /// <li><b>If the connection failed:</b> <c>(-1, null, null)</c></li> /// </ul> /// </returns> private B2UploadResult B2ApiUploadFile(B2UploadUrl b2UploadUrl, string destinationPath, Stream file) { // we want to send 'Content-Disposition: inline; filename="screenshot.png"' // this should display the uploaded data inline if possible, but if that fails, present a sensible filename // conveniently, this class will handle this for us ContentDisposition contentDisposition = new ContentDisposition("inline") { FileName = URLHelpers.GetFileName(destinationPath) }; DebugHelper.WriteLine($"B2 uploader: Content disposition is '{contentDisposition}'."); // compute SHA1 hash without loading the file fully into memory string sha1Hash; using (SHA1CryptoServiceProvider cryptoProvider = new SHA1CryptoServiceProvider()) { file.Seek(0, SeekOrigin.Begin); byte[] bytes = cryptoProvider.ComputeHash(file); sha1Hash = BitConverter.ToString(bytes).Replace("-", "").ToLower(); file.Seek(0, SeekOrigin.Begin); } DebugHelper.WriteLine($"B2 uploader: SHA1 hash is '{sha1Hash}'."); // it's showtime // https://www.backblaze.com/b2/docs/b2_upload_file.html NameValueCollection headers = new NameValueCollection() { ["Authorization"] = b2UploadUrl.authorizationToken, ["X-Bz-File-Name"] = URLHelpers.URLEncode(destinationPath), ["Content-Length"] = file.Length.ToString(), ["X-Bz-Content-Sha1"] = sha1Hash, ["X-Bz-Info-src_last_modified_millis"] = DateTimeOffset.Now.ToUnixTimeMilliseconds().ToString(), ["X-Bz-Info-b2-content-disposition"] = URLHelpers.URLEncode(contentDisposition.ToString()), }; string contentType = UploadHelpers.GetMimeType(destinationPath); using (HttpWebResponse res = GetResponse(HttpMethod.POST, b2UploadUrl.uploadUrl, contentType: contentType, headers: headers, data: file, allowNon2xxResponses: true)) { // if connection failed, res will be null, and here we -do- want to check explicitly for this // since the server might be down if (res == null) { return(new B2UploadResult(-1, null, null)); } if (res.StatusCode != HttpStatusCode.OK) { return(new B2UploadResult((int)res.StatusCode, ParseB2Error(res), null)); } string body = UploadHelpers.ResponseToString(res); DebugHelper.WriteLine($"B2 uploader: B2ApiUploadFile() reports success! '{body}'"); return(new B2UploadResult((int)res.StatusCode, null, JsonConvert.DeserializeObject <B2Upload>(body))); } }
/// <summary> /// Uploads the locally-stored database <paramref name="localDb" /> to the bucket that <paramref name="client" /> has access to. /// </summary> /// <param name="client">The <see cref="B2Client" /> created by <see cref="GetClient" /> with access to a bucket to upload to.</param> /// <param name="localDb">The local database to upload.</param> /// <returns><see langword="true" /> if the upload was successful, or <see langword="false" /> otherwise.</returns> public static async Task <bool> UploadDbAsync(B2Client client, PwDatabase localDb) { if (client == null) { return(false); } Interface.UpdateStatus("Uploading database..."); string localPath = localDb.IOConnectionInfo.Path; byte[] fileData; using (FileStream fs = File.OpenRead(localPath)) { if (!fs.CanRead) { return(false); } using (MemoryStream ms = new MemoryStream()) { fs.CopyTo(ms); fileData = ms.ToArray(); } } try { B2UploadUrl uploadUrl = await client.Files.GetUploadUrl(client.Capabilities.BucketId); B2File file = await client.Files.Upload(fileData, Path.GetFileName(localPath), uploadUrl, true, client.Capabilities.BucketId); } catch (Exception e) { if (new [] { typeof(SocketException), typeof(WebException), typeof(HttpRequestException), typeof(AggregateException), typeof(InvalidOperationException) }.Contains(e.GetType())) { Interface.UpdateStatus("Unable to upload the database to B2."); return(false); } throw; } Interface.UpdateStatus("Database upload successful."); return(true); }
/// <summary> /// Uploads one file to B2, 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. /// </summary> /// <param name="fileData"></param> /// <param name="fileName"></param> /// <param name="uploadUrl"></param> /// <param name="bucketId"></param> /// <param name="autoRetry">Retry a failed upload one time after waiting for 1 second.</param> /// <param name="fileInfo"></param> /// <param name="cancelToken"></param> /// <returns></returns> public async Task <B2File> Upload(byte[] fileData, string fileName, B2UploadUrl uploadUrl, bool autoRetry, string bucketId = "", Dictionary <string, string> fileInfo = null, CancellationToken cancelToken = default(CancellationToken)) { // Now we can upload the file var requestMessage = FileUploadRequestGenerators.Upload(_options, uploadUrl.UploadUrl, fileData, fileName, fileInfo); 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); response = await _client.SendAsync(requestMessage, cancelToken); } return(await ResponseParser.ParseResponse <B2File>(response, _api)); }
/// <summary> /// Uploads a temporary file. /// </summary> /// <param name="fileData">The file data.</param> /// <param name="fileName">Name of the file.</param> public static async Task <File> UploadTemporaryFileAsync(byte[] fileData, string fileName) { await Authorize(); B2UploadUrl uploadUrl = await _TempBucketClient.Files.GetUploadUrl(); string sha1Hash = Utilities.GetSHA1Hash(fileData); B2File uploadedFile = await _TempBucketClient.Files.Upload(fileData, fileName, uploadUrl); if (sha1Hash == uploadedFile.ContentSHA1) { string fullUrl = $"{Constants.BackblazeCDN}/file/{SettingsManager.Configuration.BackblazeTempBucket.BucketName}/{uploadedFile.FileName}"; string shortUrl = await Http.ShortenUrl(fullUrl); return(new File(SettingsManager.Configuration.BackblazeTempBucket.BucketName, uploadedFile.FileName, fullUrl, shortUrl)); } return(null); }
public override UploadResult Upload(Stream stream, string fileName) { string parsedUploadPath = NameParser.Parse(NameParserType.FolderPath, UploadPath); string destinationPath = parsedUploadPath + fileName; // docs: https://www.backblaze.com/b2/docs/ // STEP 1: authorize, get auth token, api url, download url DebugHelper.WriteLine($"B2 uploader: Attempting to authorize as '{ApplicationKeyId}'."); B2Authorization auth = B2ApiAuthorize(ApplicationKeyId, ApplicationKey, out string authError); if (authError != null) { DebugHelper.WriteLine("B2 uploader: Failed to authorize."); Errors.Add($"Could not authenticate with B2: {authError}"); return(null); } DebugHelper.WriteLine($"B2 uploader: Authorized, using API server {auth.apiUrl}, download URL {auth.downloadUrl}"); // STEP 1.25: if we have an application key, there will be a bucketId present here, but if // not, we have an account key and need to find our bucket id ourselves string bucketId = auth.allowed?.bucketId; if (bucketId == null) { DebugHelper.WriteLine("B2 uploader: Key doesn't have a bucket ID set, so I'm looking for a bucket ID."); string newBucketId = B2ApiGetBucketId(auth, BucketName, out string getBucketError); if (getBucketError != null) { DebugHelper.WriteLine($"B2 uploader: It's {newBucketId}."); bucketId = newBucketId; } } // STEP 1.5: verify whether we can write to the bucket user wants to write to, with the given prefix DebugHelper.WriteLine("B2 uploader: Checking clientside whether we have permission to upload."); bool authCheckOk = IsAuthorizedForUpload(auth, bucketId, destinationPath, out string authCheckError); if (!authCheckOk) { DebugHelper.WriteLine("B2 uploader: Key is not suitable for this upload."); Errors.Add($"B2 upload failed: {authCheckError}"); return(null); } // STEP 1.75: start upload attempt loop const int maxTries = 5; B2UploadUrl url = null; for (int tries = 1; tries <= maxTries; tries++) { string newOrSameUrl = url == null ? "New URL." : "Same URL."; DebugHelper.WriteLine($"B2 uploader: Upload attempt {tries} of {maxTries}. {newOrSameUrl}"); // sloppy, but we need exponential backoff somehow and we are not in async code // since B2Uploader should have the thread to itself, and this just occurs on rare failures, // this should be OK if (tries > 1) { int delay = (int)Math.Pow(2, tries - 1) * 1000; DebugHelper.WriteLine($"Waiting ${delay} ms for backoff."); Thread.Sleep(delay); } // STEP 2: get upload url that we need to POST to in step 3 if (url == null) { DebugHelper.WriteLine("B2 uploader: Getting new upload URL."); url = B2ApiGetUploadUrl(auth, bucketId, out string getUrlError); if (getUrlError != null) { // this is guaranteed to be unrecoverable, so bail out DebugHelper.WriteLine("B2 uploader: Got error trying to get upload URL."); Errors.Add("Could not get B2 upload URL: " + getUrlError); return(null); } } // STEP 3: upload file and see if anything went wrong DebugHelper.WriteLine($"B2 uploader: Uploading to URL {url.uploadUrl}"); B2UploadResult uploadResult = B2ApiUploadFile(url, destinationPath, stream); HashSet <string> expiredTokenCodes = new HashSet <string>(new List <string> { "expired_auth_token", "bad_auth_token" }); if (uploadResult.RC == -1) { // magic number for "connection failed", should also happen when upload // caps are exceeded DebugHelper.WriteLine("B2 uploader: Connection failed, trying with new URL."); url = null; continue; } else if (uploadResult.RC == 401 && expiredTokenCodes.Contains(uploadResult.Error.code)) { // Unauthorized, our token expired DebugHelper.WriteLine("B2 uploader: Upload auth token expired, trying with new URL."); url = null; continue; } else if (uploadResult.RC == 408) { DebugHelper.WriteLine("B2 uploader: Request Timeout, trying with same URL."); continue; } else if (uploadResult.RC == 429) { DebugHelper.WriteLine("B2 uploader: Too Many Requests, trying with same URL."); continue; } else if (uploadResult.RC != 200) { // something else happened that wasn't a success, so bail out DebugHelper.WriteLine("B2 uploader: Unknown error, upload failure."); Errors.Add("B2 uploader: Unknown error occurred while calling b2_upload_file()."); return(null); } // success! // STEP 4: compose: // the download url (e.g. "https://f567.backblazeb2.com") // /file/$bucket/$uploadPath // or // $customUrl/$uploadPath string remoteLocation = URLHelpers.CombineURL(auth.downloadUrl, "file", URLHelpers.URLEncode(BucketName), uploadResult.Upload.fileName); DebugHelper.WriteLine($"B2 uploader: Successful upload! File should be at: {remoteLocation}"); if (UseCustomUrl) { string parsedCustomUrl = NameParser.Parse(NameParserType.FolderPath, CustomUrl); remoteLocation = parsedCustomUrl + uploadResult.Upload.fileName; DebugHelper.WriteLine($"B2 uploader: But user requested custom URL, which will be: {remoteLocation}"); } return(new UploadResult() { IsSuccess = true, URL = remoteLocation }); } DebugHelper.WriteLine("B2 uploader: Ran out of attempts, aborting."); Errors.Add($"B2 upload failed: Could not upload file after {maxTries} attempts."); return(null); }
/// <summary> /// Uploads one file to B2, returning its unique file ID. Filename will be URL Encoded. /// </summary> /// <param name="fileData"></param> /// <param name="fileName"></param> /// <param name="bucketId"></param> /// <param name="cancelToken"></param> /// <returns></returns> public async Task <B2File> Upload(byte[] fileData, string fileName, B2UploadUrl uploadUrl, string bucketId = "", Dictionary <string, string> fileInfo = null, CancellationToken cancelToken = default(CancellationToken)) { return(await Upload(fileData, fileName, uploadUrl, false, bucketId, fileInfo, cancelToken)); }