Skip to content
GigadriveDocs

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

  1. Resolve the source

    Exactly one of data, path or stream. Size and content type are derived from the source and the object key unless you set contentLength and contentType yourself.

  2. 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.

  3. Open a session

    POST /applications/{app}/storage/buckets/{bucket}/uploads returns the session plus a signed URL, the headers to send with it, and the canonical URL the object answers on once it is finalized.

  4. Send the bytes

    A resumable PATCH transfer against that URL, retried on failure after 0, 1, 3 and 5 seconds.

  5. Return, or wait

    You get { session, url } as soon as the last byte lands. With waitForCompletion, the call polls until the server has verified the checksum and created the object, and adds it as object.

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

OptionTypeDefault
bucketstringrequired, the bucket name
keystringrequired
dataBlob | ArrayBuffer | Uint8Arrayone source only, and this one works everywhere
pathstringone source only, Node
streamNodeReadableLikeone source only, Node, needs contentLength and checksumSha256
contentLengthnumbermeasured, except for stream where it is required
checksumSha256stringcomputed, except for stream where it is required
contentTypestringinferred from key
checksumSha1stringsent only if you provide it
checksumMd5stringsent only if you provide it
chunkSizenumberone chunk for data, 50 MiB for path and stream
retryDelaysnumber[] | null[0, 1000, 3000, 5000], and null disables retries
onProgress(bytesSent: number, bytesTotal: number) => voidnone
signalAbortSignalnone
resumebooleanfalse
urlStorageUploadUrlStoragethe tus-js-client default for the runtime
waitForCompletionboolean | WaitForCompletionOptionsfalse
environmentstringinferred 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.

Object keys, visibility and the URLs an object is served from are covered in File storage.