Thank you for the answer. After a good night sleep I came up with the same solution.
And also realised that the client should just get the signature from the API and start uploading with the provided values from the API.
I also ended up using Uppy for easier and more complete integration.
For anyone interested or having the same scenario here’s the code.
Please note I did not test the code yet.
npm install @uppy/core @uppy/locales @uppy/svelte @uppy/tus
// tus-file-upload.svelte
<script lang="ts">
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
import Czech from '@uppy/locales/lib/cs_CZ';
import { Dashboard } from '@uppy/svelte';
// Uppy CSS
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';
import { PUBLIC_BUNNY_LIBRARY_ID } from '$env/static/public';
import { fieldProxy } from 'sveltekit-superforms';
interface TusUploadSignature {
presignedSignature: string;
expiresAt: any; // Number
videoId: string;
collectionId: string;
}
const uppy = new Uppy({
locale: Czech,
restrictions: {
allowedFileTypes: [
'.4mv',
'.amv',
'.avi',
'.flv',
'.m4p',
'.m4v',
'.mkv',
'.mov',
'.mp3',
'.mp4',
'.mpeg',
'.mpg',
'.mxf',
'.ogg',
'.ts',
'.vod',
'.wav',
'.webm',
'.wmv'
]
}
}).use(Tus, {
endpoint: 'https://video.bunnycdn.com/tusupload',
async onBeforeRequest(req, file) {
// Get the upload signature data from the API
const response = await fetch('/api/video/tus-upload-signature');
const data: TusUploadSignature = await response.json();
// Set the required request headers
req.setHeader('AuthorizationSignature', data.presignedSignature);
req.setHeader('AuthorizationExpire', data.expiresAt);
req.setHeader('VideoId', data.videoId);
req.setHeader('LibraryId', PUBLIC_BUNNY_LIBRARY_ID);
// Set the required metadata
this.metadata = {
filetype: file.type,
title: 'Test',
collection: data.collectionId
};
}
});
</script>
<Dashboard {uppy} />
// tus-file-upload/server.ts
import { json, error } from "@sveltejs/kit";
import { sha256 } from "oslo/crypto";
import { encodeHex } from "oslo/encoding";
import { eq } from 'drizzle-orm';
import { db } from "$lib/server/db";
import { videoTable } from "$lib/server/db/schema";
import type { DatabaseUserAttributes } from "$lib/server/auth";
import { BUNNY_LIBRARY_API_KEY } from "$env/static/private";
import { PUBLIC_BUNNY_LIBRARY_ID } from "$env/static/public";
const getOrCreateCollection = async (creator: DatabaseUserAttributes) => {
// If the creator already has collectionId set return it
if (creator.collectionId) return creator.collectionId;
// The creator doesn't have a collection yet
// create one and return the id of it
let collectionId: string;
await fetch(`https://video.bunnycdn.com/library/${PUBLIC_BUNNY_LIBRARY_ID}/collections`, {
method: "POST",
headers: {
accept: 'application/json',
'content-type': 'application/json',
AccessKey: BUNNY_LIBRARY_API_KEY
},
body: JSON.stringify({ name: creator.email })
})
.then(response => response.json())
.then(data => { collectionId = data.guid })
.catch(err => error(500, {
message: err
}));
// Save the newly created collectionId to the database
await db
.update(userTable)
.set({
collectionId: collectionId
})
.where(eq(userTable.id, creator.id));
return collectionId;
}
export const POST = async ({ locals }) => {
const user = locals.user;
if (!user) {
error(401, {
message: "K provedení této akce musíte být přihlášeni."
});
}
// Get or create a Bunny Stream collection for the user
const collectionId = await getOrCreateCollection(user);
// Create a video object that we will use for uploading the video to
const response = await fetch(`https://video.bunnycdn.com/library/${PUBLIC_BUNNY_LIBRARY_ID}/videos`, {
method: "POST",
headers: {
accept: 'application/json',
'content-type': 'application/json',
AccessKey: BUNNY_LIBRARY_API_KEY
},
body: JSON.stringify({ title: "Název souboru", collectionId: collectionId, thumbnailTime: 0 })
});
const data = await response.json();
const videoId: string = data.guid; // the guid of the created video object
// Create a UNIX timestamp that marks the URL expiration (5 minutes from now)
const expiresAt = Math.floor((Date.now() + 5 * 60 * 1000) / 1000);
// Create a SHA256 hash to create the URL token
const signatureHash = await sha256(new TextEncoder().encode(PUBLIC_BUNNY_LIBRARY_ID + BUNNY_LIBRARY_API_KEY + expiresAt + videoId));
const presignedSignature = encodeHex(signatureHash);
return json({
presignedSignature, // SHA256 signature (libraryId + apiKey + expiresAt + videoId)
expiresAt, // UNIX timestamp of expiry date
videoId, // ID of the created video object
collectionId // ID of the creator's video collection
})
}