Files API

Files API

The Files API handles file upload, download, and management in Cloudillo. It supports three file types: BLOB (binary files), CRDT (collaborative documents), and RTDB (real-time databases).

File Types

Type Description Use Cases
BLOB Binary files (images, PDFs, etc.) Photos, documents, attachments
CRDT Collaborative documents Rich text, spreadsheets, diagrams
RTDB Real-time databases Structured data, forms, todos

Image Variants

For BLOB images, Cloudillo automatically generates 5 variants:

Variant Code Max Dimension Quality Format
Thumbnail tn 150px Medium JPEG/WebP
Icon ic 64px Medium JPEG/WebP
SD sd 640px Medium JPEG/WebP
HD hd 1920px High JPEG/WebP
Original orig Original Original Original

Automatic format selection:

  • Modern browsers: WebP or AVIF
  • Fallback: JPEG or PNG

Endpoints

List Files

GET /api/files

List all files accessible to the authenticated user.

Authentication: Required

Query Parameters:

  • fileTp - Filter by file type (BLOB, CRDT, RTDB)
  • contentType - Filter by MIME type
  • tags - Filter by tags (comma-separated)
  • _limit - Max results (default: 50)
  • _offset - Pagination offset

Example:

const api = cloudillo.createApiClient()

// List all images
const images = await api.files.get({
  fileTp: 'BLOB',
  contentType: 'image/*',
  _limit: 20
})

// List tagged files
const projectFiles = await api.files.get({
  tags: 'project-alpha,important'
})

Response:

{
  "data": [
    {
      "fileId": "b1~abc123",
      "status": "M",
      "contentType": "image/png",
      "fileName": "photo.png",
      "fileTp": "BLOB",
      "createdAt": "2025-01-01T12:00:00Z",
      "tags": ["vacation", "beach"],
      "owner": {
        "idTag": "alice@example.com",
        "name": "Alice"
      }
    }
  ]
}

Create File Metadata (CRDT/RTDB)

POST /api/files

Create file metadata for CRDT or RTDB files. For BLOB files, use the one-step upload endpoint instead (see “Upload File (BLOB)” below).

Authentication: Required

Request Body:

{
  fileTp: string // CRDT or RTDB
  fileName?: string // Optional filename
  tags?: string // Comma-separated tags
}

Example:

// Create CRDT document
const file = await api.files.post({
  fileTp: 'CRDT',
  fileName: 'team-doc.crdt',
  tags: 'collaborative,document'
})

console.log('File ID:', file.fileId) // e.g., "f1~abc123"

// Now connect via WebSocket to edit the document
import * as Y from 'yjs'
const yDoc = new Y.Doc()
const { provider } = await cloudillo.openYDoc(yDoc, file.fileId)

Response:

{
  "data": {
    "fileId": "f1~abc123",
    "status": "P",
    "fileTp": "CRDT",
    "fileName": "team-doc.crdt",
    "createdAt": 1735000000
  },
  "time": 1735000000,
  "reqId": "req_abc123"
}

Note: For BLOB files (images, PDFs, etc.), use POST /api/files/{preset}/{file_name} instead, which creates metadata and uploads the file in a single step.

Upload File (BLOB)

POST /api/files/{preset}/{file_name}

Upload binary file data directly. This creates the file metadata and uploads the binary in a single operation.

Authentication: Required

Path Parameters:

  • preset (string, required) - Image processing preset (e.g., default, profile-picture, cover-photo)
  • file_name (string, required) - Original filename with extension

Query Parameters:

  • created_at (number, optional) - Unix timestamp (seconds) for when the file was created
  • tags (string, optional) - Comma-separated tags (e.g., vacation,beach,2025)

Content-Type: Binary content type (e.g., image/png, image/jpeg, application/pdf)

Request Body: Raw binary file data

Example:

const api = cloudillo.createApiClient()

// Upload image file
const imageFile = document.querySelector('input[type="file"]').files[0]
const blob = await imageFile.arrayBuffer()

const response = await fetch('/api/files/default/vacation-photo.jpg?tags=vacation,beach', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'image/jpeg'
  },
  body: blob
})

const result = await response.json()
console.log('File ID:', result.data.fileId) // e.g., "b1~abc123"

Complete upload with File object:

async function uploadFile(file: File, preset = 'default', tags?: string) {
  const queryParams = new URLSearchParams()
  if (tags) queryParams.set('tags', tags)

  const url = `/api/files/${preset}/${encodeURIComponent(file.name)}${
    queryParams.toString() ? '?' + queryParams.toString() : ''
  }`

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': file.type
    },
    body: file
  })

  const result = await response.json()
  return result.data.fileId
}

// Usage
const fileInput = document.querySelector('input[type="file"]')
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0]
  const fileId = await uploadFile(file, 'default', 'vacation,beach')
  console.log('Uploaded:', fileId)

  // Display uploaded image
  const img = document.createElement('img')
  img.src = `/api/files/${fileId}?variant=sd`
  document.body.appendChild(img)
})

Response:

{
  "data": {
    "fileId": "b1~abc123",
    "status": "M",
    "fileTp": "BLOB",
    "contentType": "image/jpeg",
    "fileName": "vacation-photo.jpg",
    "createdAt": 1735000000,
    "tags": ["vacation", "beach"]
  },
  "time": 1735000000,
  "reqId": "req_abc123"
}

Download File

GET /api/files/:fileId

Download a file. Returns binary data with appropriate Content-Type.

Query Parameters:

  • variant - Image variant (tn, ic, sd, hd, orig)

Example:

// Direct URL usage
<img src="/api/files/b1~abc123" />

// Get specific variant
<img src="/api/files/b1~abc123?variant=sd" />

// Fetch with API
const response = await fetch(`/api/files/${fileId}`)
const blob = await response.blob()
const url = URL.createObjectURL(blob)

Responsive images:

<picture>
  <source srcset="/api/files/b1~abc123?variant=hd" media="(min-width: 1200px)">
  <source srcset="/api/files/b1~abc123?variant=sd" media="(min-width: 600px)">
  <img src="/api/files/b1~abc123?variant=tn" alt="Photo">
</picture>

Get File Descriptor

GET /api/files/:fileId/descriptor

Get file metadata and available variants.

Example:

const descriptor = await api.files.id('b1~abc123').descriptor.get()

console.log('File type:', descriptor.contentType)
console.log('Size:', descriptor.size)
console.log('Variants:', descriptor.variants)

Response:

{
  "data": {
    "fileId": "b1~abc123",
    "contentType": "image/png",
    "fileName": "photo.png",
    "size": 1048576,
    "createdAt": "2025-01-01T12:00:00Z",
    "variants": [
      {
        "id": "tn",
        "width": 150,
        "height": 100,
        "size": 5120,
        "format": "webp"
      },
      {
        "id": "sd",
        "width": 640,
        "height": 426,
        "size": 51200,
        "format": "webp"
      },
      {
        "id": "hd",
        "width": 1920,
        "height": 1280,
        "size": 204800,
        "format": "webp"
      },
      {
        "id": "orig",
        "width": 3840,
        "height": 2560,
        "size": 1048576,
        "format": "png"
      }
    ]
  }
}

Update File Metadata

PATCH /api/files/:fileId

Update file metadata (tags, filename, etc.).

Authentication: Required (must be owner)

Request Body:

{
  fileName?: string
  tags?: string // Comma-separated
}

Example:

await api.files.id('b1~abc123').patch({
  fileName: 'renamed-photo.png',
  tags: 'vacation,beach,2025'
})

Delete File

DELETE /api/files/:fileId

Delete a file and all its variants.

Authentication: Required (must be owner)

Example:

await api.files.id('b1~abc123').delete()

List Tags

GET /api/tags

List all tags used in files owned by the authenticated user.

Authentication: Required

Response:

{
  "data": [
    {
      "tag": "vacation",
      "count": 15
    },
    {
      "tag": "project-alpha",
      "count": 8
    },
    {
      "tag": "important",
      "count": 3
    }
  ]
}

Example:

const response = await fetch('/api/tags', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
})
const { data: tags } = await response.json()

// Display tags with counts
tags.forEach(({ tag, count }) => {
  console.log(`${tag}: ${count} files`)
})

Add Tag

PUT /api/files/:fileId/tag/:tag

Add a tag to a file.

Authentication: Required (must be owner)

Example:

await fetch(`/api/files/b1~abc123/tag/important`, {
  method: 'PUT',
  headers: {
    'Authorization': `Bearer ${token}`
  }
})

Remove Tag

DELETE /api/files/:fileId/tag/:tag

Remove a tag from a file.

Authentication: Required (must be owner)

Example:

await fetch(`/api/files/b1~abc123/tag/important`, {
  method: 'DELETE',
  headers: {
    'Authorization': `Bearer ${token}`
  }
})

Get File Variant

GET /api/files/variant/:variantId

Get a specific image variant directly.

Example:

// Variant IDs are in the format: {fileId}~{variant}
<img src="/api/files/variant/b1~abc123~sd" />

File Identifiers

Cloudillo uses content-addressable identifiers:

Format: {prefix}{version}~{hash}

Prefixes:

  • b - BLOB files
  • f - CRDT files
  • r - RTDB files

Examples:

  • b1~abc123def - BLOB file
  • f1~xyz789ghi - CRDT file
  • r1~mno456pqr - RTDB file

Variants:

  • b1~abc123def~sd - SD variant of BLOB
  • b1~abc123def~tn - Thumbnail variant

CRDT Files

CRDT files store collaborative documents using Yjs.

Creating a CRDT file:

// 1. Create file metadata
const file = await api.files.post({
  fileTp: 'CRDT',
  tags: 'document,collaborative'
})

// 2. Open for editing
import * as Y from 'yjs'

const yDoc = new Y.Doc()
const { provider } = await cloudillo.openYDoc(yDoc, file.fileId)

// 3. Use shared types
const yText = yDoc.getText('content')
yText.insert(0, 'Hello, CRDT!')

See CRDT documentation for details.

RTDB Files

RTDB files store structured real-time databases.

Creating an RTDB file:

// 1. Create file metadata
const file = await api.files.post({
  fileTp: 'RTDB',
  tags: 'database,todos'
})

// 2. Connect to database
import { RtdbClient } from '@cloudillo/rtdb'

const rtdb = new RtdbClient({
  fileId: file.fileId,
  token: cloudillo.accessToken,
  url: 'wss://server.com/ws/rtdb'
})

// 3. Use collections
const todos = rtdb.collection('todos')
await todos.create({ title: 'Learn Cloudillo', done: false })

See RTDB documentation for details.

Image Presets

Configure automatic image processing with presets:

const file = await api.files.post({
  fileTp: 'BLOB',
  contentType: 'image/jpeg',
  preset: 'profile-picture' // Custom preset
})

Default presets:

  • default - Standard 5-variant generation
  • profile-picture - Square crop, 400x400 max
  • cover-photo - 16:9 crop, 1920x1080 max
  • thumbnail-only - Only generate thumbnails

Tagging

Tags help organize and filter files.

Best practices:

  • Use lowercase tags
  • Use hyphens for multi-word tags (e.g., project-alpha)
  • Limit to 3-5 tags per file
  • Use namespaced tags for projects (e.g., proj:alpha, proj:beta)

Tag filtering:

// Files with ANY of these tags
const files = await api.files.get({
  tags: 'vacation,travel'
})

// Files with ALL of these tags (use multiple requests)
const vacationFiles = await api.files.get({ tags: 'vacation' })
const summerFiles = vacationFiles.data.filter(f =>
  f.tags?.includes('summer')
)

Permissions

File access is controlled by:

  1. Ownership - Owner has full access
  2. FSHR actions - Files shared via FSHR actions grant temporary access
  3. Public files - Files attached to public actions are publicly accessible
  4. Audience - Files attached to actions inherit action audience permissions

Sharing a file:

// Share file with read access
await api.actions.post({
  type: 'FSHR',
  subject: 'bob@example.com',
  attachments: ['b1~abc123'],
  content: {
    permission: 'READ', // or 'WRITE'
    message: 'Check out this photo!'
  }
})

Storage Considerations

File size limits:

  • Free tier: 100 MB per file
  • Pro tier: 1 GB per file
  • Enterprise: Configurable

Total storage:

  • Free tier: 10 GB
  • Pro tier: 100 GB
  • Enterprise: Unlimited

Variant generation:

  • Only for image files (JPEG, PNG, WebP, AVIF, GIF)
  • Automatic async processing
  • Lanczos3 filtering for high quality
  • Progressive JPEG for faster loading

Best Practices

1. Always Create Metadata First

// ✅ Correct order
const metadata = await api.files.post({ fileTp: 'BLOB', contentType: 'image/png' })
await uploadBinary(metadata.fileId, imageBlob)

// ❌ Wrong - upload will fail without metadata
await uploadBinary('unknown-id', imageBlob)

2. Use Appropriate Variants

// ✅ Use thumbnails in lists
<img src={`/api/files/${fileId}?variant=tn`} />

// ✅ Use HD for detail views
<img src={`/api/files/${fileId}?variant=hd`} />

// ❌ Don't use original for thumbnails (wastes bandwidth)
<img src={`/api/files/${fileId}`} width="100" />

3. Handle Upload Errors

async function uploadWithRetry(file: File, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const metadata = await api.files.post({
        fileTp: 'BLOB',
        contentType: file.type
      })

      const formData = new FormData()
      formData.append('fileId', metadata.fileId)
      formData.append('file', file)

      await api.files.upload.post(formData)
      return metadata.fileId
    } catch (error) {
      if (i === maxRetries - 1) throw error
      await new Promise(r => setTimeout(r, 1000 * (i + 1)))
    }
  }
}

4. Progressive Enhancement

// Show preview immediately, upload in background
function previewAndUpload(file: File) {
  // Show local preview
  const reader = new FileReader()
  reader.onload = (e) => {
    setPreview(e.target.result)
  }
  reader.readAsDataURL(file)

  // Upload in background
  uploadImage(file).then(fileId => {
    setUploadedId(fileId)
  })
}

5. Clean Up Unused Files

// Delete old temporary files
const oldFiles = await api.files.get({
  tags: 'temp',
  createdBefore: Date.now() / 1000 - 86400 // 24 hours ago
})

for (const file of oldFiles.data) {
  await api.files.id(file.fileId).delete()
}

See Also