1###################################################################### 2# 3# File: b2/exception.py 4# 5# Copyright 2018 Backblaze Inc. All Rights Reserved. 6# 7# License https://www.backblaze.com/using_b2_code.html 8# 9###################################################################### 10 11from abc import ABCMeta 12 13import json 14import six 15 16from .utils import camelcase_to_underscore 17 18 19@six.add_metaclass(ABCMeta) 20class B2Error(Exception): 21 def __init__(self, *args, **kwargs): 22 """ 23 Python 2 does not like it when you pass unicode as the message 24 in an exception. We like to use file names in exception messages. 25 To avoid problems, if the message has any non-ascii characters in 26 it, they are replaced with backslash-uNNNN 27 28 https://pythonhosted.org/kitchen/unicode-frustrations.html#frustration-5-exceptions 29 """ 30 if six.PY2: 31 if args and isinstance(args[0], six.text_type): 32 args = tuple([json.dumps(args[0])[1:-1]] + list(args[1:])) 33 super(B2Error, self).__init__(*args, **kwargs) 34 35 @property 36 def prefix(self): 37 """ 38 nice auto-generated error message prefix 39 >>> B2SimpleError().prefix 40 'Simple error' 41 >>> AlreadyFailed().prefix 42 'Already failed' 43 """ 44 prefix = self.__class__.__name__ 45 if prefix.startswith('B2'): 46 prefix = prefix[2:] 47 prefix = camelcase_to_underscore(prefix).replace('_', ' ') 48 return prefix[0].upper() + prefix[1:] 49 50 def should_retry_http(self): 51 """ 52 Returns true if this is an error that can cause an HTTP 53 call to be retried. 54 """ 55 return False 56 57 def should_retry_upload(self): 58 """ 59 Returns true if this is an error that should tell the upload 60 code to get a new upload URL and try the upload again. 61 """ 62 return False 63 64 65@six.add_metaclass(ABCMeta) 66class B2SimpleError(B2Error): 67 """ 68 a B2Error with a message prefix 69 """ 70 71 def __str__(self): 72 return '%s: %s' % (self.prefix, super(B2SimpleError, self).__str__()) 73 74 75@six.add_metaclass(ABCMeta) 76class NotAllowedByAppKeyError(B2SimpleError): 77 """ 78 Base class for errors caused by restrictions on an application key. 79 """ 80 81 82@six.add_metaclass(ABCMeta) 83class TransientErrorMixin(object): 84 def should_retry_http(self): 85 return True 86 87 def should_retry_upload(self): 88 return True 89 90 91class AlreadyFailed(B2SimpleError): 92 pass 93 94 95class BadDateFormat(B2SimpleError): 96 prefix = 'Date from server' 97 98 99class BadFileInfo(B2SimpleError): 100 pass 101 102 103class BadJson(B2SimpleError): 104 prefix = 'Bad request' 105 106 107class BadUploadUrl(TransientErrorMixin, B2SimpleError): 108 pass 109 110 111class BrokenPipe(B2Error): 112 def __str__(self): 113 return 'Broken pipe: unable to send entire request' 114 115 def should_retry_upload(self): 116 return True 117 118 119class CapabilityNotAllowed(NotAllowedByAppKeyError): 120 pass 121 122 123class ChecksumMismatch(TransientErrorMixin, B2Error): 124 def __init__(self, checksum_type, expected, actual): 125 super(ChecksumMismatch, self).__init__() 126 self.checksum_type = checksum_type 127 self.expected = expected 128 self.actual = actual 129 130 def __str__(self): 131 return '%s checksum mismatch -- bad data' % (self.checksum_type,) 132 133 134class B2HttpCallbackException(B2SimpleError): 135 pass 136 137 138class B2HttpCallbackPostRequestException(B2HttpCallbackException): 139 pass 140 141 142class B2HttpCallbackPreRequestException(B2HttpCallbackException): 143 pass 144 145 146class BucketNotAllowed(NotAllowedByAppKeyError): 147 pass 148 149 150class ClockSkew(B2HttpCallbackPostRequestException): 151 """ 152 The clock on the server differs from the local clock by too much. 153 """ 154 155 def __init__(self, clock_skew_seconds): 156 """ 157 :param clock_skew_seconds: The different: local_clock - server_clock 158 """ 159 super(ClockSkew, self).__init__() 160 self.clock_skew_seconds = clock_skew_seconds 161 162 def __str__(self): 163 if self.clock_skew_seconds < 0: 164 return 'ClockSkew: local clock is %d seconds behind server' % ( 165 -self.clock_skew_seconds, 166 ) 167 else: 168 return 'ClockSkew; local clock is %d seconds ahead of server' % ( 169 self.clock_skew_seconds, 170 ) 171 172 173class CommandError(B2Error): 174 """ 175 b2 command error (user caused). Accepts exactly one argument. 176 We expect users of shell scripts will parse our __str__ output. 177 """ 178 179 def __init__(self, message): 180 super(CommandError, self).__init__() 181 self.message = message 182 183 def __str__(self): 184 return self.message 185 186 187class Conflict(B2SimpleError): 188 pass 189 190 191class ConnectionReset(B2Error): 192 def __str__(self): 193 return 'Connection reset' 194 195 def should_retry_upload(self): 196 return True 197 198 199class B2ConnectionError(TransientErrorMixin, B2SimpleError): 200 pass 201 202 203class B2RequestTimeout(TransientErrorMixin, B2SimpleError): 204 pass 205 206 207class DestFileNewer(B2Error): 208 def __init__(self, dest_file, source_file, dest_prefix, source_prefix): 209 super(DestFileNewer, self).__init__() 210 self.dest_file = dest_file 211 self.source_file = source_file 212 self.dest_prefix = dest_prefix 213 self.source_prefix = source_prefix 214 215 def __str__(self): 216 return 'source file is older than destination: %s%s with a time of %s cannot be synced to %s%s with a time of %s, unless --skipNewer or --replaceNewer is provided' % ( 217 self.source_prefix, 218 self.source_file.name, 219 self.source_file.latest_version().mod_time, 220 self.dest_prefix, 221 self.dest_file.name, 222 self.dest_file.latest_version().mod_time, 223 ) 224 225 def should_retry_http(self): 226 return True 227 228 229class DuplicateBucketName(B2SimpleError): 230 prefix = 'Bucket name is already in use' 231 232 233class FileAlreadyHidden(B2SimpleError): 234 pass 235 236 237class FileNameNotAllowed(NotAllowedByAppKeyError): 238 pass 239 240 241class FileNotPresent(B2SimpleError): 242 pass 243 244 245class UnusableFileName(B2SimpleError): 246 """Raise when a filename doesn't meet the rules. 247 248 Could possibly use InvalidUploadSource, but this is intended for the filename on the 249 server, which could differ. https://www.backblaze.com/b2/docs/files.html. 250 """ 251 pass 252 253 254class InvalidRange(B2Error): 255 def __init__(self, content_length, range_): 256 super(InvalidRange, self).__init__() 257 self.content_length = content_length 258 self.range_ = range_ 259 260 def __str__(self): 261 return 'A range of %d-%d was requested (size of %d), but cloud could only serve %d of that' % ( 262 self.range_[0], 263 self.range_[1], 264 self.range_[1] - self.range_[0] + 1, 265 self.content_length, 266 ) 267 268 269class InvalidUploadSource(B2SimpleError): 270 pass 271 272 273class Unauthorized(B2Error): 274 def __init__(self, message, code): 275 super(Unauthorized, self).__init__() 276 self.message = message 277 self.code = code 278 279 def __str__(self): 280 return '%s (%s)' % (self.message, self.code) 281 282 def should_retry_upload(self): 283 return True 284 285 286class InvalidAuthToken(Unauthorized): 287 """ 288 Specific type of Unauthorized that means the auth token is invalid. 289 This is not the case where the auth token is valid but does not 290 allow access. 291 """ 292 293 def __init__(self, message, code): 294 super(InvalidAuthToken, 295 self).__init__('Invalid authorization token. Server said: ' + message, code) 296 297 298class RestrictedBucket(B2Error): 299 def __init__(self, bucket_name): 300 super(RestrictedBucket, self).__init__() 301 self.bucket_name = bucket_name 302 303 def __str__(self): 304 return 'Application key is restricted to bucket: %s' % self.bucket_name 305 306 307class MaxFileSizeExceeded(B2Error): 308 def __init__(self, size, max_allowed_size): 309 super(MaxFileSizeExceeded, self).__init__() 310 self.size = size 311 self.max_allowed_size = max_allowed_size 312 313 def __str__(self): 314 return 'Allowed file size of exceeded: %s > %s' % ( 315 self.size, 316 self.max_allowed_size, 317 ) 318 319 320class MaxRetriesExceeded(B2Error): 321 def __init__(self, limit, exception_info_list): 322 super(MaxRetriesExceeded, self).__init__() 323 self.limit = limit 324 self.exception_info_list = exception_info_list 325 326 def __str__(self): 327 exceptions = '\n'.join(str(wrapped_error) for wrapped_error in self.exception_info_list) 328 return 'FAILED to upload after %s tries. Encountered exceptions: %s' % ( 329 self.limit, 330 exceptions, 331 ) 332 333 334class MissingPart(B2SimpleError): 335 prefix = 'Part number has not been uploaded' 336 337 338class NonExistentBucket(B2SimpleError): 339 prefix = 'No such bucket' 340 341 342class PartSha1Mismatch(B2Error): 343 def __init__(self, key): 344 super(PartSha1Mismatch, self).__init__() 345 self.key = key 346 347 def __str__(self): 348 return 'Part number %s has wrong SHA1' % (self.key,) 349 350 351class ServiceError(TransientErrorMixin, B2Error): 352 """ 353 Used for HTTP status codes 500 through 599. 354 """ 355 356 357class StorageCapExceeded(B2Error): 358 def __str__(self): 359 return 'Cannot upload files, storage cap exceeded.' 360 361 362class TooManyRequests(B2Error): 363 def __str__(self): 364 return 'Too many requests' 365 366 def should_retry_http(self): 367 return True 368 369 370class TruncatedOutput(TransientErrorMixin, B2Error): 371 def __init__(self, bytes_read, file_size): 372 super(TruncatedOutput, self).__init__() 373 self.bytes_read = bytes_read 374 self.file_size = file_size 375 376 def __str__(self): 377 return 'only %d of %d bytes read' % ( 378 self.bytes_read, 379 self.file_size, 380 ) 381 382 383class UnexpectedCloudBehaviour(B2SimpleError): 384 pass 385 386 387class UnknownError(B2SimpleError): 388 pass 389 390 391class UnknownHost(B2Error): 392 def __str__(self): 393 return 'unknown host' 394 395 396class UnrecognizedBucketType(B2Error): 397 pass 398 399 400def interpret_b2_error(status, code, message, post_params=None): 401 post_params = post_params or {} 402 if status == 400 and code == "already_hidden": 403 return FileAlreadyHidden(post_params.get('fileName')) 404 elif status == 400 and code == 'bad_json': 405 return BadJson(message) 406 elif ( 407 (status == 400 and code in ("no_such_file", "file_not_present")) or 408 (status == 404 and code == "not_found") 409 ): 410 # hide_file returns 400 and "no_such_file" 411 # delete_file_version returns 400 and "file_not_present" 412 # get_file_info returns 404 and "not_found" 413 # download_file_by_name/download_file_by_id return 404 and "not_found" 414 # but don't have post_params 415 if 'fileName' in post_params: 416 return FileNotPresent(post_params.get('fileName')) 417 else: 418 return FileNotPresent() 419 elif status == 400 and code == "duplicate_bucket_name": 420 return DuplicateBucketName(post_params.get('bucketName')) 421 elif status == 400 and code == "missing_part": 422 return MissingPart(post_params.get('fileId')) 423 elif status == 400 and code == "part_sha1_mismatch": 424 return PartSha1Mismatch(post_params.get('fileId')) 425 elif status == 401 and code in ("bad_auth_token", "expired_auth_token"): 426 return InvalidAuthToken(message, code) 427 elif status == 401: 428 return Unauthorized(message, code) 429 elif status == 403 and code == "storage_cap_exceeded": 430 return StorageCapExceeded() 431 elif status == 409: 432 return Conflict() 433 elif status == 429: 434 return TooManyRequests() 435 elif 500 <= status and status < 600: 436 return ServiceError('%d %s %s' % (status, code, message)) 437 else: 438 return UnknownError('%d %s %s' % (status, code, message)) 439