diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 4f27502167d..0cc895d2301 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -2,6 +2,7 @@ import os import errno import urlparse import itertools +from datetime import datetime from django.conf import settings from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation @@ -120,6 +121,27 @@ class Storage(object): """ raise NotImplementedError() + def accessed_time(self, name): + """ + Returns the last accessed time (as datetime object) of the file + specified by name. + """ + raise NotImplementedError() + + def created_time(self, name): + """ + Returns the creation time (as datetime object) of the file + specified by name. + """ + raise NotImplementedError() + + def modified_time(self, name): + """ + Returns the last modified time (as datetime object) of the file + specified by name. + """ + raise NotImplementedError() + class FileSystemStorage(Storage): """ Standard filesystem storage @@ -220,6 +242,15 @@ class FileSystemStorage(Storage): raise ValueError("This file is not accessible via a URL.") return urlparse.urljoin(self.base_url, name).replace('\\', '/') + def accessed_time(self, name): + return datetime.fromtimestamp(os.path.getatime(self.path(name))) + + def created_time(self, name): + return datetime.fromtimestamp(os.path.getctime(self.path(name))) + + def modified_time(self, name): + return datetime.fromtimestamp(os.path.getmtime(self.path(name))) + def get_storage_class(import_path=None): if import_path is None: import_path = settings.DEFAULT_FILE_STORAGE diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index 2b055bb60be..b27ea1556b1 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -13,6 +13,33 @@ 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.accessed_time(name)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.3 + +Returns a ``datetime`` object containing the last accessed time of the file. +For storage systems that aren't able to return the last accessed time, this +will raise ``NotImplementedError`` instead. + +``Storage.created_time(name)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.3 + +Returns a ``datetime`` object containing the creation time of the file. +For storage systems that aren't able to return the creation time, this +will raise ``NotImplementedError`` instead. + +``Storage.modified_time(name)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.3 + +Returns a ``datetime`` object containing the last modified time. For storage +systems that aren't able to return the last modified time, this will raise +``NotImplementedError`` instead. + ``Storage.size(name)`` ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index 46411aca875..f83b26792fa 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -6,6 +6,7 @@ import tempfile import time import unittest from cStringIO import StringIO +from datetime import datetime, timedelta from django.conf import settings from django.core.exceptions import SuspiciousOperation from django.core.files.base import ContentFile, File @@ -81,16 +82,16 @@ class GetStorageClassTests(unittest.TestCase): class FileStorageTests(unittest.TestCase): storage_class = FileSystemStorage - + def setUp(self): self.temp_dir = tempfile.mktemp() os.makedirs(self.temp_dir) self.storage = self.storage_class(location=self.temp_dir, base_url='/test_media_url/') - + def tearDown(self): shutil.rmtree(self.temp_dir) - + def test_file_access_options(self): """ Standard file access options are available, and work as expected. @@ -104,10 +105,60 @@ class FileStorageTests(unittest.TestCase): f = self.storage.open('storage_test', 'r') self.assertEqual(f.read(), 'storage contents') f.close() - + self.storage.delete('storage_test') self.failIf(self.storage.exists('storage_test')) + def test_file_accessed_time(self): + """ + File storage returns a Datetime object for the last accessed time of + a file. + """ + self.failIf(self.storage.exists('test.file')) + + f = ContentFile('custom contents') + f_name = self.storage.save('test.file', f) + atime = self.storage.accessed_time(f_name) + + self.assertEqual(atime, datetime.fromtimestamp( + os.path.getatime(self.storage.path(f_name)))) + self.assertTrue(datetime.now() - self.storage.accessed_time(f_name) < timedelta(seconds=2)) + self.storage.delete(f_name) + + def test_file_created_time(self): + """ + File storage returns a Datetime object for the creation time of + a file. + """ + self.failIf(self.storage.exists('test.file')) + + f = ContentFile('custom contents') + f_name = self.storage.save('test.file', f) + ctime = self.storage.created_time(f_name) + + self.assertEqual(ctime, datetime.fromtimestamp( + os.path.getctime(self.storage.path(f_name)))) + self.assertTrue(datetime.now() - self.storage.created_time(f_name) < timedelta(seconds=2)) + + self.storage.delete(f_name) + + def test_file_modified_time(self): + """ + File storage returns a Datetime object for the last modified time of + a file. + """ + self.failIf(self.storage.exists('test.file')) + + f = ContentFile('custom contents') + f_name = self.storage.save('test.file', f) + mtime = self.storage.modified_time(f_name) + + self.assertEqual(mtime, datetime.fromtimestamp( + os.path.getmtime(self.storage.path(f_name)))) + self.assertTrue(datetime.now() - self.storage.modified_time(f_name) < timedelta(seconds=2)) + + self.storage.delete(f_name) + def test_file_save_without_name(self): """ File storage extracts the filename from the content object if no @@ -215,7 +266,7 @@ class CustomStorage(FileSystemStorage): class CustomStorageTests(FileStorageTests): storage_class = CustomStorage - + def test_custom_get_available_name(self): first = self.storage.save('custom_storage', ContentFile('custom contents')) self.assertEqual(first, 'custom_storage')