Uploading files
Send bytes to a bucket with resumable uploads, progress and cancellation, or drive upload sessions yourself.
client.storage.upload() performs a whole upload: it hashes the bytes, opens an upload session, sends the file in resumable chunks, and hands back the object URL. Use the session API underneath it when the credentials and the bytes live in different places.
What upload() does
Resolve the source
Exactly one of
data,pathorstream. Size and content type are derived from the source and the object key unless you setcontentLengthandcontentTypeyourself.Hash the content
Gigadrive Network requires a SHA-256 on every upload session, and the SDK computes it. SHA-1 and MD5 are passed through when you supply them and are never computed for you.
Open a session
POST /applications/{app}/storage/buckets/{bucket}/uploadsreturns the session plus a signed URL, the headers to send with it, and the canonical URL the object answers on once it is finalized.Send the bytes
A resumable
PATCHtransfer against that URL, retried on failure after 0, 1, 3 and 5 seconds.Return, or wait
You get
{ session, url }as soon as the last byte lands. WithwaitForCompletion, the call polls until the server has verified the checksum and created the object, and adds it asobject.
Upload a file
import { GigadriveClient, UploadError } from '@gigadrive/sdk';
const client = new GigadriveClient();
try {
const { url, object } = await client.storage.upload({
bucket: 'reports',
key: 'reports/2026-q1.pdf',
path: './q1-report.pdf',
waitForCompletion: true,
});
console.log(`${object?.contentLength ?? 0} bytes available at ${url}`);
} catch (error) {
if (error instanceof UploadError) {
console.error('upload failed:', error.message, error.cause);
}
throw error;
}bucket is the bucket name, not its global slug, and the object key is a path inside the bucket. Content type is inferred from the key's extension, so reports/2026-q1.pdf is stored as application/pdf. An extension the SDK does not recognize leaves the content type unset rather than guessing.
url is the canonical address of the object, which a public bucket serves directly. A private bucket does not: readers need a signed URL from objects.getAccessUrl, covered in Access URLs.
Reading from path is Node-only. In a browser, pass a File from an input as data.
Every option
| Option | Type | Default |
|---|---|---|
bucket | string | required, the bucket name |
key | string | required |
data | Blob | ArrayBuffer | Uint8Array | one source only, and this one works everywhere |
path | string | one source only, Node |
stream | NodeReadableLike | one source only, Node, needs contentLength and checksumSha256 |
contentLength | number | measured, except for stream where it is required |
checksumSha256 | string | computed, except for stream where it is required |
contentType | string | inferred from key |
checksumSha1 | string | sent only if you provide it |
checksumMd5 | string | sent only if you provide it |
chunkSize | number | one chunk for data, 50 MiB for path and stream |
retryDelays | number[] | null | [0, 1000, 3000, 5000], and null disables retries |
onProgress | (bytesSent: number, bytesTotal: number) => void | none |
signal | AbortSignal | none |
resume | boolean | false |
urlStorage | UploadUrlStorage | the tus-js-client default for the runtime |
waitForCompletion | boolean | WaitForCompletionOptions | false |
environment | string | inferred from the credential |
Passing two sources throws Provide only one upload source: data, path, or stream. and passing none throws No upload source provided. Pass one of: data, path, or stream. Both are plain Errors, thrown before any request goes out.
waitForCompletion: true polls every second for up to a minute. Pass an object to change that: { timeoutMs: 300_000, pollIntervalMs: 2_000 }. A session that ends up failed or expired throws UploadError, and so does running out of time.
Progress, cancellation and resume
const file = document.querySelector<HTMLInputElement>('#file')?.files?.[0];
if (!file) throw new Error('No file selected.');
const controller = new AbortController();
document.querySelector('#cancel')?.addEventListener('click', () => controller.abort());
await client.storage.upload({
bucket: 'uploads',
key: `uploads/${file.name}`,
data: file,
resume: true,
onProgress: (sent, total) => {
console.log(`${Math.round((sent / total) * 100)}%`);
},
signal: controller.signal,
});Aborting rejects with an Error whose name is AbortError, which is not an SDK error class. Check the name rather than instanceof.
resume: true stores a fingerprint for the transfer, so a repeat call for the same bytes and the same URL continues from the byte it stopped at instead of starting over. The fingerprint is removed once the upload succeeds. Supply urlStorage to keep fingerprints somewhere you control.
Many files at once
const files = Array.from(document.querySelector<HTMLInputElement>('#files')?.files ?? []);
const results = await client.storage.uploadBatch(
files.map((file) => ({ bucket: 'uploads', key: `uploads/${file.name}`, data: file })),
{ concurrency: 6, onProgress: (done, total) => console.log(`${done}/${total}`) }
);
for (const { input, error } of results) {
if (error) console.error(`${input.key} failed:`, error);
}Four files upload at a time unless you set concurrency. One failure never cancels the rest: each result carries either result or error, and the array is in the same order as the input.
Upload sessions on their own
upload() opens the session and sends the bytes in one call. Take the two apart when they belong in different places: a service that decides what may be written, and a worker that holds the file.
Hash the bytes where they are, since the session cannot be opened without the digest and the exact size:
import { readFile } from 'node:fs/promises';
const bytes = new Uint8Array(await readFile('./avatar.png'));
const digest = await crypto.subtle.digest('SHA-256', bytes);
const sha256 = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');Open the session wherever the write is authorized:
const { session, upload } = await client.storage.uploadSessions.create('uploads', {
key: 'avatars/user-1.png',
contentLength: bytes.byteLength,
checksumSha256: sha256,
contentType: 'image/png',
});
console.log(upload.url, session.expiresAt, upload.publicObjectUrl);Then send the bytes against that URL, passing the session's headers through unchanged. The SDK adds Tus-Resumable: 1.0.0 and merges whatever else the session issued:
await client.storage.uploadSessions.uploadToUrl(
upload.url,
{ data: bytes },
{
headers: upload.headers,
onProgress: (sent, total) => console.log(`${Math.round((sent / total) * 100)}%`),
}
);uploadToUrl skips both hashing and session creation, and the signed URL carries the write authority. The process calling it still constructs a client, and the constructor still needs a credential, so this splits responsibility rather than removing the need for one.
After a dropped connection, resumeFromUrl takes the same URL and the same bytes, negotiates the offset already stored, and continues from there:
await client.storage.uploadSessions.resumeFromUrl(
upload.url,
{ data: bytes },
{
headers: upload.headers,
}
);Poll uploadSessions.get(bucket, sessionId) to watch a session move through pending, ready and completed, or land on failed or expired. uploadSessions.list(bucket) returns the sessions for a bucket.
The signed URL dies with its session
session.expiresAt is the expiry of both. A transfer that runs past it fails with UploadSessionExpiredError, and
the only fix is a new session. Size your chunks so a slow connection keeps making progress, and start a fresh upload
rather than retrying the dead URL.
Object keys, visibility and the URLs an object is served from are covered in File storage.
