serve_static.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import os
  2. import stat
  3. import posixpath
  4. import mimetypes
  5. from pathlib import Path
  6. from django.contrib.staticfiles import finders
  7. from django.views import static
  8. from django.http import StreamingHttpResponse, Http404, HttpResponse, HttpResponseNotModified
  9. from django.utils._os import safe_join
  10. from django.utils.http import http_date
  11. from django.utils.translation import gettext as _
  12. def serve_static_with_byterange_support(request, path, document_root=None, show_indexes=False):
  13. """
  14. Overrides Django's built-in django.views.static.serve function to support byte range requests.
  15. This allows you to do things like seek into the middle of a huge mp4 or WACZ without downloading the whole file.
  16. https://github.com/satchamo/django/commit/2ce75c5c4bee2a858c0214d136bfcd351fcde11d
  17. """
  18. assert document_root
  19. path = posixpath.normpath(path).lstrip("/")
  20. fullpath = Path(safe_join(document_root, path))
  21. if os.access(fullpath, os.R_OK) and fullpath.is_dir():
  22. if show_indexes:
  23. return static.directory_index(path, fullpath)
  24. raise Http404(_("Directory indexes are not allowed here."))
  25. if not os.access(fullpath, os.R_OK):
  26. raise Http404(_("“%(path)s” does not exist") % {"path": fullpath})
  27. # Respect the If-Modified-Since header.
  28. statobj = fullpath.stat()
  29. if not static.was_modified_since(request.META.get("HTTP_IF_MODIFIED_SINCE"), statobj.st_mtime):
  30. return HttpResponseNotModified()
  31. content_type, encoding = mimetypes.guess_type(str(fullpath))
  32. content_type = content_type or "application/octet-stream"
  33. # setup resposne object
  34. ranged_file = RangedFileReader(open(fullpath, "rb"))
  35. response = StreamingHttpResponse(ranged_file, content_type=content_type)
  36. response.headers["Last-Modified"] = http_date(statobj.st_mtime)
  37. # handle byte-range requests by serving chunk of file
  38. if stat.S_ISREG(statobj.st_mode):
  39. size = statobj.st_size
  40. response["Content-Length"] = size
  41. response["Accept-Ranges"] = "bytes"
  42. response["X-Django-Ranges-Supported"] = "1"
  43. # Respect the Range header.
  44. if "HTTP_RANGE" in request.META:
  45. try:
  46. ranges = parse_range_header(request.META['HTTP_RANGE'], size)
  47. except ValueError:
  48. ranges = None
  49. # only handle syntactically valid headers, that are simple (no
  50. # multipart byteranges)
  51. if ranges is not None and len(ranges) == 1:
  52. start, stop = ranges[0]
  53. if stop > size:
  54. # requested range not satisfiable
  55. return HttpResponse(status=416)
  56. ranged_file.start = start
  57. ranged_file.stop = stop
  58. response["Content-Range"] = "bytes %d-%d/%d" % (start, stop - 1, size)
  59. response["Content-Length"] = stop - start
  60. response.status_code = 206
  61. if encoding:
  62. response.headers["Content-Encoding"] = encoding
  63. return response
  64. def serve_static(request, path, **kwargs):
  65. """
  66. Serve static files below a given point in the directory structure or
  67. from locations inferred from the staticfiles finders.
  68. To use, put a URL pattern such as::
  69. from django.contrib.staticfiles import views
  70. path('<path:path>', views.serve)
  71. in your URLconf.
  72. It uses the django.views.static.serve() view to serve the found files.
  73. """
  74. normalized_path = posixpath.normpath(path).lstrip("/")
  75. absolute_path = finders.find(normalized_path)
  76. if not absolute_path:
  77. if path.endswith("/") or path == "":
  78. raise Http404("Directory indexes are not allowed here.")
  79. raise Http404("'%s' could not be found" % path)
  80. document_root, path = os.path.split(absolute_path)
  81. return serve_static_with_byterange_support(request, path, document_root=document_root, **kwargs)
  82. def parse_range_header(header, resource_size):
  83. """
  84. Parses a range header into a list of two-tuples (start, stop) where `start`
  85. is the starting byte of the range (inclusive) and `stop` is the ending byte
  86. position of the range (exclusive).
  87. Returns None if the value of the header is not syntatically valid.
  88. https://github.com/satchamo/django/commit/2ce75c5c4bee2a858c0214d136bfcd351fcde11d
  89. """
  90. if not header or "=" not in header:
  91. return None
  92. ranges = []
  93. units, range_ = header.split("=", 1)
  94. units = units.strip().lower()
  95. if units != "bytes":
  96. return None
  97. for val in range_.split(","):
  98. val = val.strip()
  99. if "-" not in val:
  100. return None
  101. if val.startswith("-"):
  102. # suffix-byte-range-spec: this form specifies the last N bytes of an
  103. # entity-body
  104. start = resource_size + int(val)
  105. if start < 0:
  106. start = 0
  107. stop = resource_size
  108. else:
  109. # byte-range-spec: first-byte-pos "-" [last-byte-pos]
  110. start, stop = val.split("-", 1)
  111. start = int(start)
  112. # the +1 is here since we want the stopping point to be exclusive, whereas in
  113. # the HTTP spec, the last-byte-pos is inclusive
  114. stop = int(stop) + 1 if stop else resource_size
  115. if start >= stop:
  116. return None
  117. ranges.append((start, stop))
  118. return ranges
  119. class RangedFileReader:
  120. """
  121. Wraps a file like object with an iterator that runs over part (or all) of
  122. the file defined by start and stop. Blocks of block_size will be returned
  123. from the starting position, up to, but not including the stop point.
  124. https://github.com/satchamo/django/commit/2ce75c5c4bee2a858c0214d136bfcd351fcde11d
  125. """
  126. block_size = 8192
  127. def __init__(self, file_like, start=0, stop=float("inf"), block_size=None):
  128. self.f = file_like
  129. self.block_size = block_size or RangedFileReader.block_size
  130. self.start = start
  131. self.stop = stop
  132. def __iter__(self):
  133. self.f.seek(self.start)
  134. position = self.start
  135. while position < self.stop:
  136. data = self.f.read(min(self.block_size, self.stop - position))
  137. if not data:
  138. break
  139. yield data
  140. position += self.block_size