Code

Ticket #5361: filestorage.2.diff

File filestorage.2.diff, 37.3 KB (added by Marty Alchin <gulopine@…>, 7 years ago)

Fixed a tyop and added missing documentation in the last patch

Line 
1Index: django/core/filestorage/__init__.py
2===================================================================
3--- django/core/filestorage/__init__.py (revision 0)
4+++ django/core/filestorage/__init__.py (revision 0)
5@@ -0,0 +1,38 @@
6+from StringIO import StringIO
7+
8+class Backend(object):
9+    def get_available_filename(self, filename):
10+        # If the filename already exists, keep adding an underscore to the name
11+        # of the file until the filename doesn't exist.
12+        while self.file_exists(filename):
13+            try:
14+                dot_index = filename.rindex('.')
15+            except ValueError: # filename has no dot
16+                filename += '_'
17+            else:
18+                filename = filename[:dot_index] + '_' + filename[dot_index:]
19+        return filename
20+
21+    def get_filename(self, filename):
22+        return filename
23+
24+class RemoteFile(StringIO):
25+    """Sends files to a remote backend automatically, when necessary."""
26+
27+    def __init__(self, data, mode, writer):
28+        self._mode = mode
29+        self._write_to_backend = writer
30+        self._is_dirty = False
31+        StringIO.__init__(self, data)
32+
33+    def write(self, data):
34+        if 'w' not in self._mode:
35+            raise AttributeError, "File was opened for read-only access."
36+        StringIO.write(self, data)
37+        self._is_dirty = True
38+
39+    def close(self):
40+        if self._is_dirty:
41+            self._write_to_backend(self.getvalue())
42+        StringIO.close(self)
43+
44Index: django/core/filestorage/filesystem.py
45===================================================================
46--- django/core/filestorage/filesystem.py       (revision 0)
47+++ django/core/filestorage/filesystem.py       (revision 0)
48@@ -0,0 +1,76 @@
49+import datetime
50+import os
51+
52+from django.conf import settings
53+from django.utils.encoding import force_unicode, smart_str
54+
55+from django.core.filestorage import Backend
56+
57+class FileSystemBackend(Backend):
58+    """Standard filesystem storage"""
59+
60+    def __init__(self, location='', media_root=None, media_url=None):
61+        self.location = location
62+        if media_root != None and media_url != None:
63+            # Both were provided, so use them
64+            pass
65+        elif media_root is None and media_url is None:
66+            # Neither were provided, so use global settings
67+            from django.conf import settings
68+            try:
69+                media_root = settings.MEDIA_ROOT
70+                media_url = settings.MEDIA_URL
71+            except AttributeError:
72+                raise ImproperlyConfigured, "Media settings not defined."
73+        else:
74+            # One or the other were provided, but not both
75+            raise ImproperlyConfigured, "Both media_root and media_url must be provided."
76+        self.media_root = media_root
77+        self.media_url = media_url
78+
79+    def _get_directory_name(self):
80+        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.location))))
81+
82+    def _get_absolute_path(self, filename):
83+        return os.path.normpath(os.path.join(self.media_root, filename))
84+
85+    # The following methods define the Backend API
86+
87+    def get_available_filename(self, filename):
88+        from django.utils.text import get_valid_filename
89+        f = os.path.join(self._get_directory_name(), get_valid_filename(os.path.basename(filename)))
90+        return Backend.get_available_filename(self, os.path.normpath(f))
91+
92+    def get_filesize(self, filename):
93+        return os.path.getsize(self._get_absolute_path(filename))
94+
95+    def get_absolute_url(self, filename):
96+        import urlparse
97+        return urlparse.urljoin(self.media_url, filename).replace('\\', '/')
98+
99+    def open(self, filename, mode='rb'):
100+        return open(self._get_absolute_path(filename), mode)
101+
102+    def file_exists(self, filename):
103+        return os.path.exists(self._get_absolute_path(filename))
104+
105+    def save_file(self, filename, raw_contents):
106+        directory = self._get_directory_name()
107+        try: # Create the date-based directory if it doesn't exist.
108+            os.makedirs(os.path.join(self.media_root, directory))
109+        except OSError: # Directory probably already exists.
110+            pass
111+        filename = self.get_available_filename(filename)
112+
113+        # Write the file to disk.
114+        fp = open(self._get_absolute_path(filename), 'wb')
115+        fp.write(raw_contents)
116+        fp.close()
117+
118+        return filename
119+
120+    def delete_file(self, filename):
121+        file_name = self._get_absolute_path(filename)
122+        # If the file exists, delete it from the filesystem.
123+        if os.path.exists(file_name):
124+            os.remove(file_name)
125Index: django/core/filestorage/__init__.py
126===================================================================
127--- django/core/filestorage/__init__.py (revision 0)
128+++ django/core/filestorage/__init__.py (revision 0)
129@@ -0,0 +1,38 @@
130+from StringIO import StringIO
131+
132+class Backend(object):
133+    def get_available_filename(self, filename):
134+        # If the filename already exists, keep adding an underscore to the name
135+        # of the file until the filename doesn't exist.
136+        while self.file_exists(filename):
137+            try:
138+                dot_index = filename.rindex('.')
139+            except ValueError: # filename has no dot
140+                filename += '_'
141+            else:
142+                filename = filename[:dot_index] + '_' + filename[dot_index:]
143+        return filename
144+
145+    def get_filename(self, filename):
146+        return filename
147+
148+class RemoteFile(StringIO):
149+    """Sends files to a remote backend automatically, when necessary."""
150+
151+    def __init__(self, data, mode, writer):
152+        self._mode = mode
153+        self._write_to_backend = writer
154+        self._is_dirty = False
155+        StringIO.__init__(self, data)
156+
157+    def write(self, data):
158+        if 'w' not in self._mode:
159+            raise AttributeError, "File was opened for read-only access."
160+        StringIO.write(self, data)
161+        self._is_dirty = True
162+
163+    def close(self):
164+        if self._is_dirty:
165+            self._write_to_backend(self.getvalue())
166+        StringIO.close(self)
167+
168Index: django/core/filestorage/filesystem.py
169===================================================================
170--- django/core/filestorage/filesystem.py       (revision 0)
171+++ django/core/filestorage/filesystem.py       (revision 0)
172@@ -0,0 +1,76 @@
173+import datetime
174+import os
175+
176+from django.conf import settings
177+from django.utils.encoding import force_unicode, smart_str
178+
179+from django.core.filestorage import Backend
180+
181+class FileSystemBackend(Backend):
182+    """Standard filesystem storage"""
183+
184+    def __init__(self, location='', media_root=None, media_url=None):
185+        self.location = location
186+        if media_root != None and media_url != None:
187+            # Both were provided, so use them
188+            pass
189+        elif media_root is None and media_url is None:
190+            # Neither were provided, so use global settings
191+            from django.conf import settings
192+            try:
193+                media_root = settings.MEDIA_ROOT
194+                media_url = settings.MEDIA_URL
195+            except AttributeError:
196+                raise ImproperlyConfigured, "Media settings not defined."
197+        else:
198+            # One or the other were provided, but not both
199+            raise ImproperlyConfigured, "Both media_root and media_url must be provided."
200+        self.media_root = media_root
201+        self.media_url = media_url
202+
203+    def _get_directory_name(self):
204+        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.location))))
205+
206+    def _get_absolute_path(self, filename):
207+        return os.path.normpath(os.path.join(self.media_root, filename))
208+
209+    # The following methods define the Backend API
210+
211+    def get_available_filename(self, filename):
212+        from django.utils.text import get_valid_filename
213+        f = os.path.join(self._get_directory_name(), get_valid_filename(os.path.basename(filename)))
214+        return Backend.get_available_filename(self, os.path.normpath(f))
215+
216+    def get_filesize(self, filename):
217+        return os.path.getsize(self._get_absolute_path(filename))
218+
219+    def get_absolute_url(self, filename):
220+        import urlparse
221+        return urlparse.urljoin(self.media_url, filename).replace('\\', '/')
222+
223+    def open(self, filename, mode='rb'):
224+        return open(self._get_absolute_path(filename), mode)
225+
226+    def file_exists(self, filename):
227+        return os.path.exists(self._get_absolute_path(filename))
228+
229+    def save_file(self, filename, raw_contents):
230+        directory = self._get_directory_name()
231+        try: # Create the date-based directory if it doesn't exist.
232+            os.makedirs(os.path.join(self.media_root, directory))
233+        except OSError: # Directory probably already exists.
234+            pass
235+        filename = self.get_available_filename(filename)
236+
237+        # Write the file to disk.
238+        fp = open(self._get_absolute_path(filename), 'wb')
239+        fp.write(raw_contents)
240+        fp.close()
241+
242+        return filename
243+
244+    def delete_file(self, filename):
245+        file_name = self._get_absolute_path(filename)
246+        # If the file exists, delete it from the filesystem.
247+        if os.path.exists(file_name):
248+            os.remove(file_name)
249Index: django/db/models/base.py
250===================================================================
251--- django/db/models/base.py    (revision 6064)
252+++ django/db/models/base.py    (working copy)
253@@ -18,6 +18,7 @@
254 import types
255 import sys
256 import os
257+from warnings import warn
258 
259 class ModelBase(type):
260     "Metaclass for all models"
261@@ -357,74 +358,29 @@
262         return getattr(self, cachename)
263 
264     def _get_FIELD_filename(self, field):
265-        if getattr(self, field.attname): # value is not blank
266-            return os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname))
267-        return ''
268+        warn("Use instance.%s.open() if you need access to the file." % field.attname, DeprecationWarning)
269+        return field.backend._get_absolute_path(self.__dict__[field.attname])
270 
271     def _get_FIELD_url(self, field):
272-        if getattr(self, field.attname): # value is not blank
273-            import urlparse
274-            return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/')
275-        return ''
276+        warn("Use instance.%s.get_absolute_url()." % field.attname, DeprecationWarning)
277+        return getattr(self, field.attname).get_absolute_url()
278 
279     def _get_FIELD_size(self, field):
280-        return os.path.getsize(self._get_FIELD_filename(field))
281+        warn("Use instance.%s.get_filesize()." % field.attname, DeprecationWarning)
282+        return getattr(self, field.attname).get_filesize()
283 
284     def _save_FIELD_file(self, field, filename, raw_contents, save=True):
285-        directory = field.get_directory_name()
286-        try: # Create the date-based directory if it doesn't exist.
287-            os.makedirs(os.path.join(settings.MEDIA_ROOT, directory))
288-        except OSError: # Directory probably already exists.
289-            pass
290-        filename = field.get_filename(filename)
291+        warn("Use instance.%s.save_file()." % field.attname, DeprecationWarning)
292+        return getattr(self, field.attname).save_file(filename, raw_contents, save)
293 
294-        # If the filename already exists, keep adding an underscore to the name of
295-        # the file until the filename doesn't exist.
296-        while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)):
297-            try:
298-                dot_index = filename.rindex('.')
299-            except ValueError: # filename has no dot
300-                filename += '_'
301-            else:
302-                filename = filename[:dot_index] + '_' + filename[dot_index:]
303-
304-        # Write the file to disk.
305-        setattr(self, field.attname, filename)
306-
307-        full_filename = self._get_FIELD_filename(field)
308-        fp = open(full_filename, 'wb')
309-        fp.write(raw_contents)
310-        fp.close()
311-
312-        # Save the width and/or height, if applicable.
313-        if isinstance(field, ImageField) and (field.width_field or field.height_field):
314-            from django.utils.images import get_image_dimensions
315-            width, height = get_image_dimensions(full_filename)
316-            if field.width_field:
317-                setattr(self, field.width_field, width)
318-            if field.height_field:
319-                setattr(self, field.height_field, height)
320-
321-        # Save the object because it has changed unless save is False
322-        if save:
323-            self.save()
324-
325-    _save_FIELD_file.alters_data = True
326-
327     def _get_FIELD_width(self, field):
328-        return self._get_image_dimensions(field)[0]
329+        warn("Use instance.%s.get_width()." % field.attname, DeprecationWarning)
330+        return getattr(self, field.attname).get_width()
331 
332     def _get_FIELD_height(self, field):
333-        return self._get_image_dimensions(field)[1]
334+        warn("Use instance.%s.get_height()." % field.attname, DeprecationWarning)
335+        return getattr(self, field.attname).get_height()
336 
337-    def _get_image_dimensions(self, field):
338-        cachename = "__%s_dimensions_cache" % field.name
339-        if not hasattr(self, cachename):
340-            from django.utils.images import get_image_dimensions
341-            filename = self._get_FIELD_filename(field)
342-            setattr(self, cachename, get_image_dimensions(filename))
343-        return getattr(self, cachename)
344-
345 ############################################
346 # HELPER FUNCTIONS (CURRIED MODEL METHODS) #
347 ############################################
348Index: django/db/models/fields/__init__.py
349===================================================================
350--- django/db/models/fields/__init__.py (revision 6064)
351+++ django/db/models/fields/__init__.py (working copy)
352@@ -694,9 +694,62 @@
353         defaults.update(kwargs)
354         return super(EmailField, self).formfield(**defaults)
355 
356+class File(object):
357+    def __init__(self, obj, field, filename):
358+        self.obj = obj
359+        self.field = field
360+        self.backend = field.backend
361+        self.filename = filename
362+
363+    def __str__(self):
364+        return self.backend.get_filename(self.filename)
365+
366+    def get_absolute_url(self):
367+        return self.backend.get_absolute_url(self.filename)
368+
369+    def get_filesize(self):
370+        if not hasattr(self, '_filesize'):
371+            self._filesize = self.backend.get_filesize(self.filename)
372+        return self._filesize
373+
374+    def open(self, mode='rb'):
375+        return self.backend.open(self.filename, mode)
376+
377+    def save_file(self, filename, raw_contents, save=True):
378+        self.filename = self.backend.save_file(filename, raw_contents)
379+
380+        # Update the filesize cache
381+        self._filesize = len(raw_contents)
382+
383+        # Save the object because it has changed, unless save is False
384+        if save:
385+            self.obj.save()
386+
387+class FileProxy(object):
388+    def __init__(self, field):
389+        self.field = field
390+        self.cache_name = self.field.get_cache_name()
391+
392+    def __get__(self, instance=None, owner=None):
393+        if instance is None:
394+            raise AttributeError, "%s can only be accessed from %s instances." % (self.field.attname, self.owner.__name__)
395+        return getattr(instance, self.cache_name)
396+
397+    def __set__(self, instance, value):
398+        if hasattr(instance, self.cache_name):
399+            raise AttributeError, "%s can not be set in this manner." % self.field.attname
400+        instance.__dict__[self.field.attname] = value
401+        attr = self.field.attr_class(instance, self.field, value)
402+        setattr(instance, self.cache_name, attr)
403+
404 class FileField(Field):
405-    def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs):
406-        self.upload_to = upload_to
407+    attr_class = File
408+
409+    def __init__(self, verbose_name=None, name=None, upload_to='', backend=None, **kwargs):
410+        if backend is None:
411+            from django.db.models.fields.backends.filesystem import FileSystemBackend
412+            backend = FileSystemBackend(location=upload_to)
413+        self.backend = self.upload_to = backend
414         Field.__init__(self, verbose_name, name, **kwargs)
415 
416     def get_db_prep_save(self, value):
417@@ -704,7 +757,7 @@
418         # Need to convert UploadedFile objects provided via a form to unicode for database insertion
419         if value is None:
420             return None
421-        return unicode(value)
422+        return unicode(value.filename)
423 
424     def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
425         field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
426@@ -744,20 +797,19 @@
427 
428     def contribute_to_class(self, cls, name):
429         super(FileField, self).contribute_to_class(cls, name)
430+        setattr(cls, self.attname, FileProxy(self))
431         setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
432         setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
433         setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
434         setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save))
435         dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)
436 
437-    def delete_file(self, instance):
438-        if getattr(instance, self.attname):
439-            file_name = getattr(instance, 'get_%s_filename' % self.name)()
440-            # If the file exists and no other object of this type references it,
441-            # delete it from the filesystem.
442-            if os.path.exists(file_name) and \
443-                not instance.__class__._default_manager.filter(**{'%s__exact' % self.name: getattr(instance, self.attname)}):
444-                os.remove(file_name)
445+    def delete_file(self, instance, sender):
446+        filename = getattr(instance, self.attname).filename
447+        # If no other object of this type references the file,
448+        # delete it from the backend.
449+        if not sender._default_manager.filter(**{self.name: filename}):
450+            self.backend.delete_file(filename)
451 
452     def get_manipulator_field_objs(self):
453         return [oldforms.FileUploadField, oldforms.HiddenField]
454@@ -768,24 +820,16 @@
455     def save_file(self, new_data, new_object, original_object, change, rel, save=True):
456         upload_field_name = self.get_manipulator_field_names('')[0]
457         if new_data.get(upload_field_name, False):
458-            func = getattr(new_object, 'save_%s_file' % self.name)
459             if rel:
460-                func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"], save)
461+                field = new_data[upload_field_name][0]
462             else:
463-                func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"], save)
464+                field = new_data[upload_field_name]
465+            getattr(new_object, self.attname).save_file(field["filename"], field["content"], save)
466 
467-    def get_directory_name(self):
468-        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
469-
470-    def get_filename(self, filename):
471-        from django.utils.text import get_valid_filename
472-        f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
473-        return os.path.normpath(f)
474-
475     def save_form_data(self, instance, data):
476         if data:
477-            getattr(instance, "save_%s_file" % self.name)(data.filename, data.content, save=False)
478-       
479+            getattr(instance, self.attnamename).save_file(data.filename, data.content, save=False)
480+
481     def formfield(self, **kwargs):
482         defaults = {'form_class': forms.FileField}
483         # If a file has been provided previously, then the form doesn't require
484@@ -814,7 +858,30 @@
485         defaults.update(kwargs)
486         return super(FloatField, self).formfield(**defaults)
487 
488+class ImageFile(File):
489+    def get_width(self):
490+        return self._get_image_dimensions()[0]
491+
492+    def get_height(self):
493+        return self._get_image_dimensions()[1]
494+
495+    def _get_image_dimensions(self):
496+        if not hasattr(self, '_dimensions_cache'):
497+            from django.utils.images import get_image_dimensions
498+            self._dimensions_cache = get_image_dimensions(self.open())
499+        return self._dimensions_cache
500+
501+    def save_file(self, filename, raw_contents):
502+        super(ImageFile, self).save_file(filename, raw_contnts)
503+       
504+        # Update the cache for image dimensions
505+        from django.utils.images import get_image_dimensions
506+        from cStringIO import StringIO
507+        self._dimensions_cache = get_image_dimensions(StringIO(raw_contents))
508+
509 class ImageField(FileField):
510+    attr_class = ImageFile
511+
512     def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
513         self.width_field, self.height_field = width_field, height_field
514         FileField.__init__(self, verbose_name, name, **kwargs)
515@@ -832,17 +899,35 @@
516             setattr(cls, 'get_%s_height' % self.name, curry(cls._get_FIELD_height, field=self))
517 
518     def save_file(self, new_data, new_object, original_object, change, rel, save=True):
519-        FileField.save_file(self, new_data, new_object, original_object, change, rel, save)
520         # If the image has height and/or width field(s) and they haven't
521         # changed, set the width and/or height field(s) back to their original
522         # values.
523-        if change and (self.width_field or self.height_field) and save:
524-            if self.width_field:
525-                setattr(new_object, self.width_field, getattr(original_object, self.width_field))
526-            if self.height_field:
527-                setattr(new_object, self.height_field, getattr(original_object, self.height_field))
528-            new_object.save()
529+        if self.width_field or self.height_field:
530+            if original_object and not change:
531+                if self.width_field:
532+                    setattr(new_object, self.width_field, getattr(original_object, self.width_field))
533+                if self.height_field:
534+                    setattr(new_object, self.height_field, getattr(original_object, self.height_field))
535+            else:
536+                from cStringIO import StringIO
537+                from django.utils.images import get_image_dimensions
538 
539+                upload_field_name = self.get_manipulator_field_names('')[0]
540+                if rel:
541+                    field = new_data[upload_field_name][0]
542+                else:
543+                    field = new_data[upload_field_name]
544+
545+                # Get the width and height from the raw content to avoid extra
546+                # unnecessary trips to the file backend.
547+                width, height = get_image_dimensions(StringIO(field["content"]))
548+
549+                if self.width_field:
550+                    setattr(new_object, self.width_field, width)
551+                if self.height_field:
552+                    setattr(new_object, self.height_field, height)
553+        FileField.save_file(self, new_data, new_object, original_object, change, rel, save)
554+
555     def formfield(self, **kwargs):
556         defaults = {'form_class': forms.ImageField}
557         return super(ImageField, self).formfield(**defaults)
558Index: django/utils/images.py
559===================================================================
560--- django/utils/images.py      (revision 6064)
561+++ django/utils/images.py      (working copy)
562@@ -6,10 +6,13 @@
563 
564 import ImageFile
565 
566-def get_image_dimensions(path):
567-    """Returns the (width, height) of an image at a given path."""
568+def get_image_dimensions(file_or_path):
569+    """Returns the (width, height) of an image, given an open file or a path."""
570     p = ImageFile.Parser()
571-    fp = open(path, 'rb')
572+    if hasattr(file_or_path, 'read'):
573+        fp = file_or_path
574+    else:
575+        fp = open(file_or_path, 'rb')
576     while 1:
577         data = fp.read(1024)
578         if not data:
579@@ -19,4 +22,4 @@
580             return p.image.size
581             break
582     fp.close()
583-    return None
584+    return None
585\ No newline at end of file
586Index: docs/db-api.txt
587===================================================================
588--- docs/db-api.txt     (revision 6064)
589+++ docs/db-api.txt     (working copy)
590@@ -1848,6 +1848,9 @@
591 get_FOO_filename()
592 ------------------
593 
594+**Deprecated in Django development version. See `managing files` for the new,
595+preferred method for dealing with files.**
596+
597 For every ``FileField``, the object will have a ``get_FOO_filename()`` method,
598 where ``FOO`` is the name of the field. This returns the full filesystem path
599 to the file, according to your ``MEDIA_ROOT`` setting.
600@@ -1858,6 +1861,9 @@
601 get_FOO_url()
602 -------------
603 
604+**Deprecated in Django development version. See `managing files` for the new,
605+preferred method for dealing with files.**
606+
607 For every ``FileField``, the object will have a ``get_FOO_url()`` method,
608 where ``FOO`` is the name of the field. This returns the full URL to the file,
609 according to your ``MEDIA_URL`` setting. If the value is blank, this method
610@@ -1866,6 +1872,9 @@
611 get_FOO_size()
612 --------------
613 
614+**Deprecated in Django development version. See `managing files` for the new,
615+preferred method for dealing with files.**
616+
617 For every ``FileField``, the object will have a ``get_FOO_size()`` method,
618 where ``FOO`` is the name of the field. This returns the size of the file, in
619 bytes. (Behind the scenes, it uses ``os.path.getsize``.)
620@@ -1873,6 +1882,9 @@
621 save_FOO_file(filename, raw_contents)
622 -------------------------------------
623 
624+**Deprecated in Django development version. See `managing files` for the new,
625+preferred method for dealing with files.**
626+
627 For every ``FileField``, the object will have a ``save_FOO_file()`` method,
628 where ``FOO`` is the name of the field. This saves the given file to the
629 filesystem, using the given filename. If a file with the given filename already
630@@ -1882,6 +1894,9 @@
631 get_FOO_height() and get_FOO_width()
632 ------------------------------------
633 
634+**Deprecated in Django development version. See `managing files` for the new,
635+preferred method for dealing with files.**
636+
637 For every ``ImageField``, the object will have ``get_FOO_height()`` and
638 ``get_FOO_width()`` methods, where ``FOO`` is the name of the field. This
639 returns the height (or width) of the image, as an integer, in pixels.
640Index: docs/files.txt
641===================================================================
642--- docs/files.txt      (revision 0)
643+++ docs/files.txt      (revision 0)
644@@ -0,0 +1,207 @@
645+==============
646+Managing files
647+==============
648+
649+When dealing with files, Django provides a number of features to make this task
650+easier and more portable. A backend protocol is available to allow files to be
651+stored in a variety of locations, and a special object is provided to allow
652+models to make use of this protocol, without having to worry about which storage
653+system is being used.
654+
655+Using files in models
656+=====================
657+
658+When accessing a ``FileField`` attached to a model, a special object provides
659+access to the file and information about it.
660+
661+get_absolute_url()
662+------------------
663+
664+Provides a URL where the content of the file can be retrieved. Therefore,
665+returned from this method is suitable for use as the destination of a link to
666+the file.
667+
668+get_filesize()
669+--------------
670+
671+Returns the size of the file, as an integer.
672+
673+open(mode='rb')
674+---------------
675+
676+Returns an open file object, providing read or write access to the file's
677+contents. The ``mode`` argument allows the same values as Python's standard
678+``open()`` function.
679+
680+save_file(filename, raw_contents, save=True)
681+--------------------------------------------
682+
683+Saves a new file with the filename and contents provided. This will not replace
684+the existing file, but will create a new file and update the object to point to
685+it. The optional ``save`` argument dictates whether the model instance will be
686+saved to the database immediately.
687+
688+get_width() and get_height()
689+----------------------------
690+
691+When using an ``ImageField``, these two methods will be available, providing
692+easy access to the dimensions of the image.
693+
694+Example
695+-------
696+
697+Consider the following model, using an ``ImageField`` to store a product photo::
698+
699+    class Product(models.Model):
700+        name = models.CharField(maxlength=255)
701+        price = models.DecimalField(max_digits=5, decimal_places=2)
702+        photo = models.ImageField(upload_to='product_photos')
703+
704+Your views can then use the ``photo`` attribute with the functions described
705+above, as follows::
706+
707+    >>> car = Product.object.get(name="'57 Chevy")
708+    >>> car.photo.get_absolute_url()
709+    '/products/photo/123.jpg'
710+    >>> car.photo.get_width(), car.photo.get_height()
711+    (800, 600)
712+
713+Specifying a storage backend
714+============================
715+
716+When using a storage backend, supply whatever options are appropriate for
717+that backend when creating a new object. Details on the requirements for the
718+included backends can be found below. Then pass that object as the ``backend``
719+argument to a ``FileField``.
720+
721+If using the ``FileSystemBackend``, it is not necessary to create a backend
722+object explicitly. Simply supplying the ``upload_to`` argument will create the
723+backend object automatically.
724+
725+See the ```FileField`` documentation`_ for more information on using the field.
726+
727+.. _FileField documentation: ../model-api/#filefield
728+
729+For example, the following code will explicitly use the ``FileSystemBackend``::
730+
731+    from django.db import models
732+    from django.core.filestorage.filesystem import FileSystemBackend
733+   
734+    fs = FileSystemBackend(location='product_photos')
735+   
736+    class Product(models.Model):
737+        name = models.CharField(maxlength=255)
738+        price = models.DecimalField(max_digits=5, decimal_places=2)
739+        photo = models.ImageField(backend=fs)
740+
741+Available backends
742+==================
743+
744+Only one storage backend is supplied in the official Django distribution, but
745+more may be available elsewhere. The documentation for the ``FileSystemBackend``
746+requirements will not necessarily be the same for other backends, so see the
747+documentation included with the backend if you use a different one.
748+
749+FileSystemBackend
750+-----------------
751+
752+This backend stores files on the system's standard filesystem. It requires just
753+one argument, while two more are optional:
754+
755+    ======================  ===================================================
756+    Argument                Description
757+    ======================  ===================================================
758+    ``location``            A local filesystem path that will be appended to
759+                            your ``MEDIA_ROOT`` setting to determine the
760+                            output of the ``get_<fieldname>_url()`` helper
761+                            function.
762+    ``media_root``          Absolute path to the directory that holds the files
763+                            for this backend. If omitted, it will be set to the
764+                            value of your ``MEDIA_ROOT`` setting.
765+    ``media_url``           URL that serves the files stored in this backend.
766+                            If omitted, it will default to the value of your
767+                            ``MEDIA_URL`` setting.
768+    ======================  ===================================================
769+
770+Writing a storage backend
771+=========================
772+
773+While filesystem storage is suitable for most needs, there are many other file
774+uses that require access to different storage mechanisms. In order to access
775+alternate storage systems, it's fairly simple to write a new storage backend,
776+creating a wrapper around whatever libraries are used to access your files.
777+
778+Storage backends extend ``django.core.filestorage.Backend`` and provide a set of
779+methods to do the work of actually interfacing the the storage system.
780+
781+get_available_filename(filename)
782+--------------------------------
783+
784+Returns a filename that is available in the storage mechanism. The ``filename``
785+argument is the name originally given to the file, so this method may take that
786+name into account when generating a new filename.
787+
788+This method is provided on the base ``Backend`` class, which simply appends
789+underscores to the filename until it finds
790+
791+get_filesize(filename)
792+----------------------
793+
794+Returns the total size of the file referenced by ``filename``, as an integer.
795+
796+get_absolute_url(filename)
797+--------------------------
798+
799+Provides a URL where the contents of the file referenced by ``filename`` can be
800+accessed.
801+
802+file_exists(filename)
803+---------------------
804+
805+Returns ``True`` or ``False, indicating whether there is already a file present
806+at the location referenced by``filename``. The ``get_available_filename()`` uses
807+this method to determine whether a file is available, before trying a new name.
808+
809+open(filename, mode='rb')
810+-------------------------
811+
812+Returns an open file, or file-like, object to provide access to the contents of
813+the file referenced by ``filename``. The ``mode`` argument allows the same
814+values as Python's standard ``open()`` function.
815+
816+The object returned should function like a standard `file object`_, but there is
817+a class available, ``django.core.filestorage.RemoteFile``, which makes this task
818+easier. Creating an instance of this object requires three arguments, which are
819+desribed below.
820+
821+    ======================  ===================================================
822+    Argument                Description
823+    ======================  ===================================================
824+    ``data``                The raw content of the file.
825+    ``mode``                The access mode that was passed to the ``open()``
826+                            method.
827+    ``writer``              A function that will be used to write the contents
828+                            to the backend-specific storage mechanism. The
829+                            function provided here will need to take a single
830+                            argument, which will be the raw content to be
831+                            written to the file.
832+    ======================  ===================================================
833+
834+.. _file object: http://docs.python.org/lib/bltin-file-objects.html
835+
836+save_file(filename, raw_contents)
837+---------------------------------
838+
839+Saves a new file using the backend-specific storage mechanism. The ``filename``
840+passed to this method will be the original file's name, and might not be
841+available in the actual storage system.
842+
843+This method is therefore responsible for identifying an available filename,
844+usually using ``get_available_filename()`` method described above. This method
845+must then return the actual filename that was used to store the file.
846+
847+delete_file(filename)
848+---------------------
849+
850+Deletes the file referenced by ``filename``. If the file does not already exist,
851+this method should simply return without throwing an error.
852\ No newline at end of file
853Index: docs/model-api.txt
854===================================================================
855--- docs/model-api.txt  (revision 6064)
856+++ docs/model-api.txt  (working copy)
857@@ -227,26 +227,34 @@
858 ``FileField``
859 ~~~~~~~~~~~~~
860 
861-A file-upload field. Has one **required** argument:
862+A file-upload field. **Requires** exactly one of the following two arguments:
863 
864     ======================  ===================================================
865     Argument                Description
866     ======================  ===================================================
867     ``upload_to``           A local filesystem path that will be appended to
868                             your ``MEDIA_ROOT`` setting to determine the
869-                            output of the ``get_<fieldname>_url()`` helper
870-                            function.
871+                            final storage destination. If this argument is
872+                            supplied, the storage backend will default to
873+                            ``FileSystemBackend``.
874+    ``backend``             **New in Django development version**
875+
876+                            A storage backend object, which handles the storage
877+                            and retrieval of your files. See `managing files`_
878+                            for details on how to provide this object.
879     ======================  ===================================================
880 
881-This path may contain `strftime formatting`_, which will be replaced by the
882-date/time of the file upload (so that uploaded files don't fill up the given
883-directory).
884+.. _managing files: ../files/
885 
886+The ``upload_to`` path may contain `strftime formatting`_, which will be
887+replaced by the date/time of the file upload (so that uploaded files don't fill
888+up the given directory).
889+
890 The admin represents this field as an ``<input type="file">`` (a file-upload
891 widget).
892 
893-Using a ``FileField`` or an ``ImageField`` (see below) in a model takes a few
894-steps:
895+Using a ``FileField`` or an ``ImageField`` (see below) in a model without a
896+specified backend takes a few steps:
897 
898     1. In your settings file, you'll need to define ``MEDIA_ROOT`` as the
899        full path to a directory where you'd like Django to store uploaded