diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 811feed349..c52d8d9a89 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -226,6 +226,9 @@ SECRET_KEY = ''
 # Path to the "jing" executable -- needed to validate XMLFields
 JING_PATH = "/usr/bin/jing"
 
+# Default file storage mechanism that holds media.
+DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
+
 # Absolute path to the directory that holds media.
 # Example: "/home/media/media.lawrence.com/"
 MEDIA_ROOT = ''
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 884171be2e..02a52702d3 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -85,8 +85,8 @@ class AdminFileWidget(forms.FileInput):
     def render(self, name, value, attrs=None):
         output = []
         if value:
-            output.append('%s <a target="_blank" href="%s%s">%s</a> <br />%s ' % \
-                (_('Currently:'), settings.MEDIA_URL, value, value, _('Change:')))
+            output.append('%s <a target="_blank" href="%s">%s</a> <br />%s ' % \
+                (_('Currently:'), value.url, value, _('Change:')))
         output.append(super(AdminFileWidget, self).render(name, value, attrs))
         return mark_safe(u''.join(output))
 
diff --git a/django/core/files/__init__.py b/django/core/files/__init__.py
index e69de29bb2..0c3ef57af8 100644
--- a/django/core/files/__init__.py
+++ b/django/core/files/__init__.py
@@ -0,0 +1 @@
+from django.core.files.base import File
diff --git a/django/core/files/base.py b/django/core/files/base.py
new file mode 100644
index 0000000000..69739d6488
--- /dev/null
+++ b/django/core/files/base.py
@@ -0,0 +1,169 @@
+import os
+
+from django.utils.encoding import smart_str, smart_unicode
+
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+class File(object):
+    DEFAULT_CHUNK_SIZE = 64 * 2**10
+
+    def __init__(self, file):
+        self.file = file
+        self._name = file.name
+        self._mode = file.mode
+        self._closed = False
+
+    def __str__(self):
+        return smart_str(self.name or '')
+
+    def __unicode__(self):
+        return smart_unicode(self.name or u'')
+
+    def __repr__(self):
+        return "<%s: %s>" % (self.__class__.__name__, self or "None")
+
+    def __nonzero__(self):
+        return not not self.name
+
+    def __len__(self):
+        return self.size
+
+    def _get_name(self):
+        return self._name
+    name = property(_get_name)
+
+    def _get_mode(self):
+        return self._mode
+    mode = property(_get_mode)
+
+    def _get_closed(self):
+        return self._closed
+    closed = property(_get_closed)
+
+    def _get_size(self):
+        if not hasattr(self, '_size'):
+            if hasattr(self.file, 'size'):
+                self._size = self.file.size
+            elif os.path.exists(self.file.name):
+                self._size = os.path.getsize(self.file.name)
+            else:
+                raise AttributeError("Unable to determine the file's size.")
+        return self._size
+
+    def _set_size(self, size):
+        self._size = size
+
+    size = property(_get_size, _set_size)
+
+    def chunks(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 = self.__class__.DEFAULT_CHUNK_SIZE
+
+        if hasattr(self, 'seek'):
+            self.seek(0)
+        # Assume the pointer is at zero...
+        counter = self.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 = self.DEFAULT_CHUNK_SIZE
+        return self.size > chunk_size
+
+    def xreadlines(self):
+        return iter(self)
+
+    def readlines(self):
+        return list(self.xreadlines())
+
+    def __iter__(self):
+        # Iterate over this file-like object by newlines
+        buffer_ = None
+        for chunk in self.chunks():
+            chunk_buffer = StringIO(chunk)
+
+            for line in chunk_buffer:
+                if buffer_:
+                    line = buffer_ + line
+                    buffer_ = None
+
+                # If this is the end of a line, yield
+                # otherwise, wait for the next round
+                if line[-1] in ('\n', '\r'):
+                    yield line
+                else:
+                    buffer_ = line
+
+        if buffer_ is not None:
+            yield buffer_
+
+    def open(self, mode=None):
+        if not self.closed:
+            self.seek(0)
+        elif os.path.exists(self.file.name):
+            self.file = open(self.file.name, mode or self.file.mode)
+        else:
+            raise ValueError("The file cannot be reopened.")
+
+    def seek(self, position):
+        self.file.seek(position)
+
+    def tell(self):
+        return self.file.tell()
+
+    def read(self, num_bytes=None):
+        if num_bytes is None:
+            return self.file.read()
+        return self.file.read(num_bytes)
+
+    def write(self, content):
+        if not self.mode.startswith('w'):
+            raise IOError("File was not opened with write access.")
+        self.file.write(content)
+
+    def flush(self):
+        if not self.mode.startswith('w'):
+            raise IOError("File was not opened with write access.")
+        self.file.flush()
+
+    def close(self):
+        self.file.close()
+        self._closed = True
+
+class ContentFile(File):
+    """
+    A File-like object that takes just raw content, rather than an actual file.
+    """
+    def __init__(self, content):
+        self.file = StringIO(content or '')
+        self.size = len(content or '')
+        self.file.seek(0)
+        self._closed = False
+
+    def __str__(self):
+        return 'Raw content'
+
+    def __nonzero__(self):
+        return True
+
+    def open(self, mode=None):
+        if self._closed:
+            self._closed = False
+        self.seek(0)
diff --git a/django/core/files/images.py b/django/core/files/images.py
new file mode 100644
index 0000000000..3fa5013027
--- /dev/null
+++ b/django/core/files/images.py
@@ -0,0 +1,42 @@
+"""
+Utility functions for handling images.
+
+Requires PIL, as you might imagine.
+"""
+
+from PIL import ImageFile as PIL
+from django.core.files import File
+
+class ImageFile(File):
+    """
+    A mixin for use alongside django.core.files.base.File, which provides
+    additional features for dealing with images.
+    """
+    def _get_width(self):
+        return self._get_image_dimensions()[0]
+    width = property(_get_width)
+
+    def _get_height(self):
+        return self._get_image_dimensions()[1]
+    height = property(_get_height)
+
+    def _get_image_dimensions(self):
+        if not hasattr(self, '_dimensions_cache'):
+            self._dimensions_cache = get_image_dimensions(self)
+        return self._dimensions_cache
+
+def get_image_dimensions(file_or_path):
+    """Returns the (width, height) of an image, given an open file or a path."""
+    p = PIL.Parser()
+    if hasattr(file_or_path, 'read'):
+        file = file_or_path
+    else:
+        file = open(file_or_path, 'rb')
+    while 1:
+        data = file.read(1024)
+        if not data:
+            break
+        p.feed(data)
+        if p.image:
+            return p.image.size
+    return None
diff --git a/django/core/files/storage.py b/django/core/files/storage.py
new file mode 100644
index 0000000000..351cc03bb5
--- /dev/null
+++ b/django/core/files/storage.py
@@ -0,0 +1,214 @@
+import os
+import urlparse
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
+from django.utils.encoding import force_unicode, smart_str
+from django.utils.text import force_unicode, get_valid_filename
+from django.utils._os import safe_join
+from django.core.files import locks, File
+
+__all__ = ('Storage', 'FileSystemStorage', 'DefaultStorage', 'default_storage')
+
+class Storage(object):
+    """
+    A base storage class, providing some default behaviors that all other
+    storage systems can inherit or override, as necessary.
+    """
+
+    # The following methods represent a public interface to private methods.
+    # These shouldn't be overridden by subclasses unless absolutely necessary.
+
+    def open(self, name, mode='rb', mixin=None):
+        """
+        Retrieves the specified file from storage, using the optional mixin
+        class to customize what features are available on the File returned.
+        """
+        file = self._open(name, mode)
+        if mixin:
+            # Add the mixin as a parent class of the File returned from storage.
+            file.__class__ = type(mixin.__name__, (mixin, file.__class__), {})
+        return file
+
+    def save(self, name, content):
+        """
+        Saves new content to the file specified by name. The content should be a
+        proper File object, ready to be read from the beginning.
+        """
+        # Check for old-style usage. Warn here first since there are multiple
+        # locations where we need to support both new and old usage.
+        if isinstance(content, basestring):
+            import warnings
+            warnings.warn(
+                message = "Representing files as strings is deprecated." \
+                          "Use django.core.files.base.ContentFile instead.",
+                category = DeprecationWarning,
+                stacklevel = 2
+            )
+            from django.core.files.base import ContentFile
+            content = ContentFile(content)
+
+        # Get the proper name for the file, as it will actually be saved.
+        if name is None:
+            name = content.name
+        name = self.get_available_name(name)
+
+        self._save(name, content)
+
+        # Store filenames with forward slashes, even on Windows
+        return force_unicode(name.replace('\\', '/'))
+
+    # These methods are part of the public API, with default implementations.
+
+    def get_valid_name(self, name):
+        """
+        Returns a filename, based on the provided filename, that's suitable for
+        use in the target storage system.
+        """
+        return get_valid_filename(name)
+
+    def get_available_name(self, name):
+        """
+        Returns a filename that's free on the target storage system, and
+        available for new content to be written to.
+        """
+        # If the filename already exists, keep adding an underscore to the name
+        # of the file until the filename doesn't exist.
+        while self.exists(name):
+            try:
+                dot_index = name.rindex('.')
+            except ValueError: # filename has no dot
+                name += '_'
+            else:
+                name = name[:dot_index] + '_' + name[dot_index:]
+        return name
+
+    def path(self, name):
+        """
+        Returns a local filesystem path where the file can be retrieved using
+        Python's built-in open() function. Storage systems that can't be
+        accessed using open() should *not* implement this method.
+        """
+        raise NotImplementedError("This backend doesn't support absolute paths.")
+
+    # The following methods form the public API for storage systems, but with
+    # no default implementations. Subclasses must implement *all* of these.
+
+    def delete(self, name):
+        """
+        Deletes the specified file from the storage system.
+        """
+        raise NotImplementedError()
+
+    def exists(self, name):
+        """
+        Returns True if a file referened by the given name already exists in the
+        storage system, or False if the name is available for a new file.
+        """
+        raise NotImplementedError()
+
+    def listdir(self, path):
+        """
+        Lists the contents of the specified path, returning a 2-tuple of lists;
+        the first item being directories, the second item being files.
+        """
+        raise NotImplementedError()
+
+    def size(self, name):
+        """
+        Returns the total size, in bytes, of the file specified by name.
+        """
+        raise NotImplementedError()
+
+    def url(self, name):
+        """
+        Returns an absolute URL where the file's contents can be accessed
+        directly by a web browser.
+        """
+        raise NotImplementedError()
+
+class FileSystemStorage(Storage):
+    """
+    Standard filesystem storage
+    """
+
+    def __init__(self, location=settings.MEDIA_ROOT, base_url=settings.MEDIA_URL):
+        self.location = os.path.abspath(location)
+        self.base_url = base_url
+
+    def _open(self, name, mode='rb'):
+        return File(open(self.path(name), mode))
+
+    def _save(self, name, content):
+        full_path = self.path(name)
+
+        directory = os.path.dirname(full_path)
+        if not os.path.exists(directory):
+            os.makedirs(directory)
+        elif not os.path.isdir(directory):
+            raise IOError("%s exists and is not a directory." % directory)
+            
+        if hasattr(content, 'temporary_file_path'):
+            # This file has a file path that we can move.
+            file_move_safe(content.temporary_file_path(), full_path)
+            content.close()
+        else:
+            # This is a normal uploadedfile that we can stream.
+            fp = open(full_path, 'wb')
+            locks.lock(fp, locks.LOCK_EX)
+            for chunk in content.chunks():
+                fp.write(chunk)
+            locks.unlock(fp)
+            fp.close()
+
+    def delete(self, name):
+        name = self.path(name)
+        # If the file exists, delete it from the filesystem.
+        if os.path.exists(name):
+            os.remove(name)
+
+    def exists(self, name):
+        return os.path.exists(self.path(name))
+
+    def listdir(self, path):
+        path = self.path(path)
+        directories, files = [], []
+        for entry in os.listdir(path):
+            if os.path.isdir(os.path.join(path, entry)):
+                directories.append(entry)
+            else:
+                files.append(entry)
+        return directories, files
+
+    def path(self, name):
+        try:
+            path = safe_join(self.location, name)
+        except ValueError:
+            raise SuspiciousOperation("Attempted access to '%s' denied." % name)
+        return os.path.normpath(path)
+
+    def size(self, name):
+        return os.path.getsize(self.path(name))
+
+    def url(self, name):
+        if self.base_url is None:
+            raise ValueError("This file is not accessible via a URL.")
+        return urlparse.urljoin(self.base_url, name).replace('\\', '/')
+
+def get_storage_class(import_path):
+    try:
+        dot = import_path.rindex('.')
+    except ValueError:
+        raise ImproperlyConfigured("%s isn't a storage module." % import_path)
+    module, classname = import_path[:dot], import_path[dot+1:]
+    try:
+        mod = __import__(module, {}, {}, [''])
+    except ImportError, e:
+        raise ImproperlyConfigured('Error importing storage module %s: "%s"' % (module, e))
+    try:
+        return getattr(mod, classname)
+    except AttributeError:
+        raise ImproperlyConfigured('Storage module "%s" does not define a "%s" class.' % (module, classname))
+
+DefaultStorage = get_storage_class(settings.DEFAULT_FILE_STORAGE)
+default_storage = DefaultStorage()
diff --git a/django/core/files/uploadedfile.py b/django/core/files/uploadedfile.py
index 5e81e968cd..a5a12930e3 100644
--- a/django/core/files/uploadedfile.py
+++ b/django/core/files/uploadedfile.py
@@ -10,6 +10,7 @@ except ImportError:
     from StringIO import StringIO
 
 from django.conf import settings
+from django.core.files.base import File
 
 from django.core.files import temp as tempfile
 
@@ -39,7 +40,7 @@ def deprecated_property(old, new, readonly=False):
     else:
         return property(getter, setter)
 
-class UploadedFile(object):
+class UploadedFile(File):
     """
     A abstract uploaded file (``TemporaryUploadedFile`` and
     ``InMemoryUploadedFile`` are the built-in concrete subclasses).
@@ -76,23 +77,6 @@ class UploadedFile(object):
 
     name = property(_get_name, _set_name)
 
-    def chunks(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.size
-
-        while counter > 0:
-            yield self.read(chunk_size)
-            counter -= chunk_size
-
     # Deprecated properties
     filename = deprecated_property(old="filename", new="name")
     file_name = deprecated_property(old="file_name", new="name")
@@ -108,18 +92,6 @@ class UploadedFile(object):
         return self.read()
     data = property(_get_data)
 
-    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.size > chunk_size
-
     # Abstract methods; subclasses *must* define read() and probably should
     # define open/close.
     def read(self, num_bytes=None):
@@ -131,33 +103,6 @@ class UploadedFile(object):
     def close(self):
         pass
 
-    def xreadlines(self):
-        return self
-
-    def readlines(self):
-        return list(self.xreadlines())
-
-    def __iter__(self):
-        # Iterate over this file-like object by newlines
-        buffer_ = None
-        for chunk in self.chunks():
-            chunk_buffer = StringIO(chunk)
-
-            for line in chunk_buffer:
-                if buffer_:
-                    line = buffer_ + line
-                    buffer_ = None
-
-                # If this is the end of a line, yield
-                # otherwise, wait for the next round
-                if line[-1] in ('\n', '\r'):
-                    yield line
-                else:
-                    buffer_ = line
-
-        if buffer_ is not None:
-            yield buffer_
-
     # Backwards-compatible support for uploaded-files-as-dictionaries.
     def __getitem__(self, key):
         warnings.warn(
diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index 18c47e86f3..cbd685547e 100644
--- a/django/db/models/__init__.py
+++ b/django/db/models/__init__.py
@@ -8,6 +8,7 @@ from django.db.models.manager import Manager
 from django.db.models.base import Model
 from django.db.models.fields import *
 from django.db.models.fields.subclassing import SubfieldBase
+from django.db.models.fields.files import FileField, ImageField
 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED
 from django.db.models import signals
 
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 3d7eac9284..59a503ff82 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -3,6 +3,7 @@ import types
 import sys
 import os
 from itertools import izip
+from warnings import warn
 try:
     set
 except NameError:
@@ -12,7 +13,7 @@ import django.db.models.manipulators    # Imported to register signal handler.
 import django.db.models.manager         # Ditto.
 from django.core import validators
 from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
-from django.db.models.fields import AutoField, ImageField
+from django.db.models.fields import AutoField
 from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
 from django.db.models.query import delete_objects, Q, CollectedObjects
 from django.db.models.options import Options
@@ -463,110 +464,42 @@ class Model(object):
         return getattr(self, cachename)
 
     def _get_FIELD_filename(self, field):
-        if getattr(self, field.attname): # Value is not blank.
-            return os.path.normpath(os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)))
-        return ''
+        warn("instance.get_%s_filename() is deprecated. Use instance.%s.path instead." % \
+            (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+        try:
+            return getattr(self, field.attname).path
+        except ValueError:
+            return ''
 
     def _get_FIELD_url(self, field):
-        if getattr(self, field.attname): # Value is not blank.
-            import urlparse
-            return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/')
-        return ''
+        warn("instance.get_%s_url() is deprecated. Use instance.%s.url instead." % \
+            (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+        try:
+            return getattr(self, field.attname).url
+        except ValueError:
+            return ''
 
     def _get_FIELD_size(self, field):
-        return os.path.getsize(self._get_FIELD_filename(field))
+        warn("instance.get_%s_size() is deprecated. Use instance.%s.size instead." % \
+            (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+        return getattr(self, field.attname).size
 
-    def _save_FIELD_file(self, field, filename, raw_field, save=True):
-        # Create the upload directory if it doesn't already exist
-        directory = os.path.join(settings.MEDIA_ROOT, field.get_directory_name())
-        if not os.path.exists(directory):
-            os.makedirs(directory)
-        elif not os.path.isdir(directory):
-            raise IOError('%s exists and is not a directory' % directory)        
-
-        # Check for old-style usage (files-as-dictionaries). Warn here first
-        # since there are multiple locations where we need to support both new
-        # and old usage.
-        if isinstance(raw_field, dict):
-            import warnings
-            warnings.warn(
-                message = "Representing uploaded files as dictionaries is deprecated. Use django.core.files.uploadedfile.SimpleUploadedFile instead.",
-                category = DeprecationWarning,
-                stacklevel = 2
-            )
-            from django.core.files.uploadedfile import SimpleUploadedFile
-            raw_field = SimpleUploadedFile.from_dict(raw_field)
-
-        elif isinstance(raw_field, basestring):
-            import warnings
-            warnings.warn(
-                message = "Representing uploaded files as strings is deprecated. Use django.core.files.uploadedfile.SimpleUploadedFile instead.",
-                category = DeprecationWarning,
-                stacklevel = 2
-            )
-            from django.core.files.uploadedfile import SimpleUploadedFile
-            raw_field = SimpleUploadedFile(filename, raw_field)
-
-        if filename is None:
-            filename = raw_field.file_name
-
-        filename = field.get_filename(filename)
-
-        # If the filename already exists, keep adding an underscore to the name
-        # of the file until the filename doesn't exist.
-        while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)):
-            try:
-                dot_index = filename.rindex('.')
-            except ValueError: # filename has no dot.
-                filename += '_'
-            else:
-                filename = filename[:dot_index] + '_' + filename[dot_index:]
-
-        # Save the file name on the object and write the file to disk.
-        setattr(self, field.attname, filename)
-        full_filename = self._get_FIELD_filename(field)
-        if hasattr(raw_field, 'temporary_file_path'):
-            # This file has a file path that we can move.
-            file_move_safe(raw_field.temporary_file_path(), full_filename)
-            raw_field.close()
-        else:
-            # This is a normal uploadedfile that we can stream.
-            fp = open(full_filename, 'wb')
-            locks.lock(fp, locks.LOCK_EX)
-            for chunk in raw_field.chunks():
-                fp.write(chunk)
-            locks.unlock(fp)
-            fp.close()
-
-        # Save the width and/or height, if applicable.
-        if isinstance(field, ImageField) and \
-                (field.width_field or field.height_field):
-            from django.utils.images import get_image_dimensions
-            width, height = get_image_dimensions(full_filename)
-            if field.width_field:
-                setattr(self, field.width_field, width)
-            if field.height_field:
-                setattr(self, field.height_field, height)
-
-        # Save the object because it has changed, unless save is False.
-        if save:
-            self.save()
+    def _save_FIELD_file(self, field, filename, content, save=True):
+        warn("instance.save_%s_file() is deprecated. Use instance.%s.save() instead." % \
+            (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+        return getattr(self, field.attname).save(filename, content, save)
 
     _save_FIELD_file.alters_data = True
 
     def _get_FIELD_width(self, field):
-        return self._get_image_dimensions(field)[0]
+        warn("instance.get_%s_width() is deprecated. Use instance.%s.width instead." % \
+            (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+        return getattr(self, field.attname).width()
 
     def _get_FIELD_height(self, field):
-        return self._get_image_dimensions(field)[1]
-
-    def _get_image_dimensions(self, field):
-        cachename = "__%s_dimensions_cache" % field.name
-        if not hasattr(self, cachename):
-            from django.utils.images import get_image_dimensions
-            filename = self._get_FIELD_filename(field)
-            setattr(self, cachename, get_image_dimensions(filename))
-        return getattr(self, cachename)
+        warn("instance.get_%s_height() is deprecated. Use instance.%s.height instead." % \
+            (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+        return getattr(self, field.attname).height()
 
 
 ############################################
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index dbb3e520c0..f19fb258a3 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -10,6 +10,7 @@ except ImportError:
 from django.db import connection, get_creation_module
 from django.db.models import signals
 from django.db.models.query_utils import QueryWrapper
+from django.dispatch import dispatcher
 from django.conf import settings
 from django.core import validators
 from django import oldforms
@@ -757,131 +758,6 @@ class EmailField(CharField):
         defaults.update(kwargs)
         return super(EmailField, self).formfield(**defaults)
 
-class FileField(Field):
-    def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs):
-        self.upload_to = upload_to
-        kwargs['max_length'] = kwargs.get('max_length', 100)
-        Field.__init__(self, verbose_name, name, **kwargs)
-
-    def get_internal_type(self):
-        return "FileField"
-
-    def get_db_prep_value(self, value):
-        "Returns field's value prepared for saving into a database."
-        # Need to convert UploadedFile objects provided via a form to unicode for database insertion
-        if hasattr(value, 'name'):
-            return value.name
-        elif value is None:
-            return None
-        else:
-            return unicode(value)
-
-    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
-        field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
-        if not self.blank:
-            if rel:
-                # This validator makes sure FileFields work in a related context.
-                class RequiredFileField(object):
-                    def __init__(self, other_field_names, other_file_field_name):
-                        self.other_field_names = other_field_names
-                        self.other_file_field_name = other_file_field_name
-                        self.always_test = True
-                    def __call__(self, field_data, all_data):
-                        if not all_data.get(self.other_file_field_name, False):
-                            c = validators.RequiredIfOtherFieldsGiven(self.other_field_names, ugettext_lazy("This field is required."))
-                            c(field_data, all_data)
-                # First, get the core fields, if any.
-                core_field_names = []
-                for f in opts.fields:
-                    if f.core and f != self:
-                        core_field_names.extend(f.get_manipulator_field_names(name_prefix))
-                # Now, if there are any, add the validator to this FormField.
-                if core_field_names:
-                    field_list[0].validator_list.append(RequiredFileField(core_field_names, field_list[1].field_name))
-            else:
-                v = validators.RequiredIfOtherFieldNotGiven(field_list[1].field_name, ugettext_lazy("This field is required."))
-                v.always_test = True
-                field_list[0].validator_list.append(v)
-                field_list[0].is_required = field_list[1].is_required = False
-
-        # If the raw path is passed in, validate it's under the MEDIA_ROOT.
-        def isWithinMediaRoot(field_data, all_data):
-            f = os.path.abspath(os.path.join(settings.MEDIA_ROOT, field_data))
-            if not f.startswith(os.path.abspath(os.path.normpath(settings.MEDIA_ROOT))):
-                raise validators.ValidationError, _("Enter a valid filename.")
-        field_list[1].validator_list.append(isWithinMediaRoot)
-        return field_list
-
-    def contribute_to_class(self, cls, name):
-        super(FileField, self).contribute_to_class(cls, name)
-        setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
-        setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
-        setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
-        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save))
-        signals.post_delete.connect(self.delete_file, sender=cls)
-
-    def delete_file(self, instance, **kwargs):
-        if getattr(instance, self.attname):
-            file_name = getattr(instance, 'get_%s_filename' % self.name)()
-            # If the file exists and no other object of this type references it,
-            # delete it from the filesystem.
-            if os.path.exists(file_name) and \
-                not instance.__class__._default_manager.filter(**{'%s__exact' % self.name: getattr(instance, self.attname)}):
-                os.remove(file_name)
-
-    def get_manipulator_field_objs(self):
-        return [oldforms.FileUploadField, oldforms.HiddenField]
-
-    def get_manipulator_field_names(self, name_prefix):
-        return [name_prefix + self.name + '_file', name_prefix + self.name]
-
-    def save_file(self, new_data, new_object, original_object, change, rel, save=True):
-        upload_field_name = self.get_manipulator_field_names('')[0]
-        if new_data.get(upload_field_name, False):
-            if rel:
-                file = new_data[upload_field_name][0]
-            else:
-                file = new_data[upload_field_name]
-
-            if not file:
-                return
-
-            # Backwards-compatible support for files-as-dictionaries.
-            # We don't need to raise a warning because Model._save_FIELD_file will
-            # do so for us.
-            try:
-                file_name = file.name
-            except AttributeError:
-                file_name = file['filename']
-
-            func = getattr(new_object, 'save_%s_file' % self.name)
-            func(file_name, file, save)
-
-    def get_directory_name(self):
-        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
-
-    def get_filename(self, filename):
-        from django.utils.text import get_valid_filename
-        f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
-        return os.path.normpath(f)
-
-    def save_form_data(self, instance, data):
-        from django.core.files.uploadedfile import UploadedFile
-        if data and isinstance(data, UploadedFile):
-            getattr(instance, "save_%s_file" % self.name)(data.name, data, save=False)
-
-    def formfield(self, **kwargs):
-        defaults = {'form_class': forms.FileField}
-        # If a file has been provided previously, then the form doesn't require
-        # that a new file is provided this time.
-        # The code to mark the form field as not required is used by
-        # form_for_instance, but can probably be removed once form_for_instance
-        # is gone. ModelForm uses a different method to check for an existing file.
-        if 'initial' in kwargs:
-            defaults['required'] = False
-        defaults.update(kwargs)
-        return super(FileField, self).formfield(**defaults)
-
 class FilePathField(Field):
     def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
         self.path, self.match, self.recursive = path, match, recursive
@@ -923,40 +799,6 @@ class FloatField(Field):
         defaults.update(kwargs)
         return super(FloatField, self).formfield(**defaults)
 
-class ImageField(FileField):
-    def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
-        self.width_field, self.height_field = width_field, height_field
-        FileField.__init__(self, verbose_name, name, **kwargs)
-
-    def get_manipulator_field_objs(self):
-        return [oldforms.ImageUploadField, oldforms.HiddenField]
-
-    def contribute_to_class(self, cls, name):
-        super(ImageField, self).contribute_to_class(cls, name)
-        # Add get_BLAH_width and get_BLAH_height methods, but only if the
-        # image field doesn't have width and height cache fields.
-        if not self.width_field:
-            setattr(cls, 'get_%s_width' % self.name, curry(cls._get_FIELD_width, field=self))
-        if not self.height_field:
-            setattr(cls, 'get_%s_height' % self.name, curry(cls._get_FIELD_height, field=self))
-
-    def save_file(self, new_data, new_object, original_object, change, rel, save=True):
-        FileField.save_file(self, new_data, new_object, original_object, change, rel, save)
-        # If the image has height and/or width field(s) and they haven't
-        # changed, set the width and/or height field(s) back to their original
-        # values.
-        if change and (self.width_field or self.height_field) and save:
-            if self.width_field:
-                setattr(new_object, self.width_field, getattr(original_object, self.width_field))
-            if self.height_field:
-                setattr(new_object, self.height_field, getattr(original_object, self.height_field))
-            new_object.save()
-
-    def formfield(self, **kwargs):
-        defaults = {'form_class': forms.ImageField}
-        defaults.update(kwargs)
-        return super(ImageField, self).formfield(**defaults)
-
 class IntegerField(Field):
     empty_strings_allowed = False
     def get_db_prep_value(self, value):
diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py
new file mode 100644
index 0000000000..76639596b5
--- /dev/null
+++ b/django/db/models/fields/files.py
@@ -0,0 +1,315 @@
+import datetime
+import os
+
+from django.conf import settings
+from django.db.models.fields import Field
+from django.core.files.base import File, ContentFile
+from django.core.files.storage import default_storage
+from django.core.files.images import ImageFile, get_image_dimensions
+from django.core.files.uploadedfile import UploadedFile
+from django.utils.functional import curry
+from django.db.models import signals
+from django.utils.encoding import force_unicode, smart_str
+from django.utils.translation import ugettext_lazy, ugettext as _
+from django import oldforms
+from django import forms
+from django.core import validators
+from django.db.models.loading import cache
+
+class FieldFile(File):
+    def __init__(self, instance, field, name):
+        self.instance = instance
+        self.field = field
+        self.storage = field.storage
+        self._name = name or u''
+        self._closed = False
+
+    def __eq__(self, other):
+        # Older code may be expecting FileField values to be simple strings.
+        # By overriding the == operator, it can remain backwards compatibility.
+        if hasattr(other, 'name'):
+            return self.name == other.name
+        return self.name == other
+
+    # The standard File contains most of the necessary properties, but
+    # FieldFiles can be instantiated without a name, so that needs to
+    # be checked for here.
+
+    def _require_file(self):
+        if not self:
+            raise ValueError("The '%s' attribute has no file associated with it." % self.field.name)
+
+    def _get_file(self):
+        self._require_file()
+        if not hasattr(self, '_file'):
+            self._file = self.storage.open(self.name, 'rb')
+        return self._file
+    file = property(_get_file)
+
+    def _get_path(self):
+        self._require_file()
+        return self.storage.path(self.name)
+    path = property(_get_path)
+
+    def _get_url(self):
+        self._require_file()
+        return self.storage.url(self.name)
+    url = property(_get_url)
+
+    def open(self, mode='rb'):
+        self._require_file()
+        return super(FieldFile, self).open(mode)
+    # open() doesn't alter the file's contents, but it does reset the pointer
+    open.alters_data = True
+
+    # In addition to the standard File API, FieldFiles have extra methods
+    # to further manipulate the underlying file, as well as update the
+    # associated model instance.
+
+    def save(self, name, content, save=True):
+        name = self.field.generate_filename(self.instance, name)
+        self._name = self.storage.save(name, content)
+        setattr(self.instance, self.field.name, self.name)
+
+        # Update the filesize cache
+        self._size = len(content)
+
+        # Save the object because it has changed, unless save is False
+        if save:
+            self.instance.save()
+    save.alters_data = True
+
+    def delete(self, save=True):
+        self.close()
+        self.storage.delete(self.name)
+
+        self._name = None
+        setattr(self.instance, self.field.name, self.name)
+
+        # Delete the filesize cache
+        if hasattr(self, '_size'):
+            del self._size
+
+        if save:
+            self.instance.save()
+    delete.alters_data = True
+
+    def __getstate__(self):
+        # FieldFile needs access to its associated model field and an instance
+        # it's attached to in order to work properly, but the only necessary
+        # data to be pickled is the file's name itself. Everything else will
+        # be restored later, by FileDescriptor below.
+        return {'_name': self.name, '_closed': False}
+
+class FileDescriptor(object):
+    def __init__(self, field):
+        self.field = field
+
+    def __get__(self, instance=None, owner=None):
+        if instance is None:
+            raise AttributeError, "%s can only be accessed from %s instances." % (self.field.name(self.owner.__name__))
+        file = instance.__dict__[self.field.name]
+        if not isinstance(file, FieldFile):
+            # Create a new instance of FieldFile, based on a given file name
+            instance.__dict__[self.field.name] = self.field.attr_class(instance, self.field, file)
+        elif not hasattr(file, 'field'):
+            # The FieldFile was pickled, so some attributes need to be reset.
+            file.instance = instance
+            file.field = self.field
+            file.storage = self.field.storage
+        return instance.__dict__[self.field.name]
+
+    def __set__(self, instance, value):
+        instance.__dict__[self.field.name] = value
+
+class FileField(Field):
+    attr_class = FieldFile
+
+    def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs):
+        for arg in ('core', 'primary_key', 'unique'):
+            if arg in kwargs:
+                raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__))
+
+        self.storage = storage or default_storage
+        self.upload_to = upload_to
+        if callable(upload_to):
+            self.generate_filename = upload_to
+
+        kwargs['max_length'] = kwargs.get('max_length', 100)
+        super(FileField, self).__init__(verbose_name, name, **kwargs)
+
+    def get_internal_type(self):
+        return "FileField"
+
+    def get_db_prep_lookup(self, lookup_type, value):
+        if hasattr(value, 'name'):
+            value = value.name
+        return super(FileField, self).get_db_prep_lookup(lookup_type, value)
+
+    def get_db_prep_value(self, value):
+        "Returns field's value prepared for saving into a database."
+        # Need to convert File objects provided via a form to unicode for database insertion
+        if value is None:
+            return None
+        return unicode(value)
+
+    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
+        field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
+        if not self.blank:
+            if rel:
+                # This validator makes sure FileFields work in a related context.
+                class RequiredFileField(object):
+                    def __init__(self, other_field_names, other_file_field_name):
+                        self.other_field_names = other_field_names
+                        self.other_file_field_name = other_file_field_name
+                        self.always_test = True
+                    def __call__(self, field_data, all_data):
+                        if not all_data.get(self.other_file_field_name, False):
+                            c = validators.RequiredIfOtherFieldsGiven(self.other_field_names, ugettext_lazy("This field is required."))
+                            c(field_data, all_data)
+                # First, get the core fields, if any.
+                core_field_names = []
+                for f in opts.fields:
+                    if f.core and f != self:
+                        core_field_names.extend(f.get_manipulator_field_names(name_prefix))
+                # Now, if there are any, add the validator to this FormField.
+                if core_field_names:
+                    field_list[0].validator_list.append(RequiredFileField(core_field_names, field_list[1].field_name))
+            else:
+                v = validators.RequiredIfOtherFieldNotGiven(field_list[1].field_name, ugettext_lazy("This field is required."))
+                v.always_test = True
+                field_list[0].validator_list.append(v)
+                field_list[0].is_required = field_list[1].is_required = False
+
+        # If the raw path is passed in, validate it's under the MEDIA_ROOT.
+        def isWithinMediaRoot(field_data, all_data):
+            f = os.path.abspath(os.path.join(settings.MEDIA_ROOT, field_data))
+            if not f.startswith(os.path.abspath(os.path.normpath(settings.MEDIA_ROOT))):
+                raise validators.ValidationError(_("Enter a valid filename."))
+        field_list[1].validator_list.append(isWithinMediaRoot)
+        return field_list
+
+    def contribute_to_class(self, cls, name):
+        super(FileField, self).contribute_to_class(cls, name)
+        setattr(cls, self.name, FileDescriptor(self))
+        setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
+        setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
+        setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
+        setattr(cls, 'save_%s_file' % self.name, lambda instance, name, content, save=True: instance._save_FIELD_file(self, name, content, save))
+        signals.post_delete.connect(self.delete_file, sender=cls)
+
+    def delete_file(self, instance, sender, **kwargs):
+        file = getattr(instance, self.attname)
+        # If no other object of this type references the file,
+        # and it's not the default value for future objects,
+        # delete it from the backend.
+        if file and file.name != self.default and \
+            not sender._default_manager.filter(**{self.name: file.name}):
+                file.delete(save=False)
+        elif file:
+            # Otherwise, just close the file, so it doesn't tie up resources.
+            file.close()
+
+    def get_manipulator_field_objs(self):
+        return [oldforms.FileUploadField, oldforms.HiddenField]
+
+    def get_manipulator_field_names(self, name_prefix):
+        return [name_prefix + self.name + '_file', name_prefix + self.name]
+
+    def save_file(self, new_data, new_object, original_object, change, rel, save=True):
+        upload_field_name = self.get_manipulator_field_names('')[0]
+        if new_data.get(upload_field_name, False):
+            if rel:
+                file = new_data[upload_field_name][0]
+            else:
+                file = new_data[upload_field_name]
+
+            # Backwards-compatible support for files-as-dictionaries.
+            # We don't need to raise a warning because the storage backend will
+            # do so for us.
+            try:
+                filename = file.name
+            except AttributeError:
+                filename = file['filename']
+            filename = self.get_filename(filename)
+
+            getattr(new_object, self.attname).save(filename, file, save)
+
+    def get_directory_name(self):
+        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
+
+    def get_filename(self, filename):
+        return os.path.normpath(self.storage.get_valid_name(os.path.basename(filename)))
+
+    def generate_filename(self, instance, filename):
+        return os.path.join(self.get_directory_name(), self.get_filename(filename))
+
+    def save_form_data(self, instance, data):
+        if data and isinstance(data, UploadedFile):
+            getattr(instance, self.name).save(data.name, data, save=False)
+
+    def formfield(self, **kwargs):
+        defaults = {'form_class': forms.FileField}
+        # If a file has been provided previously, then the form doesn't require
+        # that a new file is provided this time.
+        # The code to mark the form field as not required is used by
+        # form_for_instance, but can probably be removed once form_for_instance
+        # is gone. ModelForm uses a different method to check for an existing file.
+        if 'initial' in kwargs:
+            defaults['required'] = False
+        defaults.update(kwargs)
+        return super(FileField, self).formfield(**defaults)
+
+class ImageFieldFile(ImageFile, FieldFile):
+    def save(self, name, content, save=True):
+
+        if not hasattr(content, 'read'):
+            import warnings
+            warnings.warn(
+                message = "Representing files as strings is deprecated." \
+                          "Use django.core.files.base.ContentFile instead.",
+                category = DeprecationWarning,
+                stacklevel = 2
+            )
+            content = ContentFile(content)
+
+        # Repopulate the image dimension cache.
+        self._dimensions_cache = get_image_dimensions(content)
+        
+        # Update width/height fields, if needed
+        if self.field.width_field:
+            setattr(self.instance, self.field.width_field, self.width)
+        if self.field.height_field:
+            setattr(self.instance, self.field.height_field, self.height)
+        
+        super(ImageFieldFile, self).save(name, content, save)
+
+    def delete(self, save=True):
+        # Clear the image dimensions cache
+        if hasattr(self, '_dimensions_cache'):
+            del self._dimensions_cache
+        super(ImageFieldFile, self).delete(save)
+
+class ImageField(FileField):
+    attr_class = ImageFieldFile
+
+    def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
+        self.width_field, self.height_field = width_field, height_field
+        FileField.__init__(self, verbose_name, name, **kwargs)
+
+    def get_manipulator_field_objs(self):
+        return [oldforms.ImageUploadField, oldforms.HiddenField]
+
+    def contribute_to_class(self, cls, name):
+        super(ImageField, self).contribute_to_class(cls, name)
+        # Add get_BLAH_width and get_BLAH_height methods, but only if the
+        # image field doesn't have width and height cache fields.
+        if not self.width_field:
+            setattr(cls, 'get_%s_width' % self.name, curry(cls._get_FIELD_width, field=self))
+        if not self.height_field:
+            setattr(cls, 'get_%s_height' % self.name, curry(cls._get_FIELD_height, field=self))
+
+    def formfield(self, **kwargs):
+        defaults = {'form_class': forms.ImageField}
+        defaults.update(kwargs)
+        return super(ImageField, self).formfield(**defaults)
diff --git a/django/db/models/manipulators.py b/django/db/models/manipulators.py
index a3c917a486..c657d0158b 100644
--- a/django/db/models/manipulators.py
+++ b/django/db/models/manipulators.py
@@ -1,7 +1,8 @@
 from django.core.exceptions import ObjectDoesNotExist
 from django import oldforms
 from django.core import validators
-from django.db.models.fields import FileField, AutoField
+from django.db.models.fields import AutoField
+from django.db.models.fields.files import FileField
 from django.db.models import signals
 from django.utils.functional import curry
 from django.utils.datastructures import DotExpandedDict
diff --git a/django/utils/images.py b/django/utils/images.py
index 122c6ae233..c6cc37cf9a 100644
--- a/django/utils/images.py
+++ b/django/utils/images.py
@@ -1,22 +1,5 @@
-"""
-Utility functions for handling images.
+import warnings
 
-Requires PIL, as you might imagine.
-"""
+from django.core.files.images import get_image_dimensions
 
-import ImageFile
-
-def get_image_dimensions(path):
-    """Returns the (width, height) of an image at a given path."""
-    p = ImageFile.Parser()
-    fp = open(path, 'rb')
-    while 1:
-        data = fp.read(1024)
-        if not data:
-            break
-        p.feed(data)
-        if p.image:
-            return p.image.size
-            break
-    fp.close()
-    return None
+warnings.warn("django.utils.images has been moved to django.core.files.images.", DeprecationWarning)
diff --git a/docs/custom_model_fields.txt b/docs/custom_model_fields.txt
index 6b8f3c3ac6..a45e876fc6 100644
--- a/docs/custom_model_fields.txt
+++ b/docs/custom_model_fields.txt
@@ -596,3 +596,42 @@ smoothly:
        instance, not a ``HandField``). So if your ``__unicode__()`` method
        automatically converts to the string form of your Python object, you can
        save yourself a lot of work.
+
+Writing a ``FileField`` subclass
+=================================
+
+In addition to the above methods, fields that deal with files have a few other
+special requirements which must be taken into account. The majority of the
+mechanics provided by ``FileField``, such as controlling database storage and
+retrieval, can remain unchanged, leaving subclasses to deal with the challenge
+of supporting a particular type of file.
+
+Django provides a ``File`` class, which is used as a proxy to the file's
+contents and operations. This can be subclassed to customzie hwo the file is
+accessed, and what methods are available. It lives at
+``django.db.models.fields.files``, and its default behavior is explained in the
+`file documentation`_.
+
+Once a subclass of ``File`` is created, the new ``FileField`` subclass must be
+told to use it. To do so, simply assign the new ``File`` subclass to the special
+``attr_class`` attribute of the ``FileField`` subclass.
+
+.. _file documentation: ../files/
+
+A few suggestions
+------------------
+
+In addition to the above details, there are a few guidelines which can greatly
+improve the efficiency and readability of the field's code.
+
+    1. The source for Django's own ``ImageField`` (in
+       ``django/db/models/fields/files.py``) is a great example of how to
+       subclass ``FileField`` to support a particular type of file, as it
+       incorporates all of the techniques described above.
+
+    2. Cache file attributes wherever possible. Since files may be stored in
+       remote storage systems, retrieving them may cost extra time, or even
+       money, that isn't always necessary. Once a file is retrieved to obtain
+       some data about its content, cache as much of that data as possible to
+       reduce the number of times the file must be retrieved on subsequent
+       calls for that information.
diff --git a/docs/db-api.txt b/docs/db-api.txt
index 4f03a4810d..7e6406f334 100644
--- a/docs/db-api.txt
+++ b/docs/db-api.txt
@@ -2298,53 +2298,34 @@ For a full example, see the `lookup API sample model`_.
 get_FOO_filename()
 ------------------
 
-For every ``FileField``, the object will have a ``get_FOO_filename()`` method,
-where ``FOO`` is the name of the field. This returns the full filesystem path
-to the file, according to your ``MEDIA_ROOT`` setting.
-
-.. note::
-    It is only valid to call this method **after** saving the model when the
-    field has been set. Prior to saving, the value returned will not contain
-    the upload directory (the `upload_to` parameter) in the path.
-
-Note that ``ImageField`` is technically a subclass of ``FileField``, so every
-model with an ``ImageField`` will also get this method.
+**Deprecated in Django development version**; use ``object.FOO.name`` instead.
+See `managing files`_ for details.
 
 get_FOO_url()
 -------------
 
-For every ``FileField``, the object will have a ``get_FOO_url()`` method,
-where ``FOO`` is the name of the field. This returns the full URL to the file,
-according to your ``MEDIA_URL`` setting. If the value is blank, this method
-returns an empty string.
-
-.. note::
-    As with ``get_FOO_filename()``, it is only valid to call this method
-    **after** saving the model, otherwise an incorrect result will be
-    returned.
+**Deprecated in Django development version**; use ``object.FOO.url`` instead.
+See `managing files`_ for details.
 
 get_FOO_size()
 --------------
 
-For every ``FileField``, the object will have a ``get_FOO_size()`` method,
-where ``FOO`` is the name of the field. This returns the size of the file, in
-bytes. (Behind the scenes, it uses ``os.path.getsize``.)
+**Deprecated in Django development version**; use ``object.FOO.size`` instead.
+See `managing files`_ for details.
 
 save_FOO_file(filename, raw_contents)
 -------------------------------------
 
-For every ``FileField``, the object will have a ``save_FOO_file()`` method,
-where ``FOO`` is the name of the field. This saves the given file to the
-filesystem, using the given filename. If a file with the given filename already
-exists, Django adds an underscore to the end of the filename (but before the
-extension) until the filename is available.
+**Deprecated in Django development version**; use ``object.FOO.save()`` instead.
+See `managing files`_ for details.
 
 get_FOO_height() and get_FOO_width()
 ------------------------------------
 
-For every ``ImageField``, the object will have ``get_FOO_height()`` and
-``get_FOO_width()`` methods, where ``FOO`` is the name of the field. This
-returns the height (or width) of the image, as an integer, in pixels.
+**Deprecated in Django development version**; use ``object.FOO.width`` and
+``object.FOO.height`` instead. See `managing files`_ for details.
+
+.. _`managing files`: ../files/
 
 Shortcuts
 =========
diff --git a/docs/files.txt b/docs/files.txt
new file mode 100644
index 0000000000..9a8326806f
--- /dev/null
+++ b/docs/files.txt
@@ -0,0 +1,388 @@
+==============
+Managing files
+==============
+
+**New in Django development version**
+
+This document describes Django's file access APIs.
+
+By default, Django stores files locally, using the ``MEDIA_ROOT`` and
+``MEDIA_URL`` settings_. The examples below assume that you're using
+these defaults.
+
+However, Django provides ways to write custom `file storage systems`_ that
+allow you to completely customize where and how Django stores files. The
+second half of this document describes how these storage systems work.
+
+.. _file storage systems: `File storage`_
+.. _settings: ../settings/
+
+Using files in models
+=====================
+
+When you use a `FileField`_ or `ImageField`_, Django provides a set of APIs you can use to deal with that file.
+
+.. _filefield: ../model-api/#filefield
+.. _imagefield: ../model-api/#imagefield
+
+Consider the following model, using a ``FileField`` to store a photo::
+
+    class Car(models.Model):
+        name = models.CharField(max_length=255)
+        price = models.DecimalField(max_digits=5, decimal_places=2)
+        photo = models.ImageField(upload_to='cars')
+
+Any ``Car`` instance will have a ``photo`` attribute that you can use to get at
+the details of the attached photo::
+
+    >>> car = Car.object.get(name="57 Chevy")
+    >>> car.photo
+    <ImageFieldFile: chevy.jpg>
+    >>> car.photo.name
+    u'chevy.jpg'
+    >>> car.photo.path
+    u'/media/cars/chevy.jpg'
+    >>> car.photo.url
+    u'http://media.example.com/cars/chevy.jpg'
+    
+This object -- ``car.photo`` in the example -- is a ``File`` object, which means
+it has all the methods and attributes described below.
+
+The ``File`` object
+===================
+
+Internally, Django uses a ``django.core.files.File`` any time it needs to
+represent a file. This object is a thin wrapper around Python's `built-in file
+object`_ with some Django-specific additions.
+
+.. _built-in file object: http://docs.python.org/lib/bltin-file-objects.html
+
+Creating ``File`` instances
+---------------------------
+
+Most of the time you'll simply use a ``File`` that Django's given you (i.e. a
+file attached to an model as above, or perhaps an `uploaded file`_).
+
+.. _uploaded file: ../uploading_files/
+
+If you need to construct a ``File`` yourself, the easiest way is to create one
+using a Python built-in ``file`` object::
+
+    >>> from django.core.files import File
+
+    # Create a Python file object using open()
+    >>> f = open('/tmp/hello.world', 'w')
+    >>> myfile = File(f)
+    
+Now you can use any of the ``File`` attributes and methods defined below.
+    
+``File`` attributes and methods
+-------------------------------
+
+Django's ``File`` has the following attributes and methods:
+
+``File.path``
+~~~~~~~~~~~~~
+
+The absolute path to the file's location on a local filesystem. 
+
+Custom `file storage systems`_ may not store files locally; files stored on
+these systems will have a ``path`` of ``None``.
+
+``File.url``
+~~~~~~~~~~~~
+
+The URL where the file can be retrieved. This is often useful in templates_; for
+example, a bit of a template for displaying a ``Car`` (see above) might look
+like::
+
+    <img src='{{ car.photo.url }}' alt='{{ car.name }}' />
+
+.. _templates: ../templates/
+
+``File.size``
+~~~~~~~~~~~~~
+
+The size of the file in bytes.
+
+``File.open(mode=None)``
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Open or reopen the file (which by definition also does ``File.seek(0)``). The
+``mode`` argument allows the same values as Python's standard ``open()``.
+
+When reopening a file, ``mode`` will override whatever mode the file was
+originally opened with; ``None`` means to reopen with the original mode.
+
+``File.read(num_bytes=None)``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Read content from the file. The optional ``size`` is the number of bytes to
+read; if not specified, the file will be read to the end.
+
+``File.__iter__()``
+~~~~~~~~~~~~~~~~~~~
+
+Iterate over the file yielding one line at a time.
+
+``File.chunks(chunk_size=None)``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Iterate over the file yielding "chunks" of a given size. ``chunk_size`` defaults
+to 64 KB.
+
+This is especially useful with very large files since it allows them to be
+streamed off disk and avoids storing the whole file in memory.
+
+``File.multiple_chunks(chunk_size=None)``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Returns ``True`` if the file is large enough to require multiple chunks to
+access all of its content give some ``chunk_size``.
+
+``File.write(content)``
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Writes the specified content string to the file. Depending on the storage system
+behind the scenes, this content might not be fully committed until ``close()``
+is called on the file.
+
+``File.close()``
+~~~~~~~~~~~~~~~~
+
+Close the file.
+
+.. TODO: document the rest of the File methods.
+
+Additional ``ImageField`` attributes
+------------------------------------
+
+``File.width`` and ``File.height``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+These attributes provide the dimensions of the image. 
+
+Additional methods on files attached to objects
+-----------------------------------------------
+
+Any ``File`` that's associated with an object (as with ``Car.photo``, above)
+will also have a couple of extra methods:
+
+``File.save(name, content, save=True)``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Saves a new file with the file name and contents provided. This will not replace
+the existing file, but will create a new file and update the object to point to
+it. If ``save`` is ``True``, the model's ``save()`` method will be called once
+the file is saved. That is, these two lines::
+
+    >>> car.photo.save('myphoto.jpg', contents, save=False)
+    >>> car.save()
+
+are the same as this one line::
+
+    >>> car.photo.save('myphoto.jpg', contents, save=True)
+
+``File.delete(save=True)``
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Remove the file from the model instance and delete the underlying file. The
+``save`` argument works as above.
+
+File storage
+============
+
+Behind the scenes, Django delegates decisions about how and where to store files
+to a file storage system. This is the object that actually understands things
+like file systems, opening and reading files, etc.
+
+Django's default file storage is given by the `DEFAULT_FILE_STORAGE setting`_;
+if you don't explicitly provide a storage system, this is the one that will be
+used. 
+
+.. _default_file_storage setting: ../settings/#default-file-storage
+
+The built-in filesystem storage class
+-------------------------------------
+
+Django ships with a built-in ``FileSystemStorage`` class (defined in
+``django.core.files.storage``) which implements basic local filesystem file
+storage. Its initializer takes two arguments:
+
+======================  ===================================================
+Argument                Description
+======================  ===================================================
+``location``            Optional. Absolute path to the directory that will
+                        hold the files. If omitted, it will be set to the
+                        value of your ``MEDIA_ROOT`` setting.
+``base_url``            Optional. URL that serves the files stored at this
+                        location. If omitted, it will default to the value
+                        of your ``MEDIA_URL`` setting.
+======================  ===================================================
+
+For example, the following code will store uploaded files under
+``/media/photos`` regardless of what your ``MEDIA_ROOT`` setting is::
+
+    from django.db import models
+    from django.core.files.storage import FileSystemStorage
+
+    fs = FileSystemStorage(base_url='/media/photos')
+
+    class Car(models.Model):
+        ...
+        photo = models.ImageField(storage=fs)
+
+`Custom storage systems`_ work the same way: you can pass them in as the
+``storage`` argument to a ``FileField``.
+
+.. _custom storage systems: `writing a custom storage system`_
+
+Storage objects
+---------------
+
+Though most of the time you'll want to use a ``File`` object (which delegates to
+the proper storage for that file), you can use file storage systems directly.
+You can create an instance of some custom file storage class, or -- often more
+useful -- you can use the global default storage system::
+
+    >>> from django.core.files.storage import default_storage
+
+    >>> path = default_storage.save('/path/to/file', 'new content')
+    >>> path
+    u'/path/to/file'
+
+    >>> default_storage.filesize(path)
+    11
+    >>> default_storage.open(path).read()
+    'new content'
+
+    >>> default_storage.delete(path)
+    >>> default_storage.exists(path)
+    False
+
+Storage objects define the following methods:
+
+``Storage.exists(name)``
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+``True`` if a file exists given some ``name``.
+
+``Storge.path(name)``
+~~~~~~~~~~~~~~~~~~~~~
+
+The local filesystem path where the file can be opened using Python's standard
+``open()``. For storage systems that aren't accessible from the local
+filesystem, this will raise ``NotImplementedError`` instead.
+
+``Storage.size(name)``
+~~~~~~~~~~~~~~~~~~~~~~
+
+Returns the total size, in bytes, of the file referenced by ``name``.
+
+``Storage.url(name)``
+~~~~~~~~~~~~~~~~~~~~~
+
+Returns the URL where the contents of the file referenced by ``name`` can be
+accessed.
+
+``Storage.open(name, mode='rb')``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Opens the file given by ``name``. Note that although the returned file is
+guaranteed to be a ``File`` object, it might actually be some subclass. In the
+case of remote file storage this means that reading/writing could be quite slow,
+so be warned.
+
+``Storage.save(name, content)``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Saves a new file using the storage system, preferably with the name specified.
+If there already exists a file with this name ``name``, the storage system may
+modify the filename as necessary to get a unique name. The actual name of the
+stored file will be returned.
+
+``Storage.delete(name)``
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Deletes the file referenced by ``name``. This method won't raise an exception if
+the file doesn't exist.
+
+Writing a custom storage system
+===============================
+
+If you need to provide custom file storage -- a common example is storing files
+on some remote system -- you can do so by defining a custom storage class.
+You'll need to follow these steps:
+
+#. Your custom storage system must be a subclass of 
+   ``django.core.files.storage.Storage``::
+
+        from django.core.files.storage import Storage
+    
+        class MyStorage(Storage):
+            ...
+        
+#. Django must be able to instantiate your storage system without any arguments. 
+   This means that any settings should be taken from ``django.conf.settings``::
+   
+        from django.conf import settings
+        from django.core.files.storage import Storage
+
+        class MyStorage(Storage):
+            def __init__(self, option=None):
+                if not option:
+                    option = settings.CUSTOM_STORAGE_OPTIONS
+                ...
+
+#. Your storage class must implement the ``_open()`` and ``_save()`` methods,
+   along with any other methods appropriate to your storage class. See below for
+   more on these methods. 
+   
+   In addition, if your class provides local file storage, it must override
+   the ``path()`` method.
+   
+Custom storage system methods
+-----------------------------
+
+Your custom storage system may override any of the storage methods explained
+above in `storage objects`_. However, it's usually better to use the hooks
+specifically designed for custom storage objects. These are:
+
+``_open(name, mode='rb')``
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+**Required**.
+
+Called by ``Storage.open()``, this is the actual mechanism the storage class
+uses to open the file. This must return a ``File`` object, though in most cases,
+you'll want to return some subclass here that implements logic specific to the
+backend storage system.
+
+``_save(name, content)``
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Called by ``Storage.save()``. The ``name`` will already have gone through
+``get_valid_name()`` and ``get_available_name()``, and the ``content`` will be a
+``File`` object itself. No return value is expected.
+
+``get_valid_name(name)``
+------------------------
+
+Returns a filename suitable for use with the underlying storage system. The
+``name`` argument passed to this method is the original filename sent to the
+server, after having any path information removed. Override this to customize
+how non-standard characters are converted to safe filenames.
+
+The code provided on ``Storage`` retains only alpha-numeric characters, periods
+and underscores from the original filename, removing everything else.
+
+``get_available_name(name)``
+----------------------------
+
+Returns a filename that is available in the storage mechanism, possibly taking
+the provided filename into account. The ``name`` argument passed to this method
+will have already cleaned to a filename valid for the storage system, according
+to the ``get_valid_name()`` method described above.
+
+The code provided on ``Storage`` simply appends underscores to the filename
+until it finds one that's available in the destination directory.
\ No newline at end of file
diff --git a/docs/model-api.txt b/docs/model-api.txt
index 93b27b8c11..da5584e2bc 100644
--- a/docs/model-api.txt
+++ b/docs/model-api.txt
@@ -224,26 +224,64 @@ set to 75 by default, but you can specify it to override default behavior.
 ``FileField``
 ~~~~~~~~~~~~~
 
-A file-upload field. Has one **required** argument:
+A file-upload field. Has two special arguments, of which the first is
+**required**:
 
     ======================  ===================================================
     Argument                Description
     ======================  ===================================================
-    ``upload_to``           A local filesystem path that will be appended to
-                            your ``MEDIA_ROOT`` setting to determine the
-                            output of the ``get_<fieldname>_url()`` helper
-                            function.
+    ``upload_to``           Required. A filesystem-style path that will be
+                            prepended to the filename before being committed to
+                            the final storage destination.
+
+                            **New in Django development version**
+
+                            This may also be a callable, such as a function,
+                            which will be called to obtain the upload path,
+                            including the filename. See below for details.
+
+    ``storage``             **New in Django development version**
+
+                            Optional. A storage object, which handles the
+                            storage and retrieval of your files. See `managing
+                            files`_ for details on how to provide this object.
     ======================  ===================================================
 
-This path may contain `strftime formatting`_, which will be replaced by the
-date/time of the file upload (so that uploaded files don't fill up the given
-directory).
+.. _managing files: ../files/
+
+The ``upload_to`` path may contain `strftime formatting`_, which will be
+replaced by the date/time of the file upload (so that uploaded files don't fill
+up the given directory).
+
+**New in Django development version**
+
+If a callable is provided for the ``upload_to`` argument, that callable must be
+able to accept two arguments, and return a Unix-style path (with forward
+slashes) to be passed along to the storage system. The two arguments that will
+be passed are:
+
+    ======================  ===================================================
+    Argument                Description
+    ======================  ===================================================
+    ``instance``            An instance of the model where the ``FileField`` is
+                            defined. More specifically, this is the particular
+                            instance where the current file is being attached.
+
+                            **Note**: In most cases, this object will not have
+                            been saved to the database yet, so if it uses the
+                            default ``AutoField``, *it might not yet have a
+                            value for its primary key field*.
+
+    ``filename``            The filename that was originally given to the file.
+                            This may or may not be taken into account when
+                            determining the final destination path.
+    ======================  ===================================================
 
 The admin represents this field as an ``<input type="file">`` (a file-upload
 widget).
 
-Using a ``FileField`` or an ``ImageField`` (see below) in a model takes a few
-steps:
+Using a ``FileField`` or an ``ImageField`` (see below) in a model without a
+specified storage system takes a few steps:
 
     1. In your settings file, you'll need to define ``MEDIA_ROOT`` as the
        full path to a directory where you'd like Django to store uploaded
diff --git a/docs/settings.txt b/docs/settings.txt
index 1d1627d41e..3b3e908fab 100644
--- a/docs/settings.txt
+++ b/docs/settings.txt
@@ -426,6 +426,16 @@ Default content type to use for all ``HttpResponse`` objects, if a MIME type
 isn't manually specified. Used with ``DEFAULT_CHARSET`` to construct the
 ``Content-Type`` header.
 
+DEFAULT_FILE_STORAGE
+--------------------
+
+Default: ``'django.core.filestorage.filesystem.FileSystemStorage'``
+
+Default file storage class to be used for any file-related operations that don't
+specify a particular storage system. See the `file documentation`_ for details.
+
+.. _file documentation: ../files/
+
 DEFAULT_FROM_EMAIL
 ------------------
 
diff --git a/docs/upload_handling.txt b/docs/upload_handling.txt
index c0e8605686..488778a4e4 100644
--- a/docs/upload_handling.txt
+++ b/docs/upload_handling.txt
@@ -155,25 +155,8 @@ Three `settings`_ control Django's file upload behavior:
 ``UploadedFile`` objects
 ========================
 
-All ``UploadedFile`` objects define the following methods/attributes:
-
-    ``UploadedFile.read(self, num_bytes=None)``
-        Returns a byte string of length ``num_bytes``, or the complete file if
-        ``num_bytes`` is ``None``.
-
-    ``UploadedFile.chunks(self, chunk_size=None)``
-        A generator yielding small chunks from the file. If ``chunk_size`` isn't
-        given, chunks will be 64 KB.
-
-    ``UploadedFile.multiple_chunks(self, chunk_size=None)``
-        Returns ``True`` if you can expect more than one chunk when calling
-        ``UploadedFile.chunks(self, chunk_size)``.
-
-    ``UploadedFile.size``
-        The size, in bytes, of the uploaded file.
-
-    ``UploadedFile.name``
-        The name of the uploaded file as provided by the user.
+In addition to those inherited from `File`_, all ``UploadedFile`` objects define
+the following methods/attributes:
 
     ``UploadedFile.content_type``
         The content-type header uploaded with the file (e.g. ``text/plain`` or
@@ -186,13 +169,11 @@ All ``UploadedFile`` objects define the following methods/attributes:
         For ``text/*`` content-types, the character set (i.e. ``utf8``) supplied
         by the browser. Again, "trust but verify" is the best policy here.
 
-    ``UploadedFile.__iter__()``
-        Iterates over the lines in the file.
-
     ``UploadedFile.temporary_file_path()``
         Only files uploaded onto disk will have this method; it returns the full
         path to the temporary uploaded file.
 
+.. _File: ../files/
 
 Upload Handlers
 ===============
diff --git a/tests/modeltests/files/__init__.py b/tests/modeltests/files/__init__.py
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/modeltests/files/__init__.py
@@ -0,0 +1 @@
+
diff --git a/tests/modeltests/files/models.py b/tests/modeltests/files/models.py
new file mode 100644
index 0000000000..a2ee5a7256
--- /dev/null
+++ b/tests/modeltests/files/models.py
@@ -0,0 +1,118 @@
+"""
+42. Storing files according to a custom storage system
+
+FileField and its variations can take a "storage" argument to specify how and
+where files should be stored.
+"""
+
+import tempfile
+
+from django.db import models
+from django.core.files.base import ContentFile
+from django.core.files.storage import FileSystemStorage
+from django.core.cache import cache
+
+temp_storage = FileSystemStorage(location=tempfile.gettempdir())
+
+# Write out a file to be used as default content
+temp_storage.save('tests/default.txt', ContentFile('default content'))
+
+class Storage(models.Model):
+    def custom_upload_to(self, filename):
+        return 'foo'
+
+    def random_upload_to(self, filename):
+        # This returns a different result each time,
+        # to make sure it only gets called once.
+        import random
+        return '%s/%s' % (random.randint(100, 999), filename)
+
+    normal = models.FileField(storage=temp_storage, upload_to='tests')
+    custom = models.FileField(storage=temp_storage, upload_to=custom_upload_to)
+    random = models.FileField(storage=temp_storage, upload_to=random_upload_to)
+    default = models.FileField(storage=temp_storage, upload_to='tests', default='tests/default.txt')
+
+__test__ = {'API_TESTS':"""
+# An object without a file has limited functionality.
+
+>>> obj1 = Storage()
+>>> obj1.normal
+<FieldFile: None>
+>>> obj1.normal.size
+Traceback (most recent call last):
+...
+ValueError: The 'normal' attribute has no file associated with it.
+
+# Saving a file enables full functionality.
+
+>>> obj1.normal.save('django_test.txt', ContentFile('content'))
+>>> obj1.normal
+<FieldFile: tests/django_test.txt>
+>>> obj1.normal.size
+7
+>>> obj1.normal.read()
+'content'
+
+# Files can be read in a little at a time, if necessary.
+
+>>> obj1.normal.open()
+>>> obj1.normal.read(3)
+'con'
+>>> obj1.normal.read()
+'tent'
+>>> '-'.join(obj1.normal.chunks(chunk_size=2))
+'co-nt-en-t'
+
+# Save another file with the same name.
+
+>>> obj2 = Storage()
+>>> obj2.normal.save('django_test.txt', ContentFile('more content'))
+>>> obj2.normal
+<FieldFile: tests/django_test_.txt>
+>>> obj2.normal.size
+12
+
+# Push the objects into the cache to make sure they pickle properly
+
+>>> cache.set('obj1', obj1)
+>>> cache.set('obj2', obj2)
+>>> cache.get('obj2').normal
+<FieldFile: tests/django_test_.txt>
+
+# Deleting an object deletes the file it uses, if there are no other objects
+# still using that file.
+
+>>> obj2.delete()
+>>> obj2.normal.save('django_test.txt', ContentFile('more content'))
+>>> obj2.normal
+<FieldFile: tests/django_test_.txt>
+
+# Default values allow an object to access a single file.
+
+>>> obj3 = Storage.objects.create()
+>>> obj3.default
+<FieldFile: tests/default.txt>
+>>> obj3.default.read()
+'default content'
+
+# But it shouldn't be deleted, even if there are no more objects using it.
+
+>>> obj3.delete()
+>>> obj3 = Storage()
+>>> obj3.default.read()
+'default content'
+
+# Verify the fix for #5655, making sure the directory is only determined once.
+
+>>> obj4 = Storage()
+>>> obj4.random.save('random_file', ContentFile('random content'))
+>>> obj4.random
+<FieldFile: .../random_file>
+
+# Clean up the temporary files.
+
+>>> obj1.normal.delete()
+>>> obj2.normal.delete()
+>>> obj3.default.delete()
+>>> obj4.random.delete()
+"""}
diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
index be2a8ba835..3463cb7554 100644
--- a/tests/modeltests/model_forms/models.py
+++ b/tests/modeltests/model_forms/models.py
@@ -11,6 +11,9 @@ import os
 import tempfile
 
 from django.db import models
+from django.core.files.storage import FileSystemStorage
+
+temp_storage = FileSystemStorage(tempfile.gettempdir())
 
 ARTICLE_STATUS = (
     (1, 'Draft'),
@@ -60,7 +63,7 @@ class PhoneNumber(models.Model):
 
 class TextFile(models.Model):
     description = models.CharField(max_length=20)
-    file = models.FileField(upload_to=tempfile.gettempdir())
+    file = models.FileField(storage=temp_storage, upload_to='tests')
 
     def __unicode__(self):
         return self.description
@@ -73,9 +76,9 @@ class ImageFile(models.Model):
         # for PyPy, you need to check for the underlying modules
         # If PIL is not available, this test is equivalent to TextFile above.
         import Image, _imaging
-        image = models.ImageField(upload_to=tempfile.gettempdir())
+        image = models.ImageField(storage=temp_storage, upload_to='tests')
     except ImportError:
-        image = models.FileField(upload_to=tempfile.gettempdir())
+        image = models.FileField(storage=temp_storage, upload_to='tests')
 
     def __unicode__(self):
         return self.description
@@ -786,6 +789,8 @@ u'Assistance'
 
 # FileField ###################################################################
 
+# File forms.
+
 >>> class TextFileForm(ModelForm):
 ...     class Meta:
 ...         model = TextFile
@@ -808,9 +813,9 @@ True
 <class 'django.core.files.uploadedfile.SimpleUploadedFile'>
 >>> instance = f.save()
 >>> instance.file
-u'...test1.txt'
+<FieldFile: tests/test1.txt>
 
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
 
 >>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test1.txt', 'hello world')})
 >>> f.is_valid()
@@ -819,7 +824,7 @@ True
 <class 'django.core.files.uploadedfile.SimpleUploadedFile'>
 >>> instance = f.save()
 >>> instance.file
-u'...test1.txt'
+<FieldFile: tests/test1.txt>
 
 # Edit an instance that already has the file defined in the model. This will not
 # save the file again, but leave it exactly as it is.
@@ -828,13 +833,13 @@ u'...test1.txt'
 >>> f.is_valid()
 True
 >>> f.cleaned_data['file']
-u'...test1.txt'
+<FieldFile: tests/test1.txt>
 >>> instance = f.save()
 >>> instance.file
-u'...test1.txt'
+<FieldFile: tests/test1.txt>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
 
 # Override the file by uploading a new one.
 
@@ -843,20 +848,20 @@ u'...test1.txt'
 True
 >>> instance = f.save()
 >>> instance.file
-u'...test2.txt'
+<FieldFile: tests/test2.txt>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
 
 >>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test2.txt', 'hello world')})
 >>> f.is_valid()
 True
 >>> instance = f.save()
 >>> instance.file
-u'...test2.txt'
+<FieldFile: tests/test2.txt>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
 
 >>> instance.delete()
 
@@ -868,17 +873,17 @@ u'...test2.txt'
 True
 >>> instance = f.save()
 >>> instance.file
-''
+<FieldFile: None>
 
 >>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance)
 >>> f.is_valid()
 True
 >>> instance = f.save()
 >>> instance.file
-u'...test3.txt'
+<FieldFile: tests/test3.txt>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
 >>> instance.delete()
 
 >>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')})
@@ -886,10 +891,10 @@ u'...test3.txt'
 True
 >>> instance = f.save()
 >>> instance.file
-u'...test3.txt'
+<FieldFile: tests/test3.txt>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
 >>> instance.delete()
 
 # ImageField ###################################################################
@@ -911,10 +916,10 @@ True
 <class 'django.core.files.uploadedfile.SimpleUploadedFile'>
 >>> instance = f.save()
 >>> instance.image
-u'...test.png'
+<ImageFieldFile: tests/test.png>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_image_filename())
+>>> instance.image.delete()
 
 >>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)})
 >>> f.is_valid()
@@ -923,7 +928,7 @@ True
 <class 'django.core.files.uploadedfile.SimpleUploadedFile'>
 >>> instance = f.save()
 >>> instance.image
-u'...test.png'
+<ImageFieldFile: tests/test.png>
 
 # Edit an instance that already has the image defined in the model. This will not
 # save the image again, but leave it exactly as it is.
@@ -932,14 +937,14 @@ u'...test.png'
 >>> f.is_valid()
 True
 >>> f.cleaned_data['image']
-u'...test.png'
+<ImageFieldFile: tests/test.png>
 >>> instance = f.save()
 >>> instance.image
-u'...test.png'
+<ImageFieldFile: tests/test.png>
 
 # Delete the current image since this is not done by Django.
 
->>> os.unlink(instance.get_image_filename())
+>>> instance.image.delete()
 
 # Override the file by uploading a new one.
 
@@ -948,10 +953,10 @@ u'...test.png'
 True
 >>> instance = f.save()
 >>> instance.image
-u'...test2.png'
+<ImageFieldFile: tests/test2.png>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_image_filename())
+>>> instance.image.delete()
 >>> instance.delete()
 
 >>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data)})
@@ -959,10 +964,10 @@ u'...test2.png'
 True
 >>> instance = f.save()
 >>> instance.image
-u'...test2.png'
+<ImageFieldFile: tests/test2.png>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_image_filename())
+>>> instance.image.delete()
 >>> instance.delete()
 
 # Test the non-required ImageField
@@ -973,17 +978,17 @@ u'...test2.png'
 True
 >>> instance = f.save()
 >>> instance.image
-''
+<ImageFieldFile: None>
 
 >>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)}, instance=instance)
 >>> f.is_valid()
 True
 >>> instance = f.save()
 >>> instance.image
-u'...test3.png'
+<ImageFieldFile: tests/test3.png>
 
 # Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_image_filename())
+>>> instance.image.delete()
 >>> instance.delete()
 
 >>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)})
@@ -991,7 +996,7 @@ u'...test3.png'
 True
 >>> instance = f.save()
 >>> instance.image
-u'...test3.png'
+<ImageFieldFile: tests/test3.png>
 >>> instance.delete()
 
 # Media on a ModelForm ########################################################
diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
index 584d973c83..544806f819 100644
--- a/tests/regressiontests/admin_widgets/models.py
+++ b/tests/regressiontests/admin_widgets/models.py
@@ -1,6 +1,7 @@
 
 from django.conf import settings
 from django.db import models
+from django.core.files.storage import default_storage
 
 class Member(models.Model):
     name = models.CharField(max_length=100)
@@ -18,6 +19,7 @@ class Band(models.Model):
 class Album(models.Model):
     band = models.ForeignKey(Band)
     name = models.CharField(max_length=100)
+    cover_art = models.ImageField(upload_to='albums')
     
     def __unicode__(self):
         return self.name
@@ -46,12 +48,12 @@ HTML escaped.
 >>> print conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30)))
 <p class="datetime">Date: <input value="2007-12-01" type="text" class="vDateField" name="test_0" size="10" /><br />Time: <input value="09:30:00" type="text" class="vTimeField" name="test_1" size="8" /></p>
 
->>> w = AdminFileWidget()
->>> print conditional_escape(w.render('test', 'test'))
-Currently: <a target="_blank" href="%(MEDIA_URL)stest">test</a> <br />Change: <input type="file" name="test" />
-
 >>> band = Band.objects.create(pk=1, name='Linkin Park')
->>> album = band.album_set.create(name='Hybrid Theory')
+>>> album = band.album_set.create(name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg')
+
+>>> w = AdminFileWidget()
+>>> print conditional_escape(w.render('test', album.cover_art))
+Currently: <a target="_blank" href="%(STORAGE_URL)salbums/hybrid_theory.jpg">albums\hybrid_theory.jpg</a> <br />Change: <input type="file" name="test" />
 
 >>> rel = Album._meta.get_field('band').rel
 >>> w = ForeignKeyRawIdWidget(rel)
@@ -81,5 +83,5 @@ True
 
 """ % {
     'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
-    'MEDIA_URL': settings.MEDIA_URL,
+    'STORAGE_URL': default_storage.url(''),
 }}
diff --git a/tests/regressiontests/bug639/models.py b/tests/regressiontests/bug639/models.py
index fc241aba8c..22dd440fc3 100644
--- a/tests/regressiontests/bug639/models.py
+++ b/tests/regressiontests/bug639/models.py
@@ -1,16 +1,20 @@
 import tempfile
+
 from django.db import models
+from django.core.files.storage import FileSystemStorage
+
+temp_storage = FileSystemStorage(tempfile.gettempdir())
 
 class Photo(models.Model):
     title = models.CharField(max_length=30)
-    image = models.FileField(upload_to=tempfile.gettempdir())
+    image = models.FileField(storage=temp_storage, upload_to='tests')
     
     # Support code for the tests; this keeps track of how many times save() gets
     # called on each instance.
     def __init__(self, *args, **kwargs):
-       super(Photo, self).__init__(*args, **kwargs)
-       self._savecount = 0
+        super(Photo, self).__init__(*args, **kwargs)
+        self._savecount = 0
     
     def save(self):
         super(Photo, self).save()
-        self._savecount +=1
\ No newline at end of file
+        self._savecount += 1
diff --git a/tests/regressiontests/bug639/tests.py b/tests/regressiontests/bug639/tests.py
index 2726dec897..69e4a3ba3b 100644
--- a/tests/regressiontests/bug639/tests.py
+++ b/tests/regressiontests/bug639/tests.py
@@ -36,4 +36,4 @@ class Bug639Test(unittest.TestCase):
         Make sure to delete the "uploaded" file to avoid clogging /tmp.
         """
         p = Photo.objects.get()
-        os.unlink(p.get_image_filename())
+        p.image.delete(save=False)
diff --git a/tests/regressiontests/file_storage/__init__.py b/tests/regressiontests/file_storage/__init__.py
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/regressiontests/file_storage/__init__.py
@@ -0,0 +1 @@
+
diff --git a/tests/regressiontests/file_storage/models.py b/tests/regressiontests/file_storage/models.py
new file mode 100644
index 0000000000..8cd8b9e56c
--- /dev/null
+++ b/tests/regressiontests/file_storage/models.py
@@ -0,0 +1,44 @@
+import os
+import tempfile
+from django.db import models
+from django.core.files.storage import FileSystemStorage
+from django.core.files.base import ContentFile
+
+temp_storage = FileSystemStorage(tempfile.gettempdir())
+
+# Test for correct behavior of width_field/height_field.
+# Of course, we can't run this without PIL.
+
+try:
+    # Checking for the existence of Image is enough for CPython, but
+    # for PyPy, you need to check for the underlying modules
+    import Image, _imaging
+except ImportError:
+    Image = None
+
+# If we have PIL, do these tests
+if Image:
+    class Person(models.Model):
+        name = models.CharField(max_length=50)
+        mugshot = models.ImageField(storage=temp_storage, upload_to='tests', 
+                                    height_field='mug_height', 
+                                    width_field='mug_width')
+        mug_height = models.PositiveSmallIntegerField()
+        mug_width = models.PositiveSmallIntegerField()
+        
+    __test__ = {'API_TESTS': """
+
+>>> image_data = open(os.path.join(os.path.dirname(__file__), "test.png"), 'rb').read()
+>>> p = Person(name="Joe")
+>>> p.mugshot.save("mug", ContentFile(image_data))
+>>> p.mugshot.width
+16
+>>> p.mugshot.height
+16
+>>> p.mug_height
+16
+>>> p.mug_width
+16
+
+"""}
+    
\ No newline at end of file
diff --git a/tests/regressiontests/file_storage/test.png b/tests/regressiontests/file_storage/test.png
new file mode 100644
index 0000000000..4f17cd075d
Binary files /dev/null and b/tests/regressiontests/file_storage/test.png differ
diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py
new file mode 100644
index 0000000000..a4503d9805
--- /dev/null
+++ b/tests/regressiontests/file_storage/tests.py
@@ -0,0 +1,66 @@
+"""
+Tests for the file storage mechanism
+
+>>> import tempfile
+>>> from django.core.files.storage import FileSystemStorage
+>>> from django.core.files.base import ContentFile
+
+>>> temp_storage = FileSystemStorage(location=tempfile.gettempdir())
+
+# Standard file access options are available, and work as expected.
+
+>>> temp_storage.exists('storage_test')
+False
+>>> file = temp_storage.open('storage_test', 'w')
+>>> file.write('storage contents')
+>>> file.close()
+
+>>> temp_storage.exists('storage_test')
+True
+>>> file = temp_storage.open('storage_test', 'r')
+>>> file.read()
+'storage contents'
+>>> file.close()
+
+>>> temp_storage.delete('storage_test')
+>>> temp_storage.exists('storage_test')
+False
+
+# Files can only be accessed if they're below the specified location.
+
+>>> temp_storage.exists('..')
+Traceback (most recent call last):
+...
+SuspiciousOperation: Attempted access to '..' denied.
+>>> temp_storage.open('/etc/passwd')
+Traceback (most recent call last):
+  ...
+SuspiciousOperation: Attempted access to '/etc/passwd' denied.
+
+# Custom storage systems can be created to customize behavior
+
+>>> class CustomStorage(FileSystemStorage):
+...     def get_available_name(self, name):
+...         # Append numbers to duplicate files rather than underscores, like Trac
+...
+...         parts = name.split('.')
+...         basename, ext = parts[0], parts[1:]
+...         number = 2
+...
+...         while self.exists(name):
+...             name = '.'.join([basename, str(number)] + ext)
+...             number += 1
+...
+...         return name
+>>> custom_storage = CustomStorage(tempfile.gettempdir())
+
+>>> first = custom_storage.save('custom_storage', ContentFile('custom contents'))
+>>> first
+u'custom_storage'
+>>> second = custom_storage.save('custom_storage', ContentFile('more contents'))
+>>> second
+u'custom_storage.2'
+
+>>> custom_storage.delete(first)
+>>> custom_storage.delete(second)
+"""
diff --git a/tests/regressiontests/file_uploads/models.py b/tests/regressiontests/file_uploads/models.py
index 3701750afe..9d020509af 100644
--- a/tests/regressiontests/file_uploads/models.py
+++ b/tests/regressiontests/file_uploads/models.py
@@ -1,9 +1,10 @@
 import tempfile
 import os
 from django.db import models
+from django.core.files.storage import FileSystemStorage
 
-UPLOAD_ROOT = tempfile.mkdtemp()
-UPLOAD_TO = os.path.join(UPLOAD_ROOT, 'test_upload')
+temp_storage = FileSystemStorage(tempfile.mkdtemp())
+UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload')
 
 class FileModel(models.Model):
-    testfile = models.FileField(upload_to=UPLOAD_TO)
+    testfile = models.FileField(storage=temp_storage, upload_to='test_upload')
diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py
index dd6b7c4181..7c8b53ea89 100644
--- a/tests/regressiontests/file_uploads/tests.py
+++ b/tests/regressiontests/file_uploads/tests.py
@@ -9,7 +9,7 @@ from django.test import TestCase, client
 from django.utils import simplejson
 from django.utils.hashcompat import sha_constructor
 
-from models import FileModel, UPLOAD_ROOT, UPLOAD_TO
+from models import FileModel, temp_storage, UPLOAD_TO
 
 class FileUploadTests(TestCase):
     def test_simple_upload(self):
@@ -194,22 +194,22 @@ class DirectoryCreationTests(unittest.TestCase):
     """
     def setUp(self):
         self.obj = FileModel()
-        if not os.path.isdir(UPLOAD_ROOT):
-            os.makedirs(UPLOAD_ROOT)
+        if not os.path.isdir(temp_storage.location):
+            os.makedirs(temp_storage.location)
 
     def tearDown(self):
-        os.chmod(UPLOAD_ROOT, 0700)
-        shutil.rmtree(UPLOAD_ROOT)
+        os.chmod(temp_storage.location, 0700)
+        shutil.rmtree(temp_storage.location)
 
     def test_readonly_root(self):
         """Permission errors are not swallowed"""
-        os.chmod(UPLOAD_ROOT, 0500)
+        os.chmod(temp_storage.location, 0500)
         try:
-            self.obj.save_testfile_file('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
+            self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
         except OSError, err:
             self.assertEquals(err.errno, errno.EACCES)
-        except:
-            self.fail("OSError [Errno %s] not raised" % errno.EACCES)
+        except Exception, err:
+            self.fail("OSError [Errno %s] not raised." % errno.EACCES)
 
     def test_not_a_directory(self):
         """The correct IOError is raised when the upload directory name exists but isn't a directory"""
@@ -217,11 +217,11 @@ class DirectoryCreationTests(unittest.TestCase):
         fd = open(UPLOAD_TO, 'w')
         fd.close()
         try:
-            self.obj.save_testfile_file('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
+            self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
         except IOError, err:
             # The test needs to be done on a specific string as IOError
             # is raised even without the patch (just not early enough)
             self.assertEquals(err.args[0],
-                              "%s exists and is not a directory" % UPLOAD_TO)
+                              "%s exists and is not a directory." % UPLOAD_TO)
         except:
             self.fail("IOError not raised")
diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py
index 7d3f9d3b1d..4e2fbb1ee2 100644
--- a/tests/regressiontests/serializers_regress/models.py
+++ b/tests/regressiontests/serializers_regress/models.py
@@ -157,8 +157,8 @@ class DecimalPKData(models.Model):
 class EmailPKData(models.Model):
     data = models.EmailField(primary_key=True)
 
-class FilePKData(models.Model):
-    data = models.FileField(primary_key=True, upload_to='/foo/bar')
+# class FilePKData(models.Model):
+#    data = models.FileField(primary_key=True, upload_to='/foo/bar')
 
 class FilePathPKData(models.Model):
     data = models.FilePathField(primary_key=True)
diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py
index f990d57a17..7a38af4cf9 100644
--- a/tests/regressiontests/serializers_regress/tests.py
+++ b/tests/regressiontests/serializers_regress/tests.py
@@ -144,7 +144,7 @@ test_data = [
     (data_obj, 41, EmailData, None),
     (data_obj, 42, EmailData, ""),
     (data_obj, 50, FileData, 'file:///foo/bar/whiz.txt'),
-    (data_obj, 51, FileData, None),
+#     (data_obj, 51, FileData, None),
     (data_obj, 52, FileData, ""),
     (data_obj, 60, FilePathData, "/foo/bar/whiz.txt"),
     (data_obj, 61, FilePathData, None),
@@ -242,7 +242,7 @@ The end."""),
 #     (pk_obj, 620, DatePKData, datetime.date(2006,6,16)),
 #     (pk_obj, 630, DateTimePKData, datetime.datetime(2006,6,16,10,42,37)),
     (pk_obj, 640, EmailPKData, "hovercraft@example.com"),
-    (pk_obj, 650, FilePKData, 'file:///foo/bar/whiz.txt'),
+#     (pk_obj, 650, FilePKData, 'file:///foo/bar/whiz.txt'),
     (pk_obj, 660, FilePathPKData, "/foo/bar/whiz.txt"),
     (pk_obj, 670, DecimalPKData, decimal.Decimal('12.345')),
     (pk_obj, 671, DecimalPKData, decimal.Decimal('-12.345')),