| 1 |
import os |
|---|
| 2 |
import errno |
|---|
| 3 |
import urlparse |
|---|
| 4 |
|
|---|
| 5 |
from django.conf import settings |
|---|
| 6 |
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation |
|---|
| 7 |
from django.utils.encoding import force_unicode |
|---|
| 8 |
from django.utils.text import get_valid_filename |
|---|
| 9 |
from django.utils._os import safe_join |
|---|
| 10 |
from django.core.files import locks, File |
|---|
| 11 |
from django.core.files.move import file_move_safe |
|---|
| 12 |
|
|---|
| 13 |
__all__ = ('Storage', 'FileSystemStorage', 'DefaultStorage', 'default_storage') |
|---|
| 14 |
|
|---|
| 15 |
class Storage(object): |
|---|
| 16 |
""" |
|---|
| 17 |
A base storage class, providing some default behaviors that all other |
|---|
| 18 |
storage systems can inherit or override, as necessary. |
|---|
| 19 |
""" |
|---|
| 20 |
|
|---|
| 21 |
# The following methods represent a public interface to private methods. |
|---|
| 22 |
# These shouldn't be overridden by subclasses unless absolutely necessary. |
|---|
| 23 |
|
|---|
| 24 |
def open(self, name, mode='rb', mixin=None): |
|---|
| 25 |
""" |
|---|
| 26 |
Retrieves the specified file from storage, using the optional mixin |
|---|
| 27 |
class to customize what features are available on the File returned. |
|---|
| 28 |
""" |
|---|
| 29 |
file = self._open(name, mode) |
|---|
| 30 |
if mixin: |
|---|
| 31 |
# Add the mixin as a parent class of the File returned from storage. |
|---|
| 32 |
file.__class__ = type(mixin.__name__, (mixin, file.__class__), {}) |
|---|
| 33 |
return file |
|---|
| 34 |
|
|---|
| 35 |
def save(self, name, content): |
|---|
| 36 |
""" |
|---|
| 37 |
Saves new content to the file specified by name. The content should be a |
|---|
| 38 |
proper File object, ready to be read from the beginning. |
|---|
| 39 |
""" |
|---|
| 40 |
# Get the proper name for the file, as it will actually be saved. |
|---|
| 41 |
if name is None: |
|---|
| 42 |
name = content.name |
|---|
| 43 |
|
|---|
| 44 |
name = self.get_available_name(name) |
|---|
| 45 |
name = self._save(name, content) |
|---|
| 46 |
|
|---|
| 47 |
# Store filenames with forward slashes, even on Windows |
|---|
| 48 |
return force_unicode(name.replace('\\', '/')) |
|---|
| 49 |
|
|---|
| 50 |
# These methods are part of the public API, with default implementations. |
|---|
| 51 |
|
|---|
| 52 |
def get_valid_name(self, name): |
|---|
| 53 |
""" |
|---|
| 54 |
Returns a filename, based on the provided filename, that's suitable for |
|---|
| 55 |
use in the target storage system. |
|---|
| 56 |
""" |
|---|
| 57 |
return get_valid_filename(name) |
|---|
| 58 |
|
|---|
| 59 |
def get_available_name(self, name): |
|---|
| 60 |
""" |
|---|
| 61 |
Returns a filename that's free on the target storage system, and |
|---|
| 62 |
available for new content to be written to. |
|---|
| 63 |
""" |
|---|
| 64 |
# If the filename already exists, keep adding an underscore to the name |
|---|
| 65 |
# of the file until the filename doesn't exist. |
|---|
| 66 |
while self.exists(name): |
|---|
| 67 |
try: |
|---|
| 68 |
dot_index = name.rindex('.') |
|---|
| 69 |
except ValueError: # filename has no dot |
|---|
| 70 |
name += '_' |
|---|
| 71 |
else: |
|---|
| 72 |
name = name[:dot_index] + '_' + name[dot_index:] |
|---|
| 73 |
return name |
|---|
| 74 |
|
|---|
| 75 |
def path(self, name): |
|---|
| 76 |
""" |
|---|
| 77 |
Returns a local filesystem path where the file can be retrieved using |
|---|
| 78 |
Python's built-in open() function. Storage systems that can't be |
|---|
| 79 |
accessed using open() should *not* implement this method. |
|---|
| 80 |
""" |
|---|
| 81 |
raise NotImplementedError("This backend doesn't support absolute paths.") |
|---|
| 82 |
|
|---|
| 83 |
# The following methods form the public API for storage systems, but with |
|---|
| 84 |
# no default implementations. Subclasses must implement *all* of these. |
|---|
| 85 |
|
|---|
| 86 |
def delete(self, name): |
|---|
| 87 |
""" |
|---|
| 88 |
Deletes the specified file from the storage system. |
|---|
| 89 |
""" |
|---|
| 90 |
raise NotImplementedError() |
|---|
| 91 |
|
|---|
| 92 |
def exists(self, name): |
|---|
| 93 |
""" |
|---|
| 94 |
Returns True if a file referened by the given name already exists in the |
|---|
| 95 |
storage system, or False if the name is available for a new file. |
|---|
| 96 |
""" |
|---|
| 97 |
raise NotImplementedError() |
|---|
| 98 |
|
|---|
| 99 |
def listdir(self, path): |
|---|
| 100 |
""" |
|---|
| 101 |
Lists the contents of the specified path, returning a 2-tuple of lists; |
|---|
| 102 |
the first item being directories, the second item being files. |
|---|
| 103 |
""" |
|---|
| 104 |
raise NotImplementedError() |
|---|
| 105 |
|
|---|
| 106 |
def size(self, name): |
|---|
| 107 |
""" |
|---|
| 108 |
Returns the total size, in bytes, of the file specified by name. |
|---|
| 109 |
""" |
|---|
| 110 |
raise NotImplementedError() |
|---|
| 111 |
|
|---|
| 112 |
def url(self, name): |
|---|
| 113 |
""" |
|---|
| 114 |
Returns an absolute URL where the file's contents can be accessed |
|---|
| 115 |
directly by a web browser. |
|---|
| 116 |
""" |
|---|
| 117 |
raise NotImplementedError() |
|---|
| 118 |
|
|---|
| 119 |
class FileSystemStorage(Storage): |
|---|
| 120 |
""" |
|---|
| 121 |
Standard filesystem storage |
|---|
| 122 |
""" |
|---|
| 123 |
|
|---|
| 124 |
def __init__(self, location=settings.MEDIA_ROOT, base_url=settings.MEDIA_URL): |
|---|
| 125 |
self.location = os.path.abspath(location) |
|---|
| 126 |
self.base_url = base_url |
|---|
| 127 |
|
|---|
| 128 |
def _open(self, name, mode='rb'): |
|---|
| 129 |
return File(open(self.path(name), mode)) |
|---|
| 130 |
|
|---|
| 131 |
def _save(self, name, content): |
|---|
| 132 |
full_path = self.path(name) |
|---|
| 133 |
|
|---|
| 134 |
directory = os.path.dirname(full_path) |
|---|
| 135 |
if not os.path.exists(directory): |
|---|
| 136 |
os.makedirs(directory) |
|---|
| 137 |
elif not os.path.isdir(directory): |
|---|
| 138 |
raise IOError("%s exists and is not a directory." % directory) |
|---|
| 139 |
|
|---|
| 140 |
# There's a potential race condition between get_available_name and |
|---|
| 141 |
# saving the file; it's possible that two threads might return the |
|---|
| 142 |
# same name, at which point all sorts of fun happens. So we need to |
|---|
| 143 |
# try to create the file, but if it already exists we have to go back |
|---|
| 144 |
# to get_available_name() and try again. |
|---|
| 145 |
|
|---|
| 146 |
while True: |
|---|
| 147 |
try: |
|---|
| 148 |
# This file has a file path that we can move. |
|---|
| 149 |
if hasattr(content, 'temporary_file_path'): |
|---|
| 150 |
file_move_safe(content.temporary_file_path(), full_path) |
|---|
| 151 |
content.close() |
|---|
| 152 |
|
|---|
| 153 |
# This is a normal uploadedfile that we can stream. |
|---|
| 154 |
else: |
|---|
| 155 |
# This fun binary flag incantation makes os.open throw an |
|---|
| 156 |
# OSError if the file already exists before we open it. |
|---|
| 157 |
fd = os.open(full_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0)) |
|---|
| 158 |
try: |
|---|
| 159 |
locks.lock(fd, locks.LOCK_EX) |
|---|
| 160 |
for chunk in content.chunks(): |
|---|
| 161 |
os.write(fd, chunk) |
|---|
| 162 |
finally: |
|---|
| 163 |
locks.unlock(fd) |
|---|
| 164 |
os.close(fd) |
|---|
| 165 |
except OSError, e: |
|---|
| 166 |
if e.errno == errno.EEXIST: |
|---|
| 167 |
# Ooops, the file exists. We need a new file name. |
|---|
| 168 |
name = self.get_available_name(name) |
|---|
| 169 |
full_path = self.path(name) |
|---|
| 170 |
else: |
|---|
| 171 |
raise |
|---|
| 172 |
else: |
|---|
| 173 |
# OK, the file save worked. Break out of the loop. |
|---|
| 174 |
break |
|---|
| 175 |
|
|---|
| 176 |
if settings.FILE_UPLOAD_PERMISSIONS is not None: |
|---|
| 177 |
os.chmod(full_path, settings.FILE_UPLOAD_PERMISSIONS) |
|---|
| 178 |
|
|---|
| 179 |
return name |
|---|
| 180 |
|
|---|
| 181 |
def delete(self, name): |
|---|
| 182 |
name = self.path(name) |
|---|
| 183 |
# If the file exists, delete it from the filesystem. |
|---|
| 184 |
if os.path.exists(name): |
|---|
| 185 |
os.remove(name) |
|---|
| 186 |
|
|---|
| 187 |
def exists(self, name): |
|---|
| 188 |
return os.path.exists(self.path(name)) |
|---|
| 189 |
|
|---|
| 190 |
def listdir(self, path): |
|---|
| 191 |
path = self.path(path) |
|---|
| 192 |
directories, files = [], [] |
|---|
| 193 |
for entry in os.listdir(path): |
|---|
| 194 |
if os.path.isdir(os.path.join(path, entry)): |
|---|
| 195 |
directories.append(entry) |
|---|
| 196 |
else: |
|---|
| 197 |
files.append(entry) |
|---|
| 198 |
return directories, files |
|---|
| 199 |
|
|---|
| 200 |
def path(self, name): |
|---|
| 201 |
try: |
|---|
| 202 |
path = safe_join(self.location, name) |
|---|
| 203 |
except ValueError: |
|---|
| 204 |
raise SuspiciousOperation("Attempted access to '%s' denied." % name) |
|---|
| 205 |
return os.path.normpath(path) |
|---|
| 206 |
|
|---|
| 207 |
def size(self, name): |
|---|
| 208 |
return os.path.getsize(self.path(name)) |
|---|
| 209 |
|
|---|
| 210 |
def url(self, name): |
|---|
| 211 |
if self.base_url is None: |
|---|
| 212 |
raise ValueError("This file is not accessible via a URL.") |
|---|
| 213 |
return urlparse.urljoin(self.base_url, name).replace('\\', '/') |
|---|
| 214 |
|
|---|
| 215 |
def get_storage_class(import_path): |
|---|
| 216 |
try: |
|---|
| 217 |
dot = import_path.rindex('.') |
|---|
| 218 |
except ValueError: |
|---|
| 219 |
raise ImproperlyConfigured("%s isn't a storage module." % import_path) |
|---|
| 220 |
module, classname = import_path[:dot], import_path[dot+1:] |
|---|
| 221 |
try: |
|---|
| 222 |
mod = __import__(module, {}, {}, ['']) |
|---|
| 223 |
except ImportError, e: |
|---|
| 224 |
raise ImproperlyConfigured('Error importing storage module %s: "%s"' % (module, e)) |
|---|
| 225 |
try: |
|---|
| 226 |
return getattr(mod, classname) |
|---|
| 227 |
except AttributeError: |
|---|
| 228 |
raise ImproperlyConfigured('Storage module "%s" does not define a "%s" class.' % (module, classname)) |
|---|
| 229 |
|
|---|
| 230 |
DefaultStorage = get_storage_class(settings.DEFAULT_FILE_STORAGE) |
|---|
| 231 |
default_storage = DefaultStorage() |
|---|