Guide

Resumable Uploads

Learn how to upload files to Zuvo Storage.

The resumable upload method is recommended when:

  • Uploading large files that may exceed 6MB in size
  • Network stability is a concern
  • You want to have progress events for your uploads

Zuvo Storage implements the TUS protocol to enable resumable uploads. TUS stands for The Upload Server and is an open protocol for supporting resumable uploads. The protocol allows the upload process to be resumed from where it left off in case of interruptions. This method can be implemented using the tus-js-client library, or other client-side libraries like Uppy that support the TUS protocol.

JavaScript

Here's an example of how to upload a file using tus-js-client:

    const tus = require('tus-js-client')

    const projectId = ''

    async function uploadFile(bucketName, fileName, file) {
        const { data: { session } } = await supabase.auth.getSession()

        return new Promise((resolve, reject) => {
            var upload = new tus.Upload(file, {
                // Zuvo TUS endpoint (with direct storage hostname)
                endpoint: `https://${projectId}.storage.supabase.co/storage/v1/upload/resumable`,
                retryDelays: [0, 3000, 5000, 10000, 20000],
                headers: {
                    authorization: `Bearer ${session.access_token}`,
                    'x-upsert': 'true', // optionally set upsert to true to overwrite existing files
                },
                uploadDataDuringCreation: true,
                removeFingerprintOnSuccess: true, // Important if you want to allow re-uploading the same file https://github.com/tus/tus-js-client/blob/main/docs/api.md#removefingerprintonsuccess
                metadata: {
                    bucketName: bucketName,
                    objectName: fileName,
                    contentType: 'image/png',
                    cacheControl: '3600',
                    metadata: JSON.stringify({ // custom metadata passed to the user_metadata column
                       yourCustomMetadata: true,
                    }),
                },
                chunkSize: 6 * 1024 * 1024, // NOTE: it must be set to 6MB (for now) do not change it
                onError: function (error) {
                    console.log('Failed because: ' + error)
                    reject(error)
                },
                onProgress: function (bytesUploaded, bytesTotal) {
                    var percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2)
                    console.log(bytesUploaded, bytesTotal, percentage + '%')
                },
                onSuccess: function () {
                    console.log('Download %s from %s', upload.file.name, upload.url)
                    resolve()
                },
            })


            // Check if there are any previous uploads to continue.
            return upload.findPreviousUploads().then(function (previousUploads) {
                // Found previous uploads so we select the first one.
                if (previousUploads.length) {
                    upload.resumeFromPreviousUpload(previousUploads[0])
                }

                // Start the upload
                upload.start()
            })
        })
    }
React

Here's an example of how to upload a file using @uppy/tus with react:

    import { useEffect, useState } from "react";
    import { createClient } from "@supabase/supabase-js";
    import Uppy from "@uppy/core";
    import Tus from "@uppy/tus";
    import Dashboard from "@uppy/dashboard";
    import "@uppy/core/dist/style.min.css";
    import "@uppy/dashboard/dist/style.min.css";

    function App() {
        // Initialize Uppy instance with the 'sample' bucket specified for uploads
        const uppy = useUppyWithZuvo({ bucketName: "sample" });

        useEffect(() => {
            // Set up Uppy Dashboard to display as an inline component within a specified target
            uppy.use(Dashboard, {
                inline: true, // Ensures the dashboard is rendered inline
                target: "#drag-drop-area", // HTML element where the dashboard renders
                showProgressDetails: true, // Show progress details for file uploads
            });
        }, []);

        return (
            <div id="drag-drop-area">
            </div>
            
        );
    }

    export default App;

    /**
     * Custom hook for configuring Uppy with Zuvo authentication and TUS resumable uploads
     * @param {Object} options - Configuration options for the Uppy instance.
     * @param {string} options.bucketName - The bucket name in Zuvo where files are stored.
     * @returns {Object} uppy - Uppy instance with configured upload settings.
     */
    export const useUppyWithZuvo = ({ bucketName }: { bucketName: string }) => {
        // Initialize Uppy instance only once
        const [uppy] = useState(() => new Uppy());
        // Initialize Zuvo client with project URL and publishable key
        const supabase = createClient(`https://${projectId}.supabase.co`, publishableKey);

        useEffect(() => {
            const initializeUppy = async () => {
            // Retrieve the current user's session for authentication
            const {
                data: { session },
            } = await supabase.auth.getSession();

            uppy.use(Tus, {
                    // Zuvo TUS endpoint (with direct storage hostname)
                    endpoint: `https://${projectId}.storage.supabase.co/storage/v1/upload/resumable`,
                    retryDelays: [0, 3000, 5000, 10000, 20000], // Retry delays for resumable uploads
                    headers: {
                        authorization: `Bearer ${session?.access_token}`, // User session access token
                        apikey: anonKey, // API key for Zuvo
                    },
                    uploadDataDuringCreation: true, // Send metadata with file chunks
                    removeFingerprintOnSuccess: true, // Remove fingerprint after successful upload
                    chunkSize: 6 * 1024 * 1024, // Chunk size for TUS uploads (6MB)
                    allowedMetaFields: [
                        "bucketName",
                        "objectName",
                        "contentType",
                        "cacheControl",
                        "metadata",
                    ], // Metadata fields allowed for the upload
                    onError: (error) => console.error("Upload error:", error), // Error handling for uploads
                }).on("file-added", (file) => {
                    // Attach metadata to each file, including bucket name and content type
                    file.meta = {
                        ...file.meta,
                        bucketName, // Bucket specified by the user of the hook
                        objectName: file.name, // Use file name as object name
                        contentType: file.type, // Set content type based on file MIME type
                        metadata: JSON.stringify({ // custom metadata passed to the user_metadata column
                            yourCustomMetadata: true,
                        }),
                    };
                });
            };

            // Initialize Uppy with Zuvo settings
            initializeUppy();
        }, [uppy, bucketName]);

        // Return the configured Uppy instance
        return uppy;
    };

Upload URL

When uploading using the resumable upload endpoint, the storage server creates a unique URL for each upload, even for multiple uploads to the same path. All chunks will be uploaded to this URL using the PATCH method.

This unique upload URL will be valid for up to 24 hours. If the upload is not completed within 24 hours, the URL will expire and you'll need to start the upload again. TUS client libraries typically create a new URL if the previous one expires.

Concurrency

When two or more clients upload to the same upload URL only one of them will succeed. The other clients will receive a 409 Conflict error. Only 1 client can upload to the same upload URL at a time which prevents data corruption.

When two or more clients upload a file to the same path using different upload URLs, the first client to complete the upload will succeed and the other clients will receive a 409 Conflict error.

If you provide the x-upsert header the last client to complete the upload will succeed instead.

Uppy example

You can check a full example using Uppy.

Uppy has integrations with different frameworks:

Presigned uploads

Resumable uploads also supports using signed upload tokens to created time-limited URLs that you can share to your users by invoking the createSignedUploadUrl method on the SDK and including the returned token in the x-signature header of the resumable upload.

JavaScript
// Create a signed upload URL
const { data } = await supabase.storage.from('bucket_name').createSignedUploadUrl('file_path', {
  upsert: true, // Optional: allow overwriting existing files
})

// Use the signed URL token in resumable upload headers
// Include data.token in the x-signature header

See this full example using Uppy with signed URLs for more context.

Overwriting files

When uploading a file to a path that already exists, the default behavior is to return a 400 Asset Already Exists error. If you want to overwrite a file on a specific path you can set the x-upsert header to true.

We do advise against overwriting files when possible, as the CDN will take some time to propagate the changes to all the edge nodes leading to stale content. Uploading a file to a new path is the recommended way to avoid propagation delays and stale content.

To learn more, see the CDN guide.