Example #1
0
class CloudDropbox(Wrapper):
    """ Wraps a Dropbox connection client.
    """
    wrapper_type = 'Dropbox connection'
    required_secret_attr = 'secret'
    required_secret_label = 'an OAuth 2 access token'

    def __init__(self, *args, **kwargs):
        super(CloudDropbox, self).__init__(*args, **kwargs)
        self._impl = None  # type: DropboxClient

# ################################################################################################################################

    def _init_impl(self):

        with self.update_lock:

            # Create a pool of at most that many connections
            session = create_session(50)

            scope = as_list(self.config.default_scope, ',')

            config = {
                'session': session,
                'user_agent': self.config.user_agent,
                'oauth2_access_token': self.server.decrypt(self.config.secret),
                'oauth2_access_token_expiration': int(self.config.oauth2_access_token_expiration or 0),
                'scope': scope,
                'max_retries_on_error': int(self.config.max_retries_on_error or 0),
                'max_retries_on_rate_limit': int(self.config.max_retries_on_rate_limit or 0),
                'timeout': int(self.config.timeout),
                'headers': parse_extra_into_dict(self.config.http_headers),
            }

            # Create the actual connection object
            self._impl = DropboxClient(**config)

            # Confirm the connection was established
            self.ping()

            # We can assume we are connected now
            self.is_connected = True

# ################################################################################################################################

    def _delete(self):
        if self._impl:
            self._impl.close()

# ################################################################################################################################

    def _ping(self):
        self._impl.check_user()
Example #2
0
class Dropbox(BlackboxStorage):
    """Storage handler that uploads backups to Dropbox."""

    required_fields = ("access_token", )

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.upload_base = self.config.get("upload_directory") or "/"
        self.client = DropboxClient(self.config["access_token"])
        self.valid = self._validate_token()

    def _validate_token(self):
        """Check if dropbox token is valid."""
        try:
            return self.client.check_user("test").result == "test"
        except AuthError:
            return False

    def sync(self, file_path: Path) -> None:
        """Sync a file to Dropbox."""
        # Check if Dropbox token is valid.
        if self.valid is False:
            error = "Dropbox token is invalid!"
            self.success = False
            self.output = error
            log.error(error)
            return None

        # This is size what can be uploaded as one chunk.
        # When file is bigger than that, this will be uploaded
        # in multiple parts.
        chunk_size = 4 * 1024 * 1024

        temp_file, recompressed = self.compress(file_path)
        upload_path = f"{self.upload_base}{file_path.name}{'.gz' if recompressed else ''}"

        try:
            with temp_file as f:
                file_size = os.stat(f.name).st_size
                log.debug(file_size)
                if file_size <= chunk_size:
                    self.client.files_upload(f.read(), upload_path,
                                             WriteMode.overwrite)
                else:
                    session_start = self.client.files_upload_session_start(
                        f.read(chunk_size))
                    cursor = UploadSessionCursor(session_start.session_id,
                                                 offset=f.tell())
                    # Commit contains path in Dropbox and write mode about file
                    commit = CommitInfo(upload_path, WriteMode.overwrite)

                    while f.tell() < file_size:
                        if (file_size - f.tell()) <= chunk_size:
                            self.client.files_upload_session_finish(
                                f.read(chunk_size), cursor, commit)
                        else:
                            self.client.files_upload_session_append(
                                f.read(chunk_size), cursor.session_id,
                                cursor.offset)
                            cursor.offset = f.tell()
            self.success = True
        except (ApiError, HttpError) as e:
            log.error(e)
            self.success = False
            self.output = str(e)

    def rotate(self, database_id: str) -> None:
        """
        Rotate the files in the Dropbox directory.

        All files in base directory of backups will be deleted when they
        are older than `retention_days`, and because of this,
        it's better to have backups in isolated folder.
        """
        # Check if Dropbox token is valid.
        if self.valid is False:
            log.error("Dropbox token is invalid - Can't delete old backups!")
            return None
        # Let's rotate only this type of database
        db_type_regex = rf"{database_id}_blackbox_\d{{2}}_\d{{2}}_\d{{4}}.+"

        # Receive first batch of files.
        files_result = self.client.files_list_folder(
            self.upload_base if self.upload_base != "/" else "")
        entries = [
            entry for entry in files_result.entries
            if self._is_backup_file(entry, db_type_regex)
        ]

        # If there is more files, receive all of them.
        while files_result.has_more:
            cursor = files_result.cursor
            files_result = self.client.files_list_folder_continue(cursor)
            entries += [
                entry for entry in files_result.entries
                if self._is_backup_file(entry, db_type_regex)
            ]

        retention_days = 7
        if Blackbox.retention_days:
            retention_days = Blackbox.retention_days

        # Find all old files and delete them.
        for item in entries:
            last_modified = item.server_modified
            now = datetime.now(tz=last_modified.tzinfo)
            delta = now - last_modified
            if delta.days >= retention_days:
                self.client.files_delete(item.path_lower)

    @staticmethod
    def _is_backup_file(entry, db_type_regex) -> bool:
        """Check if file is actually this kind of database backup."""
        return isinstance(entry, FileMetadata) and re.match(
            db_type_regex, entry.name)