diff --git a/django/core/files/storage.py b/django/core/files/storage.py index b42981cb5e..b05f3647c0 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -147,7 +147,7 @@ class FileSystemStorage(Storage): Standard filesystem storage """ - def __init__(self, location=None, base_url=None): + def __init__(self, location=None, base_url=None, file_permissions_mode=None): if location is None: location = settings.MEDIA_ROOT self.base_location = location @@ -155,6 +155,10 @@ class FileSystemStorage(Storage): if base_url is None: base_url = settings.MEDIA_URL self.base_url = base_url + self.file_permissions_mode = ( + file_permissions_mode if file_permissions_mode is not None + else settings.FILE_UPLOAD_PERMISSIONS + ) def _open(self, name, mode='rb'): return File(open(self.path(name), mode)) @@ -232,8 +236,8 @@ class FileSystemStorage(Storage): # OK, the file save worked. Break out of the loop. break - if settings.FILE_UPLOAD_PERMISSIONS is not None: - os.chmod(full_path, settings.FILE_UPLOAD_PERMISSIONS) + if self.file_permissions_mode is not None: + os.chmod(full_path, self.file_permissions_mode) return name diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index 1a9fd25ca9..c65533fa0f 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -32,8 +32,6 @@ following settings: Management Commands =================== -.. highlight:: console - ``django.contrib.staticfiles`` exposes three management commands. collectstatic @@ -61,6 +59,30 @@ receives all command line options of :djadmin:`collectstatic`. This is used by the :class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage` by default. +By default, collected files receive permissions from +:setting:`FILE_UPLOAD_PERMISSIONS`. If you would like different permissions for +these files, you can subclass either of the :ref:`static files storage +classes <staticfiles-storages>` and specify the ``file_permissions_mode`` +parameter. For example:: + + from django.contrib.staticfiles import storage + + class MyStaticFilesStorage(storage.StaticFilesStorage): + def __init__(self, *args, **kwargs): + kwargs['file_permissions_mode'] = 0o640 + super(CustomStaticFilesStorage, self).__init__(*args, **kwargs) + +Then set the :setting:`STATICFILES_STORAGE` setting to +``'path.to.MyStaticFilesStorage'``. + +.. versionadded:: 1.7 + + The ability to override ``file_permissions_mode`` is new in Django 1.7. + Previously the file permissions always used + :setting:`FILE_UPLOAD_PERMISSIONS`. + +.. highlight:: console + Some commonly used options are: .. django-admin-option:: --noinput @@ -174,6 +196,8 @@ Example usage:: django-admin.py runserver --insecure +.. _staticfiles-storages: + Storages ======== diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index b9742514ea..bd8fa56787 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -29,13 +29,23 @@ Django provides two convenient ways to access the current storage class: The FileSystemStorage Class --------------------------- -.. class:: FileSystemStorage +.. class:: FileSystemStorage([location=None, base_url=None, file_permissions_mode=None]) The :class:`~django.core.files.storage.FileSystemStorage` class implements basic file storage on a local filesystem. It inherits from :class:`~django.core.files.storage.Storage` and provides implementations for all the public methods thereof. + .. attribute:: file_permissions_mode + + The file system permissions that the file will receive when it is + saved. Defaults to :setting:`FILE_UPLOAD_PERMISSIONS`. + + .. versionadded:: 1.7 + + The ``file_permissions_mode`` attribute was added. Previously files + always received :setting:`FILE_UPLOAD_PERMISSIONS` permissions. + .. note:: The ``FileSystemStorage.delete()`` method will not raise @@ -81,7 +91,6 @@ The Storage Class available for new content to be written to on the target storage system. - .. method:: get_valid_name(name) Returns a filename based on the ``name`` parameter that's suitable diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 32a8237d3f..4b813a8ac7 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1159,6 +1159,10 @@ dependent behavior. On most platforms, temporary files will have a mode of ``0600``, and files saved from memory will be saved using the system's standard umask. +This setting also determines the default permissions for collected static files +when using the :djadmin:`collectstatic` management command. See +:djadmin:`collectstatic` for details on overridding it. + .. warning:: **Always prefix the mode with a 0.** diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 7ce13066bf..9312aeaecb 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -226,6 +226,15 @@ Minor features :class:`~django.middleware.http.ConditionalGetMiddleware` to handle conditional ``GET`` requests for sitemaps which set ``lastmod``. +:mod:`django.contrib.staticfiles` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* The :ref:`static files storage classes <staticfiles-storages>` may be + subclassed to override the permissions that collected static files receive by + setting the + :attr:`~django.core.files.storage.FileSystemStorage.file_permissions_mode` + parameter. See :djadmin:`collectstatic` for example usage. + :mod:`django.contrib.syndication` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py index fa791c405b..00920a8618 100644 --- a/tests/file_storage/tests.py +++ b/tests/file_storage/tests.py @@ -442,7 +442,6 @@ class FileStoragePermissions(unittest.TestCase): self.umask = 0o027 self.old_umask = os.umask(self.umask) self.storage_dir = tempfile.mkdtemp() - self.storage = FileSystemStorage(self.storage_dir) def tearDown(self): shutil.rmtree(self.storage_dir) @@ -450,24 +449,28 @@ class FileStoragePermissions(unittest.TestCase): @override_settings(FILE_UPLOAD_PERMISSIONS=0o654) def test_file_upload_permissions(self): + self.storage = FileSystemStorage(self.storage_dir) name = self.storage.save("the_file", ContentFile("data")) actual_mode = os.stat(self.storage.path(name))[0] & 0o777 self.assertEqual(actual_mode, 0o654) @override_settings(FILE_UPLOAD_PERMISSIONS=None) def test_file_upload_default_permissions(self): + self.storage = FileSystemStorage(self.storage_dir) fname = self.storage.save("some_file", ContentFile("data")) mode = os.stat(self.storage.path(fname))[0] & 0o777 self.assertEqual(mode, 0o666 & ~self.umask) @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o765) def test_file_upload_directory_permissions(self): + self.storage = FileSystemStorage(self.storage_dir) name = self.storage.save("the_directory/the_file", ContentFile("data")) dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777 self.assertEqual(dir_mode, 0o765) @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=None) def test_file_upload_directory_default_permissions(self): + self.storage = FileSystemStorage(self.storage_dir) name = self.storage.save("the_directory/the_file", ContentFile("data")) dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777 self.assertEqual(dir_mode, 0o777 & ~self.umask) diff --git a/tests/staticfiles_tests/tests.py b/tests/staticfiles_tests/tests.py index 254e48d048..463ab47030 100644 --- a/tests/staticfiles_tests/tests.py +++ b/tests/staticfiles_tests/tests.py @@ -7,6 +7,7 @@ import posixpath import shutil import sys import tempfile +import unittest from django.template import loader, Context from django.conf import settings @@ -21,6 +22,7 @@ from django.utils._os import rmtree_errorhandler, upath from django.utils import six from django.contrib.staticfiles import finders, storage +from django.contrib.staticfiles.management.commands import collectstatic TEST_ROOT = os.path.dirname(upath(__file__)) TEST_SETTINGS = { @@ -804,3 +806,45 @@ class TestAppStaticStorage(TestCase): st.path('bar') finally: sys.getfilesystemencoding = old_enc_func + + +class CustomStaticFilesStorage(storage.StaticFilesStorage): + """ + Used in TestStaticFilePermissions + """ + def __init__(self, *args, **kwargs): + kwargs['file_permissions_mode'] = 0o640 + super(CustomStaticFilesStorage, self).__init__(*args, **kwargs) + + +@unittest.skipIf(sys.platform.startswith('win'), + "Windows only partially supports chmod.") +class TestStaticFilePermissions(BaseCollectionTestCase, StaticFilesTestCase): + + command_params = {'interactive': False, + 'post_process': True, + 'verbosity': '0', + 'ignore_patterns': ['*.ignoreme'], + 'use_default_ignore_patterns': True, + 'clear': False, + 'link': False, + 'dry_run': False} + + # Don't run collectstatic command in this test class. + def run_collectstatic(self, **kwargs): + pass + + @override_settings(FILE_UPLOAD_PERMISSIONS=0o655) + def test_collect_static_files_default_permissions(self): + collectstatic.Command().execute(**self.command_params) + test_file = os.path.join(settings.STATIC_ROOT, "test.txt") + file_mode = os.stat(test_file)[0] & 0o777 + self.assertEqual(file_mode, 0o655) + + @override_settings(FILE_UPLOAD_PERMISSIONS=0o655, + STATICFILES_STORAGE='staticfiles_tests.tests.CustomStaticFilesStorage') + def test_collect_static_files_subclass_of_static_storage(self): + collectstatic.Command().execute(**self.command_params) + test_file = os.path.join(settings.STATIC_ROOT, "test.txt") + file_mode = os.stat(test_file)[0] & 0o777 + self.assertEqual(file_mode, 0o640)