Search code examples
djangostatic-filesxhtml2pdf

Django giving error when trying to get path to static file


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?


Solution

  • 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:

    1. 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.

    2. Enhance the link_callback Function:

      Modify the link_callback function to cater to both absolute and relative paths. The function should ideally:

      • Convert relative URLs to absolute system paths.
      • Verify that the constructed path exists on the filesystem.

      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.

    3. 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.

    4. 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.

    5. 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.