rfiles-online/portal/views_shared_space.py

231 lines
7.7 KiB
Python

"""
Topic-based file sharing views.
Each topic is accessible via a subdomain (e.g., cofi.rfiles.online).
Anyone can view files and upload new ones - no password required.
"""
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse, Http404
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from files.models import SharedSpace, MediaFile, PublicShare
def get_topic_or_404(request):
"""Get the topic (shared space) from the request's subdomain slug."""
slug = getattr(request, 'shared_space_slug', None)
if not slug:
raise Http404("Topic not found")
return get_object_or_404(SharedSpace, slug=slug, is_active=True)
class SharedSpaceLoginView(View):
"""Redirect login to home - no login needed for topics."""
def get(self, request):
return redirect('shared_space_home')
def post(self, request):
return redirect('shared_space_home')
class SharedSpaceLogoutView(View):
"""Redirect to home - no logout needed."""
def get(self, request):
return redirect('shared_space_home')
class SharedSpaceHomeView(View):
"""Main upload page for topic - upload zone + files."""
def get(self, request):
space = get_topic_or_404(request)
recent_files = space.files.all().order_by('-created_at')[:20]
return render(request, 'portal/shared_space/home.html', {
'space': space,
'recent_files': recent_files,
})
@method_decorator(csrf_exempt, name='dispatch')
class SharedSpaceUploadAPIView(View):
"""Handle file uploads via AJAX for topic."""
def post(self, request):
space = get_topic_or_404(request)
if not request.FILES.get('file'):
return JsonResponse({'error': 'No file provided'}, status=400)
uploaded_file = request.FILES['file']
if space.max_file_size_mb > 0:
max_size_bytes = space.max_file_size_mb * 1024 * 1024
if uploaded_file.size > max_size_bytes:
return JsonResponse({
'error': f'File too large. Maximum size is {space.max_file_size_mb}MB'
}, status=400)
action = request.POST.get('action', '')
# Check for duplicate filename in this space
existing = MediaFile.objects.filter(
shared_space=space,
original_filename=uploaded_file.name,
).first()
if existing and action != 'overwrite':
existing_share = existing.public_shares.filter(is_active=True).first()
return JsonResponse({
'duplicate': True,
'existing_file': {
'id': str(existing.id),
'title': existing.title,
'filename': existing.original_filename,
'size': existing.file_size,
'share_url': existing_share.get_public_url() if existing_share else None,
},
}, status=409)
# Overwrite: delete the old file and its shares
if existing and action == 'overwrite':
existing.file.delete(save=False)
existing.delete()
title = request.POST.get('title', '') or uploaded_file.name
description = request.POST.get('description', '')
media_file = MediaFile.objects.create(
file=uploaded_file,
original_filename=uploaded_file.name,
title=title,
description=description,
mime_type=uploaded_file.content_type or 'application/octet-stream',
uploaded_by=request.user if request.user.is_authenticated else None,
shared_space=space,
)
share = PublicShare.objects.create(
media_file=media_file,
created_by=request.user if request.user.is_authenticated else None,
note=f'Uploaded to topic: {space.slug}',
)
return JsonResponse({
'success': True,
'file': {
'id': str(media_file.id),
'title': media_file.title,
'filename': media_file.original_filename,
'size': media_file.file_size,
'mime_type': media_file.mime_type,
},
'share': {
'token': share.token,
'url': share.get_public_url(),
}
})
@method_decorator(csrf_exempt, name='dispatch')
class DirectUploadAPIView(View):
"""Handle uploads via direct.rfiles.online (bypasses Cloudflare).
Space slug is passed as a form field instead of from the subdomain.
"""
def post(self, request):
space_slug = request.POST.get('space', '')
if not space_slug:
return JsonResponse({'error': 'Missing space parameter'}, status=400)
space = get_object_or_404(SharedSpace, slug=space_slug, is_active=True)
if not request.FILES.get('file'):
return JsonResponse({'error': 'No file provided'}, status=400)
uploaded_file = request.FILES['file']
if space.max_file_size_mb > 0:
max_size_bytes = space.max_file_size_mb * 1024 * 1024
if uploaded_file.size > max_size_bytes:
return JsonResponse({
'error': f'File too large. Maximum size is {space.max_file_size_mb}MB'
}, status=400)
action = request.POST.get('action', '')
existing = MediaFile.objects.filter(
shared_space=space,
original_filename=uploaded_file.name,
).first()
if existing and action != 'overwrite':
existing_share = existing.public_shares.filter(is_active=True).first()
return JsonResponse({
'duplicate': True,
'existing_file': {
'id': str(existing.id),
'title': existing.title,
'filename': existing.original_filename,
'size': existing.file_size,
'share_url': existing_share.get_public_url() if existing_share else None,
},
}, status=409)
if existing and action == 'overwrite':
existing.file.delete(save=False)
existing.delete()
title = request.POST.get('title', '') or uploaded_file.name
description = request.POST.get('description', '')
media_file = MediaFile.objects.create(
file=uploaded_file,
original_filename=uploaded_file.name,
title=title,
description=description,
mime_type=uploaded_file.content_type or 'application/octet-stream',
uploaded_by=request.user if request.user.is_authenticated else None,
shared_space=space,
)
share = PublicShare.objects.create(
media_file=media_file,
created_by=request.user if request.user.is_authenticated else None,
note=f'Uploaded to topic: {space.slug}',
)
return JsonResponse({
'success': True,
'file': {
'id': str(media_file.id),
'title': media_file.title,
'filename': media_file.original_filename,
'size': media_file.file_size,
'mime_type': media_file.mime_type,
},
'share': {
'token': share.token,
'url': share.get_public_url(),
}
})
class SharedSpaceFileListView(View):
"""List all files in the topic."""
def get(self, request):
space = get_topic_or_404(request)
files = space.files.all().order_by('-created_at')
return render(request, 'portal/shared_space/files.html', {
'space': space,
'files': files,
})