diff --git a/retoors/static/js/components/upload.js b/retoors/static/js/components/upload.js
new file mode 100644
index 0000000..b81f027
--- /dev/null
+++ b/retoors/static/js/components/upload.js
@@ -0,0 +1,116 @@
+export function showUploadModal() {
+ document.getElementById('upload-modal').style.display = 'block';
+ // Clear previous selections and progress
+ document.getElementById('selected-files-preview').innerHTML = '';
+ document.getElementById('upload-progress-container').innerHTML = '';
+ document.getElementById('file-input-multiple').value = ''; // Clear selected files
+ document.getElementById('start-upload-btn').disabled = true;
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const fileInput = document.getElementById('file-input-multiple');
+ const selectedFilesPreview = document.getElementById('selected-files-preview');
+ const startUploadBtn = document.getElementById('start-upload-btn');
+ const uploadProgressContainer = document.getElementById('upload-progress-container');
+
+ let filesToUpload = [];
+
+ fileInput.addEventListener('change', (event) => {
+ filesToUpload = Array.from(event.target.files);
+ selectedFilesPreview.innerHTML = ''; // Clear previous previews
+ uploadProgressContainer.innerHTML = ''; // Clear previous progress bars
+
+ if (filesToUpload.length > 0) {
+ startUploadBtn.disabled = false;
+ filesToUpload.forEach(file => {
+ const fileEntry = document.createElement('div');
+ fileEntry.className = 'file-entry';
+ fileEntry.innerHTML = `
+ ${file.name}
+ (${(file.size / 1024 / 1024).toFixed(2)} MB)
+
+ `;
+ selectedFilesPreview.appendChild(fileEntry);
+
+ // Display thumbnail for image files
+ if (file.type.startsWith('image/')) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const img = document.createElement('img');
+ img.src = e.target.result;
+ fileEntry.querySelector('.thumbnail-preview').appendChild(img);
+ };
+ reader.readAsDataURL(file);
+ }
+ });
+ } else {
+ startUploadBtn.disabled = true;
+ }
+ });
+
+ startUploadBtn.addEventListener('click', () => {
+ if (filesToUpload.length > 0) {
+ uploadFiles(filesToUpload);
+ }
+ });
+
+ async function uploadFiles(files) {
+ startUploadBtn.disabled = true; // Disable button during upload
+ uploadProgressContainer.innerHTML = ''; // Clear previous progress
+
+ const currentPath = new URLSearchParams(window.location.search).get('path') || '';
+
+ for (const file of files) {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const progressBarContainer = document.createElement('div');
+ progressBarContainer.className = 'progress-bar-container';
+ progressBarContainer.innerHTML = `
+ ${file.name}
+
+ 0%
+ `;
+ uploadProgressContainer.appendChild(progressBarContainer);
+
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', `/files/upload?current_path=${encodeURIComponent(currentPath)}`, true);
+
+ xhr.upload.addEventListener('progress', (event) => {
+ if (event.lengthComputable) {
+ const percent = (event.loaded / event.total) * 100;
+ document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `${percent}%`;
+ document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `${Math.round(percent)}%`;
+ }
+ });
+
+ xhr.addEventListener('load', () => {
+ if (xhr.status === 200) {
+ console.log(`File ${file.name} uploaded successfully.`);
+ // Update progress to 100% on completion
+ document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `100%`;
+ document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `100% (Done)`;
+ } else {
+ console.error(`Error uploading ${file.name}: ${xhr.statusText}`);
+ document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Failed (${xhr.status})`;
+ document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
+ }
+ });
+
+ xhr.addEventListener('error', () => {
+ console.error(`Network error uploading ${file.name}.`);
+ document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Network Error`;
+ document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
+ });
+
+ xhr.send(formData);
+ }
+ // After all files are sent, refresh the page to show new files
+ // A small delay to allow server to process and update file list
+ setTimeout(() => {
+ window.location.reload();
+ }, 1000);
+ }
+});
diff --git a/retoors/views/upload.py b/retoors/views/upload.py
new file mode 100644
index 0000000..c86440c
--- /dev/null
+++ b/retoors/views/upload.py
@@ -0,0 +1,50 @@
+from aiohttp import web
+import aiohttp_jinja2
+from aiohttp.web_response import json_response
+
+from ..helpers.auth import login_required
+
+class UploadView(web.View):
+ @login_required
+ async def post(self):
+ user_email = self.request["user"]["email"]
+ file_service = self.request.app["file_service"]
+ # Get current path from query parameter or form data
+ current_path = self.request.query.get("current_path", "")
+
+ try:
+ reader = await self.request.multipart()
+ files_uploaded = []
+ errors = []
+
+ while True:
+ field = await reader.next()
+ if field is None:
+ break
+
+ # Check if the field is a file input
+ if field.name == "file": # Assuming the input field name is 'file'
+ filename = field.filename
+ if not filename:
+ errors.append("Filename is required for one of the files.")
+ continue
+
+ content = await field.read()
+ # Construct the full file path relative to the user's base directory
+ full_file_path_for_service = f"{current_path}/{filename}" if current_path else filename
+
+ success = await file_service.upload_file(user_email, full_file_path_for_service, content)
+ if success:
+ files_uploaded.append(filename)
+ else:
+ errors.append(f"Failed to upload file '{filename}'")
+
+ if errors:
+ return json_response({"status": "error", "message": "Some files failed to upload", "details": errors}, status=500)
+ elif files_uploaded:
+ return json_response({"status": "success", "message": f"Successfully uploaded {len(files_uploaded)} files", "files": files_uploaded})
+ else:
+ return json_response({"status": "error", "message": "No files were uploaded"}, status=400)
+
+ except Exception as e:
+ return json_response({"status": "error", "message": f"Upload error: {str(e)}"}, status=500)