Skip to content

Commit

Permalink
Fixed django#2070: refactored Django's file upload capabilities.
Browse files Browse the repository at this point in the history
A description of the new features can be found in the new [http://www.djangoproject.com/documentation/upload_handing/ upload handling documentation]; the executive summary is that Django will now happily handle uploads of large files without issues.

This changes the representation of uploaded files from dictionaries to bona fide objects; see BackwardsIncompatibleChanges for details.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@7814 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
jacob committed Jul 1, 2008
1 parent 7baa52f commit 394318c
Show file tree
Hide file tree
Showing 38 changed files with 2,291 additions and 154 deletions.
4 changes: 3 additions & 1 deletion AUTHORS
Expand Up @@ -59,7 +59,7 @@ answer newbie questions, and generally made Django that much better:
Arthur <avandorp@gmail.com>
av0000@mail.ru
David Avsajanishvili <avsd05@gmail.com>
axiak@mit.edu
Mike Axiak <axiak@mit.edu>
Niran Babalola <niran@niran.org>
Morten Bagai <m@bagai.com>
Mikaël Barbero <mikael.barbero nospam at nospam free.fr>
Expand Down Expand Up @@ -141,7 +141,9 @@ answer newbie questions, and generally made Django that much better:
Marc Fargas <telenieko@telenieko.com>
Szilveszter Farkas <szilveszter.farkas@gmail.com>
favo@exoweb.net
fdr <drfarina@gmail.com>
Dmitri Fedortchenko <zeraien@gmail.com>
Jonathan Feignberg <jdf@pobox.com>
Liang Feng <hutuworm@gmail.com>
Bill Fenner <fenner@gmail.com>
Stefane Fermgier <sf@fermigier.com>
Expand Down
15 changes: 15 additions & 0 deletions django/conf/global_settings.py
Expand Up @@ -231,6 +231,21 @@
# Example: "http://media.lawrence.com"
MEDIA_URL = ''

# List of upload handler classes to be applied in order.
FILE_UPLOAD_HANDLERS = (
'django.core.files.uploadhandler.MemoryFileUploadHandler',
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
)

# Maximum size, in bytes, of a request before it will be streamed to the
# file system instead of into memory.
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB

# Directory in which upload streamed files will be temporarily saved. A value of
# `None` will make Django use the operating system's default temporary directory
# (i.e. "/tmp" on *nix systems).
FILE_UPLOAD_TEMP_DIR = None

# Default formatting for date objects. See all available format strings here:
# http://www.djangoproject.com/documentation/templates/#now
DATE_FORMAT = 'N j, Y'
Expand Down
Empty file added django/core/files/__init__.py
Empty file.
66 changes: 66 additions & 0 deletions django/core/files/locks.py
@@ -0,0 +1,66 @@
"""
Portable file locking utilities.
Based partially on example by Jonathan Feignberg <jdf@pobox.com> in the Python
Cookbook, licensed under the Python Software License.
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65203
Example Usage::
>>> from django.core.files import locks
>>> f = open('./file', 'wb')
>>> locks.lock(f, locks.LOCK_EX)
>>> f.write('Django')
>>> f.close()
"""

__all__ = ('LOCK_EX','LOCK_SH','LOCK_NB','lock','unlock')

system_type = None

try:
import win32con
import win32file
import pywintypes
LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
LOCK_SH = 0
LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
__overlapped = pywintypes.OVERLAPPED()
system_type = 'nt'
except (ImportError, AttributeError):
pass

try:
import fcntl
LOCK_EX = fcntl.LOCK_EX
LOCK_SH = fcntl.LOCK_SH
LOCK_NB = fcntl.LOCK_NB
system_type = 'posix'
except (ImportError, AttributeError):
pass

if system_type == 'nt':
def lock(file, flags):
hfile = win32file._get_osfhandle(file.fileno())
win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped)

def unlock(file):
hfile = win32file._get_osfhandle(file.fileno())
win32file.UnlockFileEx(hfile, 0, -0x10000, __overlapped)
elif system_type == 'posix':
def lock(file, flags):
fcntl.flock(file.fileno(), flags)

def unlock(file):
fcntl.flock(file.fileno(), fcntl.LOCK_UN)
else:
# File locking is not supported.
LOCK_EX = LOCK_SH = LOCK_NB = None

# Dummy functions that don't do anything.
def lock(file, flags):
pass

def unlock(file):
pass
59 changes: 59 additions & 0 deletions django/core/files/move.py
@@ -0,0 +1,59 @@
"""
Move a file in the safest way possible::
>>> from django.core.files.move import file_move_save
>>> file_move_save("/tmp/old_file", "/tmp/new_file")
"""

import os
from django.core.files import locks

__all__ = ['file_move_safe']

try:
import shutil
file_move = shutil.move
except ImportError:
file_move = os.rename

def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_overwrite=False):
"""
Moves a file from one location to another in the safest way possible.
First, try using ``shutils.move``, which is OS-dependent but doesn't break
if moving across filesystems. Then, try ``os.rename``, which will break
across filesystems. Finally, streams manually from one file to another in
pure Python.
If the destination file exists and ``allow_overwrite`` is ``False``, this
function will throw an ``IOError``.
"""

# There's no reason to move if we don't have to.
if old_file_name == new_file_name:
return

if not allow_overwrite and os.path.exists(new_file_name):
raise IOError("Cannot overwrite existing file '%s'." % new_file_name)

try:
file_move(old_file_name, new_file_name)
return
except OSError:
# This will happen with os.rename if moving to another filesystem
pass

# If the built-in didn't work, do it the hard way.
new_file = open(new_file_name, 'wb')
locks.lock(new_file, locks.LOCK_EX)
old_file = open(old_file_name, 'rb')
current_chunk = None

while current_chunk != '':
current_chunk = old_file.read(chunk_size)
new_file.write(current_chunk)

new_file.close()
old_file.close()

os.remove(old_file_name)
190 changes: 190 additions & 0 deletions django/core/files/uploadedfile.py
@@ -0,0 +1,190 @@
"""
Classes representing uploaded files.
"""

import os
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO

__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile')

class UploadedFile(object):
"""
A abstract uploadded file (``TemporaryUploadedFile`` and
``InMemoryUploadedFile`` are the built-in concrete subclasses).
An ``UploadedFile`` object behaves somewhat like a file object and
represents some file data that the user submitted with a form.
"""
DEFAULT_CHUNK_SIZE = 64 * 2**10

def __init__(self, file_name=None, content_type=None, file_size=None, charset=None):
self.file_name = file_name
self.file_size = file_size
self.content_type = content_type
self.charset = charset

def __repr__(self):
return "<%s: %s (%s)>" % (self.__class__.__name__, self.file_name, self.content_type)

def _set_file_name(self, name):
# Sanitize the file name so that it can't be dangerous.
if name is not None:
# Just use the basename of the file -- anything else is dangerous.
name = os.path.basename(name)

# File names longer than 255 characters can cause problems on older OSes.
if len(name) > 255:
name, ext = os.path.splitext(name)
name = name[:255 - len(ext)] + ext

self._file_name = name

def _get_file_name(self):
return self._file_name

file_name = property(_get_file_name, _set_file_name)

def chunk(self, chunk_size=None):
"""
Read the file and yield chucks of ``chunk_size`` bytes (defaults to
``UploadedFile.DEFAULT_CHUNK_SIZE``).
"""
if not chunk_size:
chunk_size = UploadedFile.DEFAULT_CHUNK_SIZE

if hasattr(self, 'seek'):
self.seek(0)
# Assume the pointer is at zero...
counter = self.file_size

while counter > 0:
yield self.read(chunk_size)
counter -= chunk_size

def multiple_chunks(self, chunk_size=None):
"""
Returns ``True`` if you can expect multiple chunks.
NB: If a particular file representation is in memory, subclasses should
always return ``False`` -- there's no good reason to read from memory in
chunks.
"""
if not chunk_size:
chunk_size = UploadedFile.DEFAULT_CHUNK_SIZE
return self.file_size < chunk_size

# Abstract methods; subclasses *must* default read() and probably should
# define open/close.
def read(self, num_bytes=None):
raise NotImplementedError()

def open(self):
pass

def close(self):
pass

# Backwards-compatible support for uploaded-files-as-dictionaries.
def __getitem__(self, key):
import warnings
warnings.warn(
message = "The dictionary access of uploaded file objects is deprecated. Use the new object interface instead.",
category = DeprecationWarning,
stacklevel = 2
)
backwards_translate = {
'filename': 'file_name',
'content-type': 'content_type',
}

if key == 'content':
return self.read()
elif key == 'filename':
return self.file_name
elif key == 'content-type':
return self.content_type
else:
return getattr(self, key)

class TemporaryUploadedFile(UploadedFile):
"""
A file uploaded to a temporary location (i.e. stream-to-disk).
"""

def __init__(self, file, file_name, content_type, file_size, charset):
super(TemporaryUploadedFile, self).__init__(file_name, content_type, file_size, charset)
self.file = file
self.path = file.name
self.file.seek(0)

def temporary_file_path(self):
"""
Returns the full path of this file.
"""
return self.path

def read(self, *args, **kwargs):
return self.file.read(*args, **kwargs)

def open(self):
self.seek(0)

def seek(self, *args, **kwargs):
self.file.seek(*args, **kwargs)

class InMemoryUploadedFile(UploadedFile):
"""
A file uploaded into memory (i.e. stream-to-memory).
"""
def __init__(self, file, field_name, file_name, content_type, charset, file_size):
super(InMemoryUploadedFile, self).__init__(file_name, content_type, charset, file_size)
self.file = file
self.field_name = field_name
self.file.seek(0)

def seek(self, *args, **kwargs):
self.file.seek(*args, **kwargs)

def open(self):
self.seek(0)

def read(self, *args, **kwargs):
return self.file.read(*args, **kwargs)

def chunk(self, chunk_size=None):
self.file.seek(0)
yield self.read()

def multiple_chunks(self, chunk_size=None):
# Since it's in memory, we'll never have multiple chunks.
return False

class SimpleUploadedFile(InMemoryUploadedFile):
"""
A simple representation of a file, which just has content, size, and a name.
"""
def __init__(self, name, content, content_type='text/plain'):
self.file = StringIO(content or '')
self.file_name = name
self.field_name = None
self.file_size = len(content or '')
self.content_type = content_type
self.charset = None
self.file.seek(0)

def from_dict(cls, file_dict):
"""
Creates a SimpleUploadedFile object from
a dictionary object with the following keys:
- filename
- content-type
- content
"""
return cls(file_dict['filename'],
file_dict['content'],
file_dict.get('content-type', 'text/plain'))

from_dict = classmethod(from_dict)

0 comments on commit 394318c

Please sign in to comment.