File: dropbox.py

package info (click to toggle)
python-django-storages 1.14.5-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 884 kB
  • sloc: python: 4,448; makefile: 119; sh: 6
file content (202 lines) | stat: -rw-r--r-- 6,899 bytes parent folder | download | duplicates (2)
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
# Dropbox storage class for Django pluggable storage system.
# Author: Anthony Monthe <anthony.monthe@gmail.com>
# License: BSD

import warnings
from io import BytesIO
from shutil import copyfileobj
from tempfile import SpooledTemporaryFile

from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import File
from django.utils._os import safe_join
from django.utils.deconstruct import deconstructible
from dropbox import Dropbox
from dropbox.exceptions import ApiError
from dropbox.files import CommitInfo
from dropbox.files import FolderMetadata
from dropbox.files import UploadSessionCursor
from dropbox.files import WriteMode

from storages.base import BaseStorage
from storages.utils import get_available_overwrite_name
from storages.utils import setting

_DEFAULT_TIMEOUT = 100
_DEFAULT_MODE = "add"


class DropboxStorageException(Exception):
    pass


DropBoxStorageException = DropboxStorageException


class DropboxFile(File):
    def __init__(self, name, storage):
        self.name = name
        self._storage = storage
        self._file = None

    def _get_file(self):
        if self._file is None:
            self._file = SpooledTemporaryFile()
            # As dropbox==9.3.0, the client returns a tuple
            # (dropbox.files.FileMetadata, requests.models.Response)
            file_metadata, response = self._storage.client.files_download(self.name)
            if response.status_code == 200:
                with BytesIO(response.content) as file_content:
                    copyfileobj(file_content, self._file)
            else:
                # JIC the exception isn't caught by the dropbox client
                raise DropboxStorageException(
                    "Dropbox server returned a {} response when accessing {}".format(
                        response.status_code, self.name
                    )
                )
            self._file.seek(0)
        return self._file

    def _set_file(self, value):
        self._file = value

    file = property(_get_file, _set_file)


DropBoxFile = DropboxFile


@deconstructible
class DropboxStorage(BaseStorage):
    """Dropbox Storage class for Django pluggable storage system."""

    CHUNK_SIZE = 4 * 1024 * 1024

    def __init__(self, oauth2_access_token=None, **settings):
        if oauth2_access_token is not None:
            settings["oauth2_access_token"] = oauth2_access_token
        super().__init__(**settings)

        if self.oauth2_access_token is None and not all(
            [self.app_key, self.app_secret, self.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.client = Dropbox(
            self.oauth2_access_token,
            app_key=self.app_key,
            app_secret=self.app_secret,
            oauth2_refresh_token=self.oauth2_refresh_token,
            timeout=self.timeout,
        )

        # Backwards compat
        if hasattr(self, "location"):
            warnings.warn(
                "Setting `root_path` with name `location` is deprecated and will be "
                "removed in a future version of django-storages. Please update the "
                "name from `location` to `root_path`",
                DeprecationWarning,
            )
            self.root_path = self.location

    def get_default_settings(self):
        return {
            "root_path": 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),
        }

    def _full_path(self, name):
        if name == "/":
            name = ""
        return safe_join(self.root_path, name).replace("\\", "/")

    def delete(self, name):
        self.client.files_delete(self._full_path(name))

    def exists(self, name):
        try:
            return bool(self.client.files_get_metadata(self._full_path(name)))
        except ApiError:
            return False

    def listdir(self, path):
        directories, files = [], []
        full_path = self._full_path(path)

        if full_path == "/":
            full_path = ""

        metadata = self.client.files_list_folder(full_path)
        for entry in metadata.entries:
            if isinstance(entry, FolderMetadata):
                directories.append(entry.name)
            else:
                files.append(entry.name)
        return directories, files

    def size(self, name):
        metadata = self.client.files_get_metadata(self._full_path(name))
        return metadata.size

    def url(self, name):
        try:
            media = self.client.files_get_temporary_link(self._full_path(name))
            return media.link
        except ApiError:
            return None

    def _open(self, name, mode="rb"):
        remote_file = DropboxFile(self._full_path(name), self)
        return remote_file

    def _save(self, name, content):
        content.open()
        if content.size <= self.CHUNK_SIZE:
            self.client.files_upload(
                content.read(), self._full_path(name), mode=WriteMode(self.write_mode)
            )
        else:
            self._chunked_upload(content, self._full_path(name))
        content.close()
        return name

    def _chunked_upload(self, content, dest_path):
        upload_session = self.client.files_upload_session_start(
            content.read(self.CHUNK_SIZE)
        )
        cursor = UploadSessionCursor(
            session_id=upload_session.session_id, offset=content.tell()
        )
        commit = CommitInfo(path=dest_path, mode=WriteMode(self.write_mode))

        while content.tell() < content.size:
            if (content.size - content.tell()) <= self.CHUNK_SIZE:
                self.client.files_upload_session_finish(
                    content.read(self.CHUNK_SIZE), cursor, commit
                )
            else:
                self.client.files_upload_session_append_v2(
                    content.read(self.CHUNK_SIZE), cursor
                )
                cursor.offset = content.tell()

    def get_available_name(self, name, max_length=None):
        """Overwrite existing file with the same name."""
        if self.write_mode == "overwrite":
            return get_available_overwrite_name(name, max_length)
        return super().get_available_name(name, max_length)


DropBoxStorage = DropboxStorage