294 lines
9.5 KiB
Python
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
|