from aiohttp import ClientSession, ContentTypeError, ClientConnectorError, ClientTimeout, ClientResponse from aiohttp.client_exceptions import ServerTimeoutError, ServerDisconnectedError, ClientOSError from backup.exceptions import GoogleUnexpectedError, GoogleInternalError, GoogleRateLimitError, GoogleCredentialsExpired, CredRefreshGoogleError, DriveQuotaExceeded, GoogleDrivePermissionDenied, GoogleDnsFailure, GoogleCantConnect, GoogleTimeoutError from backup.util import Resolver from backup.logger import getLogger from backup.config import Config, Setting from injector import singleton, inject from dns.exception import DNSException RATE_LIMIT_EXCEEDED = [403] TOO_MANY_REQUESTS = [429] INTERNAL_ERROR = [500, 503] PERMISSION_DENIED = [401] REQUEST_TIMEOUT = [408] logger = getLogger(__name__) @singleton class DriveRequester(): @inject def __init__(self, config: Config, session: ClientSession, resolver: Resolver): self.session = session self.resolver = resolver self.config = config async def request(self, method, url, headers={}, json=None, data=None) -> ClientResponse: try: response = await self.session.request(method, url, headers=headers, json=json, timeout=self.buildTimeout(), data=data) if response.status < 400: return response await self.raiseForKnownErrors(response) if response.status in PERMISSION_DENIED: response.release() raise GoogleCredentialsExpired() elif response.status in INTERNAL_ERROR: response.release() raise GoogleInternalError() elif response.status in RATE_LIMIT_EXCEEDED or response.status in TOO_MANY_REQUESTS: response.release() raise GoogleRateLimitError() elif response.status in REQUEST_TIMEOUT: response.release() raise GoogleTimeoutError() response.raise_for_status() return response except ClientConnectorError as e: logger.debug( "Ran into trouble reaching Google Drive's servers. We'll use alternate DNS servers on the next attempt.") self.resolver.toggle() if "Cannot connect to host" in str(e) or "Connection reset by peer" in str(e): raise GoogleCantConnect() if e.os_error.errno == -2: # -2 means dns lookup failed. raise GoogleDnsFailure() elif str(e.os_error) == "Domain name not found": raise GoogleDnsFailure() elif e.os_error.errno in [99, 111, 10061, 104]: # 111 means connection refused # Can't connect raise GoogleCantConnect() elif "Could not contact DNS serve" in str(e.os_error): # Wish there was a better way to identify this exception raise GoogleDnsFailure() raise except ClientOSError as e: if e.errno == 1: raise GoogleUnexpectedError() raise except ServerTimeoutError: raise GoogleTimeoutError() except ServerDisconnectedError: raise GoogleUnexpectedError() except DNSException: logger.debug( "Ran into trouble resolving Google Drive's servers. We'll use normal DNS servers on the next attempt.") self.resolver.toggle() raise GoogleDnsFailure() def buildTimeout(self): return ClientTimeout( sock_connect=self.config.get( Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS), sock_read=self.config.get(Setting.GOOGLE_DRIVE_TIMEOUT_SECONDS)) async def raiseForKnownErrors(self, response): try: message = await response.json() except ContentTypeError: return except ValueError: # parsing json failed, just give up return except TypeError: # Same return if "error" not in message: return error_obj = message["error"] if isinstance(error_obj, str): if error_obj == "expired": raise GoogleCredentialsExpired() else: raise CredRefreshGoogleError(error_obj) if "errors" not in error_obj: return for error in error_obj["errors"]: if "reason" not in error: continue if error["reason"] == "storageQuotaExceeded": raise DriveQuotaExceeded() elif error["reason"] in ["forbidden", "insufficientFilePermissions"]: raise GoogleDrivePermissionDenied()