Ejemplo n.º 1
0
        /// <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);
        }
Ejemplo n.º 2
0
        /// <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));
                }
            }
        }
Ejemplo n.º 3
0
        /// <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);
                }
            }
        }
Ejemplo n.º 4
0
        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);
        }