# Chunked Upload Implementation Guide

## Overview

This application now supports chunked file uploads to avoid nginx 413 "Request Entity Too Large" errors when deploying large JAR files through a load balancer.

## How It Works

Instead of uploading the entire JAR and manifest files in a single request, files are split into smaller chunks (default 5MB) and uploaded sequentially. The server reassembles the chunks before deployment.

## API Endpoints

### 1. Initialize Upload Session
**POST** `/api/cf/upload/init`

Creates a new upload session and returns a session ID.

**Request Body:**
```json
{
  "apiEndpoint": "https://api.cf.example.com",
  "username": "your-username",
  "password": "your-password",
  "organization": "your-org",
  "space": "your-space",
  "appName": "your-app",
  "skipSslValidation": false
}
```

**Response:**
```json
{
  "success": true,
  "uploadSessionId": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Upload session created successfully"
}
```

### 2. Upload File Chunk
**POST** `/api/cf/upload/chunk`

Upload a single chunk of a file.

**Request Parameters:**
- `uploadSessionId` (string): The session ID from step 1
- `fileType` (string): Either "jarFile" or "manifest"
- `chunkIndex` (integer): Zero-based index of this chunk (0, 1, 2, ...)
- `totalChunks` (integer): Total number of chunks for this file
- `fileName` (string, optional): Original filename (required for jarFile)
- `chunk` (multipart file): The chunk data

**Response:**
```json
{
  "success": true,
  "uploadSessionId": "550e8400-e29b-41d4-a716-446655440000",
  "fileType": "jarFile",
  "chunkIndex": 0,
  "totalChunks": 10,
  "receivedChunks": 1,
  "message": "Chunk uploaded successfully"
}
```

### 3. Get Upload Status
**GET** `/api/cf/upload/status/{uploadSessionId}`

Check the status of an upload session.

**Response:**
```json
{
  "jarFile": {
    "fileName": "myapp.jar",
    "totalChunks": 10,
    "receivedChunks": {
      "0": true,
      "1": true,
      "2": true
    }
  },
  "manifest": {
    "fileName": "manifest.yml",
    "totalChunks": 1,
    "receivedChunks": {
      "0": true
    }
  }
}
```

### 4. Finalize Upload and Deploy
**POST** `/api/cf/upload/finalize?uploadSessionId={sessionId}`

Triggers the deployment after all chunks are uploaded.

**Response:**
Same as the traditional `/api/cf/deploy` endpoint.

## Client Implementation Example (JavaScript)

```javascript
// You can use ANY chunk size - server supports variable chunk sizes!
// Recommended: 1-2MB for Tanzu with memory constraints
const CHUNK_SIZE = 1 * 1024 * 1024; // 1MB
// Other options:
// const CHUNK_SIZE = 512 * 1024;      // 512KB (very safe)
// const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB (balanced)
// const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB (if you have memory)

async function deployWithChunks(jarFile, manifestFile, deploymentConfig) {
  const apiBase = 'https://your-app.example.com/api/cf';

  // Step 1: Initialize upload session
  const initResponse = await fetch(`${apiBase}/upload/init`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(deploymentConfig)
  });

  const { uploadSessionId } = await initResponse.json();
  console.log('Upload session created:', uploadSessionId);

  // Step 2: Upload JAR file in chunks
  await uploadFileInChunks(apiBase, uploadSessionId, 'jarFile', jarFile);

  // Step 3: Upload manifest file in chunks
  await uploadFileInChunks(apiBase, uploadSessionId, 'manifest', manifestFile);

  // Step 4: Finalize and deploy
  const deployResponse = await fetch(
    `${apiBase}/upload/finalize?uploadSessionId=${uploadSessionId}`,
    { method: 'POST' }
  );

  const result = await deployResponse.json();
  console.log('Deployment result:', result);
  return result;
}

async function uploadFileInChunks(apiBase, sessionId, fileType, file) {
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  console.log(`Uploading ${fileType}: ${file.name} (${totalChunks} chunks)`);

  for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
    const start = chunkIndex * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    const chunk = file.slice(start, end);

    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('uploadSessionId', sessionId);
    formData.append('fileType', fileType);
    formData.append('chunkIndex', chunkIndex);
    formData.append('totalChunks', totalChunks);
    formData.append('fileName', file.name);

    const response = await fetch(`${apiBase}/upload/chunk`, {
      method: 'POST',
      body: formData
    });

    const result = await response.json();
    console.log(`Chunk ${chunkIndex + 1}/${totalChunks} uploaded for ${fileType}`);

    if (!result.success) {
      throw new Error(`Failed to upload chunk: ${result.message}`);
    }
  }

  console.log(`${fileType} upload complete`);
}

// Usage
const jarInput = document.getElementById('jarFile');
const manifestInput = document.getElementById('manifestFile');

const config = {
  apiEndpoint: 'https://api.cf.example.com',
  username: 'user',
  password: 'pass',
  organization: 'my-org',
  space: 'dev',
  appName: 'my-app',
  skipSslValidation: false
};

deployWithChunks(jarInput.files[0], manifestInput.files[0], config)
  .then(result => console.log('Success:', result))
  .catch(error => console.error('Error:', error));
```

## Client Implementation Example (Python)

```python
import requests
import os

# You can use ANY chunk size!
CHUNK_SIZE = 1 * 1024 * 1024  # 1MB (recommended for Tanzu)

def deploy_with_chunks(api_base, jar_path, manifest_path, deployment_config):
    # Step 1: Initialize upload session
    response = requests.post(
        f"{api_base}/upload/init",
        json=deployment_config
    )
    session_id = response.json()['uploadSessionId']
    print(f"Upload session created: {session_id}")

    # Step 2: Upload JAR file in chunks
    upload_file_in_chunks(api_base, session_id, 'jarFile', jar_path)

    # Step 3: Upload manifest file in chunks
    upload_file_in_chunks(api_base, session_id, 'manifest', manifest_path)

    # Step 4: Finalize and deploy
    response = requests.post(
        f"{api_base}/upload/finalize",
        params={'uploadSessionId': session_id}
    )

    result = response.json()
    print(f"Deployment result: {result}")
    return result

def upload_file_in_chunks(api_base, session_id, file_type, file_path):
    file_size = os.path.getsize(file_path)
    total_chunks = (file_size + CHUNK_SIZE - 1) // CHUNK_SIZE
    file_name = os.path.basename(file_path)

    print(f"Uploading {file_type}: {file_name} ({total_chunks} chunks)")

    with open(file_path, 'rb') as f:
        for chunk_index in range(total_chunks):
            chunk_data = f.read(CHUNK_SIZE)

            files = {'chunk': (f'chunk_{chunk_index}', chunk_data)}
            data = {
                'uploadSessionId': session_id,
                'fileType': file_type,
                'chunkIndex': chunk_index,
                'totalChunks': total_chunks,
                'fileName': file_name
            }

            response = requests.post(
                f"{api_base}/upload/chunk",
                files=files,
                data=data
            )

            result = response.json()
            print(f"Chunk {chunk_index + 1}/{total_chunks} uploaded for {file_type}")

            if not result.get('success'):
                raise Exception(f"Failed to upload chunk: {result.get('message')}")

    print(f"{file_type} upload complete")

# Usage
config = {
    'apiEndpoint': 'https://api.cf.example.com',
    'username': 'user',
    'password': 'pass',
    'organization': 'my-org',
    'space': 'dev',
    'appName': 'my-app',
    'skipSslValidation': False
}

deploy_with_chunks(
    'https://your-app.example.com/api/cf',
    '/path/to/app.jar',
    '/path/to/manifest.yml',
    config
)
```

## Nginx Configuration

For the chunked upload to work properly with nginx, you need minimal configuration changes:

```nginx
server {
    listen 80;
    server_name your-app.example.com;

    # Important: Set client_max_body_size for individual chunks
    # This should be slightly larger than your chunk size (5MB chunks -> 10MB limit)
    client_max_body_size 10m;

    # Increase timeouts for long deployments
    proxy_read_timeout 900s;
    proxy_connect_timeout 900s;
    proxy_send_timeout 900s;

    location /api/cf/ {
        proxy_pass http://cf-deployer-backend:8080;
        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;

        # Buffer settings for chunked uploads
        proxy_buffering off;
        proxy_request_buffering off;
    }
}
```

### Key Nginx Settings:

1. **client_max_body_size**: Set to ~10MB (double your chunk size for safety)
2. **proxy_buffering off**: Prevents nginx from buffering the entire request
3. **proxy_request_buffering off**: Allows streaming of request body
4. **Increased timeouts**: CF deployments can take several minutes

## Configuration Properties

### application.properties

```properties
# Chunked Upload Configuration
cf.upload.session.timeout-minutes=30
```

- **cf.upload.session.timeout-minutes**: How long inactive sessions are kept (default: 30 minutes)

**Note:** There is NO server-side chunk size configuration. The server accepts ANY chunk size from the client. Chunks are appended sequentially as they arrive.

## Session Management

- Upload sessions expire after 30 minutes of inactivity (configurable)
- Expired sessions are automatically cleaned up every 5 minutes
- Sessions are deleted after successful deployment
- Each session maintains its own temporary directory

## Error Handling

### Common Errors:

1. **"Upload session not found or expired"**
   - Session timed out (default: 30 minutes)
   - Invalid session ID
   - Solution: Create a new upload session

2. **"Upload incomplete. Not all file chunks received"**
   - Not all chunks were uploaded before calling finalize
   - Solution: Check upload status and retry missing chunks

3. **"Total chunks mismatch"**
   - Different totalChunks value sent for the same file
   - Solution: Ensure consistent totalChunks across all chunk uploads

## Migration from Traditional Upload

The traditional `/api/cf/deploy` endpoint remains available and functional. You can:

1. **Keep using the traditional endpoint** for deployments behind nginx if you increase nginx `client_max_body_size` to 500MB+
2. **Migrate to chunked uploads** for better reliability and to avoid nginx 413 errors without increasing limits

## Performance Considerations

- **Chunk size**: Client controls this completely
  - **Smaller chunks (512KB-1MB)**: More requests, but safer for memory-constrained servers and strict proxies
  - **Larger chunks (5-10MB)**: Fewer requests, faster uploads, but needs more memory
  - **Recommended for Tanzu**: 1MB (good balance for low-memory environments)
  - **Any size works**: Server accepts variable chunk sizes

- **Sequential upload requirement**: **CRITICAL**
  - Chunks **MUST** be uploaded in order: 0, 1, 2, 3...
  - Server validates and enforces sequential order
  - Out-of-order chunks will be rejected
  - This is necessary because chunks are appended sequentially to the file

- **Network reliability**: Chunked uploads are more resilient
  - Failed chunks can be retried individually
  - No need to re-upload the entire file on failure
  - Just retry the specific failed chunk index

## Monitoring

Check active upload sessions:

```bash
# The ChunkedUploadService tracks active sessions
# Monitor via application logs or add a custom endpoint
```

Example log output:
```
2025-10-21 10:15:30 - Created upload session: 550e8400-e29b-41d4-a716-446655440000
2025-10-21 10:15:31 - Session 550e8400...: Received chunk 1/10 for jarFile (5242880 bytes)
2025-10-21 10:15:32 - Session 550e8400...: Received chunk 2/10 for jarFile (5242880 bytes)
...
2025-10-21 10:16:00 - Session 550e8400...: File jarFile upload completed (10 chunks)
2025-10-21 10:16:01 - Starting deployment for app: my-app from session: 550e8400...
2025-10-21 10:18:00 - Deployment completed successfully
2025-10-21 10:18:00 - Deleted upload session: 550e8400-e29b-41d4-a716-446655440000
```
