Files
classroomio/apps/jobs/src/utils/storage.ts
T

106 lines
2.8 KiB
TypeScript

import { GetObjectCommand, HeadObjectCommand, NotFound, PutObjectCommand } from '@aws-sdk/client-s3';
import { Readable } from 'node:stream';
import { createWriteStream } from 'node:fs';
import { mkdir, readFile, unlink } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { getS3Client, getStorageConfig } from '../config/storage';
/**
* Streams an S3 object to a temp file on disk and returns the absolute path.
* Workers prefer disk over buffers so ffmpeg can seek without holding the
* whole video in RAM.
*/
export async function objectExists(bucket: string, key: string): Promise<boolean> {
try {
await getS3Client().send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
return true;
} catch (error) {
if (error instanceof NotFound || (error as { name?: string }).name === 'NotFound') {
return false;
}
const status = (error as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode;
if (status === 404) {
return false;
}
throw error;
}
}
export async function downloadObjectToTempFile(bucket: string, key: string, fileNameHint?: string): Promise<string> {
const dir = path.join(tmpdir(), 'cio-jobs');
await mkdir(dir, { recursive: true });
const safeName = fileNameHint?.replace(/[^a-zA-Z0-9._-]/g, '_') ?? 'media';
const localPath = path.join(dir, `${Date.now()}_${process.pid}_${safeName}`);
const response = await getS3Client().send(new GetObjectCommand({ Bucket: bucket, Key: key }));
if (!response.Body) {
throw new Error(`S3 object ${bucket}/${key} returned no body`);
}
const body = response.Body as Readable;
await pipeline(body, createWriteStream(localPath));
return localPath;
}
export async function uploadFileToBucket(
bucket: string,
key: string,
localPath: string,
contentType: string,
cacheControl?: string
): Promise<void> {
const buffer = await readFile(localPath);
await getS3Client().send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: buffer,
ContentType: contentType,
CacheControl: cacheControl
})
);
}
export async function uploadBufferToBucket(
bucket: string,
key: string,
buffer: Buffer,
contentType: string,
cacheControl?: string
): Promise<void> {
await getS3Client().send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: buffer,
ContentType: contentType,
CacheControl: cacheControl
})
);
}
export async function safeUnlink(filePath: string | undefined | null): Promise<void> {
if (!filePath) return;
try {
await unlink(filePath);
} catch {
// best-effort cleanup
}
}
export function videosBucket(): string {
return getStorageConfig().bucketVideos;
}
export function mediaBucket(): string {
return getStorageConfig().bucketMedia;
}