fix: file upload defaults private, internal network access, video inline

- _save_uploaded_file: keep is_public=False by default (no auto-public)
- PublicFileView: internal network (192.168/10/172.16-31) serves any file;
  external only serves is_public=True files
- Video files: Content-Disposition inline for browser playback
- DRF API: fix misleading error message (files not files[])
This commit is contained in:
OpenClaw Sub-agent
2026-07-15 11:40:53 +10:00
parent 2dff02cd03
commit ee98e18302
+36 -6
View File
@@ -78,7 +78,7 @@ class FileDownloadView(View):
if not os.path.exists(record.file_path):
raise Http404("File not found on disk")
FileUpload.objects.filter(pk=pk).update(download_count=F('download_count') + 1)
disposition = 'inline' if record.is_image else f'attachment; filename="{record.name}"'
disposition = 'inline' if (record.is_image or record.mime_type.startswith('video/')) else f'attachment; filename="{record.name}"'
response = FileResponse(open(record.file_path, 'rb'), content_type=record.mime_type)
response['Content-Disposition'] = disposition
return response
@@ -131,14 +131,44 @@ class FileSetExpiryView(View):
class PublicFileView(View):
"""Serve uploaded files via /public/files/{uuid}-{name}.
Access rules:
- Internal network (192.168.x.x, 10.x.x.x, 172.16-31.x.x): serve any file
- External (go.junv.cc, xgo.junv.cc): only serve is_public=True files
- Videos and images stream inline; other files force download.
"""
INTERNAL_NETS = ('192.168.', '10.', '172.16.', '172.17.', '172.18.',
'172.19.', '172.20.', '172.21.', '172.22.', '172.23.',
'172.24.', '172.25.', '172.26.', '172.27.', '172.28.',
'172.29.', '172.30.', '172.31.')
def _is_internal(self, request):
xff = request.META.get('HTTP_X_FORWARDED_FOR', '')
client_ip = xff.split(',')[0].strip() if xff else request.META.get('REMOTE_ADDR', '')
return any(client_ip.startswith(net) for net in self.INTERNAL_NETS)
def get(self, request, pk, filename=''):
record = get_object_or_404(FileUpload, pk=pk, is_public=True)
record = get_object_or_404(FileUpload, pk=pk)
# External access: only public files
if not self._is_internal(request) and not record.is_public:
raise Http404("This file is private")
if record.is_expired:
raise Http404("This public link has expired")
raise Http404("This link has expired")
if not os.path.exists(record.file_path):
raise Http404("File not found")
FileUpload.objects.filter(pk=record.pk).update(download_count=F('download_count') + 1)
disposition = 'inline' if record.is_image else f'attachment; filename="{record.name}"'
# Inline for media, attachment for everything else
if record.is_image or record.mime_type.startswith('video/'):
disposition = 'inline'
else:
disposition = f'attachment; filename="{record.name}"'
response = FileResponse(open(record.file_path, 'rb'), content_type=record.mime_type)
response['Content-Disposition'] = disposition
return response
@@ -155,7 +185,7 @@ class FileUploadViewSet(viewsets.ModelViewSet):
def create(self, request, *args, **kwargs):
uploaded = request.FILES.getlist('files')
if not uploaded:
return Response({'error': 'No files provided. Use files[] field.'}, status=status.HTTP_400_BAD_REQUEST)
return Response({'error': 'No files provided. Use "files" field.'}, status=status.HTTP_400_BAD_REQUEST)
created = [_save_uploaded_file(f) for f in uploaded]
serializer = self.get_serializer(created, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -201,7 +231,7 @@ class FileUploadViewSet(viewsets.ModelViewSet):
if not os.path.exists(record.file_path):
raise Http404("File not found")
FileUpload.objects.filter(pk=pk).update(download_count=F('download_count') + 1)
disposition = 'inline' if record.is_image else f'attachment; filename="{record.name}"'
disposition = 'inline' if (record.is_image or record.mime_type.startswith('video/')) else f'attachment; filename="{record.name}"'
response = FileResponse(open(record.file_path, 'rb'), content_type=record.mime_type)
response['Content-Disposition'] = disposition
return response