# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2013, 2014, 2015 CERN.
#
# Invenio is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# Invenio is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Invenio; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

"""Google Drive file system.

Installation:: pip install invenio[google_drive]

Known issues:

* Flush and close, both call write contents and because of that
  the file on cloud is overwrite twice...
"""

import datetime
import os
import time
from UserDict import UserDict

import httplib2
import six
from apiclient import errors
from apiclient.discovery import build
from apiclient.http import MediaInMemoryUpload
# Python filesystem imports
from fs.base import FS
from fs.errors import CreateFailedError, NoPathURLError, \
    OperationFailedError, RemoteConnectionError, ResourceInvalidError, \
    ResourceNotFoundError, UnsupportedError
from fs.filelike import SpooledTemporaryFile
from fs.remote import RemoteFileBuffer
from oauth2client.client import OAuth2Credentials

# Items in cache are considered expired after 5 minutes.
CACHE_TTL = 300
# Max size for spooling to memory before using disk (5M).
MAX_BUFFER = 1024**2 * 5
# Indicates the mimeType for a google drive folder
GD_FOLDER = "application/vnd.google-apps.folder"


class CacheItem(object):

    """Represents a path in the cache.

    There are two components to a path. It's individual metadata,
    and the children contained within it.
    """

    def __init__(self, metadata=None, children=None, timestamp=None,
                 parents=None):
        """Initialize a CacheItem instance."""
        self.metadata = metadata
        self.children = children
        self.parents = parents
        if timestamp is None:
            timestamp = time.time()
        self.timestamp = timestamp

    def add_child(self, name, client=None):
        """Add a child."""
        if self.children is None:
            if client is not None:
                # This is a fix. When you add a child to a folder that
                # was still not listed, that folder gets only one
                # child when you list it afterwards. So this fix
                # first tries to check are the files/folders inside
                # this directory on cloud.
                client.children(self.metadata['id'])
            else:
                self.children = [name]
        else:
            if name not in self.children:
                self.children.append(name)

    def del_child(self, name):
        """Delete a child."""
        if self.children is None:
            return
        try:
            i = self.children.index(name)
        except ValueError:
            return
        self.children.pop(i)

    def _get_expired(self):
        if self.timestamp <= time.time() - CACHE_TTL:
            return True
    expired = property(_get_expired)

    def renew(self):
        """Renew."""
        self.timestamp = time.time()


class GoogleDriveCache(UserDict):

    """Represent the google drive cache."""

    def __init__(self, client):
        """Initialize a GoogleDriveCache instance."""
        self._client = client
        UserDict.__init__(self)

    def set(self, path, metadata, children=None, parents=None):
        """Set metadata."""
        self[path] = CacheItem(metadata, children=children, parents=parents)
        if parents is not None:
            for parent in parents:
                if parent in self:
                    self.get(parent).add_child(path, self._client)

    def pop(self, path, default=None):
        """Pop data of a given path."""
        value = UserDict.pop(self, path, default)
        if value.parents is not None:
            for parent in value.parents:
                if parent in self:
                    self.get(parent).del_child(value.metadata['id'])
        return value


class GoogleDriveClient(object):

    """Represent the google drive client."""

    def __init__(self, credentials):
        """Initialize a GoogleDriveClient instance."""
        self.credentials = credentials
        self.service = self._build_service()
        self.cache = GoogleDriveCache(self)
        self._retry = 0

    def _build_service(self):
        http = httplib2.Http()
        http = self.credentials.authorize(http)
        service = build('drive', 'v2', http=http)
        return service

    def _retry_operation(self, method, *args):
        """Method retries an operation.

        Sometimes access_token expires and we need to rebuild it using
        the refresh token. This method does that and retries the
        operation that failed.
        """
        if self._retry < 5:
            self._retry += 1
            self.service = self._build_service()
            return method(*args)
        else:
            raise RemoteConnectionError(
                "Most probable reasons: access token has expired "
                "or user credentials are invalid.")

    def get_file(self, path):
        """Get file of a given path."""
        item = self.cache.get(path)
        if not item or item.metadata is None or item.expired:
            try:
                metadata = self.service.files().get(fileId=path).execute()
            except errors.HttpError as e:
                if e.resp.status == 404:
                    raise ResourceNotFoundError("Source file doesn't exist")
                raise OperationFailedError(opname='get_file',
                                           msg=e.resp.reason)
            except:
                return self._retry_operation(self.get_file, path)
            self._add_to_cache_from_dict(metadata, metadata['parents'])
        else:
            item = self.cache[path]
            metadata = item.metadata

        download_url = metadata.get('downloadUrl')
        resp, content = self.service._http.request(download_url)
        if resp.status == 200:
            return content
        else:
            raise OperationFailedError(opname="get_file", msg=str(resp))

    def metadata(self, path):
        """Get metadata of a given path."""
        item = self.cache.get(path)
        if not item or item.metadata is None or item.expired:
            try:
                metadata = self.service.files().get(fileId=path).execute()
            except errors.HttpError as e:
                if e.resp.status == 404:
                    raise ResourceNotFoundError(path)
                raise OperationFailedError(opname='metadata', path=path,
                                           msg=e.resp.reason)
            except:
                return self._retry_operation(self.metadata, path)
            if metadata.get('trashed', False):
                raise ResourceNotFoundError(path)
            item = self.cache[path] = CacheItem(metadata)

        # Copy the info so the caller cannot affect our cache.
        return dict(item.metadata.items())

    def children(self, path):
        """Get children of a given path."""
        update = False
        item = self.cache.get(path)
        if item:
            if item.expired:
                update = True
            else:
                if item.metadata["mimeType"] != GD_FOLDER:
                    raise ResourceInvalidError(path)
            if not item.children:
                update = True
        else:
            update = True
        if update:
            try:
                metadata = self.service.files().get(fileId=path).execute()
                if metadata["mimeType"] != GD_FOLDER:
                    raise ResourceInvalidError(path)
                param = {"q": "trashed=false and '%s' in parents" % path}
                children = []
                filesResource = self.service.files().list(**param).execute()
                for child in filesResource['items']:
                    children.append(child['id'])
                    self.cache[child['id']] = CacheItem(child, parents=[path])
                item = self.cache[path] = CacheItem(metadata, children)
            except errors.HttpError as e:
                if e.resp.status == 404:
                    raise ResourceNotFoundError(path)
                if not item or e.resp.status != 304:
                    raise OperationFailedError(opname='metadata', path=path,
                                               msg=e.resp.reason)
                # We have an item from cache (perhaps expired),
                # but it's still valid (as far as GoogleDrive is
                # concerned), so just renew it and keep using it.
                item.renew()
            except:
                return self._retry_operation(self.children, path)
        return item.children

    def file_create_folder(self, parent_id, title):
        """Add newly created directory to cache."""
        body = {
            "title": title,
            "parents": [{"id": parent_id}],
            "mimeType": "application/vnd.google-apps.folder"
        }
        try:
            metadata = self.service.files().insert(body=body).execute()
        except errors.HttpError as e:
            if e.resp.status == 405:
                raise ResourceInvalidError(parent_id)
            if e.resp.status == 404:
                raise ResourceNotFoundError(parent_id)
            raise OperationFailedError(
                opname='file_create_folder',
                msg="%s, the reasons could be: parent "
                    "doesn't exist or is a file" % (e.resp.reason, ))
        except:
            return self._retry_operation(self.file_create_folder,
                                         parent_id, title)

        self.cache.set(metadata["id"], metadata, parents=[parent_id])
        return metadata

    def file_copy(self, file_id, parent_id):
        """Copy a file to a folder of a given id."""
        body = {"parents": [{"id": parent_id}]}

        try:
            metadata = self.service.files().copy(fileId=file_id,
                                                 body=body,
                                                 ).execute()
        except errors.HttpError as e:
            if e.resp.status == 404:
                raise ResourceNotFoundError(
                    "Parent or source file don't exist.")
            raise OperationFailedError(opname='file_copy', msg=e.resp.reason)
        except:
            return self._retry_operation(self.file_copy, file_id, parent_id)

        self.cache.set(metadata['id'], metadata, parents=[parent_id])
        return metadata

    def update_file(self, file_id, new_file):
        """Update the file's contents of the given id."""
        try:
            metadata = self.service.files().update(fileId=file_id,
                                                   body=new_file
                                                   ).execute()
        except errors.HttpError as e:
            if e.resp.status == 404:
                raise ResourceNotFoundError(
                    "Parent or source file don't exist.")
            raise OperationFailedError(opname='update_file',
                                       msg=e.resp.reason)
        except:
            return self._retry_operation(self.update_file, file_id, new_file)
        self.cache.pop(file_id, None)
        self._add_to_cache_from_dict(metadata, metadata['parents'])
        return metadata

    def _add_to_cache_from_dict(self, metadata, parents):
        new_parents = []
        for one in parents:
            new_parents.append(one['id'])
        self.cache.set(metadata['id'], metadata, parents=new_parents)

    def update_file_content(self, file_id, content):
        """Update the file's contents of the given id on google drive."""
        item = self.cache.get(file_id, None)
        if not item or item.metadata is None or item.expired:
            try:
                metadata = self.service.files().get(fileId=file_id).execute()
            except errors.HttpError as e:
                raise OperationFailedError(opname='update_file_content',
                                           msg=e.resp.reason)
            except:
                return self._retry_operation(self.update_file_content,
                                             file_id, content)
            self.cache.set(metadata['id'], metadata)
        else:
            metadata = item.metadata

        media_body = MediaInMemoryUpload(content)
        try:
            updated_file = self.service.files().update(fileId=file_id,
                                                       body=metadata,
                                                       media_body=media_body
                                                       ).execute()
        except errors.HttpError as e:
            raise OperationFailedError(opname='update_file_content',
                                       msg=e.resp.reason)
        except TypeError as e:
            raise ResourceInvalidError("update_file_content %r" % e)
        except:
            return self._retry_operation(self.update_file_content, file_id,
                                         content)
        self.cache.pop(file_id, None)
        self._add_to_cache_from_dict(updated_file, updated_file['parents'])
        return updated_file

    def file_delete(self, path):
        """Delete a file."""
        try:
            self.service.files().delete(fileId=path).execute()
        except errors.HttpError as e:
            if e.resp.status == 404:
                raise ResourceNotFoundError(path)
            raise OperationFailedError(opname='file_delete',
                                       msg=e.resp.reason)
        except:
            return self._retry_operation(self.file_delete, path)
        self.cache.pop(path, None)

    def put_file(self, parent_id, title, content, description=None):
        """Add a file to folder of the given id."""
        media_body = MediaInMemoryUpload(content)
        body = {
            'title': title,
            'description': description,
            'parents': [{'id': parent_id}]
        }
        try:
            metadata = self.service.files().insert(body=body,
                                                   media_body=media_body
                                                   ).execute()
        except errors.HttpError as e:
            raise OperationFailedError(opname='put_file', msg=e.resp.reason)
        except TypeError as e:
            raise ResourceInvalidError("put_file")
        except:
            return self._retry_operation(self.put_file, parent_id, title,
                                         content, description)
        self.cache.set(metadata['id'], metadata, parents=[parent_id])
        return metadata

    def about(self):
        """Get info about the service."""
        try:
            # FIXME check if the access_token has expired
            info = self.service.about().get().execute()
            return info
        except:
            return self._retry_operation(self.about)


class GoogleDriveFS(FS):

    """Google drive file system.

    @attention: when setting variables in os.environ please note that
    GD_TOKEN_EXPIRY has to be in format: "%Y, %m, %d, %H, %M, %S, %f"
    """

    __name__ = "Google Drive"
    _meta = {'thread_safe': True,
             'virtual': False,
             'read_only': False,
             'unicode_paths': True,
             'case_insensitive_paths': False,
             'network': True,
             'atomic.move': True,
             'atomic.copy': True,
             'atomic.makedir': True,
             'atomic.rename': True,
             'atomic.setcontents': True,
             }

    def __init__(self, root=None, credentials=None, thread_synchronize=True):
        """Initialize a GoogleDriveFS instance."""
        self._root = root

        def _getDateTimeFromString(time):
            # Parses string into datetime object
            if time:
                return datetime.datetime.strptime(
                    time, "%Y, %m, %d, %H, %M, %S, %f")
            else:
                return None

        if credentials is None:
            # Get credentials need to build the google drive service
            if ("GD_ACCESS_TOKEN" not in os.environ or
                    "GD_CLIENT_ID" not in os.environ or
                    "GD_CLIENT_SECRET" not in os.environ or
                    "GD_TOKEN_EXPIRY" not in os.environ or
                    "GD_TOKEN_URI" not in os.environ,
                    "GD_REFRESH_TOKEN" not in os.environ):
                raise CreateFailedError("You need to set:\n"
                                        "GD_ACCESS_TOKEN, "
                                        "GD_CLIENT_ID, "
                                        "GD_CLIENT_SECRET "
                                        "GD_TOKEN_EXPIRY, "
                                        "GD_REFRESH_TOKEN, "
                                        "GD_TOKEN_URI in os.environ"
                                        )
            else:
                self._credentials = OAuth2Credentials(
                    os.environ.get('GD_ACCESS_TOKEN'),
                    os.environ.get('GD_CLIENT_ID'),
                    os.environ.get('GD_CLIENT_SECRET'),
                    os.environ.get('GD_REFRESH_TOKEN'),
                    _getDateTimeFromString(
                        os.environ.get('GD_TOKEN_EXPIRY')
                    ),
                    os.environ.get('GD_TOKEN_URI'),
                    None
                )
        else:
            self._credentials = OAuth2Credentials(
                credentials.get('access_token'),
                credentials.get('client_id'),
                credentials.get('client_secret'),
                credentials.get('refresh_token'),
                _getDateTimeFromString(
                    credentials.get('token_expiry')
                ),
                credentials.get('token_uri'),
                None
            )
        self.client = GoogleDriveClient(self._credentials)

        if self._root is None or root == '' or self._root == "/":
            # Root fix, if root is not set get the root folder id
            # from the user information returned by 'about'
            about = self.client.about()
            self._root = about.get("rootFolderId")

        # Initialize super class FS
        super(self.__class__, self).__init__(
            thread_synchronize=thread_synchronize
        )

    def __repr__(self):
        """Represent the google_drive filesystem and the root."""
        args = (self.__class__.__name__, self._root)
        return '<FileSystem: %s - Root Directory: %s>' % args

    __str__ = __repr__

    def __unicode__(self):
        """Represent the google_drive filesystem and the root (unicode)."""
        args = (self.__class__.__name__, self._root)
        return u'<FileSystem: %s - Root Directory: %s>' % args

    def _update(self, path, contents):
        """Update contents of an existing file.

        :param path: Id of the file for which to update content
        :param contents: Contents to write to the file
        :return: Id of the updated file
        """
        path = self._normpath(path)

        if isinstance(contents, six.string_types):
            string_data = contents
        else:
            try:
                contents.seek(0)
                string_data = contents.read()
            except:
                raise ResourceInvalidError("Unsupported type")

        return self.client.update_file_content(path, string_data)['id']

    def setcontents(self, path, contents="", chunk_size=64 * 1024, **kwargs):
        """Set new content to remote file.

        Method works only with existing files and sets
        new content to them.

        :param path: Id of the file in which to write the new content
        :param contents: File contents as a string, or any object with
            read and seek methods
        :param kwargs: additional parameters like:
            encoding: the type of encoding to use if data is text
            errors: encoding errors
        :param chunk_size: Number of bytes to read in a chunk,
            if the implementation has to resort to a read copy loop
        :return: Id of the updated file
        """
        encoding = kwargs.get("encoding", None)
        errors = kwargs.get("errors", None)

        if isinstance(contents, six.text_type):
            contents = contents.encode(encoding=encoding, errors=errors)

        return self._update(path, contents)

    def createfile(self, path, wipe=True, **kwargs):
        """Create always an empty file.

        Even if another file with the same name exists it will create
        a file with the same name.

        :param path: path to the new file. It has to be in one of
            following forms: parent_id/file_title.ext, file_title.ext or
            /file_title.ext - in this cases root directory is the parent.
        :param wipe: New file with empty content.
            In the case of google drive it will always be True
        :param kwargs: Additional parameters like:
            description - a short description of the new file
        :raises ResourceNotFoundError: If parent doesn't exist.

        :attention: Root directory is the current root directory
            of this instance of filesystem and not the root of
            your Google Drive.

        :return: Id of the created file
        """
        # Google drive doesn't work with paths. So a slight
        # work around is needed.
        path = self._normpath(path)

        parts = path.split("/")
        description = kwargs.get('description', '')
        if len(parts) == 1 and parts[0] == self._root:
            raise ResourceNotFoundError("Please, specify a filename.")
        elif len(parts) == 1:
            return self.client.put_file(self._root,
                                        parts[0], "", description)['id']
        else:
            title = parts.pop()
            parent_id = self.makedir(parts, True)
            return self.client.put_file(parent_id,
                                        title, "", description)['id']

    def open(self, path, mode='r', buffering=-1, encoding=None,
             errors=None, newline=None, line_buffering=False, **kwargs):
        """Open the named file in the given mode.

        This method downloads the file contents into a local temporary file so
        that it can be worked on efficiently.  Any changes made to the file are
        only sent back to cloud storage when the file is flushed or closed.

        :param path: Id of the file to be opened
        :param mode: In which mode to open the file
        :raises ResourceNotFoundError: If given path doesn't exist and
            'w' is not in mode
        :return: RemoteFileBuffer object
        """
        path = self._normpath(path)
        spooled_file = SpooledTemporaryFile(mode=mode, bufsize=MAX_BUFFER)

        # Truncate the file if requested
        if "w" in mode:
            try:
                self._update(path, "")
            except:
                path = self.createfile(path, True)
        else:
            try:
                spooled_file.write(self.client.get_file(path))
                spooled_file.seek(0, 0)
            except Exception as e:
                if "w" not in mode and "a" not in mode:
                    raise ResourceNotFoundError("%r" % e)
                else:
                    path = self.createfile(path, True)
        return RemoteFileBuffer(self, path, mode, spooled_file)

    def is_root(self, path):
        """Check if the given path is the root folder.

        :param path: Id of the folder to check
        """
        path = self._normpath(path)

        if path == self._root:
            return True
        else:
            return False

    def copy(self, src, dst, overwrite=False, chunk_size=1024 * 64):
        """Copy a file to another folder.

        :param src: Id of the file to be copied
        :param dst: Id of the folder in which to copy the file
        :param overwrite: This is never true for GoogleDrive
            because there can be many files with the same name
            in one folder.
        :return: Id of the copied file
        """
        return self.client.file_copy(src, dst)['id']

    def copydir(self, src, dst, overwrite=False, ignore_errors=False,
                chunk_size=16384):
        """Copy of folders is not supported.

        @attention: Google drive doesn't support copy of folders.
        To implement it over copy method will be very inefficient.
        """
        raise NotImplemented("If implemented method will be very inefficient")

    def rename(self, src, dst):
        """Rename a file of a given path.

        :param src: Id of the file to be renamed
        :param dst: New title of the file
        :raises UnsupportedError: If trying to rename the root directory
        :return: Id of the renamed file
        """
        if self.is_root(path=src):
            raise UnsupportedError("Can't rename the root directory")
        f = self.client.metadata(src)
        f['title'] = dst
        return self.client.update_file(src, f)['id']

    def remove(self, path):
        """Remove a file of a given path.

        :param path: id of the file to be deleted
        :return: None if removal was successful
        """
        path = self._normpath(path)

        if self.is_root(path=path):
            raise UnsupportedError("Can't remove the root directory")
        if self.isdir(path=path):
            raise ResourceInvalidError("Specified path is a directory. "
                                       "Please use removedir.")
        self.client.file_delete(path)

    def removedir(self, path):
        """Remove a directory of a given path.

        :param path: id of the folder to be deleted
        :return: None if removal was successful
        """
        path = self._normpath(path)

        if not self.isdir(path):
            raise ResourceInvalidError("Specified path is not a directory")
        if self.is_root(path=path):
            raise UnsupportedError("remove the root directory")
        self.client.file_delete(path)

    def makedir(self, path, recursive=False, allow_recreate=False):
        """Create a directory of a given path.

        :param path: path to the folder you want to create.
            It has to be in one of the following forms:
            ``parent_id/new_folder_name`` when recursive is False,
            ``parent_id/new_folder1/new_folder2`` when recursive is True,
            ``/new_folder_name`` to create a new folder in root directory,
            ``/new_folder1/new_folder2`` to recursively create a new folder
            in root directory.
        :param recursive: allows recursive creation of directories
        :param allow_recreate: for google drive this param is
            always False, it will never recreate a directory with
            the same id ( same names are allowed )
        :return: Id of the created directory
        """
        path = self._normpath(path)
        if '/' in path:
            parts = path.split("/")
        else:
            parts = path

        if len(parts) == 1 and parts[0] == self._root:
            raise ResourceNotFoundError("Please, specify a folder name.")
        elif len(parts) == 1:
            return self.client.file_create_folder(self._root, parts[0])['id']
        else:
            if recursive:
                resp = self.client.file_create_folder(self._root, parts[0])
                for folder in parts[1:]:
                    if folder != '':
                        resp = self.client.file_create_folder(resp['id'],
                                                              folder)
                return resp['id']
            else:
                raise UnsupportedError("Recursively create a folder.")

    def move(self, src, dst, overwrite=False, chunk_size=16384):
        """Move a file to another folder.

        .. note:: Google Drive can have many parents for one file,
            when using this method a file will be moved from all
            current parents to the new parent 'dst'.

        :param src: id of the file to be moved
        :param dst: id of the folder in which the file will be moved
        :param overwrite: for Google drive it is always false
        :param chunk_size: if using chunk upload
        :return: Id of the moved file
        """
        if self.isdir(src):
            raise ResourceInvalidError(
                "Specified src is a directory. Please use movedir.")

        f = self.client.get_file(src)
        f['parents'] = [{"id": dst}]
        return self.client.update_file(src, f)['id']

    def movedir(self, src, dst, overwrite=False, ignore_errors=False,
                chunk_size=16384):
        """Move a folder to another folder.

        .. note:: Google Drive can have many parents for one folder,
            when using this method a folder will be moved from all
            current parents to the new parent 'dst'.

        :param src: id of the folder to be moved
        :param dst: id of the folder in which the file will be moved
        :param overwrite: for Google drive it is always false
        :param chunk_size: if using chunk upload
        :return: Id of the moved folder
        """
        if self.isfile(src):
            raise ResourceInvalidError(
                "Specified src is a file. Please use move.")
        f = self.client.get_file(src)
        f['parents'] = [{"id": dst}]

        return self.client.update_file(src, f)['id']

    def isdir(self, path):
        """Check if a the specified path is a directory.

        :param path: Id of the file/folder to check
        """
        path = self._normpath(path)
        info = self.getinfo(path)
        return info['isdir']

    def isfile(self, path):
        """
        Check if a the specified path is a file.

        :param path: Id of the file/folder to check
        """
        path = self._normpath(path)
        info = self.getinfo(path)
        return not info['isdir']

    def exists(self, path):
        """Check if a the specified path exists.

        :param path: Id of the file/folder to check
        """
        path = self._normpath(path)
        try:
            self.client.metadata(path)
            return True
        except RemoteConnectionError as e:
            raise e
        except:
            return False

    def listdir(self, path=None, wildcard=None, full=False, absolute=False,
                dirs_only=False, files_only=False, overrideCache=False):
        """List the files and directories under a given path.

        The directory contents are returned as a list of unicode paths

        :param path: id of the folder to list
        :type path: string
        :param wildcard: Only returns paths that match this wildcard
        :type wildcard: string containing a wildcard, or a callable
            that accepts a path and returns a boolean
        :param full: returns full paths (relative to the root)
        :type full: bool
        :param absolute: returns absolute paths
            (paths beginning with /)
        :type absolute: bool
        :param dirs_only: if True, only return directories
        :type dirs_only: bool
        :param files_only: if True, only return files
        :type files_only: bool
        :return: a list of unicode paths
        """
        path = self._normpath(path)
        flist = self.client.children(path)

        dirContent = self._listdir_helper('', flist, wildcard, full,
                                          absolute, dirs_only, files_only)
        return dirContent

    def listdirinfo(self, path=None, wildcard=None, full=False, absolute=False,
                    dirs_only=False, files_only=False):
        """Retrieve a list of paths and path info under a given path.

        This method behaves like listdir() but instead of just returning the
        name of each item in the directory, it returns a tuple of the name and
        the info dict as returned by getinfo.

        :param path: id of the folder
        :param wildcard: filter paths that match this wildcard
        :param dirs_only: only retrieve directories
        :type dirs_only: bool
        :param files_only: only retrieve files
        :type files_only: bool
        :return: tuple of the name and the info dict as
            returned by getinfo.
        """
        return [(p, self.getinfo(p)) for p in self.listdir(
            path, wildcard=wildcard, full=full, absolute=absolute,
            dirs_only=dirs_only, files_only=files_only)]

    def getinfo(self, path):
        """Returned information is metadata from cloud service.

        A few more fields with standard names for some parts of the metadata.
        :param path: file id for which to return informations
        :return: dictionary with informations about the specific file
        """
        path = self._normpath(path)
        return self._metadata_to_info(self.client.metadata(path))

    def getpathurl(self, path, allow_none=False):
        """Get the url of a given path.

        :param path: id of the file for which to return the url path
        :param allow_none: if true, this method can return None if
            there is no URL form of the given path
        :type allow_none: bool
        :raises `fs.errors.NoPathURLError`: If no URL form exists,
            and allow_none is False (the default)
        :return: url that corresponds to the given path, if one exists
        """
        url = None
        try:
            url = self.getinfo(path)
            url = url["webContentLink"]
        except RemoteConnectionError as e:
            raise e
        except:
            if not allow_none:
                raise NoPathURLError(path=path)

        return url

    def desc(self, path):
        """Get the title of a given path.

        :return: The title for the given path.
        """
        info = self.getinfo(path)
        return info["title"]

    def about(self):
        """Get user information and settings.

        :return: information about the current user
            with whose credentials is the file system instantiated.
        """
        info = self.client.about()
        info['cloud_storage_url'] = "http://drive.google.com/"
        info['user_name'] = info.get('name')
        info['quota'] = 100 * (float(info['quotaBytesUsed']) /
                               float(info['quotaBytesTotal']))
        return info

    def _normpath(self, path):
        """Method normalises the path for google drive.

        :return: normaliesed path as a string
        """
        if path is None or path == "" or path == '/' or len(path) == 0:
            return self._root
        elif path[0] == "/":
            return path[1:]
        else:
            return path

    def _metadata_to_info(self, metadata, localtime=False):
        """Return modified metadata.

        Method adds a few standard names to the metadata:
        * size - the size of the file/folder
        * isdir - is something a file or a directory
        * created_time - the time of the creation
        * path - path to the object which metadata are we showing
        * revision - google drive doesn't have a revision parameter
        * modified - time of the last modification

        :return: The full metadata and a few more fields
            with standard names.
        """
        isdir = metadata.get("mimeType", None) == GD_FOLDER
        info = {
            'size': metadata.get('fileSize', 0),
            'isdir': isdir,
            'created_time': metadata.get('createdDate', 0),
            'path': metadata.get('id', 0),
            'revision': None,
            'modified': metadata.get("modifiedDate", 0)
        }
        info.update(metadata)

        return info
