from django.shortcuts import render, redirect # Import User from django.contrib.auth.models import User # Important to serialize your data to a form understandable by rest frameworks. #from .serlializers import NoteSerializer from rest_framework import serializers # Codes for rest_framework installation from rest_framework import viewsets from rest_framework.permissions import AllowAny from rest_framework.decorators import action from django.http import JsonResponse, HttpResponse #For Forms Importations from .forms import SignupForm #For Model Importations from .models import Profile #Importing lazy text from django.contrib import messages from django.utils.translation import gettext_lazy as _ #Importing the django login authentifications from django.contrib.auth import login #For handling db error in django from django.db import IntegrityError #For Cacheing from django.core.cache import cache import logging logger = logging.getLogger(__name__) #For google docs from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from googleapiclient.errors import HttpError import json #For time and date from datetime import datetime from django.utils import timezone from django.views.decorators.csrf import csrf_exempt import json #import requests from decimal import Decimal ####### will delete alot here# your_app/views.py import os import io import json import zipfile import tempfile import logging import requests from django.utils import timezone from django.conf import settings from django.shortcuts import redirect, render from django.http import HttpResponse, JsonResponse, StreamingHttpResponse from django.contrib import messages from rest_framework.decorators import action from google_auth_oauthlib.flow import Flow from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from googleapiclient.errors import HttpError from googleapiclient.http import MediaIoBaseDownload from google.auth.transport.requests import Request #Importation of the SignupForm class DummySerializer(serializers.Serializer): #I will add some data later on... #Simply used to derialize the information stored in the datatabse. pass class NotCloudStorage(viewsets.ModelViewSet): @action(detail=False, methods=['get'], url_path='') def default_page(self, request, *args, **kwargs): base_domain = request.build_absolute_uri('/') # Check if the user is authenticated if request.user.is_authenticated: # Redirect authenticated users to the home page #return redirect('/home') # Replace '/omoca/home_page/' with your actual home page URL return render(request, 'index.html',{'base_domain':base_domain}) else: # Redirect non-authenticated users to the login page return render(request, 'index.html',{'base_domain':base_domain}) class NotCloudStorageViewSet(viewsets.ModelViewSet): permission_classes = (AllowAny,) serializer_class = DummySerializer @action(detail=False, methods=['GET', 'POST'], url_path=r'signup') def signup(self, request): base_domain = request.build_absolute_uri('/') # Check if user came from social auth and needs profile completion social_login = request.user.is_authenticated and not hasattr(request.user, 'profile') # If user is authenticated and has profile, redirect to home if request.user.is_authenticated and hasattr(request.user, 'profile'): messages.info(request, _("You are already logged in.")) return redirect('/home/') if request.method == 'POST': if social_login: # Social login completion form = SignupForm(request.POST, social_login=True) else: # Manual registration form = SignupForm(request.POST) if form.is_valid(): cleaned_data = form.cleaned_data email = cleaned_data['email'] is_social_login = cleaned_data.get('social_login', False) try: if is_social_login and request.user.is_authenticated: # Social login completion user = request.user # Update user email if different if user.email != email: user.email = email user.save() # Create profile profile, created = Profile.objects.get_or_create( user=user, defaults={ 'email': email, 'agreed_to_terms': cleaned_data['agreed_to_terms'] } ) if created: messages.success(request, _("Account setup complete! Welcome to our platform!")) else: messages.info(request, _("Welcome back!")) return redirect('/home/') else: # Manual registration if User.objects.filter(email=email).exists(): emailError = _("An account with this email already exists. Please log in instead.") return render(request, 'signup.html', { 'form': form, 'emailError': emailError, 'base_domain': base_domain }) # Create user with unique username username = email counter = 1 while User.objects.filter(username=username).exists(): username = f"{email.split('@')[0]}{counter}" counter += 1 user = User.objects.create_user( username=username, email=email, password=cleaned_data['password1'] ) # Create profile Profile.objects.create( user=user, email=email, agreed_to_terms=cleaned_data['agreed_to_terms'] ) messages.success(request, _("Account created successfully! Please log in to continue.")) return redirect(f'{base_domain}login/') except IntegrityError as e: logger.error(f"IntegrityError during signup: {e}") messages.error(request, _("There was an error creating your account. Please try again.")) except Exception as e: logger.error(f"Error during signup: {e}") messages.error(request, _("An unexpected error occurred. Please try again.")) else: # Form validation failed return render(request, 'signup.html', { 'form': form, 'emailError': "", 'base_domain': base_domain }) else: # GET request if social_login: # Pre-populate for social auth completion form = SignupForm(initial={ 'email': request.user.email, 'social_login': True }, social_login=True) else: form = SignupForm() return render(request, 'signup.html', { 'form': form, 'emailError': "", 'base_domain': base_domain }) @action(detail=False, methods=['get', 'POST'], url_path=r'login') def admin_login(self, request): base_domain = request.build_absolute_uri('/') # If user is already authenticated, redirect to home if request.user.is_authenticated: messages.info(request, _("You are already logged in.")) return redirect(f'{base_domain}home/') if request.method == "POST": email = request.POST.get('username') password = request.POST.get('password') if email and password: user = User.objects.filter(email=email).first() if user: if user.check_password(password): login(request, user, backend='django.contrib.auth.backends.ModelBackend') messages.success(request, _("Welcome back! You have been successfully logged in.")) return redirect(f'{base_domain}home/') else: messages.error(request, _("Invalid password. Please try again.")) else: messages.error(request, _("Invalid email or password. Please try again.")) else: messages.error(request, _("Please enter both email and password.")) return render(request, 'login.html', {'base_domain': base_domain}) @action(detail=False, methods=['get'], url_path=r'home') def home(self, request): base_domain = request.build_absolute_uri('/') user = request.user # Sample data for authenticated users (you'll replace this with real data) context = { 'base_domain': base_domain, 'user': user, } # Add user-specific data if authenticated if user.is_authenticated: # You'll replace these with actual database queries context.update({ 'user_orders': [], # Will be actual orders from database 'user_stats': {}, # Will be actual stats from database }) return render(request, 'index.html', context) @action(detail=False, methods=['get'], url_path=r'logout') def logout(self, request): from django.contrib.auth import logout from django.contrib import messages # Log the user out logout(request) # Add a success message messages.success(request, _("You have been successfully logged out.")) # Redirect to login page or home page base_domain = request.build_absolute_uri('/') return redirect(f'{base_domain}login/') ########## CONNECTING CLOUD ACCOUNTS AND FILE SELECTION WITH REAL GOOGLE DRIVE API ########## @action(detail=False, methods=['get', 'post'], url_path=r'transfer/cloud-connect') def cloud_connect(self, request): """Step 1: Cloud Account Connection with Real Google Drive API""" base_domain = request.build_absolute_uri('/') user = request.user if not user.is_authenticated: return redirect(f'{base_domain}login/') context = { 'base_domain': base_domain, 'user': user, 'current_step': 1, 'cloud_services': self._get_cloud_services_info(user) } if request.method == 'POST': cloud_source = request.POST.get('cloud_source') if cloud_source in ['google_drive', 'dropbox', 'onedrive']: if cloud_source == 'google_drive': # Initiate Google OAuth flow request.session['selected_cloud'] = "google_drive" return redirect(f'{base_domain}oauth/login/google-oauth2/?next={base_domain}transfer/new/') else: # For other services, store selection and proceed request.session['selected_cloud'] = cloud_source request.session['cloud_connected'] = True messages.info(request, f"{cloud_source.replace('_', ' ').title()} connection initiated") print("I SHOULD GO TO THE TRANSFER WINDOW") return redirect(f'{base_domain}transfer/new/') else: messages.error(request, "Please select a valid cloud service") return render(request, 'transfer.html', context) def _get_cloud_services_info(self, user): """Get real Google Drive info and dummy data for other services""" google_drive_info = self._get_google_drive_info(user) return [ google_drive_info, #Replace with real API calls for Dropbox { 'id': 'dropbox', 'name': 'Dropbox', 'icon': 'dropbox', 'brand_color': '#0061FF', 'description': 'Access files from your Dropbox account', 'storage_quota': '2 GB free, upgradable', 'features': ['File sharing', 'Version history', 'Team folders'], 'connected': False, 'account_email': None, 'used_storage': '0 GB', 'total_storage': '2 GB', 'file_count': 0 }, #Replace with real API calls for Onedrive { 'id': 'onedrive', 'name': 'OneDrive', 'icon': 'microsoft', 'brand_color': '#0078D4', 'description': 'Access files from your Microsoft OneDrive', 'storage_quota': '5 GB free', 'features': ['Office Online', 'Personal vault', 'File sharing'], 'connected': False, 'account_email': None, 'used_storage': '0 GB', 'total_storage': '5 GB', 'file_count': 0 } ] def _get_google_drive_info(self, user): """Get real Google Drive information using the Google Drive API""" try: # Check if user has Google OAuth connected from social_django.models import UserSocialAuth social_auth = UserSocialAuth.objects.filter( user=user, provider='google-oauth2' ).first() if social_auth: # Get access token from social auth access_token = social_auth.extra_data.get('access_token') if access_token: # Create credentials object credentials = Credentials(token=access_token) print("Access Token:", access_token) # Build Google Drive service service = build('drive', 'v3', credentials=credentials) # Get storage quota about = service.about().get(fields="storageQuota,user").execute() storage_quota = about.get('storageQuota', {}) user_info = about.get('user', {}) # Get file count files = service.files().list( pageSize=1, fields="files(id,name),nextPageToken" ).execute() # Calculate file count (approximate) file_count = self._get_google_drive_file_count(service) # Format storage info used_bytes = int(storage_quota.get('usage', 0)) total_bytes = int(storage_quota.get('limit', 0)) used_gb = self._bytes_to_gb(used_bytes) total_gb = self._bytes_to_gb(total_bytes) usage_percentage = (used_bytes / total_bytes * 100) if total_bytes > 0 else 0 return { 'id': 'google_drive', 'name': 'Google Drive', 'icon': 'google-drive', 'brand_color': '#4285F4', 'description': 'Access files from your Google Drive account', 'storage_quota': f'{total_gb} GB total', 'features': ['Docs, Sheets, Slides', 'Photo backup', 'Team drives'], 'connected': True, 'account_email': user_info.get('emailAddress', 'Connected'), 'used_storage': f'{used_gb:.1f} GB', 'total_storage': f'{total_gb} GB', 'usage_percentage': round(usage_percentage, 1), 'file_count': file_count, 'storage_details': { 'used_bytes': used_bytes, 'total_bytes': total_bytes, 'used_gb': used_gb, 'total_gb': total_gb } } # Fallback for users without Google OAuth or if API call fails return { 'id': 'google_drive', 'name': 'Google Drive', 'icon': 'google-drive', 'brand_color': '#4285F4', 'description': 'Access files from your Google Drive account', 'storage_quota': 'Connect to view storage', 'features': ['Docs, Sheets, Slides', 'Photo backup', 'Team drives'], 'connected': False, 'account_email': None, 'used_storage': '0 GB', 'total_storage': '0 GB', 'file_count': 0 } except HttpError as error: print(f"Google Drive API error: {error}") # Return disconnected state on API error return { 'id': 'google_drive', 'name': 'Google Drive', 'icon': 'google-drive', 'brand_color': '#4285F4', 'description': 'Access files from your Google Drive account', 'storage_quota': 'Reconnect required', 'features': ['Docs, Sheets, Slides', 'Photo backup', 'Team drives'], 'connected': False, 'account_email': None, 'used_storage': '0 GB', 'total_storage': '0 GB', 'file_count': 0 } except Exception as error: print(f"Unexpected error: {error}") return { 'id': 'google_drive', 'name': 'Google Drive', 'icon': 'google-drive', 'brand_color': '#4285F4', 'description': 'Access files from your Google Drive account', 'storage_quota': 'Connection error', 'features': ['Docs, Sheets, Slides', 'Photo backup', 'Team drives'], 'connected': False, 'account_email': None, 'used_storage': '0 GB', 'total_storage': '0 GB', 'file_count': 0 } def _get_google_drive_file_count(self, service): """Get approximate file count from Google Drive""" try: # This gets a rough count - for exact count you'd need to paginate through all files result = service.files().list( pageSize=1000, fields="nextPageToken, files(id, name)" ).execute() files = result.get('files', []) return len(files) except: return 0 def _bytes_to_gb(self, bytes_value): """Convert bytes to gigabytes""" return bytes_value / (1024 ** 3) ##### NOW HANDLING THE FILE SELECTION WITH REAL GOOGLE DRIVE API ########## @action(detail=False, methods=['get', 'post'], url_path=r'transfer/new') def new_transfer(self, request): """Step 2: File Selection with Google Drive API""" base_domain = request.build_absolute_uri('/') user = request.user if not user.is_authenticated: return redirect(f'{base_domain}login/') # Check if cloud is connected cloud_source = request.session.get('selected_cloud') if not cloud_source: messages.error(request, "Please connect a cloud account first") return redirect(f'{base_domain}transfer/cloud-connect/') context = { 'base_domain': base_domain, 'user': user, 'cloud_source': cloud_source, 'current_step': 2 } if request.method == 'POST': selected_files = request.POST.getlist('selected_files') if not selected_files: messages.error(request, "Please select at least one file to transfer") return self._render_file_selection(request, context, cloud_source, user) try: # Create transfer order order = self._create_transfer_order(request, user, cloud_source, selected_files) request.session['current_order_id'] = str(order.id) #request.session['current_order_id'] = str(order.id) # Convert UUID to string request.session.modified = True return redirect(f'{base_domain}transfer/storage/') except Exception as e: messages.error(request, f"Error creating transfer: {str(e)}") print(f"Transfer creation error: {e}") # GET request - show file browser for selected cloud return self._render_file_selection(request, context, cloud_source, user) #This renders all the files from the selected cloud service def _render_file_selection(self, request, context, cloud_source, user): """Render file selection page for specific cloud with real Google Drive API""" cloud_data = self._get_cloud_data(cloud_source, user) #This is where am getting the real data from the cloud services. context.update(cloud_data) #Contains the previous data plus the cloud data. return render(request, 'transfer.html', context) #Gets the cloud data based on the user, may vary for all the cloud providers. def _get_cloud_data(self, cloud_source, user): """Get cloud-specific data structure with real Google Drive API""" if cloud_source == 'google_drive': return self._get_google_drive_data(user) elif cloud_source == 'dropbox': return self._get_dropbox_data() elif cloud_source == 'onedrive': return self._get_onedrive_data() else: return {} #Gets now google drive data exactly. def _get_google_drive_data(self, user): """Google Drive API structure with real data""" try: from social_django.models import UserSocialAuth from google.oauth2.credentials import Credentials from googleapiclient.discovery import build from googleapiclient.errors import HttpError # Check if user has Google OAuth connected social_auth = UserSocialAuth.objects.filter( user=user, provider='google-oauth2' ).first() if not social_auth or not social_auth.extra_data.get('access_token'): return { 'cloud_connected': False, 'cloud_service': 'Google Drive', 'user_email': 'Not connected', 'storage_info': { 'used': '0 GB', 'total': '0 GB', 'percentage': 0, 'available': '0 GB', 'quota_type': 'free' }, 'files': [], 'categories': [], 'total_files': 0, 'total_size': '0 GB', 'error': 'Please reconnect your Google Drive account' } # Create credentials and service credentials = Credentials(token=social_auth.extra_data.get('access_token')) service = build('drive', 'v3', credentials=credentials) # Get storage info about = service.about().get(fields="storageQuota,user").execute() storage_quota = about.get('storageQuota', {}) user_info = about.get('user', {}) used_bytes = int(storage_quota.get('usage', 0)) total_bytes = int(storage_quota.get('limit', 0)) used_gb = self._bytes_to_gb(used_bytes) total_gb = self._bytes_to_gb(total_bytes) usage_percentage = (used_bytes / total_bytes * 100) if total_bytes > 0 else 0 # Get real files from Google Drive files = self._get_real_google_drive_files(service) # Get file categories categories = self._get_google_drive_categories(service) return { 'cloud_connected': True, 'cloud_service': 'Google Drive', 'user_email': user_info.get('emailAddress', 'notconnected@gmail.com'), 'storage_info': { 'used': f'{used_gb:.1f} GB', 'total': f'{total_gb:.0f} GB', 'percentage': round(usage_percentage, 1), 'available': f'{total_gb - used_gb:.1f} GB', 'quota_type': 'free' if total_gb <= 15 else 'premium' }, 'files': files, 'categories': categories, 'total_files': len(files), 'total_size': f'{used_gb:.1f} GB' } except HttpError as error: print(f"Google Drive API error in file selection: {error}") return { 'cloud_connected': False, 'cloud_service': 'Google Drive', 'user_email': 'Connection error', 'storage_info': { 'used': '0 GB', 'total': '0 GB', 'percentage': 0, 'available': '0 GB', 'quota_type': 'free' }, 'files': [], 'categories': [], 'total_files': 0, 'total_size': '0 GB', 'error': 'Failed to load Google Drive files. Please try again.' } except Exception as error: print(f"Unexpected error in Google Drive data: {error}") return { 'cloud_connected': False, 'cloud_service': 'Google Drive', 'user_email': 'Error', 'storage_info': { 'used': '0 GB', 'total': '0 GB', 'percentage': 0, 'available': '0 GB', 'quota_type': 'free' }, 'files': [], 'categories': [], 'total_files': 0, 'total_size': '0 GB', 'error': 'An unexpected error occurred.' } def _get_real_google_drive_files(self, service): """Get real files from Google Drive API""" try: # Get files from Google Drive results = service.files().list( pageSize=500, # Limit to 50 files for performance fields="nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, webViewLink)", orderBy="modifiedTime desc" ).execute() files = results.get('files', []) formatted_files = [] for file in files: # Skip app data folders and shortcuts if file.get('mimeType') == 'application/vnd.google-apps.shortcut': continue # Format file size size_bytes = int(file.get('size', 0)) if size_bytes < 1024: size_display = f"{size_bytes} B" elif size_bytes < 1024 * 1024: size_display = f"{size_bytes / 1024:.1f} KB" elif size_bytes < 1024 * 1024 * 1024: size_display = f"{size_bytes / (1024 * 1024):.1f} MB" else: size_display = f"{size_bytes / (1024 * 1024 * 1024):.1f} GB" # Format modified time modified_time = file.get('modifiedTime') mod_display = "Unknown" if modified_time: try: mod_date = datetime.fromisoformat(modified_time.replace('Z', '+00:00')) now = datetime.now(timezone.utc) diff = now - mod_date if diff.days == 0: if diff.seconds < 60: mod_display = "Just now" elif diff.seconds < 3600: mod_display = f"{diff.seconds // 60} minutes ago" else: mod_display = f"{diff.seconds // 3600} hours ago" elif diff.days == 1: mod_display = "1 day ago" elif diff.days < 7: mod_display = f"{diff.days} days ago" elif diff.days < 30: mod_display = f"{diff.days // 7} weeks ago" else: mod_display = mod_date.strftime("%b %d, %Y") except: mod_display = "Unknown" # Determine file type and icon mime_type = file.get('mimeType', '') is_folder = mime_type == 'application/vnd.google-apps.folder' formatted_files.append({ 'id': file['id'], 'name': file['name'], 'size': size_display, 'sizeBytes': size_bytes, 'mimeType': mime_type, 'modified_display': mod_display, 'folder': is_folder, 'webViewLink': file.get('webViewLink', ''), 'item_count': 'Multiple items' if is_folder else None }) return formatted_files except Exception as e: print(f"Error getting Google Drive files: {e}") # Return empty list on error return [] def _get_google_drive_categories(self, service): """Get real file categories from Google Drive by analyzing actual files""" try: # Get a sample of files to analyze results = service.files().list( pageSize=100, fields="files(mimeType, size)", q="trashed=false" ).execute() files = results.get('files', []) # Initialize category counters categories = { 'documents': {'name': 'Documents', 'count': 0, 'icon': 'file-text', 'color': 'blue'}, 'images': {'name': 'Images', 'count': 0, 'icon': 'image', 'color': 'purple'}, 'videos': {'name': 'Videos', 'count': 0, 'icon': 'video', 'color': 'red'}, 'pdfs': {'name': 'PDFs', 'count': 0, 'icon': 'file-pdf', 'color': 'red'}, 'spreadsheets': {'name': 'Sheets', 'count': 0, 'icon': 'file-excel', 'color': 'green'}, 'presentations': {'name': 'Slides', 'count': 0, 'icon': 'file-powerpoint', 'color': 'orange'}, 'audio': {'name': 'Audio', 'count': 0, 'icon': 'music', 'color': 'indigo'}, 'archives': {'name': 'Archives', 'count': 0, 'icon': 'archive', 'color': 'yellow'}, 'others': {'name': 'Others', 'count': 0, 'icon': 'file', 'color': 'gray'} } for file in files: mime_type = file.get('mimeType', '') # Categorize by MIME type if 'application/vnd.google-apps.document' in mime_type: categories['documents']['count'] += 1 elif 'application/vnd.google-apps.spreadsheet' in mime_type: categories['spreadsheets']['count'] += 1 elif 'application/vnd.google-apps.presentation' in mime_type: categories['presentations']['count'] += 1 elif 'application/pdf' in mime_type: categories['pdfs']['count'] += 1 elif mime_type.startswith('image/'): categories['images']['count'] += 1 elif mime_type.startswith('video/'): categories['videos']['count'] += 1 elif mime_type.startswith('audio/'): categories['audio']['count'] += 1 elif any(ext in mime_type for ext in ['zip', 'rar', 'tar', '7z']): categories['archives']['count'] += 1 elif mime_type not in ['application/vnd.google-apps.folder', 'application/vnd.google-apps.shortcut']: categories['others']['count'] += 1 # Filter out empty categories and return only non-zero counts result = [] for category in categories.values(): if category['count'] > 0: result.append(category) # Sort by count (descending) result.sort(key=lambda x: x['count'], reverse=True) return result except Exception as e: print(f"Error analyzing Google Drive categories: {e}") # Fallback to basic categories return [ {'name': 'Documents', 'count': 0, 'icon': 'file-text', 'color': 'blue'}, {'name': 'Images', 'count': 0, 'icon': 'image', 'color': 'purple'}, {'name': 'Videos', 'count': 0, 'icon': 'video', 'color': 'red'}, {'name': 'PDFs', 'count': 0, 'icon': 'file-pdf', 'color': 'red'}, ] #For Dropbox Dummy Data For Now. def _get_dropbox_data(self): """Dropbox API structure (dummy data)""" return { 'cloud_connected': True, 'cloud_service': 'Dropbox', 'user_email': 'user@domain.com', 'storage_info': { 'used': '1.4 GB', 'total': '2 GB', 'percentage': 70, 'available': '0.6 GB', 'quota_type': 'free' }, 'files': self._get_dropbox_files(), 'categories': self._get_dropbox_categories(), 'total_files': 543, 'total_size': '1.4 GB' } def _get_dropbox_files(self): """Dummy Dropbox files""" return [ { 'id': 'db_file_1', 'name': 'Project Proposal.docx', 'size': '2.1 MB', 'sizeBytes': 2202009, 'mimeType': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'modified_display': '2 hours ago', 'folder': False }, { 'id': 'db_file_2', 'name': 'Vacation Photos', 'size': '156.3 MB', 'sizeBytes': 163877683, 'mimeType': 'folder', 'modified_display': '1 day ago', 'folder': True, 'item_count': 24 } ] def _get_dropbox_categories(self): """Dummy Dropbox categories""" return [ {'name': 'Documents', 'count': 45, 'icon': 'file-text', 'color': 'blue'}, {'name': 'Photos', 'count': 123, 'icon': 'image', 'color': 'purple'}, ] #For Onedrive Dummy Data For Now. def _get_onedrive_data(self): """OneDrive API structure (dummy data)""" return { 'cloud_connected': True, 'cloud_service': 'OneDrive', 'user_email': 'user@outlook.com', 'storage_info': { 'used': '3.2 GB', 'total': '5 GB', 'percentage': 64, 'available': '1.8 GB', 'quota_type': 'free' }, 'files': self._get_onedrive_files(), 'categories': self._get_onedrive_categories(), 'total_files': 892, 'total_size': '3.2 GB' } def _get_onedrive_files(self): """Dummy OneDrive files""" return [ { 'id': 'od_file_1', 'name': 'Budget Spreadsheet.xlsx', 'size': '3.8 MB', 'sizeBytes': 3984588, 'mimeType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'modified_display': '5 hours ago', 'folder': False }, { 'id': 'od_file_2', 'name': 'Work Documents', 'size': '890.2 MB', 'sizeBytes': 933232435, 'mimeType': 'folder', 'modified_display': '3 days ago', 'folder': True, 'item_count': 67 } ] def _get_onedrive_categories(self): """Dummy OneDrive categories""" return [ {'name': 'Documents', 'count': 234, 'icon': 'file-text', 'color': 'blue'}, {'name': 'Pictures', 'count': 89, 'icon': 'image', 'color': 'purple'}, {'name': 'Videos', 'count': 12, 'icon': 'video', 'color': 'red'}, ] # Google Drive Files (based on Drive v3 API) def _get_google_drive_files(self): return [ # Folders (mimeType: application/vnd.google-apps.folder) { 'id': '1A2B3C4D5E6F7G8H9I0J', 'name': 'Vacation Photos 2024', 'mimeType': 'application/vnd.google-apps.folder', 'size': '2.4 GB', 'sizeBytes': 2576980377, 'modifiedTime': '2024-06-15T10:30:00.000Z', 'modified_display': '2 days ago', 'icon': 'folder', 'icon_color': 'yellow-500', 'item_count': 342, 'parents': ['root'], 'webViewLink': 'https://drive.google.com/drive/folders/1A2B3C4D5E6F7G8H9I0J', 'shared': False }, # Google Docs { 'id': '1aB2cD3eF4gH5iJ6kL7mN', 'name': 'Project Proposal', 'mimeType': 'application/vnd.google-apps.document', 'size': '2.1 MB', 'sizeBytes': 2202009, 'modifiedTime': '2024-06-14T12:00:00.000Z', 'modified_display': '1 day ago', 'icon': 'file-alt', 'icon_color': 'blue-500', 'parents': ['root'], 'webViewLink': 'https://docs.google.com/document/d/1aB2cD3eF4gH5iJ6kL7mN/edit', 'shared': True }, # Regular files { 'id': '1zYxWvUtSrQpOnMlKjIhG', 'name': 'beach_sunset.jpg', 'mimeType': 'image/jpeg', 'size': '4.2 MB', 'sizeBytes': 4404019, 'modifiedTime': '2024-06-14T16:45:00.000Z', 'modified_display': '1 day ago', 'icon': 'image', 'icon_color': 'purple-500', 'parents': ['1A2B3C4D5E6F7G8H9I0J'], 'webViewLink': 'https://drive.google.com/file/d/1zYxWvUtSrQpOnMlKjIhG/view', 'thumbnailLink': 'https://lh3.googleusercontent.com/d/1zYxWvUtSrQpOnMlKjIhG', 'imageMediaMetadata': {'width': 4032, 'height': 3024} } ] # Dropbox Files (based on Dropbox API) def _get_dropbox_files(self): return [ # Folders { 'id': 'id:1A2B3C4D5E6F7G8H9I0J', 'name': 'Work Projects', '.tag': 'folder', 'size': '1.1 GB', 'sizeBytes': 1181116006, 'client_modified': '2024-06-14T09:30:00Z', 'server_modified': '2024-06-14T09:30:00Z', 'modified_display': '1 day ago', 'icon': 'folder', 'icon_color': 'blue-500', 'path_display': '/Work Projects', 'path_lower': '/work projects', 'shared': False, 'item_count': 156 }, # Files { 'id': 'id:1aB2cD3eF4gH5iJ6kL7mN', 'name': 'Client Presentation.pptx', '.tag': 'file', 'size': '45.2 MB', 'sizeBytes': 47395658, 'client_modified': '2024-06-13T14:20:00Z', 'server_modified': '2024-06-13T14:20:00Z', 'modified_display': '2 days ago', 'icon': 'file-powerpoint', 'icon_color': 'orange-500', 'path_display': '/Work Projects/Client Presentation.pptx', 'path_lower': '/work projects/client presentation.pptx', 'rev': '5e1b2c3d4e5f6g7h8i9j0k', 'shared': True, 'is_downloadable': True }, { 'id': 'id:1zYxWvUtSrQpOnMlKjIhG', 'name': 'team_photo.jpg', '.tag': 'file', 'size': '3.8 MB', 'sizeBytes': 3984588, 'client_modified': '2024-06-12T11:15:00Z', 'server_modified': '2024-06-12T11:15:00Z', 'modified_display': '3 days ago', 'icon': 'image', 'icon_color': 'purple-500', 'path_display': '/Photos/team_photo.jpg', 'path_lower': '/photos/team_photo.jpg', 'rev': '5a4b3c2d1e0f9g8h7i6j5k', 'shared': False, 'is_downloadable': True } ] def _get_dropbox_categories(self): return [ {'name': 'Images', 'count': 287, 'icon': 'images', 'color': 'purple-500'}, {'name': 'Documents', 'count': 134, 'icon': 'file-alt', 'color': 'blue-500'}, {'name': 'Videos', 'count': 67, 'icon': 'video', 'color': 'red-500'}, {'name': 'PDFs', 'count': 45, 'icon': 'file-pdf', 'color': 'red-400'}, {'name': 'Archives', 'count': 23, 'icon': 'archive', 'color': 'yellow-500'}, {'name': 'Others', 'count': 12, 'icon': 'file', 'color': 'gray-500'} ] # OneDrive Files (based on Microsoft Graph API) def _get_onedrive_files(self): return [ # Folders { 'id': '01A2B3C4D5E6F7G8H9I0J', 'name': 'Personal Documents', 'folder': {'childCount': 89}, 'size': '2.1 GB', 'sizeBytes': 2254857830, 'lastModifiedDateTime': '2024-06-14T16:45:00Z', 'modified_display': '1 day ago', 'icon': 'folder', 'icon_color': 'blue-500', 'webUrl': 'https://1drv.ms/f/s!ABC123def456', 'parentReference': {'path': '/drive/root:'}, 'createdDateTime': '2023-01-15T10:30:00Z' }, # Office Files { 'id': '01aB2cD3eF4gH5iJ6kL7mN', 'name': 'Budget Report.xlsx', 'file': {'mimeType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}, 'size': '3.2 MB', 'sizeBytes': 3355443, 'lastModifiedDateTime': '2024-06-13T11:20:00Z', 'modified_display': '2 days ago', 'icon': 'file-excel', 'icon_color': 'green-500', 'webUrl': 'https://1drv.ms/x/s!ABC123def456', 'parentReference': {'path': '/drive/root:/Personal Documents'}, 'createdBy': {'user': {'displayName': 'John Doe'}}, 'shared': {'scope': 'users'} }, # Regular files { 'id': '01zYxWvUtSrQpOnMlKjIhG', 'name': 'vacation_photo.jpg', 'file': {'mimeType': 'image/jpeg'}, 'size': '5.1 MB', 'sizeBytes': 5347737, 'lastModifiedDateTime': '2024-06-12T14:30:00Z', 'modified_display': '3 days ago', 'icon': 'image', 'icon_color': 'purple-500', 'webUrl': 'https://1drv.ms/i/s!ABC123def456', 'parentReference': {'path': '/drive/root:/Photos'}, 'image': {'width': 4000, 'height': 3000} } ] def _get_onedrive_categories(self): return [ {'name': 'Images', 'count': 456, 'icon': 'images', 'color': 'purple-500'}, {'name': 'Documents', 'count': 234, 'icon': 'file-alt', 'color': 'blue-500'}, {'name': 'Videos', 'count': 123, 'icon': 'video', 'color': 'red-500'}, {'name': 'Office Files', 'count': 67, 'icon': 'file-word', 'color': 'blue-400'}, {'name': 'PDFs', 'count': 45, 'icon': 'file-pdf', 'color': 'red-400'}, {'name': 'OneNote', 'count': 23, 'icon': 'sticky-note', 'color': 'purple-400'} ] def _create_transfer_order(self, request, user, cloud_source, selected_files): """Create transfer order with BULK operations for performance""" from .models import TransferOrder, FileSelection from django.db import transaction from social_django.models import UserSocialAuth #from datetime import timezone from django.utils import timezone # Get the user's OAuth tokens access_token = None refresh_token = None if cloud_source == 'google_drive': social_auth = UserSocialAuth.objects.filter( user=user, provider='google-oauth2' ).first() if social_auth: access_token = social_auth.extra_data.get('access_token') refresh_token = social_auth.extra_data.get('refresh_token') # Get all file info in ONE batch file_infos = self._get_files_info_batch(selected_files, cloud_source, user) # Calculate total size in one go total_size_bytes = sum(info.get('sizeBytes', 0) for info in file_infos.values()) # Create order (ONE database query) order = TransferOrder.objects.create( user=user, cloud_source=cloud_source, status='file_selection_complete', total_files=len(selected_files), total_size=total_size_bytes / (1024**3), access_token= access_token, refresh_token=refresh_token, token_expiry=timezone.now() + timezone.timedelta(hours=1) # Approximate expiry ) # Prepare ALL file selections for bulk create file_selections = [] for file_id in selected_files: file_info = file_infos.get(file_id) if file_info: file_selections.append( FileSelection( order=order, file_id=file_id, file_name=file_info.get('name', 'Unknown'), file_size=file_info.get('sizeBytes', 0), mime_type=file_info.get('mimeType') or file_info.get('file', {}).get('mimeType', ''), cloud_source=cloud_source, file_path=file_info.get('path_display') or file_info.get('webUrl', '') ) ) # Bulk create ALL file selections (ONE database query instead of 300!) FileSelection.objects.bulk_create(file_selections) request.session['current_order_id'] = str(order.id) # Convert UUID to string request.session.modified = True return order def _get_files_info_batch(self, file_ids, cloud_source, user): """Get file information in BATCH instead of one-by-one""" cloud_data = self._get_cloud_data(cloud_source, user) files = cloud_data.get('files', []) # Create a lookup dictionary for O(1) access file_lookup = {file['id']: file for file in files} # Return only the files that were selected return {file_id: file_lookup.get(file_id) for file_id in file_ids} def _get_file_info(self, file_id, cloud_source, user): """Get file information from mock data""" cloud_data = self._get_cloud_data(cloud_source,user) files = cloud_data.get('files', []) for file in files: if file['id'] == file_id: return file return None #### THIS HANDLES THE STORAGE SELECTION WITH SMART RECOMMENDATIONS ########## @action(detail=False, methods=['get', 'post'], url_path=r'transfer/storage') def storage_selection(self, request): """Step 3: Storage Selection with Smart Recommendations""" base_domain = request.build_absolute_uri('/') user = request.user if not user.is_authenticated: return redirect(f'{base_domain}login/') current_order_id = request.session.get('current_order_id') if not current_order_id: messages.error(request, "Please select files first") return redirect(f'{base_domain}transfer/new/') try: from .models import TransferOrder # Use select_for_update to lock the row for update in case of concurrent requests order = TransferOrder.objects.select_for_update().get(id=current_order_id, user=user) # Calculate minimum required size (total + 1GB buffer) required_gb = float(order.total_size) minimum_required_gb = required_gb + 1.0 # Add 1GB buffer context = { 'base_domain': base_domain, 'user': user, 'order': order, 'current_step': 3, 'selected_files_count': order.total_files, 'total_size_gb': required_gb, 'minimum_required_gb': minimum_required_gb, 'kenyan_counties': self._get_kenyan_counties() } if request.method == 'POST': return self._handle_storage_selection(request, order, context) # GET request - show storage options with smart recommendations context.update({ 'storage_options': self._get_storage_options(minimum_required_gb), 'size_options': self._get_size_options(minimum_required_gb) }) return render(request, 'storage_selection.html', context) except TransferOrder.DoesNotExist: messages.error(request, "Order not found") return redirect(f'{base_domain}transfer/new/') def _handle_storage_selection(self, request, order, context): """Handle storage selection form submission with Kenyan address - FIXED VERSION""" base_domain = request.build_absolute_uri('/') storage_type = request.POST.get('storage_type') storage_size = request.POST.get('storage_size') # Kenyan address fields country = request.POST.get('country', 'Kenya') county = request.POST.get('county') district = request.POST.get('district') street_address = request.POST.get('street_address') town = request.POST.get('town') postal_code = request.POST.get('postal_code') additional_instructions = request.POST.get('additional_instructions', '') # Validate required fields required_fields = [storage_type, storage_size, county, district, street_address, town] if not all(required_fields): messages.error(request, "Please fill all required fields") context.update({ 'storage_options': self._get_storage_options(context['minimum_required_gb']), 'size_options': self._get_size_options(context['minimum_required_gb']), 'kenyan_counties': self._get_kenyan_counties() }) return render(request, 'storage_selection.html', context) # Validate storage size try: required_gb = context['minimum_required_gb'] selected_gb = int(storage_size.replace('GB', '').strip()) if selected_gb < required_gb: messages.error(request, f"Selected storage ({storage_size}) is too small. Minimum {required_gb:.1f} GB required") context.update({ 'storage_options': self._get_storage_options(required_gb), 'size_options': self._get_size_options(required_gb), 'kenyan_counties': self._get_kenyan_counties() }) return render(request, 'storage_selection.html', context) # Build complete shipping address shipping_parts = [ street_address, town, district, county, f"Postal Code: {postal_code}" if postal_code else None, country ] shipping_address = ", ".join([part for part in shipping_parts if part]) if additional_instructions: shipping_address += f"\n\nAdditional Instructions: {additional_instructions}" # UPDATE ORDER WITH TRANSACTION ATOMIC FOR DATA CONSISTENCY from django.db import transaction try: with transaction.atomic(): # Update order fields order.storage_type = storage_type order.storage_size = storage_size order.shipping_address = shipping_address order.status = 'storage_selected' # Use update_fields for faster saving with only changed fields order.save(update_fields=[ 'storage_type', 'storage_size', 'shipping_address', 'status', 'updated_at' # if you have this field ]) # Verify the save worked order.refresh_from_db() if order.status != 'storage_selected': raise Exception("Order not updated correctly") return redirect(f'{base_domain}transfer/payment/') except Exception as e: messages.error(request, "Failed to save your selection. Please try again.") # Log the error for debugging print(f"Error saving order: {e}") context.update({ 'storage_options': self._get_storage_options(context['minimum_required_gb']), 'size_options': self._get_size_options(context['minimum_required_gb']), 'kenyan_counties': self._get_kenyan_counties() }) return render(request, 'storage_selection.html', context) except (ValueError, AttributeError) as e: messages.error(request, "Invalid storage size selected") context.update({ 'storage_options': self._get_storage_options(context['minimum_required_gb']), 'size_options': self._get_size_options(context['minimum_required_gb']), 'kenyan_counties': self._get_kenyan_counties() }) return render(request, 'storage_selection.html', context) def _get_storage_options(self, required_gb): """Get storage options with smart recommendations based on file size""" options = [ { 'type': 'flash_drive', 'name': 'USB Flash Drive', 'icon': 'usb', 'description': 'Portable and durable USB 3.0 drives', 'max_capacity': 1024, # GB 'recommended': required_gb <= 256 # Best for smaller transfers }, { 'type': 'memory_card', 'name': 'Memory Card', 'icon': 'sd-card', 'description': 'Compact SD/microSD cards with adapter', 'max_capacity': 512, # GB 'recommended': required_gb <= 128 # Best for photos/videos }, { 'type': 'external_hdd', 'name': 'External HDD', 'icon': 'hdd', 'description': 'Large capacity portable hard drives', 'max_capacity': 5000, # GB 'recommended': required_gb > 256 # Best for large transfers } ] # Filter out options that can't handle the required size suitable_options = [opt for opt in options if opt['max_capacity'] >= required_gb] # If no suitable options, return the largest one if not suitable_options: suitable_options = [options[-1]] # Remove max_capacity from final output for opt in suitable_options: opt.pop('max_capacity', None) return suitable_options def _get_size_options(self, required_gb): """Get size options with smart recommendations""" base_sizes = [ {'size': '64GB', 'gb': 64, 'price': 1200, 'suitable_for': 'Documents, Photos', 'popular': required_gb <= 50}, {'size': '128GB', 'gb': 128, 'price': 2200, 'suitable_for': 'Photos, Music, Videos', 'popular': required_gb <= 100}, {'size': '256GB', 'gb': 256, 'price': 3800, 'suitable_for': 'Large photo libraries, HD videos', 'popular': required_gb <= 200}, {'size': '512GB', 'gb': 512, 'price': 6500, 'suitable_for': 'Video projects, backups', 'popular': required_gb <= 400}, {'size': '1TB', 'gb': 1024, 'price': 9500, 'suitable_for': 'Large backups, media collections', 'popular': required_gb <= 800}, {'size': '2TB', 'gb': 2048, 'price': 15000, 'suitable_for': 'Extensive media libraries', 'popular': required_gb <= 1800} ] # Filter sizes that can accommodate the files + buffer suitable_sizes = [size for size in base_sizes if size['gb'] >= required_gb] # If no suitable sizes, show the largest available if not suitable_sizes: suitable_sizes = [base_sizes[-1]] # Mark the smallest suitable size as recommended (cost-effective) if suitable_sizes: suitable_sizes[0]['recommended'] = True return suitable_sizes def _get_kenyan_counties(self): """Get list of Kenyan counties for dropdown""" return [ 'Nairobi', 'Mombasa', 'Kisumu', 'Nakuru', 'Eldoret', 'Thika', 'Machakos', 'Meru', 'Nyeri', 'Garissa', 'Kakamega', 'Malindi', 'Kitale', 'Lamu', 'Lodwar', 'Mandera', 'Marsabit', 'Moyale', 'Kitui', 'Embu', 'Homa Bay', 'Bungoma', 'Busia', 'Siaya', 'Kiambu', 'Kirinyaga', 'Muranga', 'Nyandarua', 'Nandi', 'Kericho', 'Bomet', 'Narok', 'Kajiado', 'Makueni', 'Wajir', 'Isiolo', 'Kilifi', 'Kwale', 'Lamu', 'Taita Taveta', 'Tana River' ] #### ADMIN DASHBOARD FOR DOWNLOADS AND ORDER MANAGEMENT ########## @action(detail=False, methods=['get'], url_path=r'downloads') def download_dashboard(self, request): """Admin dashboard for downloading client files""" print("=== ACCESSING DOWNLOAD DASHBOARD ===") base_domain = request.build_absolute_uri('/') if not request.user.is_staff: print(f"❌ UNAUTHORIZED ACCESS: User {request.user.email} is not staff") messages.error(request, "Admin access required") return redirect(f'{base_domain}admin/login/') try: from .models import TransferOrder # Get orders with different statuses orders_needing_download = TransferOrder.objects.filter( status__in=['payment_completed', 'storage_selected'] ).select_related('user').prefetch_related('file_selections').order_by('-created_at') downloading_orders = TransferOrder.objects.filter( status__in=['download_in_progress', 'processing'] ).select_related('user').prefetch_related('file_selections').order_by('-updated_at') completed_orders = TransferOrder.objects.filter( status__in=['download_completed', 'completed'] ).select_related('user').prefetch_related('file_selections').order_by('-updated_at') failed_orders = TransferOrder.objects.filter( status__in=['download_failed', 'download_partial', 'cancelled'] ).select_related('user').prefetch_related('file_selections').order_by('-updated_at') context = { 'base_domain': base_domain, 'user': request.user, 'orders_needing_download': orders_needing_download, 'downloading_orders': downloading_orders, 'completed_orders': completed_orders, 'failed_orders': failed_orders, 'total_pending': orders_needing_download.count(), 'total_downloading': downloading_orders.count(), 'total_completed': completed_orders.count(), 'total_failed': failed_orders.count(), } print(f"✅ DASHBOARD LOADED: {orders_needing_download.count()} pending, {downloading_orders.count()} downloading") return render(request, 'admin_download_dashboard.html', context) except Exception as e: print(f"❌ DASHBOARD ERROR: {str(e)}") messages.error(request, f"Failed to load dashboard: {str(e)}") return redirect(f'{base_domain}admin/') @action(detail=False, methods=['post'], url_path=r'notcloudstorage/download-files') def download_selected_files(self, request): """Download selected files for an order and send to client""" print("=== STARTING FILE DOWNLOAD ===") if not request.user.is_staff: print("❌ UNAUTHORIZED: Non-staff user attempted download") return JsonResponse({'error': 'Admin access required'}, status=403) # Check if this is an AJAX request requested_with = request.META.get('HTTP_X_REQUESTED_WITH', '') is_ajax = requested_with.lower() == 'xmlhttprequest' print(f"🔍 REQUEST TYPE: {'AJAX' if is_ajax else 'FORM SUBMISSION'}") try: order_id = request.POST.get('order_id') file_ids = request.POST.getlist('file_ids[]') print(f"đŸ“Ļ DOWNLOAD REQUEST: Order {order_id}, Files: {len(file_ids)}") from .models import TransferOrder # Get the order order = TransferOrder.objects.get(id=order_id) # Validate order status valid_statuses = ['payment_completed', 'storage_selected', 'download_failed', 'download_partial'] if order.status not in valid_statuses: error_msg = f'Order not ready for download. Current status: {order.status}' print(f"❌ INVALID ORDER STATUS: {error_msg}") if is_ajax: return JsonResponse({'error': error_msg}, status=400) else: # For form submissions, we need to return an error page or redirect from django.contrib import messages messages.error(request, error_msg) return redirect('download-dashboard') # Get file selections file_selections = self._get_file_selections(order, file_ids) if not file_selections: print("❌ NO FILES FOUND for download") if is_ajax: return JsonResponse({'error': 'No files found for download'}, status=404) else: messages.error(request, 'No files found for download') return redirect('download-dashboard') # Get access token access_token = self._get_or_refresh_access_token(order) if not access_token: print("❌ NO VALID ACCESS TOKEN") if is_ajax: return JsonResponse({'error': 'No valid access token available for this order'}, status=403) else: messages.error(request, 'No valid access token available') return redirect('download-dashboard') # Update order status order.status = 'download_in_progress' order.download_status = f'Downloading {file_selections.count()} files...' order.save(update_fields=['status', 'download_status', 'updated_at']) print(f"✅ STARTING DOWNLOAD: {file_selections.count()} files for order {order_id}") # For AJAX requests, return JSON (for progress tracking) # For form submissions, return the actual file download if is_ajax: print("🔄 AJAX REQUEST - Returning JSON response") # Start async download process import threading thread = threading.Thread( target=self._process_download_async, args=(order, file_selections, access_token) ) thread.daemon = True thread.start() return JsonResponse({ 'success': True, 'message': f'Download started for {file_selections.count()} files', 'order_id': order_id, 'file_count': file_selections.count() }) else: print("đŸ“Ĩ FORM SUBMISSION - Returning file download") # Return actual file download for form submissions if file_selections.count() == 1: return self._download_single_file_to_client(order, file_selections.first(), access_token) else: return self._download_multiple_files_as_zip(order, file_selections, access_token) except TransferOrder.DoesNotExist: print(f"❌ ORDER NOT FOUND: {order_id}") if is_ajax: return JsonResponse({'error': 'Order not found'}, status=404) else: messages.error(request, 'Order not found') return redirect('download-dashboard') except Exception as e: print(f"❌ DOWNLOAD FAILED: {str(e)}") logger.error(f"Download failed for order {order_id}: {str(e)}", exc_info=True) # Update order status to failed try: order = TransferOrder.objects.get(id=order_id) order.status = 'download_failed' order.download_status = f"Download failed: {str(e)}" order.save(update_fields=['status', 'download_status', 'updated_at']) except Exception: pass if is_ajax: return JsonResponse({'error': f'Download failed: {str(e)}'}, status=500) else: messages.error(request, f'Download failed: {str(e)}') return redirect('download-dashboard') def _process_download_async(self, order, file_selections, access_token): """Process download asynchronously for AJAX requests""" try: print(f"🔄 PROCESSING ASYNC DOWNLOAD for order {order.id}") # Process the download (this runs in background) if file_selections.count() == 1: self._download_single_file_to_client(order, file_selections.first(), access_token) else: self._download_multiple_files_as_zip(order, file_selections, access_token) print(f"✅ ASYNC DOWNLOAD COMPLETED for order {order.id}") except Exception as e: print(f"❌ ASYNC DOWNLOAD FAILED: {str(e)}") def _get_file_selections(self, order, file_ids): """Get file selections based on provided IDs""" if file_ids: return order.file_selections.filter(id__in=file_ids) return order.file_selections.all() def _download_single_file_to_client(self, order, file_selection, access_token): """Download a single file directly to client""" from django.http import HttpResponse try: print(f"âŦ‡ī¸ DOWNLOADING SINGLE FILE: {file_selection.file_name}") # Update file status file_selection.download_status = 'downloading' file_selection.save(update_fields=['download_status']) # Get file content from cloud file_content = self._get_file_content_from_cloud( access_token, order.cloud_source, file_selection.file_id, file_selection.file_name ) if not file_content: raise Exception("Downloaded file content is empty") # Create HTTP response with file for direct download safe_filename = self._get_safe_filename(file_selection.file_name) response = HttpResponse( file_content, content_type='application/octet-stream' ) response['Content-Disposition'] = f'attachment; filename="{safe_filename}"' response['Content-Length'] = len(file_content) # Update file selection status file_selection.download_status = 'downloaded' file_selection.downloaded_at = timezone.now() file_selection.download_size = len(file_content) file_selection.save(update_fields=[ 'download_status', 'downloaded_at', 'download_size' ]) # Update order status order.status = 'download_completed' order.download_status = f'File downloaded successfully ({self._format_bytes(len(file_content))})' order.download_log = { 'downloaded_files': [{ 'name': file_selection.file_name, 'size': len(file_content), 'downloaded_at': timezone.now().isoformat() }], 'failed_files': [], 'total_files': 1, 'successful_downloads': 1, 'failed_downloads': 0, 'total_downloaded_size': len(file_content), 'completed_at': timezone.now().isoformat() } order.save(update_fields=['status', 'download_status', 'download_log', 'updated_at']) print(f"✅ SINGLE FILE DOWNLOAD COMPLETE: {file_selection.file_name}") return response except Exception as e: print(f"❌ SINGLE FILE DOWNLOAD FAILED: {file_selection.file_name} - {str(e)}") # Update file status to failed file_selection.download_status = 'failed' file_selection.download_error = str(e) file_selection.save(update_fields=['download_status', 'download_error']) # Update order status order.status = 'download_failed' order.download_status = f"Download failed: {str(e)}" order.save(update_fields=['status', 'download_status', 'updated_at']) raise def _download_multiple_files_as_zip(self, order, file_selections, access_token): """Download multiple files as zip directly to client""" import zipfile from django.http import HttpResponse from io import BytesIO try: total_files = file_selections.count() print(f"📁 PROCESSING {total_files} FILES AS ZIP") # Create in-memory zip file zip_buffer = BytesIO() with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf: downloaded_files = [] failed_files = [] total_downloaded_size = 0 for index, file_selection in enumerate(file_selections, 1): try: print(f"âŦ‡ī¸ ADDING TO ZIP {index}/{total_files}: {file_selection.file_name}") # Update file status file_selection.download_status = 'downloading' file_selection.save(update_fields=['download_status']) # Get file content from cloud file_content = self._get_file_content_from_cloud( access_token, order.cloud_source, file_selection.file_id, file_selection.file_name ) if not file_content: raise Exception("Downloaded file content is empty") # Add file to zip safe_filename = self._get_safe_filename(file_selection.file_name) zipf.writestr(safe_filename, file_content) # Update file selection file_selection.download_status = 'downloaded' file_selection.downloaded_at = timezone.now() file_selection.download_size = len(file_content) file_selection.save(update_fields=[ 'download_status', 'downloaded_at', 'download_size' ]) downloaded_files.append({ 'name': file_selection.file_name, 'size': len(file_content) }) total_downloaded_size += len(file_content) print(f"✅ ADDED TO ZIP: {file_selection.file_name} ({self._format_bytes(len(file_content))})") except Exception as file_error: print(f"❌ FILE DOWNLOAD FAILED: {file_selection.file_name} - {str(file_error)}") file_selection.download_status = 'failed' file_selection.download_error = str(file_error) file_selection.save(update_fields=['download_status', 'download_error']) failed_files.append({ 'name': file_selection.file_name, 'error': str(file_error) }) # Prepare zip file for direct download zip_buffer.seek(0) zip_filename = f"Order_{order.order_number}_Files.zip" # Create HTTP response with zip file for direct download response = HttpResponse( zip_buffer.getvalue(), content_type='application/zip' ) response['Content-Disposition'] = f'attachment; filename="{zip_filename}"' response['Content-Length'] = len(zip_buffer.getvalue()) # Update order status self._update_order_status(order, downloaded_files, failed_files, total_files) print(f"✅ ZIP DOWNLOAD READY: {zip_filename} ({self._format_bytes(len(zip_buffer.getvalue()))})") return response except Exception as e: print(f"❌ ZIP DOWNLOAD FAILED: {str(e)}") raise def _get_file_content_from_cloud(self, access_token, cloud_source, file_id, file_name): """Get file content from cloud storage provider""" try: print(f"đŸŒŠī¸ DOWNLOADING FROM {cloud_source}: {file_name}") if cloud_source == 'google_drive': return self._download_from_google_drive(access_token, file_id, file_name) elif cloud_source == 'dropbox': return self._download_from_dropbox(access_token, file_id) elif cloud_source == 'onedrive': return self._download_from_onedrive(access_token, file_id) else: raise Exception(f"Unsupported cloud source: {cloud_source}") except Exception as e: print(f"❌ CLOUD DOWNLOAD FAILED: {file_name} - {str(e)}") raise def _update_order_status(self, order, downloaded_files, failed_files, total_files): """Update order status based on download results""" successful_downloads = len(downloaded_files) total_downloaded_size = sum(f['size'] for f in downloaded_files) if len(failed_files) == 0: order.status = 'download_completed' order.download_status = f'All {total_files} files downloaded successfully ({self._format_bytes(total_downloaded_size)})' elif successful_downloads == 0: order.status = 'download_failed' order.download_status = f'All {total_files} files failed to download' else: order.status = 'download_partial' order.download_status = f'{successful_downloads}/{total_files} files downloaded successfully' order.download_log = { 'downloaded_files': downloaded_files, 'failed_files': failed_files, 'total_files': total_files, 'successful_downloads': successful_downloads, 'failed_downloads': len(failed_files), 'total_downloaded_size': total_downloaded_size, 'completed_at': timezone.now().isoformat() } order.save(update_fields=['status', 'download_status', 'download_log', 'updated_at']) print(f"✅ ORDER STATUS UPDATED: {order.status}") def _download_from_google_drive(self, access_token, file_id, file_name): """Download file from Google Drive""" try: from googleapiclient.discovery import build from googleapiclient.http import MediaIoBaseDownload from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from googleapiclient.errors import HttpError import io print(f"đŸ”ĩ GOOGLE DRIVE DOWNLOAD: {file_name} (ID: {file_id})") creds = Credentials(token=access_token) service = build('drive', 'v3', credentials=creds) # Get file metadata file_metadata = service.files().get( fileId=file_id, fields='mimeType,name,size,webViewLink' ).execute() mime_type = file_metadata.get('mimeType', '') # Handle Google Workspace documents if mime_type.startswith('application/vnd.google-apps'): export_mime_types = { 'application/vnd.google-apps.document': 'application/pdf', 'application/vnd.google-apps.spreadsheet': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.google-apps.presentation': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.google-apps.drawing': 'image/png', } download_mime_type = export_mime_types.get(mime_type) if not download_mime_type: raise Exception(f"Unsupported Google Workspace file type: {mime_type}") print(f"📄 EXPORTING GOOGLE DOC: {mime_type} -> {download_mime_type}") request = service.files().export_media(fileId=file_id, mimeType=download_mime_type) else: # Regular file download request = service.files().get_media(fileId=file_id) # Download file content fh = io.BytesIO() downloader = MediaIoBaseDownload(fh, request) done = False while not done: status, done = downloader.next_chunk() if status: print(f"📊 Download progress: {int(status.progress() * 100)}%") fh.seek(0) file_content = fh.getvalue() fh.close() if not file_content: raise Exception("Downloaded file content is empty") print(f"✅ GOOGLE DRIVE SUCCESS: {len(file_content)} bytes") return file_content except HttpError as error: print(f"❌ GOOGLE DRIVE API ERROR: {error}") if error.resp.status == 403: raise Exception("Permission denied: Check file sharing and app scopes") elif error.resp.status == 404: raise Exception("File not found or inaccessible") elif error.resp.status == 401: raise Exception("Authentication failed: Token expired or invalid") else: raise Exception(f"Google Drive API error: {error}") except Exception as e: print(f"❌ GOOGLE DRIVE DOWNLOAD FAILED: {str(e)}") raise def _download_from_dropbox(self, access_token, file_id): """Download file from Dropbox""" try: print(f"đŸ”ĩ DROPBOX DOWNLOAD: {file_id}") headers = { 'Authorization': f'Bearer {access_token}', 'Dropbox-API-Arg': f'{{"path": "{file_id}"}}' } response = requests.post( 'https://content.dropboxapi.com/2/files/download', headers=headers, stream=True, timeout=60 ) response.raise_for_status() content = b'' for chunk in response.iter_content(chunk_size=8192): if chunk: content += chunk print(f"✅ DROPBOX SUCCESS: {len(content)} bytes") return content except requests.exceptions.RequestException as e: print(f"❌ DROPBOX DOWNLOAD FAILED: {str(e)}") raise Exception(f"Dropbox download failed: {str(e)}") def _download_from_onedrive(self, access_token, file_id): """Download file from OneDrive""" try: print(f"đŸ”ĩ ONEDRIVE DOWNLOAD: {file_id}") headers = {'Authorization': f'Bearer {access_token}'} download_url = f'https://graph.microsoft.com/v1.0/me/drive/items/{file_id}/content' response = requests.get(download_url, headers=headers, stream=True, timeout=60) response.raise_for_status() content = b'' for chunk in response.iter_content(chunk_size=8192): if chunk: content += chunk print(f"✅ ONEDRIVE SUCCESS: {len(content)} bytes") return content except requests.exceptions.RequestException as e: print(f"❌ ONEDRIVE DOWNLOAD FAILED: {str(e)}") raise Exception(f"OneDrive download failed: {str(e)}") # Helper methods def _get_safe_filename(self, filename): """Convert filename to safe filesystem name""" import re import os name, ext = os.path.splitext(filename) safe_name = re.sub(r'[<>:"/\\|?*]', '_', name) safe_name = safe_name[:200] return safe_name + ext def _format_bytes(self, size_bytes): """Format bytes to human readable format""" for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if size_bytes < 1024.0: return f"{size_bytes:.2f} {unit}" size_bytes /= 1024.0 return f"{size_bytes:.2f} PB" @action(detail=False, methods=['get'], url_path=r'notcloudstorage/order-files/(?P[^/.]+)') def get_order_files(self, request, order_id=None): """Get all files for a specific order""" print(f"=== GETTING ORDER FILES: {order_id} ===") if not request.user.is_staff: print("❌ UNAUTHORIZED: Non-staff user") return JsonResponse({'error': 'Admin access required'}, status=403) try: from .models import TransferOrder order = TransferOrder.objects.get(id=order_id) file_selections = order.file_selections.all().order_by('file_name') files_data = [] for file_selection in file_selections: files_data.append({ 'id': str(file_selection.id), 'name': file_selection.file_name, 'size': file_selection.file_size, 'download_status': file_selection.download_status, 'download_error': file_selection.download_error, 'downloaded_at': file_selection.downloaded_at.isoformat() if file_selection.downloaded_at else None, 'download_size': file_selection.download_size, 'selected': False }) print(f"✅ ORDER FILES RETRIEVED: {len(files_data)} files") return JsonResponse({ 'order_id': str(order.id), 'order_number': order.order_number, 'user_email': order.user.email, 'cloud_source': order.cloud_source, 'total_files': len(files_data), 'files': files_data }) except TransferOrder.DoesNotExist: print(f"❌ ORDER NOT FOUND: {order_id}") return JsonResponse({'error': 'Order not found'}, status=404) except Exception as e: print(f"❌ GET ORDER FILES FAILED: {str(e)}") return JsonResponse({'error': f'Failed to get order files: {str(e)}'}, status=500) def _get_or_refresh_access_token(self, order): """Get valid access token for the order""" print(f"=== GETTING ACCESS TOKEN for Order {order.id} ===") from social_django.models import UserSocialAuth # Check if we have a valid stored token if order.access_token and order.token_expiry and order.token_expiry > timezone.now(): print("✅ USING EXISTING VALID TOKEN") return order.access_token # Try to refresh the token if order.refresh_token: print("🔄 REFRESHING TOKEN") new_token = self._refresh_oauth_token(order) if new_token: return new_token # Try to get from user's social auth try: print("🔍 CHECKING SOCIAL AUTH") social_auth = UserSocialAuth.objects.filter( user=order.user, provider='google-oauth2' ).first() if social_auth and social_auth.extra_data.get('access_token'): order.access_token = social_auth.extra_data.get('access_token') order.refresh_token = social_auth.extra_data.get('refresh_token') order.token_expiry = timezone.now() + timezone.timedelta(hours=1) order.save(update_fields=['access_token', 'refresh_token', 'token_expiry']) print("✅ TOKEN RETRIEVED FROM SOCIAL AUTH") return order.access_token except Exception as e: print(f"❌ SOCIAL AUTH TOKEN FAILED: {e}") print("❌ NO VALID ACCESS TOKEN AVAILABLE") return None def _refresh_oauth_token(self, order): """Refresh OAuth token if expired""" from django.conf import settings if order.cloud_source == 'google_drive' and order.refresh_token: print(f"🔄 REFRESHING GOOGLE OAUTH TOKEN for order {order.id}") try: response = requests.post('https://oauth2.googleapis.com/token', data={ 'client_id': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, 'client_secret': settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET, 'refresh_token': order.refresh_token, 'grant_type': 'refresh_token' }, timeout=30) if response.status_code == 200: token_data = response.json() new_access_token = token_data.get('access_token') if not new_access_token: raise Exception("No access token received in refresh response") order.access_token = new_access_token order.token_expiry = timezone.now() + timezone.timedelta( seconds=token_data.get('expires_in', 3600) ) order.save(update_fields=['access_token', 'token_expiry']) print("✅ TOKEN REFRESHED SUCCESSFULLY") return new_access_token else: error_msg = f"Token refresh failed with status {response.status_code}" print(f"❌ {error_msg}: {response.text}") raise Exception(error_msg) except requests.exceptions.Timeout: error_msg = "Token refresh request timed out" print(f"❌ {error_msg}") raise Exception(error_msg) except Exception as e: error_msg = f"Token refresh failed: {str(e)}" print(f"❌ {error_msg}") raise Exception(error_msg) return None @action(detail=False, methods=['get'], url_path=r'direct-download/(?P[^/.]+)') def direct_download(self, request, order_id=None): """Direct download endpoint for browser links""" print(f"🔗 DIRECT DOWNLOAD REQUEST: {order_id}") if not request.user.is_staff: messages.error(request, "Admin access required") return redirect('admin:login') try: from .models import TransferOrder order = TransferOrder.objects.get(id=order_id) file_selections = order.file_selections.all() # Get access token access_token = self._get_or_refresh_access_token(order) if not access_token: messages.error(request, "No valid access token") return redirect('download-dashboard') # Update order status order.status = 'download_in_progress' order.download_status = f'Downloading {file_selections.count()} files...' order.save(update_fields=['status', 'download_status', 'updated_at']) print(f"✅ DIRECT DOWNLOAD STARTING: {file_selections.count()} files") if file_selections.count() == 1: return self._download_single_file_to_client(order, file_selections.first(), access_token) else: return self._download_multiple_files_as_zip(order, file_selections, access_token) except Exception as e: print(f"❌ DIRECT DOWNLOAD FAILED: {str(e)}") messages.error(request, f"Download failed: {str(e)}") return redirect('download-dashboard') @action(detail=False, methods=['get', 'post'], url_path=r'transfer/payment') def payment_view(self, request): """Step 4: Payment Processing with Paystack""" base_domain = request.build_absolute_uri('/') user = request.user if not user.is_authenticated: return redirect(f'{base_domain}login/') current_order_id = request.session.get('current_order_id') if not current_order_id: messages.error(request, "Please complete storage selection first") return redirect(f'{base_domain}transfer/storage/') try: from .models import TransferOrder order = TransferOrder.objects.get(id=current_order_id, user=user) if order.status != 'storage_selected': messages.error(request, "Please complete storage selection first") return redirect(f'{base_domain}transfer/storage/') context = { 'base_domain': base_domain, 'user': user, 'order': order, 'current_step': 4, 'selected_files_count': order.total_files, 'total_size_gb': float(order.total_size) } if request.method == 'POST': return self._handle_payment(request, order, context) # GET request - show payment page context.update(self._get_payment_context(order)) return render(request, 'payment.html', context) except TransferOrder.DoesNotExist: messages.error(request, "Order not found") return redirect(f'{base_domain}transfer/new/') def _get_payment_context(self, order): """Prepare payment context with calculated amounts""" # Calculate pricing based on storage type and size base_price = self._calculate_base_price(order.storage_type, order.storage_size) shipping_cost = Decimal('4.99') # Fixed shipping cost tax_rate = Decimal('0.08') # 8% tax subtotal = base_price tax_amount = subtotal * tax_rate total_amount = subtotal + shipping_cost + tax_amount return { 'order_summary': { 'storage_type': order.storage_type, 'storage_size': order.storage_size, 'files_count': order.total_files, 'total_size': f"{order.total_size:.1f} GB", 'subtotal': f"{subtotal:.2f}", 'shipping': f"{shipping_cost:.2f}", 'tax': f"{tax_amount:.2f}", 'total': f"{total_amount:.2f}", }, 'payment_methods': [ {'id': 'card', 'name': 'Credit/Debit Card', 'icon': 'credit-card', 'description': 'Pay securely with your card'}, {'id': 'bank', 'name': 'Bank Transfer', 'icon': 'university', 'description': 'Transfer directly from your bank'}, {'id': 'mobile', 'name': 'Mobile Money', 'icon': 'mobile-alt', 'description': 'Pay with mobile money'}, ], 'paystack_public_key': 'pk_test_dummy_key_1234567890', # Dummy key for development 'order_reference': f"ORD-{order.id}-{order.created_at.strftime('%Y%m%d')}", } def _calculate_base_price(self, storage_type, storage_size): """Calculate base price based on storage selection""" size_gb = int(storage_size.replace('GB', '').strip()) # Base prices per GB for different storage types price_per_gb = { 'flash_drive': Decimal('2.50'), 'memory_card': Decimal('3.00'), 'external_hdd': Decimal('1.75') } base_price = price_per_gb.get(storage_type, Decimal('2.00')) * size_gb return max(base_price, Decimal('9.99')) # Minimum charge def _handle_payment(self, request, order, context): """Handle payment form submission""" payment_method = request.POST.get('payment_method') email = request.POST.get('email') amount = request.POST.get('amount') if not all([payment_method, email, amount]): messages.error(request, "Please fill all required fields") context.update(self._get_payment_context(order)) return render(request, 'payment.html', context) try: # Initialize Paystack payment (dummy implementation) payment_data = self._initialize_paystack_payment( email=email, amount=amount, order=order ) if payment_data.get('status'): # Store payment reference in session request.session['payment_reference'] = payment_data['data']['reference'] request.session['order_id'] = order.id # Redirect to Paystack payment page (dummy for now) messages.success(request, "Payment initialized successfully") return redirect(payment_data['data']['authorization_url']) else: messages.error(request, f"Payment initialization failed: {payment_data.get('message', 'Unknown error')}") context.update(self._get_payment_context(order)) return render(request, 'payment.html', context) except Exception as e: messages.error(request, f"Payment error: {str(e)}") context.update(self._get_payment_context(order)) return render(request, 'payment.html', context) def _initialize_paystack_payment(self, email, amount, order): """ Initialize Paystack payment (dummy implementation) In production, this would make actual API call to Paystack """ # Dummy Paystack response structure return { "status": True, "message": "Authorization URL created", "data": { "authorization_url": "https://checkout.paystack.com/dummy_checkout_page", "access_code": "dummy_access_code_123", "reference": f"paystack_ref_{order.id}_{order.created_at.strftime('%Y%m%d%H%M%S')}" } } @csrf_exempt def paystack_webhook(self, request): """Handle Paystack webhook for payment verification""" if request.method == 'POST': try: # Dummy webhook processing payload = json.loads(request.body) reference = payload.get('data', {}).get('reference') if payload.get('event') == 'charge.success': # Verify payment with Paystack (dummy) verification_result = self._verify_paystack_payment(reference) if verification_result.get('status'): # Update order status from .models import TransferOrder order_id = request.session.get('order_id') if order_id: order = TransferOrder.objects.get(id=order_id) order.status = 'payment_completed' order.payment_reference = reference order.save() # Clear session request.session.pop('current_order_id', None) request.session.pop('payment_reference', None) request.session.pop('order_id', None) return JsonResponse({'status': 'success'}) return JsonResponse({'status': 'ignored'}) except Exception as e: return JsonResponse({'status': 'error', 'message': str(e)}, status=400) return JsonResponse({'status': 'invalid_method'}, status=405) def _verify_paystack_payment(self, reference): """Verify Paystack payment (dummy implementation)""" # Dummy verification response return { "status": True, "message": "Verification successful", "data": { "reference": reference, "status": "success", "amount": 100000, # Amount in kobo "currency": "NGN", "gateway_response": "Successful" } }