From 3aa9f2919fbccbd752c2cb08c31ccac0d8c6a33e Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 27 Jul 2025 09:26:20 -0300 Subject: [PATCH] Implement batch upload functionality and enhance upload result tracking --- README.md | 43 +++++++-- src/App.tsx | 144 +++++++++++++++++++++++++--- src/useCases/useAsyncBatchUpload.ts | 88 +++++++++++++++++ 3 files changed, 256 insertions(+), 19 deletions(-) create mode 100644 src/useCases/useAsyncBatchUpload.ts diff --git a/README.md b/README.md index 33c65f3..acde3d8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ src/ │ ├── S3UploadService.ts # Upload service implementations │ └── crypto.ts # MD5 integrity checking utilities ├── useCases/ -│ └── useSequentialUpload.ts # Upload strategy implementations +│ ├── useSequentialUpload.ts # Sequential upload implementation +│ └── useAsyncBatchUpload.ts # Batch async upload implementation ├── App.tsx # Main application component └── main.tsx # Application entry point ``` @@ -81,11 +82,11 @@ Nginx proxy handles CORS issues between the frontend and LocalStack: ## Features -### Upload Strategies - -1. **Sequential Upload**: Files uploaded one after another -2. **Parallel Upload**: Multiple concurrent uploads (coming soon) -3. **Batch Upload**: Chunked parallel processing (coming soon) +### Upload Interface +- **Strategy Selection**: Radio buttons to switch between Sequential and Batch async +- **Performance Tracking**: Live results table showing run times and comparisons +- **File Management**: Multi-file selection with reset functionality +- **Upload States**: Loading indicators and disabled states during uploads ### Security Features @@ -93,6 +94,36 @@ Nginx proxy handles CORS issues between the frontend and LocalStack: - MD5 integrity checking - File validation and size limits +## Usage + +1. **Start the application**: Follow the Development Setup steps above +2. **Select files**: Use the file input to choose multiple files +3. **Choose strategy**: Select either "Sequential Upload" or "Batch Upload (Async)" +4. **Run test**: Click the upload button and observe the performance +5. **Compare results**: Switch strategies and run again to compare performance +6. **View metrics**: Check the results table for detailed timing comparisons + +## Troubleshooting + +### LocalStack Issues +- Ensure Docker is running: `docker ps` +- Check LocalStack logs: `docker-compose logs localstack` +- Verify bucket creation: Check LocalStack dashboard + +### CORS Issues +- Nginx proxy should handle CORS automatically +- Verify nginx container is running: `docker-compose ps` + +### Upload Failures +- Check network connectivity to LocalStack +- Verify pre-signed URL generation +- Check file size limits (100MB max) + +### Performance Issues +- Browser connection limits affect batch performance +- LocalStack resource constraints in Docker +- Network saturation with many concurrent uploads + ## Resources - [AWS S3 Upload Documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html) diff --git a/src/App.tsx b/src/App.tsx index 032f0c9..d1cf623 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,16 +3,68 @@ import viteLogo from "/vite.svg"; import "./App.css"; import { useSequentialUpload } from "./useCases/useSequentialUpload"; +import { useBatchUpload } from "./useCases/useAsyncBatchUpload"; +import { useState, useEffect } from "react"; + +interface UploadResult { + id: number; + algorithm: 'Sequential' | 'Batch (Async)'; + runTime: number; + percentageFaster: string | null; + timestamp: string; +} function App() { - const { files, setFiles, handleSequentialUpload, resetUploads, runTime, isUploading } = - useSequentialUpload(); + const [strategy, setStrategy] = useState<'sequential' | 'batch'>('sequential'); + const [uploadResults, setUploadResults] = useState([]); + + const sequential = useSequentialUpload(); + const batch = useBatchUpload(); + + const current = strategy === 'sequential' ? sequential : batch; + + useEffect(() => { + if (sequential.runTime) { + const runTimeMs = parseFloat(sequential.runTime); + const lastResult = uploadResults[uploadResults.length - 1]; + const percentageFaster = lastResult + ? ((lastResult.runTime - runTimeMs) / lastResult.runTime * 100).toFixed(1) + '%' + : null; + + setUploadResults(prev => [...prev, { + id: prev.length + 1, + algorithm: 'Sequential', + runTime: runTimeMs, + percentageFaster, + timestamp: new Date().toLocaleTimeString() + }]); + } + }, [sequential.runTime]); + + useEffect(() => { + if (batch.runTime) { + const runTimeMs = parseFloat(batch.runTime); + const lastResult = uploadResults[uploadResults.length - 1]; + const percentageFaster = lastResult + ? ((lastResult.runTime - runTimeMs) / lastResult.runTime * 100).toFixed(1) + '%' + : null; + + setUploadResults(prev => [...prev, { + id: prev.length + 1, + algorithm: 'Batch (Async)', + runTime: runTimeMs, + percentageFaster, + timestamp: new Date().toLocaleTimeString() + }]); + } + }, [batch.runTime]); const handleFileChange = (event: React.ChangeEvent) => { const fileList = event.target.files; if (fileList && fileList.length > 0) { const selectedFiles = Array.from(fileList); - setFiles(selectedFiles); + sequential.setFiles(selectedFiles); + batch.setFiles(selectedFiles); } }; @@ -43,27 +95,93 @@ function App() { onChange={handleFileChange} /> - {files.length > 0 && ( + {current.files.length > 0 && (
- {runTime &&

Last upload time: {runTime}

} +
+ + +
+ + {uploadResults.length > 0 && ( +
+

Upload Performance Results:

+ + + + + + + + + + + + {uploadResults.map((result) => ( + + + + + + + + ))} + +
Run #AlgorithmTime (ms)vs PreviousTimestamp
{result.id}{result.algorithm}{result.runTime.toFixed(2)} + {result.percentageFaster ? + (result.percentageFaster.startsWith('-') ? + `${result.percentageFaster.substring(1)} slower` : + `${result.percentageFaster} faster` + ) : + 'First run' + } + {result.timestamp}
+
+ )} + -
)} - {files.length > 0 && ( + {current.files.length > 0 && (
-

Selected Files ({files.length}):

- {files.map((f, index) => ( +

Selected Files ({current.files.length}):

+ {current.files.map((f, index) => (

{f.name}

))}
diff --git a/src/useCases/useAsyncBatchUpload.ts b/src/useCases/useAsyncBatchUpload.ts new file mode 100644 index 0000000..fd025d1 --- /dev/null +++ b/src/useCases/useAsyncBatchUpload.ts @@ -0,0 +1,88 @@ +import { useState, useCallback } from "react"; +import { S3UploadService, type CustomFile } from "../infra/S3UploadService"; +import { generateMD5Base64 } from "../infra/crypto"; + +export const useBatchUpload = () => { + const [files, setFiles] = useState([]); + const [uploadService] = useState(() => new S3UploadService()); + + const [runTime, setRunTime] = useState(null); + const [isUploading, setIsUploading] = useState(false); + + async function uploadFilesBatch(files: File[]): Promise { + const results: Response[] = []; + const customFiles: CustomFile[] = []; + + const md5Promises = files.map(async (file) => { + const md5Hash = await generateMD5Base64(file); + return { file, md5: md5Hash, preSignedUrl: "" }; + }); + + const resolvedCustomFiles = await Promise.all(md5Promises); + customFiles.push(...resolvedCustomFiles); + + const urlPromises = customFiles.map(async (customFile) => { + const signedUrl = await uploadService.generateSignedUploadUrl( + customFile.file.name + ); + customFile.preSignedUrl = signedUrl; + }); + + await Promise.all(urlPromises); + + const uploadPromises = customFiles.map(async (customFile) => { + return await uploadService.uploadSingleFile( + customFile, + customFile.preSignedUrl + ); + }); + + const uploadResults = await Promise.all(uploadPromises); + results.push(...uploadResults); + + return results; + } + + const handleAsyncBatchUpload = async () => { + if (files.length === 0) return; + + setIsUploading(true); + setRunTime(null); + const startTime = performance.now(); + + try { + const results = await uploadFilesBatch(files); + + results.forEach((response) => { + if (response.ok) { + console.log("File uploaded successfully:", response); + } else { + console.error("File upload failed:", response.statusText); + } + }); + } catch (error) { + console.error("Upload error:", error); + } finally { + const endTime = performance.now(); + console.log( + `Batch upload completed in ${(endTime - startTime).toFixed(2)} ms` + ); + setRunTime(`${(endTime - startTime).toFixed(2)} ms`); + setIsUploading(false); + } + }; + + const resetUploads = useCallback(() => { + setFiles([]); + }, []); + + return { + files, + setFiles, + runTime, + isUploading, + + handleAsyncBatchUpload, + resetUploads, + }; +};