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
|
# Dropbox storage class for Django pluggable storage system.
# Author: Anthony Monthe <anthony.monthe@gmail.com>
# License: BSD
#
# Usage:
#
# Add below to settings.py:
# DROPBOX_OAUTH2_TOKEN = 'YourOauthToken'
# DROPBOX_ROOT_PATH = '/dir/'
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.core.files.storage import Storage
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, FolderMetadata, UploadSessionCursor, WriteMode,
)
from storages.utils import get_available_overwrite_name, setting
_DEFAULT_TIMEOUT = 100
_DEFAULT_MODE = 'add'
class DropBoxStorageException(Exception):
pass
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 catched 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)
@deconstructible
class DropBoxStorage(Storage):
"""DropBox Storage class for Django pluggable storage system."""
location = setting('DROPBOX_ROOT_PATH', '/')
oauth2_access_token = setting('DROPBOX_OAUTH2_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'.")
self.root_path = root_path
self.write_mode = write_mode
self.client = Dropbox(oauth2_access_token, timeout=timeout)
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 modified_time(self, name):
metadata = self.client.files_get_metadata(self._full_path(name))
return metadata.server_modified
def accessed_time(self, name):
metadata = self.client.files_get_metadata(self._full_path(name))
return metadata.client_modified
def url(self, name):
media = self.client.files_get_temporary_link(self._full_path(name))
return media.link
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."""
name = self._full_path(name)
if self.write_mode == 'overwrite':
return get_available_overwrite_name(name, max_length)
return super().get_available_name(name, max_length)
|