Files
docupulse/static/js/rooms/uploadManager.js
2025-05-30 23:31:14 +02:00

363 lines
14 KiB
JavaScript

/**
* @fileoverview Manages file upload functionality for the room interface.
* This file handles:
* - File upload initialization and configuration
* - Drag and drop file handling
* - Upload progress tracking
* - File type validation
* - Overwrite handling
* - Batch upload management
*/
/**
* @class UploadManager
* @classdesc Manages file upload operations including drag-and-drop, progress tracking,
* and handling of file conflicts and validations.
*/
export class UploadManager {
/**
* Creates a new UploadManager instance.
* @param {RoomManager} roomManager - The parent RoomManager instance
*/
constructor(roomManager) {
this.roomManager = roomManager;
this.pendingUploads = [];
this.pendingUploadIdx = 0;
this.overwriteAll = false;
this.skipAll = false;
// Initialize upload-related elements
this.uploadBtn = document.getElementById('uploadBtn');
this.fileInput = document.getElementById('fileInput');
this.uploadForm = document.getElementById('uploadForm');
this.fileGrid = document.getElementById('fileGrid');
this.dropZoneOverlay = document.getElementById('dropZoneOverlay');
this.uploadProgressContainer = document.getElementById('uploadProgressContainer');
this.uploadProgressBar = document.getElementById('uploadProgressBar');
this.uploadProgressText = document.getElementById('uploadProgressText');
this.initializeUploadHandlers();
}
/**
* Initializes event handlers for file upload functionality.
* Sets up drag and drop handlers and file input change handlers.
*/
initializeUploadHandlers() {
if (!this.roomManager.canUpload) return;
// Initialize drag and drop
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
this.fileGrid.addEventListener(eventName, this.preventDefaults.bind(this), false);
document.body.addEventListener(eventName, this.preventDefaults.bind(this), false);
});
['dragenter', 'dragover'].forEach(eventName => {
this.fileGrid.addEventListener(eventName, this.highlight.bind(this), false);
});
['dragleave', 'drop'].forEach(eventName => {
this.fileGrid.addEventListener(eventName, this.unhighlight.bind(this), false);
});
// Handle dropped files
this.fileGrid.addEventListener('drop', this.handleDrop.bind(this), false);
// Handle file input change
this.uploadBtn.addEventListener('click', () => this.fileInput.click());
this.fileInput.addEventListener('change', this.handleFileSelect.bind(this));
}
/**
* Prevents default browser behavior for drag and drop events.
* @param {Event} e - The event object
*/
preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
/**
* Highlights the drop zone when files are dragged over it.
*/
highlight() {
this.dropZoneOverlay.style.display = 'block';
}
/**
* Removes highlight from the drop zone.
*/
unhighlight() {
this.dropZoneOverlay.style.display = 'none';
}
/**
* Handles files dropped onto the drop zone.
* @param {DragEvent} e - The drop event object
*/
async handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
await this.startUpload(Array.from(files));
}
}
/**
* Handles files selected through the file input.
*/
async handleFileSelect() {
if (!this.fileInput.files.length) return;
await this.startUpload(Array.from(this.fileInput.files));
}
/**
* Initiates the upload process for a set of files.
* @param {Array<File>} files - Array of files to upload
*/
async startUpload(files) {
this.uploadProgressContainer.style.display = 'block';
this.uploadProgressBar.style.width = '0%';
this.uploadProgressBar.className = 'progress-bar bg-primary-opacity-15 text-primary';
this.uploadProgressText.textContent = '';
this.pendingUploads = files;
this.pendingUploadIdx = 0;
this.overwriteAll = false;
this.skipAll = false;
await this.uploadFilesSequentially();
}
/**
* Uploads files one at a time, handling progress and errors.
* @async
*/
async uploadFilesSequentially() {
let completedFiles = 0;
let currentFileIndex = 0;
const updateProgress = () => {
if (!this.pendingUploads || currentFileIndex >= this.pendingUploads.length) {
this.uploadProgressBar.style.width = '100%';
this.uploadProgressBar.textContent = '100%';
this.uploadProgressText.textContent = 'Upload complete!';
return;
}
const progress = Math.round((completedFiles / this.pendingUploads.length) * 100);
this.uploadProgressBar.style.width = progress + '%';
this.uploadProgressBar.textContent = progress + '%';
this.uploadProgressText.textContent = `Uploading ${this.pendingUploads[currentFileIndex].name} (${currentFileIndex + 1}/${this.pendingUploads.length})`;
};
const processNextFile = async () => {
if (currentFileIndex >= this.pendingUploads.length) {
// All files processed
this.uploadProgressBar.style.width = '100%';
this.uploadProgressBar.textContent = '100%';
this.uploadProgressBar.className = 'progress-bar bg-success-opacity-15 text-success';
this.uploadProgressText.textContent = 'Upload complete!';
// Reset state
this.pendingUploads = null;
this.pendingUploadIdx = null;
this.overwriteAll = false;
this.skipAll = false;
// Hide progress after delay
setTimeout(() => {
this.uploadProgressContainer.style.display = 'none';
this.uploadProgressText.textContent = '';
this.uploadProgressBar.className = 'progress-bar bg-primary-opacity-15 text-primary';
}, 3000);
// Refresh file list
await this.roomManager.fileManager.fetchFiles();
return;
}
const file = this.pendingUploads[currentFileIndex];
const formData = new FormData(this.uploadForm);
if (this.roomManager.currentPath) {
formData.append('path', this.roomManager.currentPath);
}
formData.set('file', file);
try {
updateProgress();
// If skipAll is true, skip this file
if (this.skipAll) {
console.log('[UploadManager] Skipping file due to skipAll:', file.name);
currentFileIndex++;
updateProgress();
await processNextFile();
return;
}
// If overwriteAll is true, add overwrite flag
if (this.overwriteAll) {
console.log('[UploadManager] Overwriting file due to overwriteAll:', file.name);
formData.append('overwrite', 'true');
}
const response = await this.uploadFile(formData);
if (response.success) {
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
} else if (response.error === 'File type not allowed') {
this.handleFileTypeError(file);
currentFileIndex++;
updateProgress();
await processNextFile();
} else if (response.error === 'File exists') {
// If we have skipAll or overwriteAll set, handle it here
if (this.skipAll) {
console.log('[UploadManager] Skipping existing file due to skipAll:', file.name);
currentFileIndex++;
updateProgress();
await processNextFile();
} else if (this.overwriteAll) {
console.log('[UploadManager] Overwriting existing file due to overwriteAll:', file.name);
formData.append('overwrite', 'true');
await this.uploadFile(formData);
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
} else {
const result = await this.handleFileExists(file, formData);
if (result.continue) {
completedFiles++;
currentFileIndex++;
updateProgress();
await processNextFile();
}
}
}
} catch (error) {
console.error('Upload error:', error);
this.uploadProgressText.textContent = `Error uploading ${file.name}`;
this.uploadProgressBar.className = 'progress-bar bg-danger-opacity-15 text-danger';
currentFileIndex++;
updateProgress();
await processNextFile();
}
};
await processNextFile();
}
/**
* Uploads a single file to the server.
* @param {FormData} formData - Form data containing the file and upload parameters
* @returns {Promise<Object>} Response object containing success status and error message if any
*/
async uploadFile(formData) {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const response = await fetch(`/api/rooms/${this.roomManager.roomId}/files/upload`, {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken },
body: formData
});
const result = await response.json();
// Handle 409 Conflict response
if (response.status === 409) {
return {
success: false,
error: 'File exists'
};
}
return {
success: response.ok,
error: result.error
};
}
/**
* Handles file type validation errors.
* Displays error message with allowed file types.
* @param {File} file - The file that failed validation
*/
handleFileTypeError(file) {
const allowedTypes = [
'Documents: PDF, DOCX, DOC, TXT, RTF, ODT, MD, CSV',
'Spreadsheets: XLSX, XLS, ODS, XLSM',
'Presentations: PPTX, PPT, ODP',
'Images: JPG, JPEG, PNG, GIF, BMP, SVG, WEBP, TIFF',
'Archives: ZIP, RAR, 7Z, TAR, GZ',
'Code/Text: PY, JS, HTML, CSS, JSON, XML, SQL, SH, BAT',
'Audio: MP3, WAV, OGG, M4A, FLAC',
'Video: MP4, AVI, MOV, WMV, FLV, MKV, WEBM',
'CAD/Design: DWG, DXF, AI, PSD, EPS, INDD',
'Other: EML, MSG, VCF, ICS'
].join('\n');
const uploadError = document.getElementById('uploadError');
const uploadErrorContent = document.getElementById('uploadErrorContent');
const newError = `<div class="mb-2"><strong>File type not allowed:</strong> ${file.name}</div>`;
if (uploadErrorContent.innerHTML === '') {
uploadErrorContent.innerHTML = newError + `<div class='mt-2'><strong>Allowed file types:</strong><br>${allowedTypes}</div>`;
} else {
uploadErrorContent.innerHTML = newError + uploadErrorContent.innerHTML;
}
uploadError.style.display = 'block';
this.uploadProgressBar.className = 'progress-bar bg-danger-opacity-15 text-danger';
}
/**
* Handles file existence conflicts during upload.
* @param {File} file - The file being uploaded
* @param {FormData} formData - Form data for the upload
* @returns {Promise<Object>} Object containing continue status
*/
async handleFileExists(file, formData) {
if (this.overwriteAll) {
formData.append('overwrite', 'true');
const response = await this.uploadFile(formData);
return { continue: true };
}
if (this.skipAll) {
return { continue: true };
}
const result = await this.roomManager.modalManager.showOverwriteModal(file.name);
// Add a delay before processing the result
await new Promise(resolve => setTimeout(resolve, 500));
if (result === 'overwrite' || result === 'overwrite_all') {
if (result === 'overwrite_all') {
this.overwriteAll = true;
this.skipAll = false;
formData.append('overwrite', 'true');
await this.uploadFile(formData);
return { continue: true };
} else {
formData.append('overwrite', 'true');
await this.uploadFile(formData);
return { continue: true };
}
} else if (result === 'skip' || result === 'skip_all') {
if (result === 'skip_all') {
this.skipAll = true;
this.overwriteAll = false;
}
return { continue: true };
}
// If user closes the modal without making a choice, skip the file
return { continue: true };
}
}