2026-09-23 16:10:09 +00:00
|
|
|
import * as fs from 'node:fs'
|
|
|
|
|
import { Readable } from 'node:stream'
|
|
|
|
|
import { pipeline } from 'node:stream/promises'
|
|
|
|
|
import { S3Client, RestoreObjectCommand, HeadObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
|
|
|
|
|
|
|
|
|
|
const s3 = new S3Client({ region: process.env.AWS_REGION })
|
|
|
|
|
|
2026-09-24 15:20:16 +00:00
|
|
|
const EXTENSIONS = ['mp4', 'webm']
|
|
|
|
|
|
|
|
|
|
// 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(', ')})`)
|
2026-09-23 16:10:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function initiateRestore(videoId: string, days = 5) {
|
2026-09-24 15:20:16 +00:00
|
|
|
const key = await resolveObjectKey(videoId)
|
2026-09-23 16:10:09 +00:00
|
|
|
try {
|
|
|
|
|
await s3.send(new RestoreObjectCommand({
|
|
|
|
|
Bucket: process.env.GLACIER_BUCKET,
|
2026-09-24 15:20:16 +00:00
|
|
|
Key: key,
|
2026-09-23 16:10:09 +00:00
|
|
|
RestoreRequest: {
|
|
|
|
|
Days: days,
|
|
|
|
|
GlacierJobParameters: { Tier: 'Standard' }
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
const err = error as { name?: string }
|
|
|
|
|
if (err.name === 'RestoreAlreadyInProgress') return
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function checkRestoreStatus(videoId: string): Promise<{ ongoing: boolean, expiry: Date | null }> {
|
|
|
|
|
const head = await s3.send(new HeadObjectCommand({
|
|
|
|
|
Bucket: process.env.GLACIER_BUCKET,
|
2026-09-24 15:20:16 +00:00
|
|
|
Key: await resolveObjectKey(videoId)
|
2026-09-23 16:10:09 +00:00
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const restoreHeader = head.Restore
|
|
|
|
|
if (!restoreHeader) return { ongoing: true, expiry: null }
|
|
|
|
|
|
|
|
|
|
const ongoing = /ongoing-request="true"/.test(restoreHeader)
|
|
|
|
|
const expiryMatch = restoreHeader.match(/expiry-date="([^"]+)"/)
|
|
|
|
|
const expiry = expiryMatch ? new Date(expiryMatch[1]) : null
|
|
|
|
|
|
|
|
|
|
return { ongoing, expiry }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function downloadRestoredObjectToFile(videoId: string, destPath: string) {
|
2026-09-24 15:20:16 +00:00
|
|
|
const key = await resolveObjectKey(videoId)
|
2026-09-23 16:10:09 +00:00
|
|
|
const object = await s3.send(new GetObjectCommand({
|
|
|
|
|
Bucket: process.env.GLACIER_BUCKET,
|
2026-09-24 15:20:16 +00:00
|
|
|
Key: key
|
2026-09-23 16:10:09 +00:00
|
|
|
}))
|
|
|
|
|
|
2026-09-24 15:20:16 +00:00
|
|
|
if (!object.Body) throw new Error(`No body returned for restored object ${key}`)
|
2026-09-23 16:10:09 +00:00
|
|
|
|
|
|
|
|
const nodeStream = object.Body instanceof Readable
|
|
|
|
|
? object.Body
|
|
|
|
|
: Readable.fromWeb(object.Body as any)
|
|
|
|
|
|
|
|
|
|
await pipeline(nodeStream, fs.createWriteStream(destPath))
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 15:20:16 +00:00
|
|
|
export { resolveObjectKey, initiateRestore, checkRestoreStatus, downloadRestoredObjectToFile }
|