{
  "name": "app-files",
  "title": "App API - File Uploads",
  "description": "File uploads for deployed apps: presigned S3 URLs, progress tracking, thumbnails, up to 30GB, client helper library",
  "guid": "sk_plat_afil",
  "category": "App services",
  "requiredTools": [],
  "content": "# App API - File Uploads\n\nFile 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.\n\n## How It Works\n\n1. **Init** - App calls `POST /api/<APP_GUID>/uploads/init` with file metadata. Server returns a presigned S3 URL.\n2. **Upload** - Client PUTs the file directly to the presigned URL (S3). No auth header needed - the URL is pre-authorized.\n3. **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).\n\n## Client Helper Library\n\nInclude the helper script for automatic progress tracking, multipart handling, and retries:\n\n```html\n<script src=\"https://media.gipity.ai/scripts/gipity-upload.js\"></script>\n```\n\nUsage:\n```js\nconst result = await Gipity.upload(fileInput.files[0], {\n  appGuid: '<APP_GUID>',\n  appToken: token,\n  onProgress: (pct) => progressBar.style.width = pct + '%',\n  public: false,\n  table: 'attachments',\n  recordId: 'rec_123',\n});\nconsole.log(result.guid, result.url);\n```\n\nThe helper handles single-part uploads (< 5GB) and multipart uploads (5GB+) transparently. It retries failed parts up to 3 times with exponential backoff.\n\n## Endpoints\n\n### POST /api/<APP_GUID>/uploads/init\n\nGet a presigned upload URL.\n\n**Auth:** App token (`X-App-Token`), API key (`X-Api-Key`), or session cookie.\n\n**Request:**\n```json\n{\n  \"filename\": \"photo.jpg\",\n  \"content_type\": \"image/jpeg\",\n  \"size\": 2048576,\n  \"public\": false,\n  \"table\": \"incidents\",\n  \"record_id\": \"42\",\n  \"path\": \"/uploads\"\n}\n```\n\n- `filename` (required) - original filename\n- `content_type` (required) - MIME type\n- `size` (required) - file size in bytes (max 30GB)\n- `public` (optional, default false) - public CDN URL or signed URL\n- `table` (optional) - associate with a record table\n- `record_id` (optional) - associate with a specific record\n- `path` (optional, default \"/uploads\") - virtual directory path\n\n**Response (single-part, < 5GB):**\n```json\n{\n  \"data\": {\n    \"upload_guid\": \"fl_Xk7mNp2R\",\n    \"method\": \"PUT\",\n    \"url\": \"https://muda.gipity.ai.s3...\",\n    \"headers\": { \"Content-Type\": \"image/jpeg\" },\n    \"expires_in\": 600\n  }\n}\n```\n\n**Response (multipart, >= 5GB):**\n```json\n{\n  \"data\": {\n    \"upload_guid\": \"fl_Xk7mNp2R\",\n    \"method\": \"multipart\",\n    \"upload_id\": \"abc123...\",\n    \"part_size\": 104857600,\n    \"parts\": [\n      { \"partNumber\": 1, \"url\": \"https://...\" },\n      { \"partNumber\": 2, \"url\": \"https://...\" }\n    ],\n    \"expires_in\": 600\n  }\n}\n```\n\n### POST /api/<APP_GUID>/uploads/complete\n\nFinalize the upload after the client has PUT the file to S3.\n\n**Request:**\n```json\n{\n  \"upload_guid\": \"fl_Xk7mNp2R\",\n  \"parts\": [\n    { \"part_number\": 1, \"etag\": \"\\\"abc123\\\"\" },\n    { \"part_number\": 2, \"etag\": \"\\\"def456\\\"\" }\n  ]\n}\n```\n\n- `upload_guid` (required) - from the init response\n- `parts` (required for multipart only) - part numbers and ETags from each S3 PUT response\n\n**Response:**\n```json\n{\n  \"data\": {\n    \"guid\": \"fl_Xk7mNp2R\",\n    \"name\": \"photo.jpg\",\n    \"size\": 2048576,\n    \"content_type\": \"image/jpeg\",\n    \"width\": 1920,\n    \"height\": 1080,\n    \"url\": \"https://media.gipity.ai/app-files/...\",\n    \"is_public\": false,\n    \"table\": \"incidents\",\n    \"record_id\": \"42\",\n    \"variants\": [\n      { \"type\": \"thumbnail\", \"url\": \"...\", \"content_type\": \"image/jpeg\", \"status\": \"complete\" }\n    ]\n  }\n}\n```\n\n### GET /api/<APP_GUID>/files\n\nList uploaded files. Supports filtering.\n\n**Query params:** `table`, `record_id`, `path`, `limit` (default 50, max 200), `offset`\n\n### GET /api/<APP_GUID>/files/:guid\n\nGet file metadata + variant list.\n\n### GET /api/<APP_GUID>/files/:guid/content\n\nDownload file. Redirects to S3 URL (public) or signed URL (private).\n\n### GET /api/<APP_GUID>/files/:guid/variants/:type\n\nGet a specific variant (e.g., thumbnail). Redirects to URL.\n\n## Variants\n\nVariants are auto-generated on upload for small files (< 10MB):\n- **thumbnail** - Images resized to 200x200px (JPEG)\n- **text** - PDFs have text extracted to plain text\n\n## Limits\n\n- Max file size: 30GB\n- Presigned URL expiry: 10 minutes\n- Rate limit: 300 uploads per minute\n- Multipart part size: 100MB\n\n## Manual Upload (without helper library)\n\nIf you can't use the helper library, implement the three-step flow directly:\n\n```js\n// 1. Init\nconst init = await fetch(`https://a.gipity.ai/api/${appGuid}/uploads/init`, {\n  method: 'POST',\n  headers: { 'X-App-Token': token, 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    filename: file.name,\n    content_type: file.type,\n    size: file.size,\n  }),\n}).then(r => r.json());\n\n// 2. Upload to S3\nawait fetch(init.data.url, {\n  method: 'PUT',\n  headers: { 'Content-Type': file.type },\n  body: file,\n});\n\n// 3. Complete\nconst result = await fetch(`https://a.gipity.ai/api/${appGuid}/uploads/complete`, {\n  method: 'POST',\n  headers: { 'X-App-Token': token, 'Content-Type': 'application/json' },\n  body: JSON.stringify({ upload_guid: init.data.upload_guid }),\n}).then(r => r.json());\n\nconsole.log(result.data.url); // permanent file URL\n```\n\n## Integration with Records\n\nFiles can be associated with records via `table` and `record_id` fields (soft reference). Example flow:\n1. User uploads a screenshot with `table: 'incidents', record_id: '42'`\n2. Frontend lists attachments via `GET /api/:app/files?table=incidents&record_id=42`\n3. Each attachment shows the thumbnail variant as a preview\n"
}
