def get_download_auth(self, file_path_or_prefix: str, auth_duration: int, bucket_id: str = '') -> str: """ Gets an authorization token for downloading specified files. :param file_path: """ # duration = Min duration is 1 second, max is 604800 (one week). self._ensure_auth() data = { 'bucketId': bucket_id if bucket_id else self.bucket_id, 'fileNamePrefix': file_path_or_prefix, 'validDurationInSeconds': auth_duration, } response = self._http.post(self._make_url( Endpoints.download_auth.value), json=data, headers=self._headers()) data = json_loads(response.text) check_b2_errors( data, 'Failed to get download auth for prefix "{}" on bucket "{}": {}.'. format(file_path_or_prefix, data['bucketId'], data.get('message', ''))) return data['authorizationToken']
def create_key(self, account_id: str, capabilities: List[KeyCapabilities], key_name: str, bucket_id: str = '', prefix: str = '', duration: int = 0) -> Json: self._ensure_auth() params = { 'accountId': account_id, 'capabilities': [item.value for item in capabilities], 'keyName': key_name, } if duration: params['validDurationInSeconds'] = duration if bucket_id: params['bucketId'] = bucket_id if prefix: params['namePrefix'] = prefix response = self._http.post(self._make_url(Endpoints.create_key.value), json=params, headers=self._headers()) data = json_loads(response.text) check_b2_errors( data, 'Failed to create key "{}": {}.'.format(key_name, data.get('message', ''))) return data
def upload_part(self, data: bytes, upload_url: str, part_number: int, auth_token: str) -> Json: """ Uploads part of a large file. :param data: The partial file data being uploaded. :param upload_url: The URL used to upload the partial file data. :param part_number: The number of the part. Be aware that part numbers start at 1, not 0! :param auth_token: The authorization token returned by BackBlazeB2.get_upload_part_url(). :returns: A json-like 6-dict containing the keys: fileId, partNumber, contentLength, contentSha1, contentMd5, uploadTimestamp. .. seealso:: `Reference <https://www.backblaze.com/b2/docs/b2_upload_part.html>`_. """ self._ensure_auth() headers = { 'Authorization': auth_token, 'X-Bz-Part-Number': str(part_number), 'Content-Length': str(len(data)), 'X-Bz-Content-Sha1': sha1(data).hexdigest(), } response = self._http.post(upload_url, data=data, headers=headers, timeout=None) result = json_loads(response.text) check_b2_errors( result, f'Failed to upload file part number #{part_number}: {result}') return result
def start_large_file(self, bucket_id: str, file_name: str, content_type: str = 'b2/x-auto', file_info: Optional[Json] = None) -> Json: """ :returns: A json-like 11-dict containing the keys: accountId, action, bucketId, contentType, contentLength, contentSha1, contentMd5, fileId, fileInfo, fileName, uploadTimestamp. .. seealso:: `Reference <https://www.backblaze.com/b2/docs/b2_start_large_file.html>`_. """ self._ensure_auth() params = { 'bucketId': bucket_id, 'fileName': quote(file_name), 'contentType': content_type, 'fileInfo': file_info, } response = self._http.post(self._make_url( Endpoints.start_large_file.value), json=params, headers=self._headers()) result = json_loads(response.text) check_b2_errors(result, f'Failed to start large file upload: {result}') return result
def list_buckets(self, bucket_id: Optional[str] = None, bucket_name: Optional[str] = None, bucket_types: Optional[str] = None) -> Json: """ Lists all buckets associated with the authenticated account. This method can be used to get a single bucket information by passing the 'bucket_id' or 'bucket_name' parameters. :param bucket_id: If specified, will fetch only the bucket with that ID. If the bucket is not found, the server will return an empty list. :param bucket_name: If specified, will fetch only the bucket with that name. If the bucket is not found, the server will return an empty list. :param bucket_types: Used to filter private or public buckets. Valid values are ('allPublic', 'allPrivate', 'snapshot', 'all'). :returns: A dict with the json-encoded response data. :raises RequestError: If the user is not authenticated. :raises ResponseError: If the server returned an error. """ self._ensure_auth() params = self._base_params() params.update({ 'bucketId': bucket_id, 'bucketName': bucket_name, 'bucketTypes': bucket_types, }) response = self._http.post(self._make_url( Endpoints.list_buckets.value), json=params, headers=self._headers()) data = json_loads(response.text) check_b2_errors(data, f'Failed to list buckets ({data}).') return data
def test_check_b2_errors__invalid_response__raises_response_error(self): """ The check_b2_errors() raises a RequestError if an error is found. """ data = {'status': 400} message = 'error raised' try: utils.check_b2_errors(data, message) except ResponseError as error: self.assertEqual(str(error), message)
def delete_key(self, key_id: str) -> Json: self._ensure_auth() response = self._http.post(self._make_url(Endpoints.delete_key.value), json={'applicationKeyId': key_id}, headers=self._headers()) data = json_loads(response.text) check_b2_errors( data, 'Failed to delete key "{}": {}.'.format(key_id, data.get('message', ''))) return data
def create_bucket(self, bucket_name: str, private: bool, bucket_info: Optional[Json] = None, cors_rules: Optional[Json] = None, lifecycle_rules: Optional[Json] = None) -> Json: """ Creates a bucket to which you can upload files. Bucket creation is slow, thus this method can take a reasonably long time (30s+) to return. :param bucket_name: The name of the bucket to be created. :param private: True to set the bucket to private, False to leave it public. Private files can be downloaded by others if an authorization token is provided. :param bucket_info: User-defined information to be stored with the bucket. :param cors_rules: The initial CORS rules for the bucket. :param lifecycle_rules: The initial lifecycle rules for the bucket. :returns: A json-like 9-dict containing the keys: accountId, bucketId, bucketName, bucketType, bucketInfo, corsRules, lifecycleRules, revision, options. :raises RequestError: If the user is not authenticated. :raises RequestError: If the bucket name is not valid. .. seealso:: `Reference <https://www.backblaze.com/b2/docs/b2_create_bucket.html>`_. .. seealso:: `Buckets <https://www.backblaze.com/b2/docs/buckets.html>`_. .. seealso:: `CORS Rules <https://www.backblaze.com/b2/docs/cors_rules.html>`_. .. seealso:: `Lifecycle Rules <https://www.backblaze.com/b2/docs/lifecycle_rules.html>`_. """ self._ensure_auth() if not valid_bucket_name(bucket_name): raise RequestError(f'Invalid bucket name "{bucket_name}".') params = self._base_params() params.update({ 'bucketName': bucket_name, 'bucketType': self.BUCKET_TYPE_PRIVATE if private else self.BUCKET_TYPE_PUBLIC, 'bucketInfo': bucket_info, 'corsRules': cors_rules, 'lifecycleRules': lifecycle_rules, }) # Endpoint /b2_create_bucket can take a long time to respond, a larger timeout is required response = self._http.post(self._make_url( Endpoints.create_bucket.value), json=params, headers=self._headers(), timeout=90.0) data = json_loads(response.text) check_b2_errors(data, f'Failed to create bucket "{bucket_name}" ({data}).') return data
def list_files(self, prefix: Optional[str] = None, delimiter: Optional[str] = None, max_files: int = 0, start_name: str = '', bucket_id: Optional[str] = None) -> Json: """ Lists the files contained on a bucket and show their information. The file information shown for each file is the same as returned by "get_file_info()". See `b2_list_file_names <https://www.backblaze.com/b2/docs/b2_list_file_names.html>`_. :param prefix: Returns only files which names start with the specified prefix. :param delimiter: Used to list only files in a directory. :param max_files: Number of files to return in the response. Maximum is 10000, default 100. :param start_name: If a file matches this path, it will be the first file returned. :param bucket_id: The id of the bucket to have its files listed. If empty, will try to use the bucket set with BackBlazeB2.set_bucket(). :returns: A dict with the json-encoded response data. :raises RequestError: If the user is not authenticated. :example: >>> # List all files which name starts with 'foo' >>> BackBlazeB2().list_files(prefix='foo') >>> # List all files with the in the 'snafu' virtual folder >>> BackBlazeB2().list_files(prefix='snafu/', delimiter='/') """ self._ensure_auth() if not prefix or self.limited_account: prefix = self.prefix() params = { 'bucketId': bucket_id if bucket_id else self.bucket_id, 'prefix': prefix, 'delimiter': delimiter if delimiter is not None else self.delimiter, 'maxFileCount': max_files, 'startFileName': start_name, } response = self._http.post(self._make_url(Endpoints.list_files.value), json=params, headers=self._headers()) data = json_loads(response.text) check_b2_errors( data, 'Failed to get list files <prefix={}, delimiter={}, start_name={}, max_files={}, ' 'bucket_id={}> ({}).'.format(prefix, delimiter, start_name, max_files, bucket_id, data.get('message', ''))) return data
def get_upload_part_url(self, file_id: str) -> Json: """ :returns: A json-like 3-dict containing the keys: fileId, uploadUrl, authorizationToken. .. seealso:: `Reference <https://www.backblaze.com/b2/docs/b2_get_upload_part_url.html>`_. """ self._ensure_auth() response = self._http.post(self._make_url( Endpoints.get_upload_part_url.value), json={'fileId': file_id}, headers=self._headers()) result = json_loads(response.text) check_b2_errors( result, f'Failed to get upload part url for file "{file_id}": {result}') return result
def list_keys(self, account_id: str, max_key_count: int = 0, start_app_key_id: str = ''): self._ensure_auth() params = {'accountId': account_id} if max_key_count: params['maxKeyCount'] = max_key_count if start_app_key_id: params['startApplicationKeyId'] = start_app_key_id response = self._http.post(self._make_url(Endpoints.list_keys.value), json=params, headers=self._headers()) data = json_loads(response.text) check_b2_errors( data, 'Failed to list keys: {}.'.format(data.get('message', ''))) return data
def cancel_large_file(self, file_id: str) -> Json: """ Cancels a large file upload and deletes the parts that were already uploaded. :param file_id: The file id returned by BackBlazeB2.start_large_file(). :returns: A json-like 4-dict containing the keys: fileId, accountId, bucketId, fileName. .. seealso:: `Reference <https://www.backblaze.com/b2/docs/b2_cancel_large_file.html>`_. """ self._ensure_auth() response = self._http.post(self._make_url( Endpoints.cancel_large_file.value), json={'fileId': file_id}, headers=self._headers()) result = json_loads(response.text) check_b2_errors( result, f'Failed to cancel large file with id "{file_id}": {result}') return result
def authenticate(self, account_id: Optional[str] = None, app_key: Optional[str] = None) -> Json: """ Authenticates (or re-authenticates, if already authenticated) an account. If the "account_id" and "app_key" attributes were already set in the constructor, it's not necessary to specify them again. Specifying them will re-authenticate the account. IMPORTANT: When authenticating with a non-master application key, use the key id as the account id, otherwise the response will produce a 401 Unauthorized error. :param account_id: The account id to be used for authentication, or the key id if authenticating with a non-master application key. :param app_key: The secret value of the application key to be used for authentication. :returns: A dict with the json-encoded response data. :raises BlazeError: If failed to make the request to the server. :raises ResponseError: If the server returned an error. """ self.account_id = account_id or self.account_id self.app_key = app_key or self.app_key response = self._http.get(self.BASE_URL + Endpoints.auth.value, auth=HTTPBasicAuth(self.account_id, self.app_key)) data = json_loads(response.text) check_b2_errors( data, 'Failed to authenticate with BackBlaze (account_id={}, app_key={}) ({})' .format(self.account_id, self.app_key, data)) self.api_url = data['apiUrl'] self.auth_token = data['authorizationToken'] self.download_url = data['downloadUrl'] allowed = data.get('allowed', {}) self._capabilities = allowed.get('capabilities', []) allowed_bucket_id = allowed.get('bucketId', '') allowed_bucket_name = allowed.get('bucketName', '') if allowed_bucket_id: self._bucket_id = allowed_bucket_id self._limited_account = True if allowed_bucket_name: self._bucket_name = allowed_bucket_name self._limited_account = True if self.limited_account: self.set_prefix(allowed.get('namePrefix', '')) return data
def delete_file(self, file_id: str, file_path: str): """ Deletes a file from the bucket. Both the ID and the full path of the file on the server must be provided to delete the file. :param file_id: The id of the file in the backblaze service. :param file_path: The absolute path to the file in the backblaze service. """ self._ensure_auth() data = {'fileName': file_path, 'fileId': file_id} response = self._http.post(self._make_url(Endpoints.delete_file.value), json=data, headers=self._headers()) data = json_loads(response.text) check_b2_errors( data, 'Failed to delete file with id "{}" and path "{}" ({}).'.format( file_id, file_path, data.get('message', ''))) return data
def upload_file(self, data: bytes, upload_url: str, auth_token: str, file_name: str, content_type: str = '', last_modified_ms: Optional[int] = None, content_disposition: Optional[str] = None, language: Optional[str] = None, expires: Optional[str] = None, cache_control: Optional[str] = None, encoding: Optional[str] = None, content_type_header: Optional[str] = None, info: Optional[Dict[str, str]] = None) -> Json: self._ensure_auth() headers = { 'Authorization': auth_token, 'X-Bz-File-Name': quote(file_name), 'Content-Type': content_type if content_type else 'b2/x-auto', 'Content-Length': str(len(data)), 'X-Bz-Content-Sha1': sha1(data).hexdigest(), 'X-Bz-Info-src_last_modified_millis': last_modified_ms, 'X-Bz-Info-b2-content-disposition': content_disposition, 'X-Bz-Info-b2-content-language': language, 'X-Bz-Info-b2-expires': expires, 'X-Bz-Info-b2-cache-control': cache_control, 'X-Bz-Info-b2-content-encoding': encoding, 'X-Bz-Info-b2-content-type': content_type_header, } if info: headers.update({ f'X-Bz-Info-{key}': quote(value) for key, value in info.items() }) response = self._http.post(upload_url, data=data, headers=headers, timeout=None) result = json_loads(response.text) check_b2_errors(result, f'Failed to upload file "{file_name}": {result}') return result
def get_upload_url(self, bucket_id: str) -> Json: """ Gets the URL to upload to a bucket. :param bucket_id: The ID of the bucket to which files will be uploaded to. :returns: A json-like 3-dict containing the keys: bucketId, uploadUrl, authorizationToken. :raises ResponseError: If failed to obtain the upload information. """ self._ensure_auth() response = self._http.post(self._make_url( Endpoints.get_upload_url.value), json={'bucketId': bucket_id}, headers=self._headers()) result = json_loads(response.text) check_b2_errors(result, f'Failed to get uploading authorization: {result}.') self.upload_url = result['uploadUrl'] self.upload_token = result['authorizationToken'] self._bucket_id = bucket_id return result
def get_file_info(self, file_id: str) -> Json: """ Gets the information for a file on the server. The content of this response is the same as the one in each file listed by "list_files()". :param file_id: The ID given to the file when it was uploaded. :returns: A dict with the json-encoded response data. :raises RequestError: If the user is not authenticated. :raises ResponseError: If the server returned an error (e.g.: file does not exist). """ self._ensure_auth() response = self._http.post(self._make_url(Endpoints.file_info.value), json={'fileId': quote(file_id)}, headers=self._headers()) data = json_loads(response.text) check_b2_errors( data, 'Failed to get files info <file_id={}> ({}).'.format( file_id, data.get('message', ''))) return data
def delete_bucket(self, bucket_id: str) -> Json: """ Deletes a bucket. :param bucket_id: The id of the bucket to be deleted. :returns: A dict with the json-encoded response data. :raises RequestError: If the user is not authenticated. """ self._ensure_auth() params = self._base_params() params.update({'bucketId': bucket_id}) # Endpoint /b2_delete_bucket takes a long time to respond, so a larger timeout is warranted response = self._http.post(self._make_url( Endpoints.delete_bucket.value), json=params, headers=self._headers(), timeout=90.0) data = json_loads(response.text) check_b2_errors( data, f'Failed to delete bucket with id "{bucket_id}" ({data}).') return data
def finish_large_file(self, file_id: str, parts_sha1: List[str]) -> Json: """ :param file_id: The ID returned by b2_start_large_file. :param parts_sha1: A list of hex SHA1 checksums of the parts of the large file. :returns: A json-like 11-dict containing the keys: accountId, action, bucketId, fileName, fileId, contentLength, contentSha1, contentMd5, contentType, fileInfo, uploadTimestamp. .. seealso:: `Reference <https://www.backblaze.com/b2/docs/b2_finish_large_file.html>`_. """ self._ensure_auth() params = { 'fileId': file_id, 'partSha1Array': parts_sha1, } response = self._http.post(self._make_url( Endpoints.finish_large_file.value), json=params, headers=self._headers()) result = json_loads(response.text) check_b2_errors( result, f'Failed to finish large file with id "{file_id}": {result}') return result
def test_check_b2_errors__valid_response__nothing_happens(self): """ The check_b2_errors() does nothing if no error is found. """ data = {'status': 200} self.assertIsNone(utils.check_b2_errors(data, ''))