fix(glacier): resolve object key extension (mp4 or webm)
Cold storage objects keep their original extension, but the key was hardcoded to .mp4, so restoring webm videos failed with UnknownError. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
79a7afdd80
commit
ee9973466c
|
|
@ -5,15 +5,30 @@ import { S3Client, RestoreObjectCommand, HeadObjectCommand, GetObjectCommand } f
|
||||||
|
|
||||||
const s3 = new S3Client({ region: process.env.AWS_REGION })
|
const s3 = new S3Client({ region: process.env.AWS_REGION })
|
||||||
|
|
||||||
function objectKey(videoId: string) {
|
const EXTENSIONS = ['mp4', 'webm']
|
||||||
return `${videoId}.mp4`
|
|
||||||
|
// glacier objects keep their original extension, so find which one this video has
|
||||||
|
async function resolveObjectKey(videoId: string): Promise<string> {
|
||||||
|
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) {
|
async function initiateRestore(videoId: string, days = 5) {
|
||||||
|
const key = await resolveObjectKey(videoId)
|
||||||
try {
|
try {
|
||||||
await s3.send(new RestoreObjectCommand({
|
await s3.send(new RestoreObjectCommand({
|
||||||
Bucket: process.env.GLACIER_BUCKET,
|
Bucket: process.env.GLACIER_BUCKET,
|
||||||
Key: objectKey(videoId),
|
Key: key,
|
||||||
RestoreRequest: {
|
RestoreRequest: {
|
||||||
Days: days,
|
Days: days,
|
||||||
GlacierJobParameters: { Tier: 'Standard' }
|
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 }> {
|
async function checkRestoreStatus(videoId: string): Promise<{ ongoing: boolean, expiry: Date | null }> {
|
||||||
const head = await s3.send(new HeadObjectCommand({
|
const head = await s3.send(new HeadObjectCommand({
|
||||||
Bucket: process.env.GLACIER_BUCKET,
|
Bucket: process.env.GLACIER_BUCKET,
|
||||||
Key: objectKey(videoId)
|
Key: await resolveObjectKey(videoId)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const restoreHeader = head.Restore
|
const restoreHeader = head.Restore
|
||||||
|
|
@ -43,12 +58,13 @@ async function checkRestoreStatus(videoId: string): Promise<{ ongoing: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadRestoredObjectToFile(videoId: string, destPath: string) {
|
async function downloadRestoredObjectToFile(videoId: string, destPath: string) {
|
||||||
|
const key = await resolveObjectKey(videoId)
|
||||||
const object = await s3.send(new GetObjectCommand({
|
const object = await s3.send(new GetObjectCommand({
|
||||||
Bucket: process.env.GLACIER_BUCKET,
|
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
|
const nodeStream = object.Body instanceof Readable
|
||||||
? object.Body
|
? object.Body
|
||||||
|
|
@ -57,4 +73,4 @@ async function downloadRestoredObjectToFile(videoId: string, destPath: string) {
|
||||||
await pipeline(nodeStream, fs.createWriteStream(destPath))
|
await pipeline(nodeStream, fs.createWriteStream(destPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
export { initiateRestore, checkRestoreStatus, downloadRestoredObjectToFile }
|
export { resolveObjectKey, initiateRestore, checkRestoreStatus, downloadRestoredObjectToFile }
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import * as fs from 'node:fs'
|
||||||
import { db } from '@/utils/database'
|
import { db } from '@/utils/database'
|
||||||
import redis from '@/utils/redis'
|
import redis from '@/utils/redis'
|
||||||
import { uploadVideo } from '@/utils/upload'
|
import { uploadVideo } from '@/utils/upload'
|
||||||
import { checkRestoreStatus, downloadRestoredObjectToFile } from '@/utils/glacier'
|
import { resolveObjectKey, checkRestoreStatus, downloadRestoredObjectToFile } from '@/utils/glacier'
|
||||||
import { sendRestoreCompleteEmail } from '@/utils/mail'
|
import { sendRestoreCompleteEmail } from '@/utils/mail'
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 10 * 60000
|
const POLL_INTERVAL_MS = 10 * 60000
|
||||||
|
|
@ -16,7 +16,7 @@ async function processRestoringRow(row: { uuid: string, videoId: string, request
|
||||||
.where('uuid', '=', row.uuid)
|
.where('uuid', '=', row.uuid)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
const filePath = `./videos/${row.videoId}.mp4`
|
const filePath = `./videos/${await resolveObjectKey(row.videoId)}`
|
||||||
try {
|
try {
|
||||||
await downloadRestoredObjectToFile(row.videoId, filePath)
|
await downloadRestoredObjectToFile(row.videoId, filePath)
|
||||||
const videoUrl = await uploadVideo(filePath)
|
const videoUrl = await uploadVideo(filePath)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue