[data-reveal]{opacity:1!important;transform:none!important}
File Operations
File Storage

File Operations

The interface provides functions for uploading files via presigned URLs, generating download URLs, deleting files, checking existence, and retrieving metadata.

Upload Flow

Uploading a file is a two-step process:

  1. Your server calls CreateUploadUrl to generate a presigned upload URL.
  2. The client sends the file directly to the storage provider using that URL.

This pattern keeps file data off your application server and reduces bandwidth costs.

Complete upload example

After authenticating the caller and authorizing an avatar change, your server can issue an upload URL like this. Derive uploadedBy from the authenticated session; the fixed ID below is example data, not a value to accept from the client.

import {
  CreateUploadUrl,
  UploadValidationError,
} from "@antelopejs/interface-file-storage";

async function handleUploadRequest(file: {
  name: string;
  size: number;
  type: string;
}) {
  try {
    const response = await CreateUploadUrl(
      {
        filename: file.name,
        size: file.size,
        mimetype: file.type,
        path: "user-uploads/avatars",
        metadata: { uploadedBy: "usr_abc123" },
      },
      {
        maxSize: 5 * 1024 * 1024, // 5 MB
        allowedMimetypes: ["image/jpeg", "image/png", "image/webp"],
      },
    );

    // Return the presigned URL and headers to the client
    return {
      uploadUrl: response.uploadUrl,
      resourceKey: response.resourceKey,
      headers: response.headers,
      expiresAt: response.expiresAt,
    };
  } catch (error) {
    if (error instanceof UploadValidationError) {
      throw new Error(`Upload rejected: ${error.message}`);
    }
    throw error;
  }
}

The client then performs the upload using the presigned URL:

// Client-side upload using the response from the server
async function uploadFile(
  file: File,
  presignedData: { uploadUrl: string; headers: Record<string, string> },
) {
  const response = await fetch(presignedData.uploadUrl, {
    method: "PUT",
    headers: presignedData.headers,
    body: file,
  });

  if (!response.ok) {
    throw new Error("Upload failed");
  }
}

Important: The client must include all headers from the headers field in the PUT request. The presigned URL signature covers Content-Type and Content-Length, so any modification to these values causes the request to be rejected.

Uploads use fresh keys and create-only acceptance by default. Replaying a URL must not overwrite an accepted object. Concurrent or repeated attempts return an explicit conflict response, such as HTTP 409 or a provider precondition failure; request a fresh upload URL to upload again. Forward every returned header, including If-None-Match when supplied, and allow those headers in the storage CORS policy. The provider must enforce the condition, not merely return an advisory header.

This does not reserve a deleted key forever. A still-valid provider URL can recreate a deleted source; application admission must not reuse the original upload key. Staging expiration and source cleanup remain independent of business cancellation.

The UploadRequest Object

Use this reference when adding options to the upload example:

FieldTypeRequiredDescription
filenamestringYesOriginal filename
sizenumberYesExact file size in bytes
mimetypestringYesMIME type, such as "image/jpeg"
pathstringNoFolder prefix for the generated key
metadataRecord<string, string>NoCustom file metadata
visibility"public" | "private"NoOverride the storage default
stagingbooleanNoUse temporary staging (default false); see promotion

The UploadConstraints Object

Constraints validate the request before the provider generates a URL. Invalid requests throw UploadValidationError. Request validation is not a content scan; validate the final object before attaching it to an application record.

FieldTypeDescription
maxSizenumberMaximum file size in bytes
allowedMimetypesstring[]Permitted MIME types

The PresignedUploadResponse Object

FieldTypeDescription
uploadUrlstringURL for the client's PUT request
resourceKeystringKey to retain for later operations
expiresAtnumberUpload URL expiration timestamp
headersRecord<string, string>Required PUT headers

Staging Uploads and Promotion

Direct-to-storage uploads create a problem: a file uploaded via a presigned URL lands in storage immediately. If the user never confirms the form, nothing ends up referencing the key, leaving an orphaned object behind forever.

The staging flow solves this. Set staging: true on the upload request and the generated resourceKey is placed under a reserved staging prefix (STAGING_PREFIX, i.e. __staging__/), preserving the requested path after the prefix:

import { CreateUploadUrl } from "@antelopejs/interface-file-storage";

const upload = await CreateUploadUrl({
  filename: "draft.png",
  size: 204800,
  mimetype: "image/png",
  path: "user-uploads/avatars",
  visibility: "private",
  staging: true,
});

console.log(upload.resourceKey); // e.g. "__staging__/user-uploads/avatars/<uuid>.png"

When staging expiration is configured, the backend expires abandoned objects using an S3 lifecycle rule or a local periodic sweep. The existing expiration settings remain unchanged; staging alone does not enable cleanup.

Staging key helpers

These public helpers operate on strings only. They do not check existence, authorize access, upload, move or promote a file, and take no storage argument.

ExportBehavior
STAGING_PREFIXThe reserved prefix "__staging__/".
isStagedKey(resourceKey: string): booleanTests whether the key begins with the prefix.
toStagedKey(resourceKey: string): stringAdds the prefix unless it is already present.
stripStagingPrefix(resourceKey: string): stringRemoves one leading prefix; leaves a non-staged key unchanged.
import {
  isStagedKey,
  toStagedKey,
  stripStagingPrefix,
} from "@antelopejs/interface-file-storage";

const staged = toStagedKey("avatars/photo.png");
console.log(staged); // "__staging__/avatars/photo.png"
console.log(isStagedKey(staged)); // true
console.log(toStagedKey(staged)); // unchanged
console.log(stripStagingPrefix(staged)); // "avatars/photo.png"
console.log(stripStagingPrefix("avatars/photo.png")); // unchanged

Use PromoteFile below to publish a confirmed upload. Computing its final key with stripStagingPrefix does not establish that promotion succeeded.

Promoting a confirmed file

Before admitting a file into the referencing record, call PromoteFile for its staged key. The provider strips the staging prefix and publishes complete bytes, metadata, and trusted source provenance at the canonical final key without overwriting an existing destination. It preserves the selected visibility in the same storage. Source cleanup follows verified final publication.

import { PromoteFile } from "@antelopejs/interface-file-storage";

const { resourceKey } = await PromoteFile(
  "__staging__/user-uploads/avatars/<uuid>.png",
);
console.log(resourceKey); // "user-uploads/avatars/<uuid>.png"

PromoteFile is idempotent and safe to call repeatedly:

  • Promoting a key that is not staged is a no-op and returns it unchanged.
  • Promoting the same staged key twice returns the final key both times only when the destination is complete and has trusted provenance matching that source. This also supports retries after source cleanup.
  • If the staged object no longer exists (for example it expired before being promoted) and was not already promoted, PromoteFile throws FileNotFoundError rather than returning a key that points to nothing.
  • A foreign destination or an incomplete destination that cannot be safely repaired throws FileConflictError with code FILE_CONFLICT. Mere existence, equal size, or upload-supplied metadata is not proof of a matching promotion.
  • Unknown provider outcomes throw. Retry the same key and storage to reconcile; never treat an error or an existence check as proof of success.

Provider and application responsibilities

Application authors must enforce the admission and access rules below. Backend authors must also implement the provider promotion contract. There is no fallback from PromoteFile to MoveFile plus FileExists.

Publish a complete object

If you implement a storage backend, follow the complete-publication requirements. Application authors can continue with admission and access below.

Admit the file into application data

Applications persist permanent source ownership and validate final metadata after promotion before committing the business admission. Preserve the same storage selection throughout upload, promotion, validation, reading, and cleanup; do not rebind it to another backend during the workflow.

Cancellation and receipts belong to application logic. Storage provides no cancellation tombstone or fence: delayed work can leave an orphan after cancellation. Applications must never attach that orphan to a cancelled admission.

Protect private files

For private workflows, the server must impose visibility: "private" through its trusted upload authorization and prevent client overrides. Providers must enforce private access for both staging and final bytes, not merely issue signed URLs over publicly accessible storage.

Applications must also authorize metadata/read-URL routes against the business admission. Private storage alone does not prevent an overly permissive application route from issuing an unauthorized read capability. Previously issued read URLs can remain usable until expiration.

Moving an object

MoveFile remains an independent generic move with its existing overwrite and missing-source behavior. It is not a substitute for no-clobber promotion and is not used by PromoteFile.

import { MoveFile } from "@antelopejs/interface-file-storage";

await MoveFile("reports/draft.pdf", "reports/review.pdf");

Backend expiration of staged files

Each implementation is responsible for expiring abandoned staged objects:

  • S3 (@antelopejs/file-storage-s3) — set stagingExpirationDays on a storage config to apply a bucket lifecycle rule (scoped to the staging prefix) at bootstrap. If you prefer to manage infrastructure externally, leave it unset and provision an equivalent lifecycle rule via Terraform or the console.
  • Local (@antelopejs/file-storage-local) — set stagingExpiration (in seconds) to enable a periodic sweep that deletes staged files older than that age. Leave it unset to keep staged files indefinitely (development only).

Download Flow

The CreateReadUrl function generates a URL to access a stored file.

The PresignedReadResponse Object

FieldTypeDescription
urlstringURL to access the file
expiresAtnumber | undefinedExpiration timestamp (undefined for public files)

Generate a download URL

import { CreateReadUrl } from "@antelopejs/interface-file-storage";

// Generate a read URL with 1-hour expiration (for private files)
const { url, expiresAt } = await CreateReadUrl(
  "user-uploads/avatars/photo.jpg",
  3600,
);

console.log(url); // Presigned or public URL
console.log(expiresAt); // Expiration timestamp, or undefined if public

Serve files in an API response

import { CreateReadUrl } from "@antelopejs/interface-file-storage";

async function getFileDownloadUrl(resourceKey: string) {
  const { url, expiresAt } = await CreateReadUrl(resourceKey, 300); // 5 minutes

  return {
    downloadUrl: url,
    expiresAt,
  };
}

Delete a File

The DeleteFile function removes a file from storage.

import { DeleteFile } from "@antelopejs/interface-file-storage";

await DeleteFile("user-uploads/avatars/old-photo.jpg");

To delete a file from a specific storage bucket:

import { DeleteFile } from "@antelopejs/interface-file-storage";

await DeleteFile("backups/archive-2025.zip", "backup-bucket");

Check File Existence

The FileExists function returns true if the file exists, or false otherwise.

import { FileExists } from "@antelopejs/interface-file-storage";

const exists = await FileExists("user-uploads/avatars/photo.jpg");

if (!exists) {
  console.log("File not found, using default avatar.");
}

File Metadata

The GetFileMetadata function retrieves detailed information about a stored file. It throws a FileNotFoundError if the file does not exist.

The FileMetadata Object

FieldTypeDescription
resourceKeystringUnique resource key
filenamestringOriginal filename
sizenumberFile size in bytes
mimetypestringMIME type
lastModifiednumberLast modified timestamp
metadataRecord<string, string> | undefinedOptional custom metadata attached to the file

Retrieve file metadata

import {
  GetFileMetadata,
  FileNotFoundError,
} from "@antelopejs/interface-file-storage";

try {
  const meta = await GetFileMetadata("user-uploads/avatars/photo.jpg");

  console.log(meta.filename); // "photo.jpg"
  console.log(meta.size); // 204800
  console.log(meta.mimetype); // "image/jpeg"
  console.log(meta.lastModified); // 1711800000000
  console.log(meta.metadata); // { uploadedBy: "usr_abc123" }
} catch (error) {
  if (error instanceof FileNotFoundError) {
    console.error("The requested file does not exist.");
  }
}

Complete Workflow Example

Run this in a server-side test with a configured storage implementation and native fetch. It uploads a small text report, reads its metadata, and deletes it. In a browser flow, the PUT runs on the client instead; requesting a URL alone never creates the object. A staged workflow also requires promotion and application admission before issuing reads.

import {
  CreateUploadUrl,
  CreateReadUrl,
  GetFileMetadata,
  DeleteFile,
  FileExists,
} from "@antelopejs/interface-file-storage";

const bytes = Buffer.from("Quarterly report\nRevenue: 12500 EUR\n", "utf8");

// 1. Generate a presigned upload URL for these exact bytes.
const upload = await CreateUploadUrl({
  filename: "report.txt",
  size: bytes.length,
  mimetype: "text/plain",
  path: "reports/2026",
  visibility: "private",
});

// 2. Upload before trying to read the object.
const uploaded = await fetch(upload.uploadUrl, {
  method: "PUT",
  headers: upload.headers,
  body: bytes,
});
if (!uploaded.ok) {
  throw new Error(`Upload failed: ${uploaded.status}`);
}

// 3. After a successful upload, generate a read URL.
const read = await CreateReadUrl(upload.resourceKey, 3600);
console.log("Read URL expires at:", read.expiresAt);

// 4. Retrieve metadata.
const metadata = await GetFileMetadata(upload.resourceKey);
console.log("File size:", metadata.size, "bytes");

// 5. Check existence before deleting this disposable file.
const exists = await FileExists(upload.resourceKey);
if (exists) {
  await DeleteFile(upload.resourceKey);
  console.log("File deleted.");
}