Skip to main content

File uploads (client)

The client talks to a file resource for uploads, downloads, listing, and deletion — with upload progress and optional presigned direct-to-bucket transfers.

Imperative client​

import { createFileClient } from "covara/client";

const files = createFileClient({ transport: client.transport, resourcePath: "/api/files" });

const result = await files.upload(file, { onProgress: ({ percent }) => console.log(`${percent}%`) });
const result2 = await files.uploadWithPresignedUrl(file, { onProgress: ({ percent }) => {} }); // S3/R2
const { data } = await files.list({ limit: 20 });
await files.delete(fileId);

uploadWithPresignedUrl requests a presigned URL, uploads the bytes directly to the bucket, then confirms — offloading the transfer from your server (S3/R2 only). The server sniffs the uploaded bytes on confirm and rejects content that contradicts the declared type (see content sniffing).

Resumable uploads​

For large files or flaky connections, uploadResumable splits the file into parts, uploads them in parallel with per-part retries, and can pick up where it left off after a network failure or a page reload. It uses the server's resumable upload protocol.

const file = await files.uploadResumable(bigFile, {
concurrency: 4, // parallel parts (default 3)
maxRetries: 5, // per part; network errors, 408/425/429/5xx (default 5)
retryDelayMs: 500, // exponential backoff base
partSize: 16 * 1024 * 1024, // a hint; the server clamps it
checksum: true, // sha256 per part + whole file (WebCrypto)
direct: true, // S3/R2: PUT parts straight to the bucket when the server allows it
onProgress: ({ loaded, total, percent }) => console.log(percent),
});

Resume after reload. The session id (and, for anonymous sessions, the upload token) is saved under a key derived from the resource path and the file's name, size, lastModified and type: in localStorage when available, in memory otherwise. Pick the same file again and uploadResumable asks the server which parts it already has and uploads only the rest. A saved session that expired or was aborted is discarded and a new one is started. Pass resumeStore to use your own storage (it needs get/set/remove, sync or async, which suits AsyncStorage on React Native), resumeStore: false to disable resuming, or resumeKey to choose the key yourself.

Pause and abort. startResumableUpload returns a handle:

const handle = files.startResumableUpload(bigFile);
handle.pause(); // stop now; the server session is kept for a later resume
await handle.abort(); // stop and delete the server session + saved resume record
const result = await handle.promise; // rejects with AbortError after pause/abort

files.getResumableUploadStatus(uploadId, token?) and files.abortResumableUpload(uploadId, token?) are available for custom flows. Progress is reported per completed part. A content rejection (400, e.g. HTML disguised as an image) or another non-retryable error forgets the saved session.

React hooks​

import { useFileUpload, useFile, useFiles } from "covara/client/react";

function UploadButton() {
const { upload, isUploading, progress, error } = useFileUpload({
resourcePath: "/api/files",
onSuccess: (file) => console.log("uploaded", file.id),
});
return (
<input type="file" disabled={isUploading}
onChange={(e) => e.target.files?.[0] && upload(e.target.files[0])} />
);
}

function FileList() {
const { files, isLoading, deleteFile, getDownloadUrl } = useFiles({ resourcePath: "/api/files" });
return (
<ul>
{files.map((f) => (
<li key={f.id}>
<a href={getDownloadUrl(f.id)}>{f.filename}</a>
<button onClick={() => deleteFile(f.id)}>Delete</button>
</li>
))}
</ul>
);
}
  • useFileUpload — { upload, isUploading, progress, error, uploadId, pause, abort }, with onSuccess. Pass resumable: true (or resumable options such as { concurrency: 4 }) to upload through the resumable protocol; pause() keeps the session for a later resume, abort() deletes it.
  • useFile — load a single file's metadata/URL.
  • useFiles — { files, isLoading, deleteFile, getDownloadUrl }.

getDownloadUrl(id) returns a URL suitable for <img>/<a>; on React Native use it to fetch bytes — see React Native.

Relating files​

Upload returns a file record whose id you can store on another resource (e.g. todo.imageId) and load via ?include=. See Storage → relating files.