-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmixin.py
456 lines (344 loc) · 16.9 KB
/
mixin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
"""
========
mixin.py
========
Classes to provide high level Dropbox access, built on top of async_dropbox and Cache.
Dependencies
============
Python (tested on 2.7.1), tornado, and async_dropbox (a copy is provided).
Classes
=======
DropboxUserHandler
Mixin on top of async_dropbox.DropboxMixin to provide nicer user access.
Handler to provide nicer user access.
DropboxLoginHandler
Handler to handle Dropbox login; redirects to / on success.
DropboxAPIMixin
High level Dropbox API access for a single folder, built on top of
async_dropbox.DropboxMixin, Cache, and DropboxUserMixin.
See the documentation for each class for more details and specifics on which
application settings and cookies are used.
Example Usage
=============
::
class MainHandler(DropboxUserHandler, DropboxAPIMixin):
@tornado.web.asynchronous
@tornado.gen.engine
def get(self):
if not self.current_user:
self.render("welcome.html")
else:
files = yield tornado.gen.Task(self.get_files)
self.render("list.html", title="all files", files=files)
class LoginHandler(DropboxLoginHandler):
def set_application_cookies(self):
self.set_secure_cookie("dropbox_folder_path", "<folder path>")
class ViewHandler(DropboxUserHandler, DropboxAPIMixin):
@tornado.web.authenticated
@tornado.web.asynchronous
@tornado.gen.engine
def get(self, file_name):
res = yield tornado.gen.Task(self.get_data, file_name)
filedata = res[0][1]
self.render("view.html", title=file_name, contents=filedata)
logging.basicConfig(level=logging.DEBUG)
cache = SqliteCache("<folder path>")
settings = {
"cookie_secret": "<COOKIE SECRET>",
"static_path": os.path.join(os.path.dirname(__file__), "static"),
"login_url": "/login",
"template_path": "templates/",
"dropbox_consumer_key": "<KEY HERE>",
"dropbox_consumer_secret": "<SECRET HERE>",
"xsrf_cookies": True,
"debug": True,
"dropbox_cache": cache,
"dropbox_api_type": "dropbox",
}
application = tornado.web.Application([
tornado.web.URLSpec(r"/", MainHandler, name="main"),
tornado.web.URLSpec(r"/view/([^/]+)", ViewHandler, name="view"),
tornado.web.URLSpec(r"/login", LoginHandler, name="login"),
], **settings)
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
Contributing
============
If you use and like this, please let me know! Patches, pull requests, suggestions etc. are all
gratefully accepted.
License
=======
The included copy of async_dropbox.py is not subject to this license, but retains the
license, if any, applied by its creator.
Copyright 2012 Benedict Singer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import logging
import json
import datetime
import tornado.gen
from async_dropbox import DropboxMixin
from tornado.escape import utf8
from urllib import quote
logger = logging.getLogger(__name__)
class DropboxUserHandler(tornado.web.RequestHandler):
"""Handler to provide nicer user access.
Uses a secure cookie called 'user', the value of which should be a JSON string
that is returned from the Dropbox auth process; get_current_user will return
it as a decoded dict.
See DropboxLoginHandler for an example handler that sets this cookie correctly.
"""
def get_current_user(self):
logger.debug("user cookie '%s'", self.get_secure_cookie("user"))
if self.get_secure_cookie("user"):
return json.loads(self.get_secure_cookie("user"))
else:
return None
class DropboxLoginHandler(tornado.web.RequestHandler, DropboxMixin):
"""Handler to handle Dropbox login; redirects to / on success.
Sets the Dropbox user JSON string into a secure cookie called 'user';
thus works nicely with DropboxUserMixin.
"""
@tornado.web.asynchronous
@tornado.gen.engine
def get(self):
if self.get_argument("oauth_token", None):
logger.debug("no oauth token, calling get_auth_user")
user = yield tornado.gen.Task(self.get_authenticated_user)
if not user:
raise tornado.web.HTTPError(500, "Dropbox auth failed")
logger.debug("got user, setting cookie and redirecting to /")
self.set_secure_cookie("user", json.dumps(user))
self.set_application_cookies()
self.redirect('/')
else:
logger.debug("calling authorize_redirect")
self.authorize_redirect(callback_uri=self.request.full_url())
def set_application_cookies(self):
"""Override this to set any application level cookies that are required after login.
Setting your default dropbox_folder_path here is recommended.
"""
pass
class DropboxAPIMixin(DropboxMixin):
"""High level Dropbox API access for a single folder, built on top of async_dropbox.DropboxMixin and Cache.
Provides listing, file retrieval, upload, move, and remove operations. All operations will
update the cache automatically if the dropbox_folder_path cookie is detected to have changed.
Uses keys from the settings dict as follows:
dropbox_api_type - must be 'sandbox' or 'dropbox'; default 'sandbox'
dropbox_cache - an object implementing methods from tornado_dropcache.Cache; default is an EmptyCache using dropbox_folder_path
Uses secure cookies as follows:
dropbox_folder_path - the path (relative to dropbox api type) of the folder that this app is managing; default is empty string
"""
def _get_access_token(self):
"""Helper function to get the Dropbox access token for API calls."""
# json turns this into unicode strings, but we need bytes for oauth signatures.
return dict((utf8(k), utf8(v)) for (k, v) in self.current_user["access_token"].iteritems())
def _get_setting(self, key, default_func):
if key in self.settings:
return self.settings[key]
else:
return default_func()
def _get_api_type(self):
return self._get_setting("dropbox_api_type", lambda: "sandbox")
def _get_folder_path(self):
if not self.get_secure_cookie("dropbox_folder_path"):
return ""
else:
return self.get_secure_cookie("dropbox_folder_path")
def _get_cache(self):
return self._get_setting("dropbox_cache", lambda: EmptyCache(self._get_folder_path()))
@tornado.web.authenticated
@tornado.web.asynchronous
@tornado.gen.engine
def get_files(self, callback):
"""Retrieve a sequence of filenames in the folder under consideration.
Filenames are returned with the folder path stripped off.
callback - callback that will receive the filename sequence
"""
cache = self._get_cache()
uid = self.current_user["uid"]
user = cache.get_user(uid)
if datetime.datetime.now() - user["folder_metadata_ts"] > cache.timeout:
logger.debug("making dropbox list request")
response = None
if "hash" in user["folder_metadata"]:
response = yield tornado.gen.Task(self.dropbox_request,
"api", "/1/metadata/%s/%s" % (self._get_api_type(), quote(self._get_folder_path())),
access_token=self._get_access_token(),
list="true", hash=user["folder_metadata"]["hash"])
else:
response = yield tornado.gen.Task(self.dropbox_request,
"api", "/1/metadata/%s/%s" % (self._get_api_type(), quote(self._get_folder_path())),
access_token=self._get_access_token(),
list="true")
try:
response.rethrow()
except tornado.httpclient.HTTPError as e:
if e.code == 304:
logger.debug("using cached value after 304 response")
cache.update_folder_metadata_timestamp(uid, datetime.datetime.now())
user = cache.get_user(uid)
callback(self._files_from_metadata(user["folder_metadata"]))
return
else:
raise
metadata = json.load(response.buffer)
cache.update_folder_metadata(uid, datetime.datetime.now(), json.dumps(metadata))
callback(self._files_from_metadata(metadata))
else:
logger.debug("using cached value")
callback(self._files_from_metadata(user["folder_metadata"]))
def _files_from_metadata(self, metadata):
logger.debug('metadata contents %s', metadata["contents"])
return [content["path"].replace(self._get_folder_path(), "", 1).lstrip("/") for content in metadata["contents"]]
@tornado.web.authenticated
@tornado.web.asynchronous
@tornado.gen.engine
def get_data(self, file_name, callback, blank_on_404=False):
"""Retrieve the file data for a specified file.
file_name - the file to retrieve
callback - callback that will receive the file name and data
blank_on_404 - return a blank file on a 404 error; use when planning to upload a new file in the next step
"""
cache = self._get_cache()
uid = self.current_user["uid"]
f = cache.get_file(uid, file_name)
if not f:
logger.debug("retrieving file for first time")
response = yield tornado.gen.Task(self.dropbox_request,
"api-content", "/1/files/%s/%s/%s" % (self._get_api_type(), quote(self._get_folder_path()), quote(file_name)),
access_token=self._get_access_token())
try:
response.rethrow()
except tornado.httpclient.HTTPError as e:
if e.code == 404 and blank_on_404:
logger.debug("returning empty file from 404, expect to create it soon")
callback(file_name, "")
return
else:
raise
# grab metadata from header, insert new row into cache
metadata = json.loads(response.headers["x-dropbox-metadata"])
cache.add_file(uid, file_name, datetime.datetime.now(), json.dumps(metadata), response.body)
callback(file_name, response.body)
else:
if datetime.datetime.now() - f["file_metadata_ts"] > cache.timeout:
logger.debug("requesting new metadata")
response = yield tornado.gen.Task(self.dropbox_request,
"api", "/1/metadata/%s/%s/%s" % (self._get_api_type(), quote(self._get_folder_path()), quote(file_name)),
access_token=self._get_access_token(),
list="false")
response.rethrow()
# grab metadata from response, compare rev to cache metadata rev
# do GET if rev does not match, callback to _on_updated_data
# otherwise update metadata timestamp in cache and render from cached value
metadata = json.load(response.buffer)
local_rev = f["file_metadata"]["rev"]
remote_rev = metadata["rev"]
if local_rev == remote_rev:
logger.debug("new metadata has same rev, updating timestamp and rendering local data")
cache.update_file_timestamp(uid, file_name, datetime.datetime.now())
callback(file_name, f["file_data"])
else:
logger.debug("retrieving updated copy of file")
response = yield tornado.gen.Task(self.dropbox_request,
"api-content", "/1/files/%s/%s/%s" % (self._get_api_type(), quote(self._get_folder_path()), quote(file_name)),
access_token=self._get_access_token())
response.rethrow()
# grab metadata from header, update cache
metadata = json.loads(response.headers["x-dropbox-metadata"])
cache.update_file(uid, file_name, datetime.datetime.now(), json.dumps(metadata), response.body)
callback(file_name, response.body)
else:
logger.debug("under timeout, using old data")
callback(file_name, f["file_data"])
@tornado.web.authenticated
@tornado.web.asynchronous
@tornado.gen.engine
def upload_data(self, file_name, data, callback):
"""Upload new data to the specified file, creating it if it does not exist.
file_name - the filename to upload to
data - the new file data
callback - callback that will receive the filename
"""
cache = self._get_cache()
uid = self.current_user["uid"]
f = cache.get_file(uid, file_name)
logger.debug("uploading new %s file: '%s'", file_name, data)
if f:
logger.debug("previous rev:")
logger.debug(f["file_metadata"]["rev"])
response = None
if f:
response = yield tornado.gen.Task(self.dropbox_request,
"api-content", "/1/files_put/%s/%s/%s" % (self._get_api_type(), quote(self._get_folder_path()), quote(file_name)),
access_token=self._get_access_token(),
put_body=data, parent_rev=f["file_metadata"]["rev"])
else:
response = yield tornado.gen.Task(self.dropbox_request,
"api-content", "/1/files_put/%s/%s/%s" % (self._get_api_type(), quote(self._get_folder_path()), quote(file_name)),
access_token=self._get_access_token(),
put_body=data)
response.rethrow()
if not f:
response = yield tornado.gen.Task(self.dropbox_request,
"api-content", "/1/files/%s/%s/%s" % (self._get_api_type(), quote(self._get_folder_path()), quote(file_name)),
access_token=self._get_access_token())
response.rethrow()
# grab metadata from header, insert new row into cache
metadata = json.loads(response.headers["x-dropbox-metadata"])
cache.add_file(uid, file_name, datetime.datetime.now(), json.dumps(metadata), response.body)
cache.update_folder_metadata_timestamp(uid, datetime.datetime.min)
else:
cache.update_file_timestamp(uid, file_name, datetime.datetime.min)
callback(file_name)
@tornado.web.authenticated
@tornado.web.asynchronous
@tornado.gen.engine
def move_file(self, file_name, new_file_name, callback):
"""Move a file within the folder.
file_name - file to move
new_file_name - new filename
callback - will be called when complete
"""
cache = self._get_cache()
uid = self.current_user["uid"]
logger.debug("moving %s to %s", file_name, new_file_name)
response = yield tornado.gen.Task(self.dropbox_request,
"api", "/1/fileops/move",
access_token=self._get_access_token(),
post_args={ "root" : self._get_api_type(), "from_path" : "%s/%s" % (self._get_folder_path(), file_name), "to_path" : "%s/%s" % (self._get_folder_path(), new_file_name) })
response.rethrow()
# remove the old one, and just get the new one next time we request it
cache.remove_file(uid, file_name)
cache.update_folder_metadata_timestamp(uid, datetime.datetime.min)
callback()
@tornado.web.authenticated
@tornado.web.asynchronous
@tornado.gen.engine
def delete_file(self, file_name, callback):
"""Delete a file within the folder.
file_name - file to delete
callback - will be called when complete
"""
cache = self._get_cache()
uid = self.current_user["uid"]
logger.debug("deleting %s", file_name)
response = yield tornado.gen.Task(self.dropbox_request,
"api", "/1/fileops/delete",
access_token=self._get_access_token(),
post_args={ "root" : self._get_api_type(), "path" : "%s/%s" % (self._get_folder_path(), file_name) })
response.rethrow()
# remove the file, and just get a new folder list next time it's requested
cache.remove_file(uid, file_name)
cache.update_folder_metadata_timestamp(uid, datetime.datetime.min)
callback()