import os from collections import OrderedDict from django.apps import apps from django.conf import settings from django.contrib.staticfiles import utils from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import ( FileSystemStorage, Storage, default_storage, ) from django.utils import lru_cache, six from django.utils._os import safe_join from django.utils.functional import LazyObject, empty from django.utils.module_loading import import_string # To keep track on which directories the finder has searched the static files. searched_locations = [] class BaseFinder(object): """ A base file finder to be used for custom staticfiles finder classes. """ def find(self, path, all=False): """ Given a relative file path this ought to find an absolute file path. If the ``all`` parameter is ``False`` (default) only the first found file path will be returned; if set to ``True`` a list of all found files paths is returned. """ raise NotImplementedError('subclasses of BaseFinder must provide a find() method') def list(self, ignore_patterns): """ Given an optional list of paths to ignore, this should return a two item iterable consisting of the relative path and storage instance. """ raise NotImplementedError('subclasses of BaseFinder must provide a list() method') class FileSystemFinder(BaseFinder): """ A static files finder that uses the ``STATICFILES_DIRS`` setting to locate files. """ def __init__(self, app_names=None, *args, **kwargs): # List of locations with static files self.locations = [] # Maps dir paths to an appropriate storage instance self.storages = OrderedDict() if not isinstance(settings.STATICFILES_DIRS, (list, tuple)): raise ImproperlyConfigured( "Your STATICFILES_DIRS setting is not a tuple or list; " "perhaps you forgot a trailing comma?") for root in settings.STATICFILES_DIRS: if isinstance(root, (list, tuple)): prefix, root = root else: prefix = '' if settings.STATIC_ROOT and os.path.abspath(settings.STATIC_ROOT) == os.path.abspath(root): raise ImproperlyConfigured( "The STATICFILES_DIRS setting should " "not contain the STATIC_ROOT setting") if (prefix, root) not in self.locations: self.locations.append((prefix, root)) for prefix, root in self.locations: filesystem_storage = FileSystemStorage(location=root) filesystem_storage.prefix = prefix self.storages[root] = filesystem_storage super(FileSystemFinder, self).__init__(*args, **kwargs) def find(self, path, all=False): """ Looks for files in the extra locations as defined in ``STATICFILES_DIRS``. """ matches = [] for prefix, root in self.locations: if root not in searched_locations: searched_locations.append(root) matched_path = self.find_location(root, path, prefix) if matched_path: if not all: return matched_path matches.append(matched_path) return matches def find_location(self, root, path, prefix=None): """ Finds a requested static file in a location, returning the found absolute path (or ``None`` if no match). """ if prefix: prefix = '%s%s' % (prefix, os.sep) if not path.startswith(prefix): return None path = path[len(prefix):] path = safe_join(root, path) if os.path.exists(path): return path def list(self, ignore_patterns): """ List all files in all locations. """ for prefix, root in self.locations: storage = self.storages[root] for path in utils.get_files(storage, ignore_patterns): yield path, storage class AppDirectoriesFinder(BaseFinder): """ A static files finder that looks in the directory of each app as specified in the source_dir attribute. """ storage_class = FileSystemStorage source_dir = 'static' def __init__(self, app_names=None, *args, **kwargs): # The list of apps that are handled self.apps = [] # Mapping of app names to storage instances self.storages = OrderedDict() app_configs = apps.get_app_configs() if app_names: app_names = set(app_names) app_configs = [ac for ac in app_configs if ac.name in app_names] for app_config in app_configs: app_storage = self.storage_class( os.path.join(app_config.path, self.source_dir)) if os.path.isdir(app_storage.location): self.storages[app_config.name] = app_storage if app_config.name not in self.apps: self.apps.append(app_config.name) super(AppDirectoriesFinder, self).__init__(*args, **kwargs) def list(self, ignore_patterns): """ List all files in all app storages. """ for storage in six.itervalues(self.storages): if storage.exists(''): # check if storage location exists for path in utils.get_files(storage, ignore_patterns): yield path, storage def find(self, path, all=False): """ Looks for files in the app directories. """ matches = [] for app in self.apps: app_location = self.storages[app].location if app_location not in searched_locations: searched_locations.append(app_location) match = self.find_in_app(app, path) if match: if not all: return match matches.append(match) return matches def find_in_app(self, app, path): """ Find a requested static file in an app's static locations. """ storage = self.storages.get(app, None) if storage: # only try to find a file if the source dir actually exists if storage.exists(path): matched_path = storage.path(path) if matched_path: return matched_path class BaseStorageFinder(BaseFinder): """ A base static files finder to be used to extended with an own storage class. """ storage = None def __init__(self, storage=None, *args, **kwargs): if storage is not None: self.storage = storage if self.storage is None: raise ImproperlyConfigured("The staticfiles storage finder %r " "doesn't have a storage class " "assigned." % self.__class__) # Make sure we have an storage instance here. if not isinstance(self.storage, (Storage, LazyObject)): self.storage = self.storage() super(BaseStorageFinder, self).__init__(*args, **kwargs) def find(self, path, all=False): """ Looks for files in the default file storage, if it's local. """ try: self.storage.path('') except NotImplementedError: pass else: if self.storage.location not in searched_locations: searched_locations.append(self.storage.location) if self.storage.exists(path): match = self.storage.path(path) if all: match = [match] return match return [] def list(self, ignore_patterns): """ List all files of the storage. """ for path in utils.get_files(self.storage, ignore_patterns): yield path, self.storage class DefaultStorageFinder(BaseStorageFinder): """ A static files finder that uses the default storage backend. """ storage = default_storage def __init__(self, *args, **kwargs): super(DefaultStorageFinder, self).__init__(*args, **kwargs) base_location = getattr(self.storage, 'base_location', empty) if not base_location: raise ImproperlyConfigured("The storage backend of the " "staticfiles finder %r doesn't have " "a valid location." % self.__class__) def find(path, all=False): """ Find a static file with the given path using all enabled finders. If ``all`` is ``False`` (default), return the first matching absolute path (or ``None`` if no match). Otherwise return a list. """ searched_locations[:] = [] matches = [] for finder in get_finders(): result = finder.find(path, all=all) if not all and result: return result if not isinstance(result, (list, tuple)): result = [result] matches.extend(result) if matches: return matches # No match. return [] if all else None def get_finders(): for finder_path in settings.STATICFILES_FINDERS: yield get_finder(finder_path) @lru_cache.lru_cache(maxsize=None) def get_finder(import_path): """ Imports the staticfiles finder class described by import_path, where import_path is the full Python path to the class. """ Finder = import_string(import_path) if not issubclass(Finder, BaseFinder): raise ImproperlyConfigured('Finder "%s" is not a subclass of "%s"' % (Finder, BaseFinder)) return Finder()