diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index d16dab540c..02e95d56b9 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import os -import sys from collections import OrderedDict from optparse import make_option @@ -82,12 +81,8 @@ class Command(NoArgsCommand): Split off from handle_noargs() to facilitate testing. """ - if self.symlink: - if sys.platform == 'win32': - raise CommandError("Symlinking is not supported by this " - "platform (%s)." % sys.platform) - if not self.local: - raise CommandError("Can't symlink to a remote destination.") + if self.symlink and not self.local: + raise CommandError("Can't symlink to a remote destination.") if self.clear: self.clear_dir('') @@ -279,7 +274,18 @@ class Command(NoArgsCommand): os.makedirs(os.path.dirname(full_path)) except OSError: pass - os.symlink(source_path, full_path) + try: + os.symlink(source_path, full_path) + except AttributeError: + import platform + raise CommandError("Symlinking is not supported by Python %s." % + platform.python_version()) + except NotImplementedError: + import platform + raise CommandError("Symlinking is not supported in this " + "platform (%s)." % platform.platform()) + except OSError as e: + raise CommandError(e) if prefixed_path not in self.symlinked_files: self.symlinked_files.append(prefixed_path) diff --git a/django/utils/_os.py b/django/utils/_os.py index 1cf250e09e..93b965c7c6 100644 --- a/django/utils/_os.py +++ b/django/utils/_os.py @@ -1,6 +1,7 @@ import os import stat import sys +import tempfile from os.path import join, normcase, normpath, abspath, isabs, sep, dirname from django.utils.encoding import force_text @@ -99,3 +100,24 @@ def rmtree_errorhandler(func, path, exc_info): os.chmod(path, stat.S_IWRITE) # use the original function to repeat the operation func(path) + + +def symlinks_supported(): + """ + A function to check if creating symlinks are supported in the + host platform and/or if they are allowed to be created (e.g. + on Windows it requires admin permissions). + """ + tmpdir = tempfile.mkdtemp() + original_path = os.path.join(tmpdir, 'original') + symlink_path = os.path.join(tmpdir, 'symlink') + os.makedirs(original_path) + try: + os.symlink(original_path, symlink_path) + supported = True + except (OSError, NotImplementedError, AttributeError): + supported = False + else: + os.remove(symlink_path) + finally: + return supported diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 5abc06da7e..905d0a3bf5 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -565,6 +565,9 @@ Management Commands * Management commands can now produce syntax colored output under Windows if the ANSICON third-party tool is installed and active. +* :djadmin:`collectstatic` command with symlink option is now supported on + Windows NT 6 (Windows Vista and newer). + Models ^^^^^^ diff --git a/tests/staticfiles_tests/tests.py b/tests/staticfiles_tests/tests.py index 1d3daff11e..c67a4672b4 100644 --- a/tests/staticfiles_tests/tests.py +++ b/tests/staticfiles_tests/tests.py @@ -17,7 +17,7 @@ from django.core.management import call_command from django.test import TestCase, override_settings from django.utils.encoding import force_text from django.utils.functional import empty -from django.utils._os import rmtree_errorhandler, upath +from django.utils._os import rmtree_errorhandler, upath, symlinks_supported from django.utils import six from django.contrib.staticfiles import finders, storage @@ -645,24 +645,25 @@ class TestCollectionSimpleCachedStorage(BaseCollectionTestCase, self.assertNotIn(b"cached/other.css", content) self.assertIn(b"other.deploy12345.css", content) -if sys.platform != 'win32': - class TestCollectionLinks(CollectionTestCase, TestDefaults): +@unittest.skipUnless(symlinks_supported(), + "Must be able to symlink to run this test.") +class TestCollectionLinks(CollectionTestCase, TestDefaults): + """ + Test ``--link`` option for ``collectstatic`` management command. + + Note that by inheriting ``TestDefaults`` we repeat all + the standard file resolving tests here, to make sure using + ``--link`` does not change the file-selection semantics. + """ + def run_collectstatic(self): + super(TestCollectionLinks, self).run_collectstatic(link=True) + + def test_links_created(self): """ - Test ``--link`` option for ``collectstatic`` management command. - - Note that by inheriting ``TestDefaults`` we repeat all - the standard file resolving tests here, to make sure using - ``--link`` does not change the file-selection semantics. + With ``--link``, symbolic links are created. """ - def run_collectstatic(self): - super(TestCollectionLinks, self).run_collectstatic(link=True) - - def test_links_created(self): - """ - With ``--link``, symbolic links are created. - """ - self.assertTrue(os.path.islink(os.path.join(settings.STATIC_ROOT, 'test.txt'))) + self.assertTrue(os.path.islink(os.path.join(settings.STATIC_ROOT, 'test.txt'))) class TestServeStatic(StaticFilesTestCase):