Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -81,18 +82,48 @@ 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

- Pre-signed URLs for secure uploads
- 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)
Expand Down
144 changes: 131 additions & 13 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<UploadResult[]>([]);

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<HTMLInputElement>) => {
const fileList = event.target.files;
if (fileList && fileList.length > 0) {
const selectedFiles = Array.from(fileList);
setFiles(selectedFiles);
sequential.setFiles(selectedFiles);
batch.setFiles(selectedFiles);
}
};

Expand Down Expand Up @@ -43,27 +95,93 @@ function App() {
onChange={handleFileChange}
/>

{files.length > 0 && (
{current.files.length > 0 && (
<div>
{runTime && <p>Last upload time: {runTime}</p>}
<div>
<label>
<input
type="radio"
value="sequential"
checked={strategy === 'sequential'}
onChange={(e) => setStrategy(e.target.value as 'sequential' | 'batch')}
/>
Sequential Upload
</label>
<label style={{ marginLeft: '1rem' }}>
<input
type="radio"
value="batch"
checked={strategy === 'batch'}
onChange={(e) => setStrategy(e.target.value as 'sequential' | 'batch')}
/>
Batch Upload (Async)
</label>
</div>

{uploadResults.length > 0 && (
<div style={{ margin: '1rem 0' }}>
<h4>Upload Performance Results:</h4>
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '0.5rem' }}>
<thead>
<tr>
<th>Run #</th>
<th>Algorithm</th>
<th>Time (ms)</th>
<th>vs Previous</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{uploadResults.map((result) => (
<tr key={result.id}>
<td>{result.id}</td>
<td>{result.algorithm}</td>
<td>{result.runTime.toFixed(2)}</td>
<td style={{
color: result.percentageFaster ? (result.percentageFaster.startsWith('-') ? 'red' : 'green') : 'gray'
}}>
{result.percentageFaster ?
(result.percentageFaster.startsWith('-') ?
`${result.percentageFaster.substring(1)} slower` :
`${result.percentageFaster} faster`
) :
'First run'
}
</td>
<td>{result.timestamp}</td>
</tr>
))}
</tbody>
</table>
</div>
)}

<button
type="button"
onClick={handleSequentialUpload}
disabled={isUploading}
onClick={strategy === 'sequential' ? sequential.handleSequentialUpload : batch.handleAsyncBatchUpload}
disabled={current.isUploading}
>
{isUploading ? "Uploading..." : "Upload Sequentially"}
{current.isUploading ? "Uploading..." : `Upload ${strategy === 'sequential' ? 'Sequentially' : 'in Batches'}`}
</button>
<button type="button" onClick={resetUploads}>
Reset

<button
type="button"
onClick={() => {
sequential.resetUploads();
batch.resetUploads();
setUploadResults([]);
}}
>
Reset All
</button>
</div>
)}
</div>

{files.length > 0 && (
{current.files.length > 0 && (
<div className="card">
<h3>Selected Files ({files.length}):</h3>
{files.map((f, index) => (
<h3>Selected Files ({current.files.length}):</h3>
{current.files.map((f, index) => (
<p key={index}>{f.name}</p>
))}
</div>
Expand Down
88 changes: 88 additions & 0 deletions src/useCases/useAsyncBatchUpload.ts
Original file line number Diff line number Diff line change
@@ -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<File[]>([]);
const [uploadService] = useState(() => new S3UploadService());

const [runTime, setRunTime] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);

async function uploadFilesBatch(files: File[]): Promise<Response[]> {
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,
};
};