Implémentez un driver FUSE pour Google Drive

GNU/Linux Magazine HS n° 090 | mai 2017 | Sylvain Peyrefitte
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Google aime beaucoup le Python et le fait savoir. Avant même de vouloir compter dans ses rangs le créateur du langage, il mettait à la disponibilité des programmeurs Python une grande partie de leurs API. Et Google Drive n'échappe pas à la règle.

Derrière l'appellation cloud se cache souvent beaucoup de concepts, qui à mon sens n’ont rien à y faire. À l'inverse, les services proposant une gestion de vos données multimédias disponibles partout où une connexion internet est disponible, assurant par là même une sauvegarde et redondance de vos données, constituent un réel service de cloud. Google Drive est le service de stockage de données « offert » par Google dans la limite d'un stockage de 15 Gio. Pour plus d'espace, il faudra, comme la plupart des concurrents, utiliser la carte bancaire. Mais comment faire pour intégrer un partage Google Drive à Linux ? Python bien sûr. Nous aborderons les dessous de l'API Python, offerte par Google, avant d'en explorer le fonctionnement. Ensuite, nous verrons comment l'intégrer de façon transparente dans Linux via l'API FUSE et son binding Python.

1. REST

L'API Google Drive est une API de type REST (REpresentational State Transfer), elle respecte donc une convention permettant l'interrogation du service au travers du protocole HTTP et, dans notre cas, HTTP2. De nombreuses applications Google reposent sur cette convention.

Mais un problème subsiste quand on implémente une API respectant une telle architecture, que l'on définit souvent sous le terme de RESTFull : c'est l'aspect « sans état ». Ceci implique que chaque requête est complètement décorrélée, et donc exclut automatiquement tout moyen d'authentification et donc d'identification des ressources auxquelles un tiers peut avoir droit. Le protocole OAuth2 répond à cette problématique.

2. OAuth2

L'API permettant de communiquer avec Google Drive repose, comme l'intégralité des API Google, sur le protocole OAuth. OAuth et son successeur OAuth2 ne sont pas à proprement parler, des protocoles d'authentification, mais il faut plutôt les voir comme des protocoles de délégation d'identité.

Une application possède de nombreuses ressources qu'il peut être intéressant de rendre disponibles, du moins en partie, à d'autres applications. C'est bien cette problématique que tente de résoudre le protocole OAuth2 (car OAuth 1 n'est plus recommandé). Il fut initié par Blaine Cook, alors ingénieur chez Twitter. Il était responsable de l'implémentation de OpenID, un service de mutualisation d'authentication afin d'accéder avec un compte unique à plusieurs applications web. Durant cette implémentation, il s'est rendu compte qu'il n'existait aucun standard permettent de déléguer une partie des ressources d'une application à une autre. C'est pour cela qu'il a eu l'idée d'initier le protocole OAuth. Ce projet fût très vite soutenu par Google qui y voyait un moyen de développer la communauté des développeurs autour de leurs applications et ainsi inciter les utilisateurs à utiliser indirectement les ressources mises à disposition par Google.

OAuth est donc basé sur de la délégation d'autorisations. On voit donc deux notions émerger :

- une application de type fournisseur offrant un service via une API ;

- une application de type consommatrice utilisant le service de la précédente.

Concrètement, l'utilisateur de l'application de type fournisseur va définir un périmètre fonctionnel qu'il va ensuite déléguer à l'application de type consommatrice au travers d'un token. Ce dernier sera donc présenté à chaque appel de l'API afin de l'identifier.

3. API Python

L'API Google repose donc sur un protocole ouvert, OAuth2, et le protocole HTTP2. Cette dernière est très bien documentée et nous allons décrire les étapes afin de créer une application permettant de s'interfacer avec le compte Google Drive de n'importe quel client.

3.1 Génération du token OAuth2

Dans un premier temps, il faut créer notre token nous permettant de nous authentifier auprès du service. Pour ce faire, il nous faut un compte Google et ensuite nous connecter au service de développeur à l'adresse suivante https://console.developers.google.com (voir figure 1). Ensuite, il faut créer une nouvelle application que nous allons nommer Test Linux Magazine.

Fig. 1 : Console de développeur Google. Liste de toutes les APIs Google disponibles.

Il faut donc activer Google Drive. Ensuite, il faut créer notre token OAuth2 en sélectionnant l'onglet identifiant puis ID client Oauth(voir figure 2).

Fig. 2 : Liste des identifiants disponibles pour une application.

Ensuite nous allons télécharger ce token au format JSON pour la suite des opérations et que nous allons sauvegarder sous oauth_token.json.

3.2 Python

Notre code Python va donc se découper en une partie d'authentification basée sur la bibliothèque OAuth2 de Python, et enfin une partie plus spécifique utilisant un wrapper REST pour le Google Drive API.

Nous allons donc commencer par installer les dépendances nécessaires pour notre petite application.

# pip install httplib2 google-api-python-client

3.2.1 Authentification

L'authentification de l'application repose entièrement sur le protocole OAuth2 :

from oauth2client import client

from oauth2client import tools

from oauth2client.file import Storage

def get_credentials():

    home_dir = os.path.expanduser('~')

    credential_dir = os.path.join(home_dir, '.credentials')

    if not os.path.exists(credential_dir):

        os.makedirs(credential_dir)

    credential_path = os.path.join(credential_dir,

                                   'test_linux_magazine.json')

    store = Storage(credential_path)

    credentials = store.get()

    if not credentials or credentials.invalid:

        flow = client.flow_from_clientsecrets('oauth_token.json', 'https://www.googleapis.com/auth/drive')

        flow.user_agent = 'Test Linux Magazine'

        if flags:

            credentials = tools.run_flow(flow, store, flags)

        else:

            credentials = tools.run(flow, store)

    return credentials

Rien de bien extraordinaire dans ce code où toute la complexité est cachée par l'API. Ici, nous optimisons simplement les appels et autorisations que nous sauvegardons dans le répertoire caché, sous le répertoire root de l'utilisateur, .credentials.

Si nous exécutons cette fonction, nous verrons que notre navigateur sera automatiquement lancé sur une page d'identification Google. Après avoir rentré vos identifiants, une nouvelle page d'autorisation sera affichée, permettant donc de déléguer une partie de Google Drive à votre nouvelle application (voir figure 3).

Fig. 3 : Fenêtre d'autorisation OAuth2 pour Google Drive.

3.2.2 REST

Nous allons donc explorer les fonctionnalités que nous offre l'API REST de Google Drive. Pour cela, Google nous propose un wrapper autour des requêtes et surtout des mécanismes nous permettant de manipuler simplement les résultats en Python.

Par exemple, il est simple de lister l'ensemble des fichiers disponibles pour un utilisateur :

from apiclient import discovery

credentials = get_credentials()

http = credentials.authorize(httplib2.Http())

service = discovery.build('drive', 'v3', http=http)

results = service.files().list(fields="nextPageToken, files(id, name)").execute()

items = results.get('files', [])

L'objet items contient l'ensemble des informations de vos fichiers présents dans votre Drive.

Il est bien sûr possible de télécharger vos fichiers :

from apiclient import discovery, http

file_id = '0BwwA4oUTeiV1UVNwOHItT0xfa2M'

request = service.files().get_media(fileId=file_id)

fh = io.BytesIO()

downloader = http.MediaIoBaseDownload(fh, request)

done = False

while done is False:

    status, done = downloader.next_chunk()

    print("Download %d%%." % int(status.progress() * 100))

Il est aussi possible de créer un répertoire via l'API :

file_metadata = {

  'name' : 'Images',

  'mimeType' : 'application/vnd.google-apps.folder'

}

file = service.files().create(body=file_metadata,

                                    fields='id').execute()

print('Folder ID: %s' % file.get('id'))

Ici nous nous sommes attardés sur des aspects de consultation, mais il est tout aussi possible d'uploader des fichiers. Mais bien sûr ceci requiert un scope différent pour l'application.

Cette API m'a donc donné une idée. Et si nous pouvions interagir avec nos données Google Drive au travers d'un explorateur de fichiers ? Nous allons donc implémenter un driver FUSE simple pour Google Drive écrit entièrement en Python.

4. FUSE

FUSE signifie Filesystem in UserSpacE. Concrètement, FUSE est une technique qui va nous permettre d'émuler un système de fichiers depuis l'espace utilisateur, et donc sans droits root. FUSE se décompose en une partie driver relayant les appels à une bibliothèque, libfuse, en espace utilisateur.

Il existe des bindings pour presque tous les langages et bien sûr Python n'échappe pas à la règle. Il existe de nombreux bindingset ici nous avons fait le choix de fusepy.

# pip install fusepy

Nous allons ensuite reprendre le code source que nous avons testé précédemment pour initier notre « driver » en userspace.

Pour cela, nous allons implémenter une classe simple qui va permettre de lister nos fichiers depuis le compte Google Drive de notre client, et ensuite télécharger à la volée les fichiers demandés.

Nous allons donc initier une classe GoogleDriveFS qui va implémenter la méthode init() suivante :

class GoogleDriveFS(Operations):

    def __init__(self):

        self.credentials = get_credentials()

        self.http = self.credentials.authorize(httplib2.Http())

        self.service = discovery.build('drive', 'v3', http=self.http)

        self.items = {}

        self.fh = {}

        self.next_fh = 0

Les credentials sont issus de la fonction précédemment décrite ; de plus, nous rajoutons un contexte permettant de gérer nos fichiers :

- items pour le résultat des requêtes REST ;

- fh qui sera tous les fichiers ouverts que nous stockerons bêtement en mémoire ;

- next_fh est un compteur de descripteur de fichiers.

Nous allons ensuite implémenter l'interface readdir appelée pour lister le contenu d'un répertoire. Dans cet exemple, nous ne gérons pas les répertoires Google Drive ; tout est contenu dans la racine.

    def readdir(self, path, fh):

        results = self.service.files().list(fields="nextPageToken, files(id, name, size)").execute()

        self.items = dict([(item['name'], (item['id'], int(item.get('size') or '0'))) for item in results.get('files', [])])

        return ['.', '..'] + list(self.items.keys())

Nous retrouvons ici notre requête précédente, plus une information de taille via l'attribut size. Nous remarquons qu'il est nécessaire d'ajouter les pseudo-répertoires . et ...

Ensuite, la méthodegetattr est appelée à chaque fois que des méta-informations sont demandées. Ici pour nous tout est un fichier, nous rajoutons l'information de taille.

    def getattr(self, path, fh=None):

        if path == '/':

            return dict(st_mode=(S_IFDIR | 0o755), st_nlink=2)

        if path[1:] not in self.items:

            raise FuseOSError(EROFS)

Enfin, nous allons implémenter les méthodes de lecture de fichiers. La fonction openest appelée pour l'ouverture d'un fichier puis la fonction read de façon répétée pour lire l'intégralité d'un fichier.

    def open(self, path, flags):

        if path[1:] not in self.items:

            raise FuseOSError(EROFS)

        request = self.service.files().get_media(fileId=self.items[path[1:]][0])

        fh = io.BytesIO()

        downloader = http.MediaIoBaseDownload(fh, request)

        done = False

        while done is False:

            status, done = downloader.next_chunk()

        fh_id = self.next_fh

        self.next_fh += 1

        self.fh[fh_id] = fh

        return fh_id

    def read(self, path, size, offset, fh):

        if fh not in self.fh:

            raise FuseOSError(EROFS)

        self.fh[fh].seek(offset)

returnself.fh[fh].read(size)

Ici nous manipulons des objets au sein de la mémoire au travers d'un io.Bytes.

Il suffit de lancer notre driver et nous pouvons interagir, de façon encore un peu simpliste, avec nos informations dans le Google Drive (voir figures 4 et 5).

Fig. 4 : Intégration au sein de l'application Files sous Ubuntu.

Fig. 5 : Affichage d'une image présente depuis Google Drive.

Conclusion

100 lignes de code Python au total pour réaliser un « driver » pour Google Drive. Cela révèle la maturité de Python ainsi que son intégration dans l'écosystème Linux et réseau. Google offre une API simple pour son Drive ainsi qu'un binding Python adapté, associé aux contributeurs réalisant des bibliothèques telles que fusepy qui peuvent nous permettre d'imaginer des solutions toujours plus innovantes !