I'm trying to implement xhtml2pdf, and in my html file, it references a static file. I have the file in my project at jobs/static/jobs/style.css
. When using the code xhtml2pdf provides in their docs, I am getting an error when it gets to result = finders.find(uri)
saying django.core.exceptions.SuspiciousFileOperation: The joined path (/static/jobs/css/style.css) is located outside of the base path component (/opt/project/myproject/static)
. If I navigate to the url that the file should be at, http://127.0.0.1:8000/static/jobs/css/style.css
, I see the code of the css file, so it is in the generally correct location.
This project is being served in a docker container, and there is nothing stored in /opt, so I don't know why it coming up with this "base path component" at (/opt/project/myproject/static). I don't know where it is getting this path. I have searched my entire project, and there is no path, in any file, that includes opt
or project
. The project is stored in /app in the container.
Here is the code I got from their site:
from xhtml2pdf import pisa
from django.template.loader import get_template
from django.http import HttpResponse
from django.conf import settings
import os
import io
from django.contrib.staticfiles import finders
def link_callback(uri, rel):
"""
Convert HTML URIs to absolute system paths so xhtml2pdf can access those
resources
"""
result = finders.find(uri)
if result:
if not isinstance(result, (list, tuple)):
result = [result]
result = list(os.path.realpath(path) for path in result)
path = result[0]
else:
sUrl = settings.STATIC_URL # Typically /static/
sRoot = settings.STATIC_ROOT # Typically /home/userX/project_static/
mUrl = settings.MEDIA_URL # Typically /media/
mRoot = settings.MEDIA_ROOT # Typically /home/userX/project_static/media/
if uri.startswith(mUrl):
path = os.path.join(mRoot, uri.replace(mUrl, ""))
elif uri.startswith(sUrl):
path = os.path.join(sRoot, uri.replace(sUrl, ""))
else:
return uri
# make sure that file exists
if not os.path.isfile(path):
raise Exception(
'media URI must start with %s or %s' % (sUrl, mUrl)
)
return path
def render_to_pdf(template_src, context_dict, bytes=False):
template = get_template(template_src)
context = context_dict
if bytes:
response = io.BytesIO()
else:
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="contract.pdf"'
html = template.render(context)
pisa_status = pisa.CreatePDF(html, dest=response, link_callback=link_callback)
# if error then show some funny view
if pisa_status.err:
return HttpResponse('We had some errors <pre>' + html + '</pre>')
return response
Here is the relevant portion of my settings.py:
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent # Resolves to /app
APPS_DIR = ROOT_DIR / "myproject" # Resolves to /app/myproject
STATIC_ROOT = str(ROOT_DIR / "staticfiles") # Resolves to /app/staticfiles
STATIC_URL = "/static/"
STATICFILES_DIRS = [
str(APPS_DIR / "static"),
str(ROOT_DIR / "node_modules"),
] # Resolves to ['/app/myproject/static', '/app/node_modules']
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
MEDIA_ROOT = str(APPS_DIR / "media") # Resolves to /app/media/myproject/media
MEDIA_URL = "/media/"
The line I have in my html template that is being the problem is:
<link rel="stylesheet" href="{% static 'jobs/css/style.css' %}">
I have another project I have been using this in for about a year, using this same code, and it has been working without this error. The other project is not inside a docker container, but I don't see why that should make a difference.
Where else should I look?
When integrating xhtml2pdf
with Django, especially within Docker, you might encounter file path issues due to the way Django handles static files. The SuspiciousFileOperation
error typically arises when a path is evaluated as being outside the expected base directory.
Here's a consolidated solution, breaking down the possible causes and providing the recommended fixes:
Static Files Handling:
xhtml2pdf
requires absolute system paths to access static and media resources. When running in a Docker container, the file paths inside the container might differ from those on the host machine. Ensure your static settings are properly set up to reflect the paths within the Docker container.
Enhance the link_callback
Function:
Modify the link_callback
function to cater to both absolute and relative paths. The function should ideally:
Here's an optimized link_callback
function using settings approach:
import os
from django.conf import settings
def link_callback(uri, rel):
if uri.startswith(settings.MEDIA_URL):
path = os.path.join(settings.MEDIA_ROOT, uri.replace(settings.MEDIA_URL, ""))
elif uri.startswith(settings.STATIC_URL):
path = os.path.join(settings.STATIC_ROOT, uri.replace(settings.STATIC_URL, ""))
else:
return uri
# Ensure the file exists
if not os.path.isfile(path):
raise Exception(f"Path does not exist: {path}")
return path
If you're using finders
, you're trying to get the actual path of the file based on its URL. The problem might arise if the path returned is not what you expect due to different folder structures or volume mappings inside the Docker container.
from django.contrib.staticfiles import finders
path = finders.find(uri)
Given your current scenario and the SuspiciousFileOperation
error you're facing, I recommend sticking to the settings
approach. It's more straightforward and makes fewer assumptions about your project's structure.
However, if you prefer to use finders
, ensure you understand how it's resolving paths and make sure the resolved paths align with the actual file structure inside your Docker container.
Docker Volume Mapping:
Ensure that the volume mapping between your host machine and Docker container is correctly configured. Double-check the Docker Compose or Docker command to verify that the application directory (/app
in your case) is accurately mapped.
Collect Static Files:
If DEBUG
is set to False
in Django settings, make sure to run collectstatic
to gather all static files in the STATIC_ROOT
location:
python manage.py collectstatic
This step ensures that all static files are available in the designated STATIC_ROOT
directory, allowing xhtml2pdf
to access them.
File Permissions:
Sometimes, it might be a permission issue. Ensure that the user running the Django application inside the Docker container has the necessary permissions to access and read the static and media files.
By combining these recommendations, you should be able to successfully generate PDFs with xhtml2pdf
in your Django project running inside a Docker container.