range_response.py 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. import hashlib
  2. import mimetypes
  3. from datetime import datetime
  4. from pathlib import Path
  5. from typing import Generator
  6. from fastapi import Request
  7. from fastapi.responses import Response, StreamingResponse
  8. mimetypes.init()
  9. def get_range_response(file: Path, request: Request, chunk_size: int) -> Response:
  10. """Get a Response for the given file, supporting range-requests, E-Tag and Last-Modified."""
  11. file_size = file.stat().st_size
  12. last_modified_time = datetime.utcfromtimestamp(file.stat().st_mtime)
  13. start = 0
  14. end = file_size - 1
  15. status_code = 200
  16. e_tag = hashlib.md5((str(last_modified_time) + str(file_size)).encode()).hexdigest()
  17. if_match_header = request.headers.get('If-None-Match')
  18. if if_match_header and if_match_header == e_tag:
  19. return Response(status_code=304) # Not Modified
  20. headers = {
  21. 'E-Tag': e_tag,
  22. 'Last-Modified': last_modified_time.strftime("%a, %d %b %Y %H:%M:%S GMT")
  23. }
  24. range_header = request.headers.get('range')
  25. media_type = mimetypes.guess_type(str(file))[0] or 'application/octet-stream'
  26. if range_header is not None:
  27. byte1, byte2 = range_header.split('=')[1].split('-')
  28. start = int(byte1)
  29. if byte2:
  30. end = int(byte2)
  31. status_code = 206 # Partial Content
  32. content_length = end - start + 1
  33. headers.update({
  34. 'Content-Length': str(content_length),
  35. 'Content-Range': f'bytes {start}-{end}/{file_size}',
  36. 'Accept-Ranges': 'bytes',
  37. })
  38. def content_reader(file: Path, start: int, end: int) -> Generator[bytes, None, None]:
  39. with open(file, 'rb') as data:
  40. data.seek(start)
  41. remaining_bytes = end - start + 1
  42. while remaining_bytes > 0:
  43. chunk = data.read(min(chunk_size, remaining_bytes))
  44. if not chunk:
  45. break
  46. yield chunk
  47. remaining_bytes -= len(chunk)
  48. return StreamingResponse(
  49. content_reader(file, start, end),
  50. media_type=media_type,
  51. headers=headers,
  52. status_code=status_code,
  53. )