""" Upload portal views - clean interface for file uploads and sharing. """ import json import os import re from django.shortcuts import render, get_object_or_404, redirect from django.http import JsonResponse, FileResponse, HttpResponse from django.views import View from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator from django.conf import settings from django.contrib.staticfiles import finders from urllib.parse import urlencode from files.models import MediaFile, PublicShare, FileAccessLog, SharedSpace class LandingPageView(View): """Landing page - enter a topic to start sharing.""" def get(self, request): active_topics = SharedSpace.objects.filter(is_active=True).order_by('-created_at')[:8] return render(request, 'portal/landing.html', { 'active_topics': active_topics, }) class CreateTopicView(View): """Create or go to a topic (SharedSpace without password).""" def post(self, request): topic = request.POST.get('topic', '').strip().lower() topic = re.sub(r'[^a-z0-9-]', '', topic) if not topic: return render(request, 'portal/landing.html', { 'error': 'Please enter a topic name', 'active_topics': SharedSpace.objects.filter(is_active=True)[:8], }) if len(topic) < 2: return render(request, 'portal/landing.html', { 'error': 'Topic name must be at least 2 characters', 'active_topics': SharedSpace.objects.filter(is_active=True)[:8], }) if topic in ('www', 'api', 'admin', 'static', 'media', 'app'): return render(request, 'portal/landing.html', { 'error': 'This name is reserved', 'active_topics': SharedSpace.objects.filter(is_active=True)[:8], }) space, created = SharedSpace.objects.get_or_create( slug=topic, defaults={ 'name': topic, 'is_active': True, } ) return redirect(f'https://{topic}.rfiles.online') class UploadPageView(View): """Main upload page - now redirects to landing.""" def get(self, request): return redirect('portal:landing') @method_decorator(csrf_exempt, name='dispatch') class UploadAPIView(View): """Handle file uploads via AJAX.""" def post(self, request): if not request.FILES.get('file'): return JsonResponse({'error': 'No file provided'}, status=400) uploaded_file = request.FILES['file'] 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, ) share = PublicShare.objects.create( media_file=media_file, created_by=request.user if request.user.is_authenticated else None, note='Auto-created from upload portal', ) 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 FileListView(View): """List all uploaded files.""" def get(self, request): files = MediaFile.objects.all()[:50] return render(request, 'portal/files.html', { 'files': files, }) class FileDetailView(View): """View details of a specific file.""" def get(self, request, file_id): media_file = get_object_or_404(MediaFile, id=file_id) shares = media_file.public_shares.all() return render(request, 'portal/file_detail.html', { 'file': media_file, 'shares': shares, }) @method_decorator(csrf_exempt, name='dispatch') class CreateShareView(View): """Create a new share link for a file.""" def post(self, request, file_id): media_file = get_object_or_404(MediaFile, id=file_id) try: data = json.loads(request.body) if request.body else {} except json.JSONDecodeError: data = {} from django.utils import timezone expires_hours = data.get('expires_in_hours') expires_at = None if expires_hours: expires_at = timezone.now() + timezone.timedelta(hours=int(expires_hours)) share = PublicShare.objects.create( media_file=media_file, created_by=request.user if request.user.is_authenticated else None, expires_at=expires_at, max_downloads=data.get('max_downloads'), note=data.get('note', ''), ) if data.get('password'): share.set_password(data['password']) return JsonResponse({ 'success': True, 'share': { 'id': str(share.id), 'token': share.token, 'url': share.get_public_url(), 'expires_at': share.expires_at.isoformat() if share.expires_at else None, } }) @method_decorator(csrf_exempt, name='dispatch') class DeleteFileView(View): """Delete a file.""" def post(self, request, file_id): media_file = get_object_or_404(MediaFile, id=file_id) media_file.delete() return JsonResponse({'success': True}) @method_decorator(csrf_exempt, name='dispatch') class RevokeShareView(View): """Revoke a share link.""" def post(self, request, share_id): share = get_object_or_404(PublicShare, id=share_id) share.is_active = False share.save() return JsonResponse({'success': True}) @method_decorator(csrf_exempt, name='dispatch') class ShareTargetView(View): """Handle Web Share Target API - receives shared content from Android/Chrome.""" def post(self, request): title = request.POST.get('title', '') text = request.POST.get('text', '') url = request.POST.get('url', '') files = request.FILES.getlist('files') uploaded_count = 0 share_urls = [] if files: for uploaded_file in files: file_title = title or uploaded_file.name description = f"{text}\n{url}".strip() if (text or url) else '' media_file = MediaFile.objects.create( file=uploaded_file, original_filename=uploaded_file.name, title=file_title, description=description, mime_type=uploaded_file.content_type or 'application/octet-stream', uploaded_by=request.user if request.user.is_authenticated else None, ) share = PublicShare.objects.create( media_file=media_file, created_by=request.user if request.user.is_authenticated else None, note='Shared from Android', ) share_urls.append(share.get_public_url()) uploaded_count += 1 params = {'shared': 'files', 'count': uploaded_count} return redirect(f"/?{urlencode(params)}") if url or text: params = {'shared': 'true'} if title: params['title'] = title if text: params['text'] = text if url: params['url'] = url return redirect(f"/?{urlencode(params)}") return redirect('/') def get(self, request): return redirect('/') class ServiceWorkerView(View): """Serve the service worker from root path for proper scope.""" def get(self, request): sw_path = finders.find('portal/sw.js') if sw_path: with open(sw_path, 'r') as f: content = f.read() else: static_path = os.path.join(settings.STATIC_ROOT or '', 'portal', 'sw.js') if os.path.exists(static_path): with open(static_path, 'r') as f: content = f.read() else: return HttpResponse('Service worker not found', status=404) response = HttpResponse(content, content_type='application/javascript') response['Service-Worker-Allowed'] = '/' response['Cache-Control'] = 'no-cache' return response class ManifestView(View): """Serve the manifest from root path for proper PWA detection.""" def get(self, request): manifest_path = finders.find('portal/manifest.json') if manifest_path: with open(manifest_path, 'r') as f: content = f.read() else: static_path = os.path.join(settings.STATIC_ROOT or '', 'portal', 'manifest.json') if os.path.exists(static_path): with open(static_path, 'r') as f: content = f.read() else: return HttpResponse('Manifest not found', status=404) response = HttpResponse(content, content_type='application/manifest+json') response['Cache-Control'] = 'no-cache' return response