| 1 | """
|
|---|
| 2 | Custom views for serving files with Unicode filename support.
|
|---|
| 3 | """
|
|---|
| 4 | import os
|
|---|
| 5 | import mimetypes
|
|---|
| 6 | from django.http import FileResponse, Http404
|
|---|
| 7 | from django.utils.http import http_date
|
|---|
| 8 | from django.views.static import was_modified_since
|
|---|
| 9 | from pathlib import Path
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 | def serve_unicode(request, path, document_root=None):
|
|---|
| 13 | """
|
|---|
| 14 | Serve static files, handling Unicode filenames correctly under Apache/WSGI.
|
|---|
| 15 |
|
|---|
| 16 | This is a modified version of django.views.static.serve that properly
|
|---|
| 17 | handles UTF-8 filenames in Apache/WSGI environments where the default
|
|---|
| 18 | encoding is ASCII.
|
|---|
| 19 | """
|
|---|
| 20 | # Reconstruct the full path
|
|---|
| 21 | path = path.lstrip('/')
|
|---|
| 22 |
|
|---|
| 23 | if document_root is not None:
|
|---|
| 24 | # Handle Unicode path encoding issues in Apache/WSGI
|
|---|
| 25 | if isinstance(path, str):
|
|---|
| 26 | try:
|
|---|
| 27 | # Fix UTF-8 mojibake: UTF-8 bytes incorrectly decoded as Latin-1
|
|---|
| 28 | path = path.encode('latin-1').decode('utf-8')
|
|---|
| 29 | except (UnicodeDecodeError, UnicodeEncodeError):
|
|---|
| 30 | # Path is already correctly decoded
|
|---|
| 31 | pass
|
|---|
| 32 |
|
|---|
| 33 | # Security check - ensure path doesn't contain '..'
|
|---|
| 34 | if '..' in path.split('/'):
|
|---|
| 35 | raise Http404("Invalid path")
|
|---|
| 36 |
|
|---|
| 37 | # Build full path as string first
|
|---|
| 38 | fullpath_str = os.path.join(document_root, path)
|
|---|
| 39 |
|
|---|
| 40 | # Normalize the path
|
|---|
| 41 | fullpath_str = os.path.normpath(fullpath_str)
|
|---|
| 42 |
|
|---|
| 43 | # Verify it's still within document_root
|
|---|
| 44 | if not fullpath_str.startswith(os.path.normpath(document_root)):
|
|---|
| 45 | raise Http404("Invalid path")
|
|---|
| 46 |
|
|---|
| 47 | # Encode to UTF-8 bytes explicitly for filesystem operations
|
|---|
| 48 | try:
|
|---|
| 49 | fullpath_bytes = fullpath_str.encode('utf-8')
|
|---|
| 50 | except UnicodeEncodeError:
|
|---|
| 51 | raise Http404("Invalid filename encoding")
|
|---|
| 52 |
|
|---|
| 53 | # Check if file exists using bytes path
|
|---|
| 54 | try:
|
|---|
| 55 | if not os.path.exists(fullpath_bytes):
|
|---|
| 56 | raise Http404("File not found")
|
|---|
| 57 |
|
|---|
| 58 | if not os.path.isfile(fullpath_bytes):
|
|---|
| 59 | raise Http404("Not a file")
|
|---|
| 60 | except OSError:
|
|---|
| 61 | raise Http404("File access error")
|
|---|
| 62 |
|
|---|
| 63 | # Get file stats using bytes path
|
|---|
| 64 | try:
|
|---|
| 65 | statobj = os.stat(fullpath_bytes)
|
|---|
| 66 | except OSError:
|
|---|
| 67 | raise Http404("Cannot access file")
|
|---|
| 68 |
|
|---|
| 69 | # Check if-modified-since header
|
|---|
| 70 | if not was_modified_since(
|
|---|
| 71 | request.META.get('HTTP_IF_MODIFIED_SINCE'),
|
|---|
| 72 | statobj.st_mtime
|
|---|
| 73 | ):
|
|---|
| 74 | from django.http import HttpResponseNotModified
|
|---|
| 75 | return HttpResponseNotModified()
|
|---|
| 76 |
|
|---|
| 77 | # Determine content type
|
|---|
| 78 | content_type, encoding = mimetypes.guess_type(fullpath_str)
|
|---|
| 79 | content_type = content_type or 'application/octet-stream'
|
|---|
| 80 |
|
|---|
| 81 | # Open file using bytes path to avoid encoding issues
|
|---|
| 82 | try:
|
|---|
| 83 | # Use the byte path for opening
|
|---|
| 84 | response = FileResponse(open(fullpath_bytes, 'rb'), content_type=content_type)
|
|---|
| 85 | except OSError:
|
|---|
| 86 | raise Http404("Cannot read file")
|
|---|
| 87 |
|
|---|
| 88 | response['Last-Modified'] = http_date(statobj.st_mtime)
|
|---|
| 89 |
|
|---|
| 90 | # Handle Content-Disposition for download
|
|---|
| 91 | if 'filename' in request.GET:
|
|---|
| 92 | filename = request.GET['filename']
|
|---|
| 93 | # Encode filename for Content-Disposition header (RFC 6266)
|
|---|
| 94 | response['Content-Disposition'] = f'attachment; filename*=UTF-8\'\'{filename}'
|
|---|
| 95 |
|
|---|
| 96 | response['Content-Length'] = statobj.st_size
|
|---|
| 97 |
|
|---|
| 98 | if encoding:
|
|---|
| 99 | response['Content-Encoding'] = encoding
|
|---|
| 100 |
|
|---|
| 101 | return response
|
|---|
| 102 |
|
|---|
| 103 | raise Http404("Document root not specified")
|
|---|