1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
|
import contextlib
import uuid
from contextvars import ContextVar
from os.path import join, normpath
from django.contrib.staticfiles import finders, storage
from django.dispatch import Signal
from django.utils.translation import gettext_lazy as _, ngettext
from debug_toolbar import panels
# This will record and map the StaticFile instances with its associated
# request across threads and async concurrent requests state.
request_id_context_var = ContextVar("djdt_request_id_store")
record_static_file_signal = Signal()
class URLMixin:
def url(self, path, *args, **kwargs):
url = super().url(path, *args, **kwargs)
with contextlib.suppress(LookupError):
# For LookupError:
# The ContextVar wasn't set yet. Since the toolbar wasn't properly
# configured to handle this request, we don't need to capture
# the static file.
request_id = request_id_context_var.get()
record_static_file_signal.send(
sender=self,
staticfile=(str(path), url, finders.find(str(path))),
request_id=request_id,
)
return url
class StaticFilesPanel(panels.Panel):
"""
A panel to display the found staticfiles.
"""
is_async = True
name = "Static files"
template = "debug_toolbar/panels/staticfiles.html"
@property
def title(self):
stats = self.get_stats()
return _("Static files (%(num_found)s found, %(num_used)s used)") % {
"num_found": stats.get("num_found"),
"num_used": stats.get("num_used"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.num_found = 0
self.used_paths = set()
self.request_id = str(uuid.uuid4())
@classmethod
def ready(cls):
cls = storage.staticfiles_storage.__class__
if URLMixin not in cls.mro():
cls.__bases__ = (URLMixin, *cls.__bases__)
def _store_static_files_signal_handler(self, sender, staticfile, **kwargs):
# Only record the static file if the request_id matches the one
# that was used to create the panel.
# as sender of the signal and this handler will have multiple
# concurrent connections and we want to avoid storing of same
# staticfile from other connections as well.
if request_id_context_var.get() == self.request_id:
self.used_paths.add(staticfile)
def enable_instrumentation(self):
self.ctx_token = request_id_context_var.set(self.request_id)
record_static_file_signal.connect(self._store_static_files_signal_handler)
def disable_instrumentation(self):
record_static_file_signal.disconnect(self._store_static_files_signal_handler)
request_id_context_var.reset(self.ctx_token)
nav_title = _("Static files")
@property
def nav_subtitle(self):
num_used = self.get_stats().get("num_used")
return ngettext(
"%(num_used)s file used", "%(num_used)s files used", num_used
) % {"num_used": num_used}
def generate_stats(self, request, response):
self.record_stats(
{
"num_found": self.num_found,
"num_used": len(self.used_paths),
"staticfiles": sorted(self.used_paths),
"staticfiles_apps": self.get_staticfiles_apps(),
"staticfiles_dirs": self.get_staticfiles_dirs(),
"staticfiles_finders": self.get_staticfiles_finders(),
}
)
def get_staticfiles_finders(self):
"""
Returns a sorted mapping between the finder path and the list
of relative and file system paths which that finder was able
to find.
"""
finders_mapping = {}
for finder in finders.get_finders():
try:
for path, finder_storage in finder.list([]):
if getattr(finder_storage, "prefix", None):
prefixed_path = join(finder_storage.prefix, path)
else:
prefixed_path = path
finder_cls = finder.__class__
finder_path = ".".join([finder_cls.__module__, finder_cls.__name__])
real_path = finder_storage.path(path)
payload = (prefixed_path, real_path)
finders_mapping.setdefault(finder_path, []).append(payload)
self.num_found += 1
except OSError:
# This error should be captured and presented as a part of run_checks.
pass
return finders_mapping
def get_staticfiles_dirs(self):
"""
Returns a list of paths to inspect for additional static files
"""
dirs = []
for finder in finders.get_finders():
if isinstance(finder, finders.FileSystemFinder):
dirs.extend(finder.locations)
return [(prefix, normpath(dir)) for prefix, dir in dirs]
def get_staticfiles_apps(self):
"""
Returns a list of app paths that have a static directory
"""
apps = []
for finder in finders.get_finders():
if isinstance(finder, finders.AppDirectoriesFinder):
for app in finder.apps:
if app not in apps:
apps.append(app)
return apps
|