From ee98e18302a90c453243c263ef014e6e7402df22 Mon Sep 17 00:00:00 2001 From: OpenClaw Sub-agent Date: Wed, 15 Jul 2026 11:40:40 +1000 Subject: [PATCH] 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[]) --- links/file_views.py | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/links/file_views.py b/links/file_views.py index 0dd180f..c3cb588 100644 --- a/links/file_views.py +++ b/links/file_views.py @@ -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