/// <summary> /// Checks whether the authorization allows uploading to the specific bucket and path (without accessing the B2 API.) /// </summary> /// <param name="auth">The authorization response.</param> /// <param name="bucketId">The bucket to upload to.</param> /// <param name="destinationPath">The path of the file that will be uploaded.</param> /// <param name="error">Will be set to a non-null value on failure.</param> /// <returns>True if we have authorization for uploading, otherwise, false. Iff false, <c>error</c> will be set /// to an error message describing why there is no permission.</returns> private static bool IsAuthorizedForUpload(B2Authorization auth, string bucketId, string destinationPath, out string error) { string allowedBucketId = auth.allowed?.bucketId; if (allowedBucketId != null && bucketId != allowedBucketId) { DebugHelper.WriteLine($"B2 uploader: Error, user is only allowed to access '{allowedBucketId}', " + $"but user is trying to access '{bucketId}'."); error = "No permission to upload to this bucket. Are you using the right application key?"; return(false); } string allowedPrefix = auth.allowed?.namePrefix; if (allowedPrefix != null && !destinationPath.StartsWith(allowedPrefix)) { DebugHelper.WriteLine($"B2 uploader: Error, key is restricted to prefix '{allowedPrefix}'."); error = "Your upload path conflicts with the key's name prefix setting."; return(false); } List <string> caps = auth.allowed?.capabilities; if (caps != null && !caps.Contains("writeFiles")) { DebugHelper.WriteLine($"B2 uploader: No permission to write to '{bucketId}'."); error = "Your key does not allow uploading to this bucket."; return(false); } error = null; return(true); }
/// <summary> /// Gets a <see cref="B2UploadUrl"/> for the given bucket. Requires <c>writeFile</c> permission. /// </summary> /// <param name="auth">The B2 API authorization.</param> /// <param name="bucketId">The bucket ID to get an upload URL for.</param> /// <param name="error">Will be set to a non-null value on failure.</param> /// <returns>Null if an error occurs, and <c>error</c> will contain an error message. Otherwise, a <see cref="B2UploadUrl"/></returns> private B2UploadUrl B2ApiGetUploadUrl(B2Authorization auth, string bucketId, out string error) { NameValueCollection headers = new NameValueCollection() { ["Authorization"] = auth.authorizationToken }; Dictionary <string, string> reqBody = new Dictionary <string, string> { ["bucketId"] = bucketId }; using (Stream data = CreateJsonBody(reqBody)) { using (HttpWebResponse res = GetResponse(HttpMethod.POST, auth.apiUrl + B2GetUploadUrlPath, contentType: ApplicationJson, headers: headers, data: data, allowNon2xxResponses: true)) { if (res.StatusCode != HttpStatusCode.OK) { error = StringifyB2Error(res); return(null); } string body = UploadHelpers.ResponseToString(res); error = null; return(JsonConvert.DeserializeObject <B2UploadUrl>(body)); } } }
/// <summary> /// Gets the bucket ID for the given bucket name. Requires <c>listBuckets</c> permission. /// </summary> /// <param name="auth">The B2 API authorization.</param> /// <param name="bucketName">The bucket to get the ID for.</param> /// <param name="error">Will be set to a non-null value on failure.</param> /// <returns>Null if an error occurs, and <c>error</c> will contain an error message. Otherwise, the bucket ID.</returns> private string B2ApiGetBucketId(B2Authorization auth, string bucketName, out string error) { var headers = new NameValueCollection() { ["Authorization"] = auth.authorizationToken }; var reqBody = new Dictionary <string, string> { ["accountId"] = auth.accountId, ["bucketName"] = bucketName }; using (var data = CreateJsonBody(reqBody)) { using (var res = GetResponse(HttpMethod.POST, auth.apiUrl + B2ListBucketsPath, contentType: ApplicationJson, headers: headers, data: data, allowNon2xxResponses: true)) { if (res.StatusCode != HttpStatusCode.OK) { error = StringifyB2Error(res); return(null); } var body = ResponseToString(res); JObject json; try { json = JObject.Parse(body); } catch (JsonException e) { DebugHelper.WriteLine($"B2 uploader: Could not parse b2_list_buckets response: {e}"); error = "B2 upload failed: Couldn't parse b2_list_buckets response."; return(null); } var bucketId = json .SelectToken("buckets") ?.FirstOrDefault(b => b["bucketName"].ToString() == bucketName) ?.SelectToken("bucketId")?.ToString() ?? ""; if (!string.IsNullOrWhiteSpace(bucketId)) { error = null; return(bucketId); } error = $"B2 upload failed: Couldn't find bucket {bucketName}."; 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); }