File size: 8,716 Bytes
5ef6e9d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | /**
* videoStorage.ts
*
* Downloads generated videos from Grok CDN and stores them in an
* S3-compatible object storage (hi168.com OSS).
*
* Upload: @aws-sdk/lib-storage (multipart, handles large files)
* Download / stream: @aws-sdk/client-s3 (GetObject)
*/
import {
S3Client,
CreateBucketCommand,
HeadBucketCommand,
GetObjectCommand,
HeadObjectCommand,
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { Readable } from "stream";
import { randomUUID } from "crypto";
// βββ S3 configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const S3_ENDPOINT = process.env.S3_ENDPOINT ?? "";
const S3_BUCKET = process.env.S3_BUCKET ?? "starforge-videos";
const S3_REGION = process.env.S3_REGION ?? "us-east-1";
const ACCESS_KEY = process.env.S3_ACCESS_KEY ?? "";
const SECRET_KEY = process.env.S3_SECRET_KEY ?? "";
let s3: S3Client | null = null;
function getS3(): S3Client {
if (!s3) {
s3 = new S3Client({
endpoint: S3_ENDPOINT,
region: S3_REGION,
credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY },
forcePathStyle: true, // required for most non-AWS S3 endpoints
});
}
return s3;
}
/** Returns true if S3 credentials are configured. */
export function isStorageReady(): boolean {
return !!(S3_ENDPOINT && ACCESS_KEY && SECRET_KEY);
}
// βββ Bucket init (create if missing) βββββββββββββββββββββββββββββββββββββββββ
let bucketReady = false;
async function ensureBucket(): Promise<void> {
if (bucketReady) return;
const client = getS3();
try {
await client.send(new HeadBucketCommand({ Bucket: S3_BUCKET }));
bucketReady = true;
console.log(`[video-storage] bucket "${S3_BUCKET}" exists`);
} catch (err: any) {
if (err.$metadata?.httpStatusCode === 404 || err.name === "NoSuchBucket" || err.name === "NotFound") {
try {
await client.send(new CreateBucketCommand({ Bucket: S3_BUCKET }));
bucketReady = true;
console.log(`[video-storage] bucket "${S3_BUCKET}" created`);
} catch (createErr: any) {
console.error("[video-storage] bucket create error:", createErr?.message ?? createErr);
}
} else {
// treat unknown errors as "bucket exists, proceed"
console.warn("[video-storage] HeadBucket warn (proceeding):", err?.message ?? err);
bucketReady = true;
}
}
}
// βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
// βββ Upload βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Download a video from remoteUrl and upload it to S3.
*
* Returns the serving path `/api/videos/stored/<id>.mp4`, or null on failure.
*/
export async function downloadAndStoreVideo(
remoteUrl: string,
bearerToken: string | null,
ext = "mp4",
): Promise<string | null> {
if (!isStorageReady()) {
console.warn("[video-storage] S3 not configured β skipping download");
return null;
}
await ensureBucket();
const id = randomUUID();
const key = `videos/${id}.${ext}`;
let hostname = "";
try { hostname = new URL(remoteUrl).hostname; } catch { /* ok */ }
// Cloudflare R2 pre-signed URLs are publicly accessible β no auth needed.
const isR2 = hostname.endsWith(".r2.cloudflarestorage.com");
// assets.grok.com is behind Cloudflare; use grok.com as Referer+Origin.
const isGrokCdn = hostname === "assets.grok.com";
const referer = isGrokCdn ? "https://grok.com/" : "https://geminigen.ai/";
const origin = isGrokCdn ? "https://grok.com" : "https://geminigen.ai";
const baseHeaders: Record<string, string> = {
"User-Agent": USER_AGENT,
Accept: "video/mp4,video/*,*/*",
...(isR2 ? {} : { Referer: referer, Origin: origin }),
};
// R2 pre-signed URLs work without any auth; try without token first.
// For other CDNs, try with Bearer token first.
const strategies: Array<Record<string, string>> = isR2
? [{ ...baseHeaders }]
: [
...(bearerToken ? [{ ...baseHeaders, Authorization: `Bearer ${bearerToken}` }] : []),
{ ...baseHeaders },
];
let videoBuffer: Buffer | null = null;
let contentType = "video/mp4";
for (const headers of strategies) {
try {
const resp = await fetch(remoteUrl, { headers });
console.log(
`[video-storage] fetch β ${resp.status} from ${new URL(remoteUrl).hostname}`,
);
if (resp.ok) {
const ab = await resp.arrayBuffer();
videoBuffer = Buffer.from(ab);
const ct = resp.headers.get("content-type");
if (ct && ct.startsWith("video/")) contentType = ct;
break;
}
} catch (err) {
console.warn(
"[video-storage] fetch error:",
err instanceof Error ? err.message : err,
);
}
}
if (!videoBuffer || videoBuffer.length < 1024) {
console.warn("[video-storage] all download strategies failed");
return null;
}
try {
const upload = new Upload({
client: getS3(),
params: {
Bucket: S3_BUCKET,
Key: key,
Body: videoBuffer,
ContentType: contentType,
},
});
await upload.done();
console.log(`[video-storage] uploaded ${videoBuffer.length} bytes β s3://${S3_BUCKET}/${key}`);
return `/api/videos/stored/${id}.${ext}`;
} catch (err) {
console.error(
"[video-storage] S3 upload error:",
err instanceof Error ? err.message : err,
);
return null;
}
}
// βββ Stream / serve βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Serve a stored video from S3 to an Express response.
* objectPath: the `<id>.mp4` portion after /api/videos/stored/
*/
export async function streamStoredVideo(
objectPath: string,
res: import("express").Response,
rangeHeader?: string,
): Promise<void> {
if (!isStorageReady()) {
res.status(503).json({ error: "Storage not configured" });
return;
}
const key = `videos/${objectPath}`;
try {
const client = getS3();
// HEAD to get size & content-type
let size = 0;
let contentType = "video/mp4";
try {
const head = await client.send(
new HeadObjectCommand({ Bucket: S3_BUCKET, Key: key }),
);
size = Number(head.ContentLength ?? 0);
contentType = head.ContentType ?? contentType;
} catch {
// fallback: stream without size
}
res.setHeader("Content-Type", contentType);
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Cache-Control", "public, max-age=86400");
let byteRange: { start: number; end: number } | undefined;
if (rangeHeader && size) {
const m = /bytes=(\d*)-(\d*)/.exec(rangeHeader);
if (m) {
const start = m[1] ? parseInt(m[1]) : 0;
const end = m[2] ? parseInt(m[2]) : size - 1;
byteRange = { start, end };
res.setHeader("Content-Range", `bytes ${start}-${end}/${size}`);
res.setHeader("Content-Length", end - start + 1);
res.status(206);
}
} else if (size) {
res.setHeader("Content-Length", size);
res.status(200);
} else {
res.status(200);
}
const getCmd = new GetObjectCommand({
Bucket: S3_BUCKET,
Key: key,
...(byteRange
? { Range: `bytes=${byteRange.start}-${byteRange.end}` }
: {}),
});
const obj = await client.send(getCmd);
if (!obj.Body) {
throw new Error("Empty body from S3");
}
// obj.Body is a SdkStreamMixin β convert to Node.js Readable
const stream = obj.Body as Readable;
stream.pipe(res);
stream.on("error", (err) => {
console.error("[video-storage] stream pipe error:", err.message);
if (!res.headersSent) res.status(500).end();
});
} catch (err) {
console.error(
"[video-storage] stream error:",
err instanceof Error ? err.message : err,
);
if (!res.headersSent) res.status(404).json({ error: "Video not found in storage" });
}
}
|