~duplicity-team/duplicity/0.7-series

« back to all changes in this revision

Viewing changes to duplicity/backends/pydrivebackend.py

  • Committer: kenneth at loafman
  • Date: 2019-06-11 15:52:04 UTC
  • mfrom: (1372.2.8 0-7-snap-duplicity)
  • Revision ID: kenneth@loafman.com-20190611155204-ck3otzhy9d3x8kxv
* Merged in lp:~aaron-whitehouse/duplicity/07-snap
  - Add snapcraft packaging instructions for 0.7 series

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
 
2
#
 
3
# Copyright 2015 Yigal Asnis
 
4
#
 
5
# This file is free software; you can redistribute it and/or modify it
 
6
# under the terms of the GNU General Public License as published by the
 
7
# Free Software Foundation; either version 2 of the License, or (at your
 
8
# option) any later version.
 
9
#
 
10
# It is distributed in the hope that it will be useful, but
 
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
13
# General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with duplicity; if not, write to the Free Software Foundation,
 
17
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
18
 
 
19
import string
 
20
import os
 
21
 
 
22
import duplicity.backend
 
23
from duplicity import log
 
24
from duplicity.errors import BackendException
 
25
 
 
26
 
 
27
class PyDriveBackend(duplicity.backend.Backend):
 
28
    """Connect to remote store using PyDrive API"""
 
29
 
 
30
    def __init__(self, parsed_url):
 
31
        duplicity.backend.Backend.__init__(self, parsed_url)
 
32
        try:
 
33
            global pydrive
 
34
            import httplib2
 
35
            from apiclient.discovery import build
 
36
            from pydrive.auth import GoogleAuth
 
37
            from pydrive.drive import GoogleDrive
 
38
            from pydrive.files import FileNotUploadedError
 
39
        except ImportError as e:
 
40
            raise BackendException("""\
 
41
PyDrive backend requires PyDrive installation.  Please read the manpage for setup details.
 
42
Exception: %s""" % str(e))
 
43
 
 
44
        # let user get by with old client while he can
 
45
        try:
 
46
            from oauth2client.client import SignedJwtAssertionCredentials
 
47
            self.oldClient = True
 
48
        except:
 
49
            from oauth2client.service_account import ServiceAccountCredentials
 
50
            from oauth2client import crypt
 
51
            self.oldClient = False
 
52
 
 
53
        if 'GOOGLE_DRIVE_ACCOUNT_KEY' in os.environ:
 
54
            account_key = os.environ['GOOGLE_DRIVE_ACCOUNT_KEY']
 
55
            if self.oldClient:
 
56
                credentials = SignedJwtAssertionCredentials(parsed_url.username +
 
57
                                                            '@' + parsed_url.hostname,
 
58
                                                            account_key,
 
59
                                                            scopes='https://www.googleapis.com/auth/drive')
 
60
            else:
 
61
                signer = crypt.Signer.from_string(account_key)
 
62
                credentials = ServiceAccountCredentials(parsed_url.username + '@' + parsed_url.hostname, signer,
 
63
                                                        scopes='https://www.googleapis.com/auth/drive')
 
64
            credentials.authorize(httplib2.Http())
 
65
            gauth = GoogleAuth()
 
66
            gauth.credentials = credentials
 
67
        elif 'GOOGLE_DRIVE_SETTINGS' in os.environ:
 
68
            gauth = GoogleAuth(settings_file=os.environ['GOOGLE_DRIVE_SETTINGS'])
 
69
            gauth.CommandLineAuth()
 
70
        elif ('GOOGLE_SECRETS_FILE' in os.environ and 'GOOGLE_CREDENTIALS_FILE' in os.environ):
 
71
            gauth = GoogleAuth()
 
72
            gauth.LoadClientConfigFile(os.environ['GOOGLE_SECRETS_FILE'])
 
73
            gauth.LoadCredentialsFile(os.environ['GOOGLE_CREDENTIALS_FILE'])
 
74
            if gauth.credentials is None:
 
75
                gauth.CommandLineAuth()
 
76
            elif gauth.access_token_expired:
 
77
                gauth.Refresh()
 
78
            else:
 
79
                gauth.Authorize()
 
80
            gauth.SaveCredentialsFile(os.environ['GOOGLE_CREDENTIALS_FILE'])
 
81
        else:
 
82
            raise BackendException(
 
83
                'GOOGLE_DRIVE_ACCOUNT_KEY or GOOGLE_DRIVE_SETTINGS environment '
 
84
                'variable not set. Please read the manpage to fix.')
 
85
        self.drive = GoogleDrive(gauth)
 
86
 
 
87
        # Dirty way to find root folder id
 
88
        file_list = self.drive.ListFile({'q': "'Root' in parents and trashed=false"}).GetList()
 
89
        if file_list:
 
90
            parent_folder_id = file_list[0]['parents'][0]['id']
 
91
        else:
 
92
            file_in_root = self.drive.CreateFile({'title': 'i_am_in_root'})
 
93
            file_in_root.Upload()
 
94
            parent_folder_id = file_in_root['parents'][0]['id']
 
95
            file_in_root.Delete()
 
96
 
 
97
        # Fetch destination folder entry and create hierarchy if required.
 
98
        folder_names = string.split(parsed_url.path, '/')
 
99
        for folder_name in folder_names:
 
100
            if not folder_name:
 
101
                continue
 
102
            file_list = self.drive.ListFile({'q': "'" + parent_folder_id +
 
103
                                                  "' in parents and trashed=false"}).GetList()
 
104
            folder = next((item for item in file_list if item['title'] == folder_name and
 
105
                           item['mimeType'] == 'application/vnd.google-apps.folder'), None)
 
106
            if folder is None:
 
107
                folder = self.drive.CreateFile({'title': folder_name,
 
108
                                                'mimeType': "application/vnd.google-apps.folder",
 
109
                                                'parents': [{'id': parent_folder_id}]})
 
110
                folder.Upload()
 
111
            parent_folder_id = folder['id']
 
112
        self.folder = parent_folder_id
 
113
        self.id_cache = {}
 
114
 
 
115
    def file_by_name(self, filename):
 
116
        from pydrive.files import ApiRequestError
 
117
        if filename in self.id_cache:
 
118
            # It might since have been locally moved, renamed or deleted, so we
 
119
            # need to validate the entry.
 
120
            file_id = self.id_cache[filename]
 
121
            drive_file = self.drive.CreateFile({'id': file_id})
 
122
            try:
 
123
                if drive_file['title'] == filename and not drive_file['labels']['trashed']:
 
124
                    for parent in drive_file['parents']:
 
125
                        if parent['id'] == self.folder:
 
126
                            log.Info("PyDrive backend: found file '%s' with id %s in ID cache" %
 
127
                                     (filename, file_id))
 
128
                            return drive_file
 
129
            except ApiRequestError as error:
 
130
                # A 404 occurs if the ID is no longer valid
 
131
                if error.args[0].resp.status != 404:
 
132
                    raise
 
133
            # If we get here, the cache entry is invalid
 
134
            log.Info("PyDrive backend: invalidating '%s' (previously ID %s) from ID cache" %
 
135
                     (filename, file_id))
 
136
            del self.id_cache[filename]
 
137
 
 
138
        # Not found in the cache, so use directory listing. This is less
 
139
        # reliable because there is no strong consistency.
 
140
        q = "title='%s' and '%s' in parents and trashed=false" % (filename, self.folder)
 
141
        fields = 'items(title,id,fileSize,downloadUrl,exportLinks),nextPageToken'
 
142
        flist = self.drive.ListFile({'q': q, 'fields': fields}).GetList()
 
143
        if len(flist) > 1:
 
144
            log.FatalError(_("PyDrive backend: multiple files called '%s'.") % (filename,))
 
145
        elif flist:
 
146
            file_id = flist[0]['id']
 
147
            self.id_cache[filename] = flist[0]['id']
 
148
            log.Info("PyDrive backend: found file '%s' with id %s on server, "
 
149
                     "adding to cache" % (filename, file_id))
 
150
            return flist[0]
 
151
        log.Info("PyDrive backend: file '%s' not found in cache or on server" %
 
152
                 (filename,))
 
153
        return None
 
154
 
 
155
    def id_by_name(self, filename):
 
156
        drive_file = self.file_by_name(filename)
 
157
        if drive_file is None:
 
158
            return ''
 
159
        else:
 
160
            return drive_file['id']
 
161
 
 
162
    def _put(self, source_path, remote_filename):
 
163
        drive_file = self.file_by_name(remote_filename)
 
164
        if drive_file is None:
 
165
            # No existing file, make a new one
 
166
            drive_file = self.drive.CreateFile({'title': remote_filename,
 
167
                                                'parents': [{"kind": "drive#fileLink",
 
168
                                                             "id": self.folder}]})
 
169
            log.Info("PyDrive backend: creating new file '%s'" % (remote_filename,))
 
170
        else:
 
171
            log.Info("PyDrive backend: replacing existing file '%s' with id '%s'" % (
 
172
                remote_filename, drive_file['id']))
 
173
        drive_file.SetContentFile(source_path.name)
 
174
        drive_file.Upload()
 
175
        self.id_cache[remote_filename] = drive_file['id']
 
176
 
 
177
    def _get(self, remote_filename, local_path):
 
178
        drive_file = self.file_by_name(remote_filename)
 
179
        drive_file.GetContentFile(local_path.name)
 
180
 
 
181
    def _list(self):
 
182
        drive_files = self.drive.ListFile({
 
183
            'q': "'" + self.folder + "' in parents and trashed=false",
 
184
            'fields': 'items(title,id),nextPageToken'}).GetList()
 
185
        filenames = set(item['title'] for item in drive_files)
 
186
        # Check the cache as well. A file might have just been uploaded but
 
187
        # not yet appear in the listing.
 
188
        # Note: do not use iterkeys() here, because file_by_name will modify
 
189
        # the cache if it finds invalid entries.
 
190
        for filename in self.id_cache.keys():
 
191
            if (filename not in filenames) and (self.file_by_name(filename) is not None):
 
192
                filenames.add(filename)
 
193
        return list(filenames)
 
194
 
 
195
    def _delete(self, filename):
 
196
        file_id = self.id_by_name(filename)
 
197
        if file_id != '':
 
198
            self.drive.auth.service.files().delete(fileId=file_id).execute()
 
199
        else:
 
200
            log.Warn("File '%s' does not exist while trying to delete it" % (filename,))
 
201
 
 
202
    def _query(self, filename):
 
203
        drive_file = self.file_by_name(filename)
 
204
        if drive_file is None:
 
205
            size = -1
 
206
        else:
 
207
            size = int(drive_file['fileSize'])
 
208
        return {'size': size}
 
209
 
 
210
    def _error_code(self, operation, error):
 
211
        from pydrive.files import ApiRequestError, FileNotUploadedError
 
212
        if isinstance(error, FileNotUploadedError):
 
213
            return log.ErrorCode.backend_not_found
 
214
        elif isinstance(error, ApiRequestError):
 
215
            return log.ErrorCode.backend_permission_denied
 
216
        return log.ErrorCode.backend_error
 
217
 
 
218
duplicity.backend.register_backend('pydrive', PyDriveBackend)
 
219
""" pydrive is an alternate way to access gdocs """
 
220
duplicity.backend.register_backend('pydrive+gdocs', PyDriveBackend)
 
221
""" register pydrive as the default way to access gdocs """
 
222
duplicity.backend.register_backend('gdocs', PyDriveBackend)
 
223
 
 
224
duplicity.backend.uses_netloc.extend(['pydrive', 'pydrive+gdocs', 'gdocs'])