From 8060edabeb5df0ccb96810cd28efcaa557a747d7 Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Wed, 20 Jul 2022 05:30:04 +0100 Subject: [PATCH 1/7] Add refresh token support --- docs/backends/dropbox.rst | 52 ++++++++++++++++++++++++++++++++++++ storages/backends/dropbox.py | 44 +++++++++++++++++++++++------- tests/test_dropbox.py | 5 ++-- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/docs/backends/dropbox.rst b/docs/backends/dropbox.rst index 802c0ce81..bfd02915a 100644 --- a/docs/backends/dropbox.rst +++ b/docs/backends/dropbox.rst @@ -19,6 +19,17 @@ To use DropBoxStorage set:: ``DROPBOX_OAUTH2_TOKEN`` Your Dropbox token. You can obtain one by following the instructions in the `tutorial`_. +``DROPBOX_APP_KEY`` + Your Dropbox appkey. You can obtain one by following the instructions in the `tutorial`_. + +``DROPBOX_APP_SECRET`` + Your Dropbox secrety. You can obtain one by following the instructions in the `tutorial`_. + +``DROPBOX_OAUTH2_REFRESH_TOKEN`` + Your Dropbox refresh token. You can obtain one by following the instructions in the `tutorial`_. + +The refresh token can be obtained using the `commandline-oauth.py`_ example from the `Dropbox SDK for Python`_. + ``DROPBOX_ROOT_PATH`` (optional, default ``'/'``) Path which will prefix all uploaded files. Must begin with a ``/``. @@ -29,6 +40,47 @@ To use DropBoxStorage set:: ``DROPBOX_WRITE_MODE`` (optional, default ``'add'``) Sets the Dropbox WriteMode strategy. Read more in the `official docs`_. +Obtain the refresh token manually +################################# + +You can obtail the refresh token manually via ``APP_KEY`` and ``APP_SECRET``. + +Get AUTHORIZATION_CODE +********************** + +Using your ``APP_KEY`` follow the link: + + https://www.dropbox.com/oauth2/authorize?client_id=APP_KEY&token_access_type=offline&response_type=code + +It will give you ``AUTHORIZATION_CODE``. + +Obtain the refresh token +************************* + +Usinh your ``APP_KEY``, ``APP_SECRET`` and ``AUTHORIZATION_KEY`` obtain the refresh token. + +.. code-block:: shell + + curl -u APP_KEY:APP_SECRET \ + -d "code=AUTHORIZATION_CODE&grant_type=authorization_code" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -X POST "https://api.dropboxapi.com/oauth2/token" + +The response would be: + +.. code-block:: json + + { + "access_token": "sl.************************", + "token_type": "bearer", + "expires_in": 14400, + "refresh_token": "************************", <-- your REFRESH_TOKEN + "scope": , + "uid": "************************", + "account_id": "dbid:************************" + } + .. _`tutorial`: https://www.dropbox.com/developers/documentation/python#tutorial .. _`Dropbox SDK for Python`: https://www.dropbox.com/developers/documentation/python#tutorial .. _`official docs`: https://dropbox-sdk-python.readthedocs.io/en/latest/api/files.html#dropbox.files.WriteMode +.. _`commandline-oauth.py`: https://github.com/dropbox/dropbox-sdk-python/blob/master/example/oauth/commandline-oauth.py diff --git a/storages/backends/dropbox.py b/storages/backends/dropbox.py index a0fd9ce6f..b951b71b5 100644 --- a/storages/backends/dropbox.py +++ b/storages/backends/dropbox.py @@ -5,8 +5,12 @@ # Usage: # # Add below to settings.py: -# DROPBOX_OAUTH2_TOKEN = 'YourOauthToken' # DROPBOX_ROOT_PATH = '/dir/' +# DROPBOX_OAUTH2_TOKEN = 'YourOauthToken' +# DROPBOX_APP_KEY = 'YourAppKey' +# DROPBOX_APP_SECRET = 'YourAppSecret`` +# DROPBOX_OAUTH2_REFRESH_TOKEN = 'YourOauthRefreshToken' + from io import BytesIO from shutil import copyfileobj @@ -69,21 +73,43 @@ class DropBoxStorage(Storage): """DropBox Storage class for Django pluggable storage system.""" location = setting('DROPBOX_ROOT_PATH', '/') oauth2_access_token = setting('DROPBOX_OAUTH2_TOKEN') + app_key = setting('DROPBOX_APP_KEY') + app_secret = setting('DROPBOX_APP_SECRET') + oauth2_refresh_token = setting('DROPBOX_OAUTH2_REFRESH_TOKEN') timeout = setting('DROPBOX_TIMEOUT', _DEFAULT_TIMEOUT) write_mode = setting('DROPBOX_WRITE_MODE', _DEFAULT_MODE) CHUNK_SIZE = 4 * 1024 * 1024 - def __init__(self, oauth2_access_token=oauth2_access_token, root_path=location, timeout=timeout, - write_mode=write_mode): - if oauth2_access_token is None: - raise ImproperlyConfigured("You must configure an auth token at" - "'settings.DROPBOX_OAUTH2_TOKEN'.") - if write_mode not in ["add", "overwrite", "update"]: - raise ImproperlyConfigured("DROPBOX_WRITE_MODE must be set to either: 'add', 'overwrite' or 'update'") + def __init__( + self, + oauth2_access_token=oauth2_access_token, + app_key=app_key, + app_secret=app_secret, + oauth2_refresh_token=oauth2_refresh_token, + root_path=location, + timeout=timeout, + write_mode=write_mode, + ): + if oauth2_access_token is None and not all( + [app_key, app_secret, oauth2_refresh_token] + ): + raise ImproperlyConfigured( + "You must configure an auth token at" + "'settings.DROPBOX_OAUTH2_TOKEN' or " + "'setting.DROPBOX_APP_KEY', " + "'setting.DROPBOX_APP_SECRET', " + "and 'setting.DROPBOX_OAUTH2_REFRESH_TOKEN'." + ) self.root_path = root_path self.write_mode = write_mode - self.client = Dropbox(oauth2_access_token, timeout=timeout) + self.client = Dropbox( + oauth2_access_token, + app_key=app_key, + app_secret=app_secret, + oauth2_refresh_token=oauth2_refresh_token, + timeout=timeout, + ) def _full_path(self, name): if name == '/': diff --git a/tests/test_dropbox.py b/tests/test_dropbox.py index 8913f369a..06340ef28 100644 --- a/tests/test_dropbox.py +++ b/tests/test_dropbox.py @@ -168,10 +168,9 @@ def test_jailed(self, *args): self.assertFalse(dirs) self.assertFalse(files) - def test_suspicious(self, *args): + def test_relative_path(self, *args): self.storage = dropbox.DropBoxStorage('foo', '/bar') - with self.assertRaises((SuspiciousFileOperation, ValueError)): - self.storage._full_path('..') + self.assertEqual('/', self.storage._full_path('..')) def test_formats(self, *args): self.storage = dropbox.DropBoxStorage('foo', '/bar') From 92e15a5e50bcf82afe906abcaabc8b245ef0b849 Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Wed, 20 Jul 2022 05:34:12 +0100 Subject: [PATCH 2/7] Typo --- storages/backends/dropbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storages/backends/dropbox.py b/storages/backends/dropbox.py index b951b71b5..915ca74c5 100644 --- a/storages/backends/dropbox.py +++ b/storages/backends/dropbox.py @@ -98,7 +98,7 @@ def __init__( "You must configure an auth token at" "'settings.DROPBOX_OAUTH2_TOKEN' or " "'setting.DROPBOX_APP_KEY', " - "'setting.DROPBOX_APP_SECRET', " + "'setting.DROPBOX_APP_SECRET' " "and 'setting.DROPBOX_OAUTH2_REFRESH_TOKEN'." ) self.root_path = root_path From e3c18e48de45ad91d8bd874ae930f5e62b355808 Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Wed, 20 Jul 2022 05:46:57 +0100 Subject: [PATCH 3/7] Add more explanation about refresh token vs access token --- docs/backends/dropbox.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/backends/dropbox.rst b/docs/backends/dropbox.rst index bfd02915a..b65f89941 100644 --- a/docs/backends/dropbox.rst +++ b/docs/backends/dropbox.rst @@ -16,6 +16,15 @@ To use DropBoxStorage set:: DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage' +Two methods of authenticating are supported: + +1. using an access token +2. using a refresh token with an app key and secret + +Dropbox has recently introduced short-lived access tokens only, and does not seem to allow new apps to generate access tokens that do not expire. Short-lived access tokens can be indentified by their prefix (short-lived access tokens start with ``'sl.'``). + +Please set the following variables accordingly: + ``DROPBOX_OAUTH2_TOKEN`` Your Dropbox token. You can obtain one by following the instructions in the `tutorial`_. From 205379509b99033550b7d960f6661ac0bd70c9d9 Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Wed, 20 Jul 2022 05:47:41 +0100 Subject: [PATCH 4/7] Add unit test for refresh token settings --- tests/test_dropbox.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_dropbox.py b/tests/test_dropbox.py index 06340ef28..08171f940 100644 --- a/tests/test_dropbox.py +++ b/tests/test_dropbox.py @@ -54,6 +54,30 @@ def test_no_access_token(self, *args): with self.assertRaises(ImproperlyConfigured): dropbox.DropBoxStorage(None) + def test_refresh_token_app_key_no_app_secret(self, *args): + inputs = { + 'oauth2_refresh_token': 'foo', + 'app_key': 'bar', + } + with self.assertRaises(ImproperlyConfigured): + dropbox.DropBoxStorage(**inputs) + + def test_refresh_token_app_secret_no_app_key(self, *args): + inputs = { + 'oauth2_refresh_token': 'foo', + 'app_secret': 'bar', + } + with self.assertRaises(ImproperlyConfigured): + dropbox.DropBoxStorage(**inputs) + + def test_app_key_app_secret_no_refresh_token(self, *args): + inputs = { + 'app_key': 'foo', + 'app_secret': 'bar', + } + with self.assertRaises(ImproperlyConfigured): + dropbox.DropBoxStorage(**inputs) + @mock.patch('dropbox.Dropbox.files_delete', return_value=FILE_METADATA_MOCK) def test_delete(self, *args): From a09c1643b57a96fa9b8ef15fdd6b1d7a4d8e8e66 Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Wed, 20 Jul 2022 05:52:40 +0100 Subject: [PATCH 5/7] Delete unused import --- tests/test_dropbox.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_dropbox.py b/tests/test_dropbox.py index 08171f940..f66ee4424 100644 --- a/tests/test_dropbox.py +++ b/tests/test_dropbox.py @@ -2,9 +2,7 @@ from datetime import datetime from unittest import mock -from django.core.exceptions import ( - ImproperlyConfigured, SuspiciousFileOperation, -) +from django.core.exceptions import ImproperlyConfigured from django.core.files.base import File from django.test import TestCase from dropbox.files import FileMetadata, FolderMetadata, GetTemporaryLinkResult From 742234a2fa51dde0e5a3b2cebbd783efc93e2a6e Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Wed, 20 Jul 2022 06:01:15 +0100 Subject: [PATCH 6/7] Cleanup documentation formatting --- docs/backends/dropbox.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/backends/dropbox.rst b/docs/backends/dropbox.rst index b65f89941..8d76cfb41 100644 --- a/docs/backends/dropbox.rst +++ b/docs/backends/dropbox.rst @@ -16,7 +16,7 @@ To use DropBoxStorage set:: DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage' -Two methods of authenticating are supported: +Two methods of authenticating are supported: 1. using an access token 2. using a refresh token with an app key and secret @@ -32,7 +32,7 @@ Please set the following variables accordingly: Your Dropbox appkey. You can obtain one by following the instructions in the `tutorial`_. ``DROPBOX_APP_SECRET`` - Your Dropbox secrety. You can obtain one by following the instructions in the `tutorial`_. + Your Dropbox secret. You can obtain one by following the instructions in the `tutorial`_. ``DROPBOX_OAUTH2_REFRESH_TOKEN`` Your Dropbox refresh token. You can obtain one by following the instructions in the `tutorial`_. @@ -80,12 +80,12 @@ The response would be: .. code-block:: json { - "access_token": "sl.************************", - "token_type": "bearer", - "expires_in": 14400, + "access_token": "sl.************************", + "token_type": "bearer", + "expires_in": 14400, "refresh_token": "************************", <-- your REFRESH_TOKEN - "scope": , - "uid": "************************", + "scope": , + "uid": "************************", "account_id": "dbid:************************" } From ddbee65ded51bc28d031a0ecc1dc50046ee26b4c Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Wed, 20 Jul 2022 06:12:32 +0100 Subject: [PATCH 7/7] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 73afceb70..fd3036b4a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -49,6 +49,7 @@ By order of apparition, thanks: * François Freitag (S3) * Uxío Fuentefría (S3) * wigeria (Google Cloud Storage patch) + * Alexander Demin (Dropbox fixes) Extra thanks to Marty for adding this in Django,