# App API - File Uploads

File uploads for deployed apps use presigned S3 URLs. The client uploads directly to S3 (no proxy through the server), supporting files up to 30GB with progress tracking.

## How It Works

1. **Init** - App calls `POST /api/<APP_GUID>/uploads/init` with file metadata. Server returns a presigned S3 URL.
2. **Upload** - Client PUTs the file directly to the presigned URL (S3). No auth header needed - the URL is pre-authorized.
3. **Complete** - App calls `POST /api/<APP_GUID>/uploads/complete`. Server copies the file to permanent storage, creates the VFS record, generates variants (thumbnails for images, text for PDFs).

## Client Helper Library

Include the helper script for automatic progress tracking, multipart handling, and retries:

```html
<script src="https://media.gipity.ai/scripts/gipity-upload.js"></script>
```

Usage:
```js
const result = await Gipity.upload(fileInput.files[0], {
  appGuid: '<APP_GUID>',
  appToken: token,
  onProgress: (pct) => progressBar.style.width = pct + '%',
  public: false,
  table: 'attachments',
  recordId: 'rec_123',
});
console.log(result.guid, result.url);
```

The helper handles single-part uploads (< 5GB) and multipart uploads (5GB+) transparently. It retries failed parts up to 3 times with exponential backoff.

## Endpoints

### POST /api/<APP_GUID>/uploads/init

Get a presigned upload URL.

**Auth:** App token (`X-App-Token`), API key (`X-Api-Key`), or session cookie.

**Request:**
```json
{
  "filename": "photo.jpg",
  "content_type": "image/jpeg",
  "size": 2048576,
  "public": false,
  "table": "incidents",
  "record_id": "42",
  "path": "/uploads"
}
```

- `filename` (required) - original filename
- `content_type` (required) - MIME type
- `size` (required) - file size in bytes (max 30GB)
- `public` (optional, default false) - public CDN URL or signed URL
- `table` (optional) - associate with a record table
- `record_id` (optional) - associate with a specific record
- `path` (optional, default "/uploads") - virtual directory path

**Response (single-part, < 5GB):**
```json
{
  "data": {
    "upload_guid": "fl_Xk7mNp2R",
    "method": "PUT",
    "url": "https://muda.gipity.ai.s3...",
    "headers": { "Content-Type": "image/jpeg" },
    "expires_in": 600
  }
}
```

**Response (multipart, >= 5GB):**
```json
{
  "data": {
    "upload_guid": "fl_Xk7mNp2R",
    "method": "multipart",
    "upload_id": "abc123...",
    "part_size": 104857600,
    "parts": [
      { "partNumber": 1, "url": "https://..." },
      { "partNumber": 2, "url": "https://..." }
    ],
    "expires_in": 600
  }
}
```

### POST /api/<APP_GUID>/uploads/complete

Finalize the upload after the client has PUT the file to S3.

**Request:**
```json
{
  "upload_guid": "fl_Xk7mNp2R",
  "parts": [
    { "part_number": 1, "etag": "\"abc123\"" },
    { "part_number": 2, "etag": "\"def456\"" }
  ]
}
```

- `upload_guid` (required) - from the init response
- `parts` (required for multipart only) - part numbers and ETags from each S3 PUT response

**Response:**
```json
{
  "data": {
    "guid": "fl_Xk7mNp2R",
    "name": "photo.jpg",
    "size": 2048576,
    "content_type": "image/jpeg",
    "width": 1920,
    "height": 1080,
    "url": "https://media.gipity.ai/app-files/...",
    "is_public": false,
    "table": "incidents",
    "record_id": "42",
    "variants": [
      { "type": "thumbnail", "url": "...", "content_type": "image/jpeg", "status": "complete" }
    ]
  }
}
```

### GET /api/<APP_GUID>/files

List uploaded files. Supports filtering.

**Query params:** `table`, `record_id`, `path`, `limit` (default 50, max 200), `offset`

### GET /api/<APP_GUID>/files/:guid

Get file metadata + variant list.

### GET /api/<APP_GUID>/files/:guid/content

Download file. Redirects to S3 URL (public) or signed URL (private).

### GET /api/<APP_GUID>/files/:guid/variants/:type

Get a specific variant (e.g., thumbnail). Redirects to URL.

## Variants

Variants are auto-generated on upload for small files (< 10MB):
- **thumbnail** - Images resized to 200x200px (JPEG)
- **text** - PDFs have text extracted to plain text

## Limits

- Max file size: 30GB
- Presigned URL expiry: 10 minutes
- Rate limit: 300 uploads per minute
- Multipart part size: 100MB

## Manual Upload (without helper library)

If you can't use the helper library, implement the three-step flow directly:

```js
// 1. Init
const init = await fetch(`https://a.gipity.ai/api/${appGuid}/uploads/init`, {
  method: 'POST',
  headers: { 'X-App-Token': token, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    filename: file.name,
    content_type: file.type,
    size: file.size,
  }),
}).then(r => r.json());

// 2. Upload to S3
await fetch(init.data.url, {
  method: 'PUT',
  headers: { 'Content-Type': file.type },
  body: file,
});

// 3. Complete
const result = await fetch(`https://a.gipity.ai/api/${appGuid}/uploads/complete`, {
  method: 'POST',
  headers: { 'X-App-Token': token, 'Content-Type': 'application/json' },
  body: JSON.stringify({ upload_guid: init.data.upload_guid }),
}).then(r => r.json());

console.log(result.data.url); // permanent file URL
```

## Integration with Records

Files can be associated with records via `table` and `record_id` fields (soft reference). Example flow:
1. User uploads a screenshot with `table: 'incidents', record_id: '42'`
2. Frontend lists attachments via `GET /api/:app/files?table=incidents&record_id=42`
3. Each attachment shows the thumbnail variant as a preview

