// Upload functionality and UI management class GatewayUI { constructor() { this.currentUpload = null; this.recentUploads = JSON.parse(localStorage.getItem('recentUploads') || '[]'); this.serviceStatus = {}; this.initializeElements(); this.attachEventListeners(); this.initializeTheme(); this.loadRecentUploads(); this.checkServiceStatus(); this.loadServerFiles(); // Update service status every 30 seconds setInterval(() => this.checkServiceStatus(), 30000); } initializeElements() { // Upload elements this.uploadArea = document.getElementById('upload-area'); this.fileInput = document.getElementById('file-input'); this.uploadProgress = document.getElementById('upload-progress'); this.progressFill = document.getElementById('progress-fill'); this.progressPercent = document.getElementById('progress-percent'); this.progressSpeed = document.getElementById('progress-speed'); this.progressEta = document.getElementById('progress-eta'); this.uploadFilename = document.getElementById('upload-filename'); // Options this.announceDht = document.getElementById('announce-dht'); this.storeBlossom = document.getElementById('store-blossom'); // Lists this.uploadsList = document.getElementById('uploads-list'); // Toast container this.toastContainer = document.getElementById('toast-container'); } attachEventListeners() { // File upload - only add if not already attached if (!this.uploadArea.hasAttribute('data-events-attached')) { this.uploadArea.addEventListener('click', (e) => { // Prevent double clicks if (e.detail === 1) { this.fileInput.click(); } }); this.uploadArea.setAttribute('data-events-attached', 'true'); } this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e.target.files)); // Drag and drop this.uploadArea.addEventListener('dragover', (e) => this.handleDragOver(e)); this.uploadArea.addEventListener('dragleave', (e) => this.handleDragLeave(e)); this.uploadArea.addEventListener('drop', (e) => this.handleDrop(e)); // Prevent default drag behaviors on document document.addEventListener('dragover', (e) => e.preventDefault()); document.addEventListener('drop', (e) => e.preventDefault()); } initializeTheme() { const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); } handleDragOver(e) { e.preventDefault(); e.stopPropagation(); this.uploadArea.classList.add('drag-over'); } handleDragLeave(e) { e.preventDefault(); e.stopPropagation(); this.uploadArea.classList.remove('drag-over'); } handleDrop(e) { e.preventDefault(); e.stopPropagation(); this.uploadArea.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files); this.handleFileSelect(files); } handleFileSelect(files) { if (files.length === 0) return; // For now, handle one file at a time const file = files[0]; // Validate file const validation = this.validateFile(file); if (!validation.valid) { this.showToast(validation.message, 'error'); this.fileInput.value = ''; // Clear the input return; } this.uploadFile(file); } validateFile(file) { // Check file existence if (!file) { return { valid: false, message: 'No file selected' }; } // Check file size (10GB default limit - server will enforce actual limit) const maxSize = 10 * 1024 * 1024 * 1024; // 10GB if (file.size > maxSize) { return { valid: false, message: `File too large. Maximum size is ${this.formatBytes(maxSize)} (selected: ${this.formatBytes(file.size)})` }; } if (file.size === 0) { return { valid: false, message: 'Cannot upload empty file' }; } // Check filename if (!file.name || file.name.trim() === '') { return { valid: false, message: 'File must have a valid name' }; } if (file.name.length > 255) { return { valid: false, message: 'Filename too long (max 255 characters)' }; } // Check for dangerous characters in filename const dangerousChars = ['..', '/', '\\', ':', '*', '?', '"', '<', '>', '|']; for (const char of dangerousChars) { if (file.name.includes(char)) { return { valid: false, message: `Filename cannot contain '${char}' character` }; } } // Check file type (basic validation) const allowedTypes = [ // Video 'video/mp4', 'video/avi', 'video/mkv', 'video/mov', 'video/webm', // Audio 'audio/mp3', 'audio/wav', 'audio/flac', 'audio/m4a', 'audio/ogg', // Images 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', // Documents 'application/pdf', 'text/plain', 'application/zip', 'application/x-rar-compressed', // Archives 'application/x-7z-compressed', 'application/x-tar', 'application/gzip' ]; // If type is provided and not in allowed list, show warning but allow if (file.type && !allowedTypes.includes(file.type) && !file.type.startsWith('application/')) { console.warn(`Unusual file type: ${file.type}`); } return { valid: true }; } async uploadFile(file) { if (this.currentUpload) { this.showToast('Another upload is in progress', 'warning'); return; } // Create FormData const formData = new FormData(); formData.append('file', file); // Add options if (this.announceDht.checked) { formData.append('announce_dht', 'true'); } if (this.storeBlossom.checked) { formData.append('store_blossom', 'true'); } // Show progress this.showUploadProgress(file.name); try { this.currentUpload = { file: file, startTime: Date.now(), abort: new AbortController() }; const headers = {}; if (window.nostrAuth && window.nostrAuth.sessionToken) { headers['Authorization'] = `Bearer ${window.nostrAuth.sessionToken}`; console.log('Upload with auth token:', window.nostrAuth.sessionToken.substring(0, 20) + '...'); } else { console.log('Upload without auth - nostrAuth:', !!window.nostrAuth, 'sessionToken:', !!window.nostrAuth?.sessionToken); } const response = await fetch('/api/upload', { method: 'POST', headers: headers, body: formData, signal: this.currentUpload.abort.signal }); if (!response.ok) { throw new Error(`Upload failed: ${response.status} ${response.statusText}`); } const result = await response.json(); this.handleUploadSuccess(file, result); } catch (error) { if (error.name === 'AbortError') { this.showToast('Upload cancelled', 'warning'); } else { console.error('Upload error:', error); // Provide user-friendly error messages let message = 'Upload failed'; if (error.message.includes('413') || error.message.includes('too large')) { message = 'File too large. Please choose a smaller file.'; } else if (error.message.includes('415') || error.message.includes('unsupported')) { message = 'File type not supported. Please try a different file.'; } else if (error.message.includes('429') || error.message.includes('rate limit')) { message = 'Upload rate limit exceeded. Please wait and try again.'; } else if (error.message.includes('401') || error.message.includes('unauthorized')) { message = 'Please login to upload files.'; } else if (error.message.includes('403') || error.message.includes('forbidden')) { message = 'Upload not allowed. Check your permissions.'; } else if (error.message.includes('507') || error.message.includes('storage')) { message = 'Server storage full. Please try again later.'; } else if (error.message.includes('NetworkError') || error.message.includes('fetch')) { message = 'Network error. Please check your connection and try again.'; } else if (error.message.includes('timeout')) { message = 'Upload timed out. Please try again with a smaller file.'; } this.showToast(message, 'error'); } } finally { this.hideUploadProgress(); this.currentUpload = null; } } showUploadProgress(filename) { this.uploadFilename.textContent = filename; this.uploadProgress.classList.remove('hidden'); this.uploadArea.style.display = 'none'; // Start progress simulation (since we can't track real progress easily) this.simulateProgress(); } simulateProgress() { let progress = 0; const startTime = Date.now(); const updateProgress = () => { if (!this.currentUpload) return; // Simulate realistic progress curve progress += (100 - progress) * 0.05; const elapsed = (Date.now() - startTime) / 1000; const speed = (this.currentUpload.file.size * (progress / 100)) / elapsed; const remaining = (this.currentUpload.file.size - (this.currentUpload.file.size * (progress / 100))) / speed; this.progressFill.style.width = `${progress}%`; this.progressPercent.textContent = `${Math.round(progress)}%`; this.progressSpeed.textContent = this.formatBytes(speed) + '/s'; this.progressEta.textContent = this.formatTime(remaining); if (progress < 95 && this.currentUpload) { setTimeout(updateProgress, 100); } }; updateProgress(); } hideUploadProgress() { this.uploadProgress.classList.add('hidden'); this.uploadArea.style.display = 'block'; this.progressFill.style.width = '0%'; this.fileInput.value = ''; } handleUploadSuccess(file, result) { this.showToast('File uploaded successfully!', 'success'); // Add to recent uploads const uploadRecord = { id: result.file_hash || result.hash, name: file.name, size: file.size, hash: result.file_hash || result.hash, torrentHash: result.torrent_hash, magnetLink: result.magnet_link, timestamp: Date.now(), type: file.type, isVideo: file.type.startsWith('video/') }; this.recentUploads.unshift(uploadRecord); this.recentUploads = this.recentUploads.slice(0, 10); // Keep only last 10 localStorage.setItem('recentUploads', JSON.stringify(this.recentUploads)); this.loadServerFiles(); } async loadServerFiles() { // Show loading state if (this.uploadsList) { this.uploadsList.innerHTML = '
No recent uploads
'; return; } const files = this.recentUploads.map(upload => ({ file_hash: upload.hash, name: upload.name, size: upload.size, is_video: upload.isVideo, torrent_hash: upload.torrentHash, magnet_link: upload.magnetLink })); this.displayFiles(files); } displayFiles(files) { if (files.length === 0) { this.uploadsList.innerHTML = 'No files uploaded
'; return; } this.uploadsList.innerHTML = files.map(file => `