working FileIndex and user Profile pages/settings
This commit is contained in:
+102
-2
@@ -1,3 +1,103 @@
|
||||
from django.shortcuts import render
|
||||
import posixpath
|
||||
from pathlib import Path
|
||||
|
||||
# Create your views here.
|
||||
from django.conf import settings
|
||||
from django.http import FileResponse, Http404, HttpResponse, StreamingHttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from .models import FileIndex
|
||||
|
||||
|
||||
def _open_indexed_file(file_index):
|
||||
"""Return an open binary file handle for the indexed file (404-safe)."""
|
||||
stored = file_index.stored_path
|
||||
if not stored or posixpath.isabs(stored) or '..' in stored.split('/'):
|
||||
raise Http404
|
||||
file_path = (Path(settings.MEDIA_ROOT) / stored).resolve()
|
||||
if not file_path.is_relative_to(Path(settings.MEDIA_ROOT).resolve()):
|
||||
raise Http404
|
||||
if not file_path.is_file():
|
||||
raise Http404
|
||||
return open(file_path, 'rb')
|
||||
|
||||
|
||||
def _unsatisfiable(content_length):
|
||||
resp = HttpResponse(status=416)
|
||||
resp['Content-Range'] = f'bytes */{content_length}'
|
||||
return resp
|
||||
|
||||
|
||||
def file_request(request, file_id):
|
||||
"""Serve an indexed file behind the gate, honouring HTTP byte ranges.
|
||||
|
||||
GET /api/files/<uuid>/ → full file (200)
|
||||
Range: bytes=0-99 → partial content (206)
|
||||
Range: bytes=<start>- → tail from <start>
|
||||
Range: bytes=-<suffix> → last <suffix> bytes
|
||||
?download=1 → Content-Disposition: attachment
|
||||
"""
|
||||
file_index = get_object_or_404(FileIndex, uuid=file_id)
|
||||
fh = _open_indexed_file(file_index)
|
||||
|
||||
content_type = file_index.content_type or 'application/octet-stream'
|
||||
content_length = file_index.size or 0
|
||||
disposition = ''
|
||||
if request.GET.get('download') == '1':
|
||||
filename = file_index.original_filename or posixpath.basename(file_index.stored_path)
|
||||
disposition = f'attachment; filename="{filename}"'
|
||||
|
||||
range_header = request.META.get('HTTP_RANGE', '').strip()
|
||||
if not range_header or not range_header.lower().startswith('bytes='):
|
||||
response = FileResponse(fh, content_type=content_type)
|
||||
response['Accept-Ranges'] = 'bytes'
|
||||
response['Content-Length'] = str(content_length)
|
||||
if disposition:
|
||||
response['Content-Disposition'] = disposition
|
||||
return response
|
||||
|
||||
try:
|
||||
start, _, end = range_header[6:].partition('-')
|
||||
start = int(start) if start else None
|
||||
end = int(end) if end else None
|
||||
except ValueError:
|
||||
return _unsatisfiable(content_length)
|
||||
|
||||
if start is None and end is None:
|
||||
return _unsatisfiable(content_length)
|
||||
|
||||
if start is None:
|
||||
suffix = end
|
||||
if suffix <= 0:
|
||||
return _unsatisfiable(content_length)
|
||||
start = max(content_length - suffix, 0)
|
||||
end = content_length - 1
|
||||
elif end is None:
|
||||
end = content_length - 1
|
||||
|
||||
if start < 0 or start >= content_length or end < start:
|
||||
return _unsatisfiable(content_length)
|
||||
|
||||
end = min(end, content_length - 1)
|
||||
length = end - start + 1
|
||||
fh.seek(start)
|
||||
|
||||
def chunk_generator():
|
||||
try:
|
||||
remaining = length
|
||||
while remaining > 0:
|
||||
data = fh.read(min(8192, remaining))
|
||||
if not data:
|
||||
break
|
||||
remaining -= len(data)
|
||||
yield data
|
||||
finally:
|
||||
fh.close()
|
||||
|
||||
part = StreamingHttpResponse(chunk_generator(), content_type=content_type)
|
||||
part['Content-Range'] = f'bytes {start}-{end}/{content_length}'
|
||||
part['Accept-Ranges'] = 'bytes'
|
||||
part['Content-Length'] = str(length)
|
||||
part.status_code = 206
|
||||
if disposition:
|
||||
part['Content-Disposition'] = disposition
|
||||
return part
|
||||
|
||||
Reference in New Issue
Block a user