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
147 changes: 17 additions & 130 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,136 +4,23 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.*
!.env.example

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
node_modules
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Sveltekit cache directory
.svelte-kit/

# vitepress build output
**/.vitepress/dist

# vitepress cache directory
**/.vitepress/cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# Firebase cache directory
.firebase/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

/localstack-data/
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,101 @@
# frontend-concurrency-control
# Frontend Concurrency Control

A React + TypeScript project demonstrating different approaches to file upload concurrency control using AWS S3 with LocalStack for local development.

## Overview

This project explores various concurrency patterns for file uploads to S3, starting with sequential uploads and progressing to more sophisticated parallel upload strategies. It uses LocalStack to simulate AWS S3 locally for development and testing.

## Tech Stack

- **Frontend**: React 19 + TypeScript + Vite
- **AWS SDK**: @aws-sdk/client-s3 + @aws-sdk/s3-request-presigner
- **Local Infrastructure**: LocalStack + Docker + Nginx (CORS proxy)
- **Development**: ESLint + TypeScript strict mode

## Project Structure

```
src/
├── infra/
│ ├── S3Client.ts # LocalStack S3 client configuration
│ ├── S3UploadService.ts # Upload service implementations
│ └── crypto.ts # MD5 integrity checking utilities
├── useCases/
│ └── useSequentialUpload.ts # Upload strategy implementations
├── App.tsx # Main application component
└── main.tsx # Application entry point
```

## Prerequisites

- Node.js (18+)
- Docker & Docker Compose
- npm or yarn

## Development Setup

1. **Clone and install dependencies**:
```bash
git clone <repo-url>
cd frontend-concurrency-control
npm install
```

2. **Start LocalStack infrastructure**:
```bash
docker-compose up -d
```
This starts:
- LocalStack S3 service on port 4566
- Nginx CORS proxy for frontend access

3. **Start development server**:
```bash
npm run dev
```
Application will be available at `http://localhost:5173`

## Available Scripts

- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run lint` - Run ESLint
- `npm run preview` - Preview production build

## LocalStack Configuration

The project uses LocalStack to simulate AWS S3 locally:

- **Endpoint**: `http://localhost:4566`
- **Region**: `us-east-1`
- **Credentials**: `test`/`test` (development only)
- **Default Bucket**: `test-uploads`

### CORS Configuration

Nginx proxy handles CORS issues between the frontend and LocalStack:
- Frontend requests go to `localhost:4566` (Nginx)
- Nginx proxies to LocalStack with proper CORS headers
- Supports file uploads up to 100MB

## 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)

### Security Features

- Pre-signed URLs for secure uploads
- MD5 integrity checking
- File validation and size limits

## Resources

- [AWS S3 Upload Documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html)
- [Pre-signed URLs Guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html)
- [Object Integrity Checking](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html)
- [LocalStack Documentation](https://docs.localstack.cloud/)
33 changes: 33 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
services:
localstack:
image: localstack/localstack:latest
environment:
- SERVICES=s3
- DEBUG=1
- DATA_DIR=/var/lib/localstack
- DISABLE_CORS_CHECKS=1
- SKIP_CORS_CHECK=1
- CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:4566
- CORS_ALLOWED_HEADERS=authorization,content-type,content-md5,cache-control,x-amz-content-sha256,x-amz-date,x-amz-security-token,x-amz-user-agent,x-amz-target,x-amz-acl,x-amz-version-id,x-localstack-target,x-amz-tagging
- CORS_ALLOWED_METHODS=HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
- EXTRA_CORS_ALLOWED_HEADERS=x-amz-request-id,x-amz-id-2
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
tmpfs:
- /var/lib/localstack
networks:
- localstack-net

cors-proxy:
image: nginx:alpine
ports:
- "4566:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- localstack
networks:
- localstack-net

networks:
localstack-net:
23 changes: 23 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'

export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
48 changes: 48 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
events {
worker_connections 1024;
}

http {
client_max_body_size 100M;

server {
listen 80;
server_name localhost;
client_max_body_size 100M;

location / {
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, HEAD, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Max-Age' 1728000 always;
add_header 'Content-Type' 'text/plain; charset=utf-8' always;
add_header 'Content-Length' 0 always;
return 204;
}

# Proxy to LocalStack
proxy_pass http://localstack:4566;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# Remove problematic headers to prevent LocalStack CORS conflicts
proxy_set_header Origin "";
proxy_set_header Referer "";

# Hide LocalStack CORS headers to prevent duplication
proxy_hide_header 'Access-Control-Allow-Origin';
proxy_hide_header 'Access-Control-Allow-Methods';
proxy_hide_header 'Access-Control-Allow-Headers';

# Add our own CORS headers after hiding LocalStack's
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, HEAD, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range,ETag' always;
}
}
}
Loading