diff --git a/src/utils/glacier.ts b/src/utils/glacier.ts index 38ee54c..364606b 100644 --- a/src/utils/glacier.ts +++ b/src/utils/glacier.ts @@ -5,15 +5,30 @@ import { S3Client, RestoreObjectCommand, HeadObjectCommand, GetObjectCommand } f const s3 = new S3Client({ region: process.env.AWS_REGION }) -function objectKey(videoId: string) { - return `${videoId}.mp4` +const EXTENSIONS = ['mp4', 'webm'] + +// glacier objects keep their original extension, so find which one this video has +async function resolveObjectKey(videoId: string): Promise { + for (const ext of EXTENSIONS) { + const key = `${videoId}.${ext}` + try { + await s3.send(new HeadObjectCommand({ Bucket: process.env.GLACIER_BUCKET, Key: key })) + return key + } catch (error: unknown) { + const err = error as { name?: string, $metadata?: { httpStatusCode?: number } } + if (err.name === 'NotFound' || err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) continue + throw error + } + } + throw new Error(`No object found in cold storage bucket for ${videoId} (tried ${EXTENSIONS.join(', ')})`) } async function initiateRestore(videoId: string, days = 5) { + const key = await resolveObjectKey(videoId) try { await s3.send(new RestoreObjectCommand({ Bucket: process.env.GLACIER_BUCKET, - Key: objectKey(videoId), + Key: key, RestoreRequest: { Days: days, GlacierJobParameters: { Tier: 'Standard' } @@ -29,7 +44,7 @@ async function initiateRestore(videoId: string, days = 5) { async function checkRestoreStatus(videoId: string): Promise<{ ongoing: boolean, expiry: Date | null }> { const head = await s3.send(new HeadObjectCommand({ Bucket: process.env.GLACIER_BUCKET, - Key: objectKey(videoId) + Key: await resolveObjectKey(videoId) })) const restoreHeader = head.Restore @@ -43,12 +58,13 @@ async function checkRestoreStatus(videoId: string): Promise<{ ongoing: boolean, } async function downloadRestoredObjectToFile(videoId: string, destPath: string) { + const key = await resolveObjectKey(videoId) const object = await s3.send(new GetObjectCommand({ Bucket: process.env.GLACIER_BUCKET, - Key: objectKey(videoId) + Key: key })) - if (!object.Body) throw new Error(`No body returned for restored object ${objectKey(videoId)}`) + if (!object.Body) throw new Error(`No body returned for restored object ${key}`) const nodeStream = object.Body instanceof Readable ? object.Body @@ -57,4 +73,4 @@ async function downloadRestoredObjectToFile(videoId: string, destPath: string) { await pipeline(nodeStream, fs.createWriteStream(destPath)) } -export { initiateRestore, checkRestoreStatus, downloadRestoredObjectToFile } +export { resolveObjectKey, initiateRestore, checkRestoreStatus, downloadRestoredObjectToFile } diff --git a/src/utils/glacierPoller.ts b/src/utils/glacierPoller.ts index dc48b5a..c00b255 100644 --- a/src/utils/glacierPoller.ts +++ b/src/utils/glacierPoller.ts @@ -2,7 +2,7 @@ import * as fs from 'node:fs' import { db } from '@/utils/database' import redis from '@/utils/redis' import { uploadVideo } from '@/utils/upload' -import { checkRestoreStatus, downloadRestoredObjectToFile } from '@/utils/glacier' +import { resolveObjectKey, checkRestoreStatus, downloadRestoredObjectToFile } from '@/utils/glacier' import { sendRestoreCompleteEmail } from '@/utils/mail' const POLL_INTERVAL_MS = 10 * 60000 @@ -16,7 +16,7 @@ async function processRestoringRow(row: { uuid: string, videoId: string, request .where('uuid', '=', row.uuid) .execute() - const filePath = `./videos/${row.videoId}.mp4` + const filePath = `./videos/${await resolveObjectKey(row.videoId)}` try { await downloadRestoredObjectToFile(row.videoId, filePath) const videoUrl = await uploadVideo(filePath)