Commit 1ff6e37d authored by James Aylett's avatar James Aylett Committed by Tim Graham
Browse files

Fixed #23832 -- Added timezone aware Storage API.

New Storage.get_{accessed,created,modified}_time() methods convert the
naive time from now-deprecated {accessed,created_modified}_time()
methods into aware objects in UTC if USE_TZ=True.
parent eda306f1
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
@@ -237,15 +237,14 @@ class Command(BaseCommand):
        if self.storage.exists(prefixed_path):
            try:
                # When was the target file modified last time?
                target_last_modified = \
                    self.storage.modified_time(prefixed_path)
                target_last_modified = self.storage.get_modified_time(prefixed_path)
            except (OSError, NotImplementedError, AttributeError):
                # The storage doesn't support ``modified_time`` or failed
                # The storage doesn't support get_modified_time() or failed
                pass
            else:
                try:
                    # When was the source file modified last time?
                    source_last_modified = source_storage.modified_time(path)
                    source_last_modified = source_storage.get_modified_time(path)
                except (OSError, NotImplementedError, AttributeError):
                    pass
                else:
+121 −3
Original line number Diff line number Diff line
import errno
import os
import warnings
from datetime import datetime

from django.conf import settings
@@ -7,9 +8,11 @@ from django.core.exceptions import SuspiciousFileOperation
from django.core.files import File, locks
from django.core.files.move import file_move_safe
from django.core.signals import setting_changed
from django.utils import timezone
from django.utils._os import abspathu, safe_join
from django.utils.crypto import get_random_string
from django.utils.deconstruct import deconstructible
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import filepath_to_uri, force_text
from django.utils.functional import LazyObject, cached_property
from django.utils.module_loading import import_string
@@ -140,24 +143,101 @@ class Storage(object):
    def accessed_time(self, name):
        """
        Returns the last accessed time (as datetime object) of the file
        specified by name.
        specified by name. Deprecated: use get_accessed_time() instead.
        """
        warnings.warn(
            'Storage.accessed_time() is deprecated in favor of get_accessed_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        raise NotImplementedError('subclasses of Storage must provide an accessed_time() method')

    def created_time(self, name):
        """
        Returns the creation time (as datetime object) of the file
        specified by name.
        specified by name. Deprecated: use get_created_time() instead.
        """
        warnings.warn(
            'Storage.created_time() is deprecated in favor of get_created_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        raise NotImplementedError('subclasses of Storage must provide a created_time() method')

    def modified_time(self, name):
        """
        Returns the last modified time (as datetime object) of the file
        specified by name.
        specified by name. Deprecated: use get_modified_time() instead.
        """
        warnings.warn(
            'Storage.modified_time() is deprecated in favor of get_modified_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        raise NotImplementedError('subclasses of Storage must provide a modified_time() method')

    def get_accessed_time(self, name):
        """
        Return the last accessed time (as a datetime) of the file specified by
        name. The datetime will be timezone-aware if USE_TZ=True.
        """
        # At the end of the deprecation:
        # raise NotImplementedError('subclasses of Storage must provide a get_accessed_time() method')
        warnings.warn(
            'Storage.accessed_time() is deprecated. '
            'Storage backends should implement get_accessed_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        dt = self.accessed_time(name)
        return _possibly_make_aware(dt)

    def get_created_time(self, name):
        """
        Return the creation time (as a datetime) of the file specified by name.
        The datetime will be timezone-aware if USE_TZ=True.
        """
        # At the end of the deprecation:
        # raise NotImplementedError('subclasses of Storage must provide a get_created_time() method')
        warnings.warn(
            'Storage.created_time() is deprecated. '
            'Storage backends should implement get_created_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        dt = self.created_time(name)
        return _possibly_make_aware(dt)

    def get_modified_time(self, name):
        """
        Return the last modified time (as a datetime) of the file specified by
        name. The datetime will be timezone-aware if USE_TZ=True.
        """
        # At the end of the deprecation:
        # raise NotImplementedError('subclasses of Storage must provide a get_modified_time() method')
        warnings.warn(
            'Storage.modified_time() is deprecated. '
            'Storage backends should implement get_modified_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        dt = self.modified_time(name)
        return _possibly_make_aware(dt)


def _possibly_make_aware(dt):
    """
    Convert a datetime object in the local timezone to aware
    in UTC, if USE_TZ is True.
    """
    # This function is only needed to help with the deprecations above and can
    # be removed in Django 2.0, RemovedInDjango20Warning.
    if settings.USE_TZ:
        tz = timezone.get_default_timezone()
        return timezone.make_aware(dt, tz).astimezone(timezone.utc)
    else:
        return dt


@deconstructible
class FileSystemStorage(Storage):
@@ -328,14 +408,52 @@ class FileSystemStorage(Storage):
        return urljoin(self.base_url, filepath_to_uri(name))

    def accessed_time(self, name):
        warnings.warn(
            'FileSystemStorage.accessed_time() is deprecated in favor of '
            'get_accessed_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        return datetime.fromtimestamp(os.path.getatime(self.path(name)))

    def created_time(self, name):
        warnings.warn(
            'FileSystemStorage.created_time() is deprecated in favor of '
            'get_created_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        return datetime.fromtimestamp(os.path.getctime(self.path(name)))

    def modified_time(self, name):
        warnings.warn(
            'FileSystemStorage.modified_time() is deprecated in favor of '
            'get_modified_time().',
            RemovedInDjango20Warning,
            stacklevel=2,
        )
        return datetime.fromtimestamp(os.path.getmtime(self.path(name)))

    def _datetime_from_timestamp(self, ts):
        """
        If timezone support is enabled, make an aware datetime object in UTC;
        otherwise make a naive one in the local timezone.
        """
        if settings.USE_TZ:
            # Safe to use .replace() because UTC doesn't have DST
            return datetime.utcfromtimestamp(ts).replace(tzinfo=timezone.utc)
        else:
            return datetime.fromtimestamp(ts)

    def get_accessed_time(self, name):
        return self._datetime_from_timestamp(os.path.getatime(self.path(name)))

    def get_created_time(self, name):
        return self._datetime_from_timestamp(os.path.getctime(self.path(name)))

    def get_modified_time(self, name):
        return self._datetime_from_timestamp(os.path.getmtime(self.path(name)))


def get_storage_class(import_path=None):
    return import_string(import_path or settings.DEFAULT_FILE_STORAGE)
+3 −0
Original line number Diff line number Diff line
@@ -135,6 +135,9 @@ details on these changes.

* Support for the template ``Context.has_key()`` method will be removed.

* Support for the ``django.core.files.storage.Storage.accessed_time()``,
  ``created_time()``, and ``modified_time()`` methods will be removed.

.. _deprecation-removed-in-1.10:

1.10
+48 −4
Original line number Diff line number Diff line
@@ -73,10 +73,9 @@ The ``Storage`` class
    as necessary.

    .. note::
        For methods returning naive ``datetime`` objects, the
        effective timezone used will be the current value of
        ``os.environ['TZ']``; note that this is usually set from
        Django's :setting:`TIME_ZONE`.
        When methods return naive ``datetime`` objects, the effective timezone
        used will be the current value of ``os.environ['TZ']``; note that this
        is usually set from Django's :setting:`TIME_ZONE`.

    .. method:: accessed_time(name)

@@ -85,6 +84,10 @@ The ``Storage`` class
        able to return the last accessed time this will raise
        ``NotImplementedError`` instead.

        .. deprecated:: 1.10

            Use :meth:`get_accessed_time` instead.

    .. method:: created_time(name)

        Returns a naive ``datetime`` object containing the creation
@@ -92,6 +95,10 @@ The ``Storage`` class
        return the creation time this will raise
        ``NotImplementedError`` instead.

        .. deprecated:: 1.10

            Use :meth:`get_created_time` instead.

    .. method:: delete(name)

        Deletes the file referenced by ``name``. If deletion is not supported
@@ -104,6 +111,17 @@ The ``Storage`` class
        in the storage system, or ``False`` if the name is available for a new
        file.

    .. method:: get_accessed_time(name)

        .. versionadded:: 1.10

        Returns a :class:`~datetime.datetime` of the last accessed time of the
        file. For storage systems unable to return the last accessed time this
        will raise :exc:`NotImplementedError`.

        If :setting:`USE_TZ` is ``True``, returns an aware ``datetime``,
        otherwise returns a naive ``datetime`` in the local timezone.

    .. method:: get_available_name(name, max_length=None)

        Returns a filename based on the ``name`` parameter that's free and
@@ -119,6 +137,28 @@ The ``Storage`` class
        7 character alphanumeric string is appended to the filename before
        the extension.

    .. method:: get_created_time(name)

        .. versionadded:: 1.10

        Returns a :class:`~datetime.datetime` of the creation time of the file.
        For storage systems unable to return the creation time this will raise
        :exc:`NotImplementedError`.

        If :setting:`USE_TZ` is ``True``, returns an aware ``datetime``,
        otherwise returns a naive ``datetime`` in the local timezone.

    .. method:: get_modified_time(name)

        .. versionadded:: 1.10

        Returns a :class:`~datetime.datetime` of the last modified time of the
        file. For storage systems unable to return the last modified time this
        will raise :exc:`NotImplementedError`.

        If :setting:`USE_TZ` is ``True``, returns an aware ``datetime``,
        otherwise returns a naive ``datetime`` in the local timezone.

    .. method:: get_valid_name(name)

        Returns a filename based on the ``name`` parameter that's suitable
@@ -138,6 +178,10 @@ The ``Storage`` class
        the last modified time, this will raise
        ``NotImplementedError`` instead.

        .. deprecated:: 1.10

            Use :meth:`get_modified_time` instead.

    .. method:: open(name, mode='rb')

        Opens the file given by ``name``. Note that although the returned file
+22 −1
Original line number Diff line number Diff line
@@ -203,7 +203,13 @@ Email
File Storage
~~~~~~~~~~~~

* ...
* Storage backends now present a timezone-aware API with new methods
  :meth:`~django.core.files.storage.Storage.get_accessed_time`,
  :meth:`~django.core.files.storage.Storage.get_created_time`, and
  :meth:`~django.core.files.storage.Storage.get_modified_time`. They return a
  timezone-aware ``datetime`` if :setting:`USE_TZ` is ``True`` and a naive
  ``datetime`` in the local timezone otherwise.


File Uploads
~~~~~~~~~~~~
@@ -623,6 +629,21 @@ added in Django 1.9::

This prevents confusion about an assignment resulting in an implicit save.

Non-timezone-aware :class:`~django.core.files.storage.Storage` API
------------------------------------------------------------------

The old, non-timezone-aware methods ``accessed_time()``, ``created_time()``,
and ``modified_time()`` are deprecated in favor of the new ``get_*_time()``
methods.

Third-party storage backends should implement the new methods and mark the old
ones as deprecated. Until then, the new ``get_*_time()`` methods on the base
:class:`~django.core.files.storage.Storage` class convert ``datetime``\s from
the old methods as required and emit a deprecation warning as they do so.

Third-party storage backends may retain the old methods as long as they
wish to support earlier versions of Django.

:mod:`django.contrib.gis`
-------------------------

Loading