Files

Files are the unified content primitive — every piece of user content is a file row, with images and videos as automatic specializations.

Files are the unified content primitive in Aeontel. Every piece of user content — documents, images, videos, datasets, anything — is a file row. There is no separate "upload" or "asset" entity: a file starts in the uploading state, transitions to ready once bytes are in R2 and the MIME type has been sniffed, or to error on any terminal failure.

Storage model

Each workspace gets its own R2 bucket (aeontel-wsp-{"{id}"} ), created on workspace.created. R2 storage is flat and keyed by file id — every file object lives at files/{"{fileId}"}. Hierarchical organization lives entirely in the database via directories ; R2 never sees a directory path.

Multipart upload state (the R2 multipart ID, per-part ETags, total size) lives inline on the file row and is nulled out once status = "ready". There is no separate bookkeeping table.

Upload lifecycle

Uploads are a standard R2 multipart flow directly on the file entity:

  1. POST /api/files — initiate. Server creates the file row in uploading state and returns signed PUT URLs for each part.
  2. For each part, the client PUTs the raw bytes directly to the signed URL and captures the ETag from the response.
  3. POST /api/files/:id/parts — sign more parts if the initial batch wasn't enough.
  4. POST /api/files/:id/complete — finalize. Server completes the R2 multipart upload, sniffs the MIME type, and flips the row to ready.
  5. POST /api/files/:id/abort — on any failure. Deletes the row and buffered R2 parts.

The CLI's aeontel file upload command drives this whole lifecycle from Node with a progress bar. In the browser, the platform Content page uses the same flow with parallel part uploads.

Image and video specializations

Images and videos are 1

side tables on file, keyed by file id. They are populated automatically when a file with an image/* or video/* MIME type reaches the ready state — an event handler emits image.provision_requested or video.provision_requested, a downstream handler provisions the asset in Cloudflare Images or Cloudflare Stream, and the side-table row is inserted. You never need to "promote" a file — upload with the right MIME type and the specialization appears.

Operations

  • GET /api/files — list files, filter by workspace_id, directory_id, or mime prefix
  • GET /api/files/:id — retrieve file metadata
  • POST /api/files / /parts / /complete / /abort — the multipart upload flow
  • PATCH /api/files/:id — rename, move, or set expiresAt
  • GET /api/files/:id/download — short-lived signed R2 download URL
  • DELETE /api/files/:id — delete the file (handler cleans up R2, CF Images, and CF Stream)
  • POST /api/files/pdf-from-url — render a URL to PDF via Cloudflare Browser Rendering and store the result as a file
  • POST /api/files/screenshot-from-url — capture a URL as a screenshot and store the result as a file
  • POST /api/files/markdown-from-url — render a URL and extract its content as markdown, stored as a text/markdown file
  • POST /api/files/html-from-url — capture the post-render HTML of a URL, stored as a text/html file
  • POST /api/files/:id/to-markdown — convert an uploaded file (PDF, DOCX, PPTX, XLSX, image, HTML) to markdown via Cloudflare Workers AI, stored as a new text/markdown file

All operations are available via the CLI ( aeontel file list/get/upload/update/download/read/delete/pdf-from-url/screenshot-from-url/markdown-from-url/html-from-url/to-markdown ), the MCP server, and the @aeontel/react hooks.

TTL and browser-rendered files

Every file row supports an optional expiresAt timestamp. When set, a daily cleanup worker removes the row and its R2 object once past the deadline — the standard file.deleted event still fires so downstream CF cleanup cascades normally.

Files generated by pdf-from-url / screenshot-from-url default to temp: they land at the workspace root with a 1-day TTL. Passing a directoryId switches to permanent placement; passing an explicit ttlMs (or null for "never") overrides either default. To promote a temp file to permanent, PATCH /api/files/:id with expiresAt: null (and optionally a new directoryId).

Screenshots produced via screenshot-from-url are plain image files — they are not auto-provisioned as CF Images entities. Callers that want variants can materialize an image from a permanent copy.

Events

  • file.created — file row inserted in uploading state
  • file.ready — multipart upload finalized, MIME sniffed
  • file.error — terminal failure
  • file.updated — rename or move
  • file.deleted — delete; payload carries r2Key , bucketName, and optionally cfImageId / cfStreamUid so the cleanup handler can cascade into CF Images and CF Stream.