rfiles-online/portal/views.py

294 lines
9.5 KiB
Python

"""
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