예제 #1
0
    def __init__(self,
                 event_sink,
                 storage_id,
                 storage_cred_reader,
                 storage_cred_writer,
                 storage_cache_dir=None,
                 polling_interval=5):
        """

        :param event_sink: sink to report events to
        :param storage_id: unique sotrage id assigned to this storage
        :param storage_cred_reader: reader to reaad credentials or other auth data from
        :param storage_cred_writer: writer to write credentials or other auth data to
        :param storage_cache_dir: cache directory to store cached models at
        :param polling_interval: the intervall to poll for changes from the service
        """
        # pylint: disable=too-many-arguments

        super().__init__(event_sink,
                         storage_id,
                         storage_cred_reader,
                         storage_cred_writer,
                         storage_cache_dir=storage_cache_dir,
                         default_model=IndexingNode(None, indexes=['_id']))

        self.event_poller = None
        self.polling_interval = polling_interval
예제 #2
0
def root(request):
    """
    Creates a default Root node
    :return: Node
    """
    if request.param == 'standard':
        return Node(name=None)
    return IndexingNode(name=None, indexes=['_id'])
예제 #3
0
def filter_tree_by_path(tree, filter_tree, children_default=True):
    """
    Copies the tree. It is filtered by the filters argument.

    :param tree: Tree to filter
    :param filters: The bool element states if the children on the same level should be
    included or not. If the filter for that item does not exist `children_default` is
    assumed. The filter with the longest path in a branch will also include all children
    recursively.
    :return: filtered tree
    """

    if isinstance(tree, IndexingNode):
        new_root = IndexingNode(name=None, indexes=tree.index.keys())
    else:
        new_root = type(tree)(name=None)

    for node in tree:
        if is_path_synced(node.path, filter_tree,
                          node.props.get('is_dir', False), children_default):
            new_root.get_node_safe(node.path).props.update(node.props)

    return new_root
예제 #4
0
def indexing_node(request):
    """ fixture to test the indexing node """
    if request.param == 'standard':
        return IndexingNode(None, indexes=['_id'])

    elif request.param == 'factory':
        old_root = IndexingNode(None, indexes=['_id'])
        new_root = old_root.create_instance()
        old_root.name = 'hello'
        old_root.parent = new_root
        return new_root
    elif request.param == 'copied':
        old_root = IndexingNode(None, indexes=['_id'])
        return deepcopy(old_root)
    elif request.param == 'pickled':
        old_root = IndexingNode(None, indexes=['_id'])
        fobj = io.BytesIO()
        pickle.dump(old_root, fobj)
        fobj.seek(0)
        return pickle.load(fobj)

    else:
        assert False
예제 #5
0
class GoogleDrive(jars.OAuthBasicStorage):
    """ Main class to use google drive"""

    # Credentials you get from registering a new application
    client_identificator = 144880602865
    client_id = '{}.apps.googleusercontent.com'.format(client_identificator)
    client_secret = 'CLIENT_SECRET'
    redirect_uri = 'http://localhost:9324'

    # OAuth endpoints given in the Google API documentation
    authorization_base_url = "https://accounts.google.com/o/oauth2/v2/auth"
    token_url = "https://accounts.google.com/o/oauth2/token"
    scope = [
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/drive"
    ]
    base_url = 'https://www.googleapis.com/drive/v3/'

    storage_name = "gdrive"
    storage_display_name = "Google Drive"

    def __init__(self,
                 event_sink,
                 storage_id,
                 storage_cred_reader,
                 storage_cred_writer,
                 storage_cache_dir=None,
                 polling_interval=5):
        """

        :param event_sink: sink to report events to
        :param storage_id: unique sotrage id assigned to this storage
        :param storage_cred_reader: reader to reaad credentials or other auth data from
        :param storage_cred_writer: writer to write credentials or other auth data to
        :param storage_cache_dir: cache directory to store cached models at
        :param polling_interval: the intervall to poll for changes from the service
        """
        # pylint: disable=too-many-arguments

        super().__init__(event_sink,
                         storage_id,
                         storage_cred_reader,
                         storage_cred_writer,
                         storage_cache_dir=storage_cache_dir,
                         default_model=IndexingNode(None, indexes=['_id']))

        self.event_poller = None
        self.polling_interval = polling_interval

    def _init_poller(self):
        """initalize poller"""
        def set_offline(value):
            """Set it offline"""
            self.offline = value

        # creating poller getting events from the storage
        return jars.PollingScheduler(self.polling_interval,
                                     target=self.update_model,
                                     target_kwargs={'emit_events': True},
                                     offline_callback=set_offline)

    def check_available(self):
        # check authentication
        try:
            url = urllib.parse.urljoin(self.base_url,
                                       'about?fields=storageQuota,user')
            about = self.oauth_session.get(url)
            error_mapper(about.raise_for_status)()
        except requests.exceptions.HTTPError as exception:
            if exception.response.status_code == 401:
                raise AuthenticationError(storage_id=self.storage_id,
                                          origin_error=exception)
            else:
                raise StorageOfflineError(storage_id=self.storage_id,
                                          origin_error=exception)
        except BaseException as error:
            raise StorageOfflineError(storage_id=self.storage_id,
                                      origin_error=error)

    @error_mapper
    @gdrive_error_mapper
    def get_tree(self, cached=False, filtered=True):
        # pylint: disable=arguments-differ, too-many-locals
        logger.debug('requested tree with filter %s', self.filter_tree)
        if cached:
            with self.model.lock:
                if filtered:
                    logger.debug('requested tree with filter %s',
                                 self.filter_tree)
                    return jars.utils.filter_tree_by_path(
                        tree=self.model, filter_tree=self.filter_tree)
                else:
                    return copy.deepcopy(self.model)

        root = IndexingNode(name=None, indexes=['_id'])

        # fetch and store current cursor
        url = urllib.parse.urljoin(self.base_url, 'changes/startPageToken')
        token = self.oauth_session.get(url)
        token.raise_for_status()
        root.props['_cursor'] = token.json()['startPageToken']

        # get root id
        url = urllib.parse.urljoin(self.base_url, 'files/root?fields=id')
        root_id = self.oauth_session.get(url)
        root_id.raise_for_status()
        root.props['_id'] = root_id.json()['id']

        # fetch metrics
        url = urllib.parse.urljoin(self.base_url,
                                   'about?fields=storageQuota,user')
        about = self.oauth_session.get(url)
        about.raise_for_status()
        metrics = about.json()

        # this is relevant to client#970
        total_space = int(metrics['storageQuota'].get('limit', sys.maxsize))
        free_space = total_space - int(metrics['storageQuota']['usageInDrive'])
        root.props[METRICS] = StorageMetrics(storage_id=self.storage_id,
                                             free_space=free_space,
                                             total_space=total_space)
        # fetch tree
        url = urllib.parse.urljoin(self.base_url, 'files')

        page_token = None

        nodes = []

        # fetch the whole tree into the parent_map
        while True:
            params = {
                'pageSize': '1000',
                'q': 'trashed=false',
                'orderBy': 'folder',
                'fields': 'nextPageToken,files(' + FIELDS + ')'
            }
            if page_token is not None:
                params['pageToken'] = page_token
            files_resp = self.oauth_session.get(url, params=params)

            files_resp.raise_for_status()
            resp_json = files_resp.json()
            files = resp_json['files']
            page_token = resp_json.get('nextPageToken', None)

            for file in files:
                if 'parents' in file and is_relevant_file(file):
                    for parent_id in file['parents']:
                        # for each parent add this element
                        if is_relevant_file(file):
                            nodes.append(
                                NodeChange(action='UPSERT',
                                           parent_id=parent_id,
                                           name=file['name'],
                                           props=gdrive_to_node_props(file)))

            if page_token is None:
                break

        root.add_merge_set_with_id(nodes, key='_id')
        self._adjust_share_ids(nodes)

        self.offline = False

        if filtered:
            logger.debug('requested tree with filter %s', self.filter_tree)
            root = jars.utils.filter_tree_by_path(root, self.filter_tree)
        return root

    def _get_children(self, parent_id):
        body = {
            'q': "'{}' in parents and trashed=false".format(parent_id),
            'fields': 'files({})'.format(FIELDS)
        }

        response = self.oauth_session.get(
            'https://www.googleapis.com/drive/v3/files', params=body)

        response.raise_for_status()

        result = []
        for file in response.json()['files']:
            if not is_relevant_file(file):
                continue
            props = gdrive_to_node_props(file)
            result.append((file['name'], props))
        return result

    @error_mapper
    @gdrive_error_mapper
    def get_tree_children(self, path):
        """
        :param path: if path is a list, it is used like a path, if a string, it is used as
         parent_id
        :param ids: if the function should return the _id field in the properties
        :return:
        """
        if path:
            parent_id = self._path_to_file_id(path)
        else:
            # [] is the root path, no need for extra resolve
            parent_id = 'root'

        return self._get_children(parent_id)

    def start_events(self):
        """ starts the event poller """
        if (self.event_poller and not self.event_poller.is_alive()) \
                or not self.event_poller:
            # recreate poller instance
            self.event_poller = self._init_poller()
            self.event_poller.start()

    def stop_events(self, join=False):
        if self.event_poller:
            self.event_poller.stop(join=join)

    def clear_model(self):
        self.model = IndexingNode(None, indexes=['_id'])

    def get_internal_model(self):
        return self.model

    def update(self):
        with self.model.lock:
            if '_cursor' not in self.model.props:
                # no model ever fetched, fetch model
                self.model = self.get_tree(filtered=False)
            else:
                self.update_model(emit_events=False)

    @error_mapper
    @gdrive_error_mapper
    def update_model(self, emit_events=False):
        """
        updates a model with a given cursor and returns the new cursor
        if emit_events is set it also triggers events on changes
        """

        # TODO lock less, create iterator
        with self.model.lock:
            mergeset = []
            assert '_cursor' in self.model.props

            page_token = str(self.model.props['_cursor'])

            while page_token is not None:
                urlparams = {
                    'pageToken': page_token,
                    'includeRemoved': 'true',
                    'pageSize': '1000',
                    'fields': 'changes,newStartPageToken,nextPageToken'
                }

                url = urllib.parse.urljoin(self.base_url, 'changes')

                changes_resp = self.oauth_session.get(url, params=urlparams)

                changes_resp.raise_for_status()
                changes = changes_resp.json()
                # logger.debug(threading.current_thread().name)
                # logger.debug(changes)
                page_token = changes.get('nextPageToken')
                if 'newStartPageToken' in changes:
                    self.model.props['_cursor'] = changes['newStartPageToken']

                for change in changes['changes']:
                    merges = transform_gdrive_change(change)
                    mergeset.extend(merges)

            logger.info("Finished fetching updates from gdrive.")

            with contextlib.ExitStack() as stack:
                if emit_events:
                    stack.enter_context(
                        jars.TreeToSyncEngineEngineAdapter(
                            node=self.model,
                            storage_id=self.storage_id,
                            sync_engine=self._event_sink))
                left_overs = self.model.add_merge_set_with_id(
                    mergeset, '_id', using_update=True)
                if left_overs:
                    logger.info('Left overs from delta: %s', left_overs)
                logger.debug('After update')
                self._adjust_share_ids(mergeset)
                logger.debug('After _adjust_share_ids')

    def _adjust_share_ids(self, mergeset):
        """Helper to identify the unique share ids on gdrive, if the permissions of the parent are
         different then the ones on the children, we assume this is a different share"""

        for change in mergeset:
            action, parent_id, name, props = change
            logger.info("_adjust_share_ids, change: %s", change)

            # Removing files does not change any share_ids
            if action == 'DELETE':
                logger.debug("Not handling DELETEs")
                continue

            # The node isn't shared with anyone, skip it
            if SHARED_WITH not in props:
                logger.debug("Node isn't shared with anyone, skipping")
                continue

            # The node does not exist in our index(?)
            if not props['_id'] in self.model.index['_id']:
                logger.debug("Node does not exist in our model")
                continue

            # Get a reference to the props dict of our model
            props = self.model.index['_id'][props['_id']].props
            shared_with = props[SHARED_WITH]

            # Get the node's parent - this can be None, if the node is root
            parent_node = self.model.index['_id'].get(parent_id, None)

            logger.info(
                "Checking change for file '%s' (shared_with: %s, parent_node shared_with: %s)",
                name, shared_with,
                set() if parent_node is None else parent_node.props.get(
                    SHARED_WITH, set()))

            if parent_node is None or shared_with != parent_node.props.get(
                    SHARED_WITH, set()):
                # Either we are looking at root, or the node has been shared with different people
                #  than it's parent -> it's a different share

                logger.debug(
                    'setting share id for: %s (Parent: %s, Node: %s) to %s',
                    name,
                    set() if parent_node is None else parent_node.props.get(
                        SHARED_WITH, set()), props.get(SHARED_WITH,
                                                       set()), props['_id'])

                props[jars.SHARE_ID] = props['_id']
            else:
                # The node has the same list of people it is shared to as it's parent
                #  -> they belong to the same share
                # Remove the share_id on the node, we only need it on the parent
                logger.debug(
                    'Removing share_id from %s (Parent: %s, Node: %s)', name,
                    parent_node.props.get(SHARED_WITH, set()),
                    props.get(SHARED_WITH, set()))
                props.pop(jars.SHARE_ID, None)

    @error_mapper
    @gdrive_error_mapper
    def move(self,
             source,
             target,
             expected_source_vid=None,
             expected_target_vid=None):
        new_fields = {}
        params = {'fields': 'mimeType,' + VERSION_FIELD}

        # rename
        if source[-1] != target[-1]:
            new_fields['name'] = target[-1]

        target_id = None
        source_id = None
        source_dir_id = None

        with self.model.lock:
            try:
                source_node = self.model.get_node(source)
            except KeyError:
                raise FileNotFoundError()
            source_id = source_node.props['_id']
            source_dir_id = source_node.parent.props['_id']
            try:
                target_node = self.model.get_node(target)
                target_id = target_node.props['_id']
            except KeyError:
                target_id = None

        # move to another dir?
        if source[:-1] != target[:-1]:
            target_dir_id = self.make_dir(target[:-1], return_id=True)
            params['addParents'] = target_dir_id
            params['removeParents'] = source_dir_id

        # before the actual action check the version
        self._check_version_id(source_id, expected_source_vid, source)

        if target_id is not None:
            # replace also involves deletion of the orignal item,
            # version checking is done there
            self.delete(target, expected_target_vid)

        request = self.oauth_session.patch(urllib.parse.urljoin(
            self.base_url, 'files/{}'.format(source_id)),
                                           json=new_fields,
                                           params=params)
        request.raise_for_status()

        file = request.json()

        if file['mimeType'] == MIMETYPE_FOLDER:
            return IS_DIR
        else:
            return file[VERSION_FIELD]

    def _check_version_id(self, file_id, expected_version_id, path=None):
        url = urllib.parse.urljoin(self.base_url, 'files/{}'.format(file_id))
        if expected_version_id is not None:
            response = self.oauth_session.get(
                url, params={'fields': 'mimeType,' + VERSION_FIELD})
            if expected_version_id == 'is_dir' \
                    and response.json()['mimeType'] == MIMETYPE_FOLDER:
                # its fine, dir to dir
                return
            response.raise_for_status()
            if response.json()[VERSION_FIELD] != expected_version_id:
                raise jars.VersionIdNotMatchingError(
                    self.storage_id,
                    path=path,
                    version_b=expected_version_id,
                    version_a=response.json()[VERSION_FIELD])

    @error_mapper
    @gdrive_error_mapper
    def open_read(self, path, expected_version_id=None):
        file_id = self._path_to_file_id(path)

        self._check_version_id(file_id, expected_version_id, path)

        url = urllib.parse.urljoin(self.base_url, 'files/{}'.format(file_id))

        # 'Accept-Encoding' is removed from the header until
        # https://github.com/shazow/urllib3/issues/437
        # is fixed and merged into requests
        response = self.oauth_session.get(url,
                                          params={'alt': 'media'},
                                          headers={'Accept-Encoding': None},
                                          stream=True)
        response.raise_for_status()

        response.raw.decode_content = True
        return response.raw

    @error_mapper
    @gdrive_error_mapper
    def make_dir(self, path, return_id=False):
        # pylint: disable=arguments-differ

        # which parents to create
        # this needs to be locket until everything is done, since it might run parallel
        with jars.TreeToSyncEngineEngineAdapter(node=self.model,
                                                storage_id=self.storage_id,
                                                sync_engine=self._event_sink):
            parent = self.model
            # iterate the path
            for elem in path:
                if parent.has_child(elem):
                    parent = parent.get_node([elem])
                else:
                    url = urllib.parse.urljoin(self.base_url, 'files/')
                    body = {
                        'mimeType': 'application/vnd.google-apps.folder',
                        'name': elem,
                        'parents': [parent.props['_id']]
                    }
                    resp = self.oauth_session.post(url,
                                                   json=body,
                                                   params={'fields': FIELDS})
                    resp.raise_for_status()

                    # node.props['_id'] = resp.json()['id']
                    props = gdrive_to_node_props(resp.json())
                    node = parent.add_child(name=elem, props=props)
                    parent = node
            if return_id:
                return self.model.get_node(path).props['_id']
            else:
                return 'is_dir'

    @error_mapper
    @gdrive_error_mapper
    def _path_to_file_id(self, path):
        """
        resolves the path via the model or if this does not work via http requests
        """
        try:
            with self.model.lock:
                node = self.model.get_node(path)
                return node.props['_id']
        except KeyError:
            # try to resolve the path via requests
            path_elm_id = 'root'
            for path_elm in path:
                children = self._get_children(path_elm_id)
                path_elm_id = \
                    next((props['_id'] for name, props in children if name == path_elm),
                         None)

            if path_elm_id is not None:
                return path_elm_id

            raise FileNotFoundError("File does not exist", path)

    @error_mapper
    @gdrive_error_mapper
    def delete(self, path, original_version_id):
        """ Delete can be done in 2 ways on GoogleDrive: either with a permanent delete by using
        https://developers.google.com/drive/v3/reference/files/delete or set trashed to true with a
        patch https://developers.google.com/drive/v3/reference/files/update.


        """

        file_id = self._path_to_file_id(path)
        file_url = urllib.parse.urljoin(self.base_url, 'files/' + str(file_id))

        # fetch current version id since Google Drive has node If-Match header
        if original_version_id is not None:
            version_id_resp = self.oauth_session.get(
                file_url, params={'fields': 'mimeType,' + VERSION_FIELD})
            version_id_resp.raise_for_status()
            response = version_id_resp.json()
            if response['mimeType'] == 'application/vnd.google-apps.folder':
                version_id = 'is_dir'
            else:
                version_id = response[VERSION_FIELD]
            if version_id != original_version_id:
                raise jars.VersionIdNotMatchingError(
                    self.storage_id,
                    version_a=version_id,
                    version_b=original_version_id)

        resp = self.oauth_session.patch(file_url, json={'trashed': True})
        resp.raise_for_status()

    @error_mapper
    @gdrive_error_mapper
    def write(self, path, file_obj, original_version_id=None, size=0):
        """
        https://developers.google.com/drive/v3/web/manage-uploads
        """
        # pylint: disable=too-many-locals,too-many-statements

        parent_id = None
        node_id = None
        http_verb = ''

        with self.model.lock:
            with contextlib.suppress(KeyError):
                parent = self.model.get_node(path[:-1])
                parent_id = parent.props['_id']
                with contextlib.suppress(KeyError):
                    node = parent.get_node([path[-1]])
                    node_id = node.props['_id']

        # create parent and fetch id
        if parent_id is None:
            self.make_dir(path[:-1])
            with self.model.lock:
                parent = self.model.get_node(path[:-1])
                parent_id = parent.props['_id']

        url = 'https://www.googleapis.com/upload/drive/v3/files'

        # check version id, if there is one to check
        if original_version_id is not None:
            res = self.oauth_session.get(
                'https://www.googleapis.com/drive/v3/files/{}'.format(node_id),
                params={'fields': VERSION_FIELD})
            res.raise_for_status()
            if res.json()[VERSION_FIELD] != original_version_id:
                raise jars.VersionIdNotMatchingError(
                    self.storage_id,
                    version_a=res.json()[VERSION_FIELD],
                    version_b=original_version_id)

        body = {}  # 'parents': [parent_id], 'name': path[-1]}
        if node_id is not None:
            http_verb = 'PATCH'
            body['fileId'] = node_id
            url += '/' + node_id
        else:
            http_verb = 'POST'
            body['parents'] = [parent_id]
            body['name'] = path[-1]

        # read the first kb to see if it is an empty file
        # that is a workaround for https://github.com/kennethreitz/requests/issues/3066
        first_chunk = file_obj.read(1024 * 16)

        response = self.oauth_session.request(
            http_verb,
            url,
            json=body,
            params={
                'uploadType': 'resumable',
                'fields': FIELDS
            },
            #  headers={'X-Upload-Content-Length': str(size)}
        )

        response.raise_for_status()
        upload_url = response.headers['Location']

        if not first_chunk:
            response = self.oauth_session.put(upload_url)
        else:
            chunker = FragmentingChunker(file_obj,
                                         first_chunk=first_chunk,
                                         chunk_size=1024)
            headers = {}
            if size:
                headers['Content-Range'] = 'bytes 0-{}/{}'.format(
                    size - 1, size)

            response = self.oauth_session.put(upload_url,
                                              data=iter(chunker),
                                              headers=headers)

        response.raise_for_status()

        new_file = response.json()
        with jars.TreeToSyncEngineEngineAdapter(node=self.model,
                                                storage_id=self.storage_id,
                                                sync_engine=self._event_sink):
            props = gdrive_to_node_props(new_file)
            if parent.has_child(new_file['name']):
                parent.get_node([new_file['name']]).props.update(props)
            else:
                parent.add_child(new_file['name'], props)

        return new_file[VERSION_FIELD]

    supports_open_in_web_link = True

    @error_mapper
    @gdrive_error_mapper
    def create_open_in_web_link(self, path):
        try:
            file_id = self._path_to_file_id(path)
            response = self.oauth_session.get(
                'https://www.googleapis.com/drive/v3/files/{}'.format(file_id),
                params={'fields': 'webViewLink'})
            response.raise_for_status()

            return response.json()['webViewLink']
        except (TypeError, FileNotFoundError) as ex:
            raise InvalidOperationError(self.storage_id, ex)

    supports_sharing_link = False

    supports_serialization = True

    def create_public_sharing_link(self, path):
        raise NotImplementedError

    def serialize(self):
        """
        See docs from super.
        """
        # letting super serialize the model
        super(GoogleDrive, self).serialize()

    @classmethod
    def _oauth_get_unique_id(cls, oauth_session):
        """
        return unique id for this account
        """
        user_info = get_user_metadata(cls.base_url, oauth_session)
        return user_info['user']['permissionId']

    def get_shared_folders(self):
        """ Returns a list of :class:`jars.SharedFolder`"""
        shared_folders = []
        shared_folders_nodes = [
            node for node in self.model if node.props.get('shared') is True
            and node.parent.props.get('shared', False) is False
        ]
        for node in shared_folders_nodes:
            shared_folders.append(
                SharedFolder(path=node.path,
                             share_id=node.props['_id'],
                             sp_user_ids=node.props['_shared_with']))
        return shared_folders

    def create_user_name(self):
        """Return a username for Google Drive.

        In this case the username will be 'displayName', obtained using
        https://developers.google.com/drive/v3/reference/about#resource
        """
        return get_user_metadata(self.base_url,
                                 self.oauth_session)['user']['displayName']
예제 #6
0
 def clear_model(self):
     self.model = IndexingNode(None, indexes=['_id'])
예제 #7
0
    def get_tree(self, cached=False, filtered=True):
        # pylint: disable=arguments-differ, too-many-locals
        logger.debug('requested tree with filter %s', self.filter_tree)
        if cached:
            with self.model.lock:
                if filtered:
                    logger.debug('requested tree with filter %s',
                                 self.filter_tree)
                    return jars.utils.filter_tree_by_path(
                        tree=self.model, filter_tree=self.filter_tree)
                else:
                    return copy.deepcopy(self.model)

        root = IndexingNode(name=None, indexes=['_id'])

        # fetch and store current cursor
        url = urllib.parse.urljoin(self.base_url, 'changes/startPageToken')
        token = self.oauth_session.get(url)
        token.raise_for_status()
        root.props['_cursor'] = token.json()['startPageToken']

        # get root id
        url = urllib.parse.urljoin(self.base_url, 'files/root?fields=id')
        root_id = self.oauth_session.get(url)
        root_id.raise_for_status()
        root.props['_id'] = root_id.json()['id']

        # fetch metrics
        url = urllib.parse.urljoin(self.base_url,
                                   'about?fields=storageQuota,user')
        about = self.oauth_session.get(url)
        about.raise_for_status()
        metrics = about.json()

        # this is relevant to client#970
        total_space = int(metrics['storageQuota'].get('limit', sys.maxsize))
        free_space = total_space - int(metrics['storageQuota']['usageInDrive'])
        root.props[METRICS] = StorageMetrics(storage_id=self.storage_id,
                                             free_space=free_space,
                                             total_space=total_space)
        # fetch tree
        url = urllib.parse.urljoin(self.base_url, 'files')

        page_token = None

        nodes = []

        # fetch the whole tree into the parent_map
        while True:
            params = {
                'pageSize': '1000',
                'q': 'trashed=false',
                'orderBy': 'folder',
                'fields': 'nextPageToken,files(' + FIELDS + ')'
            }
            if page_token is not None:
                params['pageToken'] = page_token
            files_resp = self.oauth_session.get(url, params=params)

            files_resp.raise_for_status()
            resp_json = files_resp.json()
            files = resp_json['files']
            page_token = resp_json.get('nextPageToken', None)

            for file in files:
                if 'parents' in file and is_relevant_file(file):
                    for parent_id in file['parents']:
                        # for each parent add this element
                        if is_relevant_file(file):
                            nodes.append(
                                NodeChange(action='UPSERT',
                                           parent_id=parent_id,
                                           name=file['name'],
                                           props=gdrive_to_node_props(file)))

            if page_token is None:
                break

        root.add_merge_set_with_id(nodes, key='_id')
        self._adjust_share_ids(nodes)

        self.offline = False

        if filtered:
            logger.debug('requested tree with filter %s', self.filter_tree)
            root = jars.utils.filter_tree_by_path(root, self.filter_tree)
        return root
예제 #8
0
def test_adjust_share_ids():
    """Test adjust_share_ids given different trees."""
    import logging
    logging.basicConfig(level=logging.DEBUG)
    gdrive = Mock(GoogleDrive)
    gdrive.model = IndexingNode(None, indexes=['_id'])
    tree = gdrive.model

    # Empty mergesets should mean no changes
    mergeset = []
    GoogleDrive._adjust_share_ids(gdrive, mergeset)
    assert tree.props == {}

    # if the node is located in the root is not necessary to compare properties with the parent
    child_1 = tree.add_child(name='child_1', props={'_id': 'id_1'})
    mergeset = [
        NodeChange(action='UPSERT',
                   parent_id=None,
                   name=child_1.name,
                   props=child_1.props)
    ]

    GoogleDrive._adjust_share_ids(gdrive, mergeset)

    assert len(child_1.props) is 1

    # if the file is not located on the root, it should be compared with the parent
    child_1_1 = child_1.add_child(name='child_1_1', props={'_id': 'id_1_1'})
    mergeset = [
        NodeChange(action='UPSERT',
                   parent_id=child_1.props['_id'],
                   name=child_1_1.name,
                   props=child_1_1.props)
    ]

    # shared_with property is the same and no share_id -> nothing happens
    GoogleDrive._adjust_share_ids(gdrive, mergeset)

    assert child_1_1.props == {'_id': 'id_1_1'}

    # child_1_1 shared_with atribute is different than his parent
    child_1_1.props['_shared_with'] = ('random_123', )

    # Now that the mergeset fields are not hardcoded there should not be need for this step
    # mergeset = [
    #         ('UPSERT', 'id_1', 'child_1_1', {'_id': 'id_1_1', '_shared_with': ('random_123',)})
    # ]

    # child_1_1 share_id should be set to child_1_1 _id
    GoogleDrive._adjust_share_ids(gdrive, mergeset)

    assert child_1_1.props['_id'] == child_1_1.props['share_id']

    # same case as above, but child_1 is shared too
    child_1.props['_shared_with'] = ('random_321', )
    mergeset = [(NodeChange(action='UPSERT',
                            parent_id=None,
                            name='child_1',
                            props={
                                '_id': 'id_1',
                                '_shared_with': ('random_321', )
                            }))]

    GoogleDrive._adjust_share_ids(gdrive, mergeset)

    assert child_1_1.props['_id'] == child_1_1.props['share_id']
    assert child_1.props['_id'] == child_1.props['share_id']

    # child_1 and child_1_1 are shared with the same users -> same share -> remove child_1_1
    # share_id
    child_1_1.props['_shared_with'] = ('random_321', )
    mergeset = [
        NodeChange(action='UPSERT',
                   parent_id='id_1',
                   name='child_1_1',
                   props={
                       '_id': 'id_1_1',
                       '_shared_with': ('random_321', )
                   })
    ]

    GoogleDrive._adjust_share_ids(gdrive, mergeset)

    assert child_1.props['share_id'] == child_1.props['_id']
    assert child_1_1.props.get('share_id', None) is None

    # DELETEs should be skipped
    mergeset = [
        NodeChange(action='DELETE',
                   parent_id='id_1',
                   name='child_1_1',
                   props={'_id': 'id_1_1'})
    ]
    GoogleDrive._adjust_share_ids(gdrive, mergeset)
    assert child_1.props['share_id'] == child_1.props['_id']
    assert child_1_1.props.get('share_id', None) is None

    # Merges of nodes that don't exist in our index should also be skipped
    mergeset = [
        NodeChange(action='UPSERT',
                   parent_id='id_1',
                   name='child_1_2',
                   props={
                       '_id': 'id_1_2',
                       '_shared_with': ('random_123', )
                   })
    ]
    GoogleDrive._adjust_share_ids(gdrive, mergeset)
    assert child_1.props['share_id'] == child_1.props['_id']
    assert child_1_1.props.get('share_id', None) is None
예제 #9
0
def test_indexing_node_no_parent_or_index():
    with pytest.raises(ValueError):
        node = IndexingNode(None)
예제 #10
0
def indexing_node_cust_props():
    """ fixture to test the indexing node """
    return IndexingNode(None, indexes=['_id'], props_cls=MyFancyProperties)