import { S3Client, GetObjectCommand, PutObjectCommand, HeadObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { Response } from "express"; import { randomUUID } from "crypto"; import { Readable } from "stream"; const S3_ENDPOINT = process.env.S3_ENDPOINT || "https://fsn1.your-objectstorage.com"; const S3_BUCKET = process.env.S3_BUCKET || "folxspeed"; const S3_PREFIX = process.env.S3_PREFIX || "video-folx-tv"; // prefix within bucket const S3_PUBLIC_BASE = process.env.S3_PUBLIC_BASE || "https://folxvideos.b-cdn.net"; const s3 = new S3Client({ region: process.env.S3_REGION || "fsn1", endpoint: S3_ENDPOINT, forcePathStyle: true, credentials: { accessKeyId: process.env.S3_ACCESS_KEY_ID || "", secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "", }, }); export class ObjectNotFoundError extends Error { constructor() { super("Object not found"); this.name = "ObjectNotFoundError"; Object.setPrototypeOf(this, ObjectNotFoundError.prototype); } } export class ObjectStorageService { constructor() {} getPublicObjectSearchPaths(): Array { return [`${S3_PREFIX}/public`]; } getPrivateObjectDir(): string { return `${S3_PREFIX}/.private`; } async getThumbnailUploadURL(): Promise { const objectId = randomUUID(); const key = `${this.getPrivateObjectDir()}/thumbnails/${objectId}`; const cmd = new PutObjectCommand({ Bucket: S3_BUCKET, Key: key, ACL: "public-read", }); return await getSignedUrl(s3, cmd, { expiresIn: 900 }); } async downloadObject(objectKey: string, res: Response, cacheTtlSec: number = 3600) { try { const cmd = new GetObjectCommand({ Bucket: S3_BUCKET, Key: objectKey }); const obj = await s3.send(cmd); res.set({ "Content-Type": obj.ContentType || "application/octet-stream", "Content-Length": String(obj.ContentLength || ""), "Cache-Control": `public, max-age=${cacheTtlSec}`, }); const stream = obj.Body as Readable; stream.on("error", (err) => { console.error("Stream error:", err); if (!res.headersSent) res.status(500).json({ error: "Error streaming file" }); }); stream.pipe(res); } catch (error) { console.error("Error downloading file:", error); if (!res.headersSent) res.status(500).json({ error: "Error downloading file" }); } } async getObjectFile(objectPath: string): Promise { if (!objectPath.startsWith("/objects/")) throw new ObjectNotFoundError(); const parts = objectPath.slice(1).split("/"); if (parts.length < 2) throw new ObjectNotFoundError(); const entityId = parts.slice(1).join("/"); const key = `${this.getPrivateObjectDir()}/${entityId}`; try { await s3.send(new HeadObjectCommand({ Bucket: S3_BUCKET, Key: key })); return key; // returns S3 key instead of File object } catch { throw new ObjectNotFoundError(); } } normalizeObjectPath(rawPath: string): string { // Convert old Replit/GCS URL or full S3 URL into /objects/{entityId} const privateDir = this.getPrivateObjectDir(); const patterns = [ `https://storage.googleapis.com/`, `${S3_PUBLIC_BASE}/`, `${S3_ENDPOINT}/${S3_BUCKET}/`, ]; let path = rawPath; for (const p of patterns) { if (path.startsWith(p)) { path = "/" + path.slice(p.length); break; } } const idx = path.indexOf(privateDir); if (idx >= 0) { const entityId = path.slice(idx + privateDir.length).replace(/^\//, ""); return `/objects/${entityId}`; } return rawPath; } }