- add FileEntity, ScenarioFileEntity, ScenarioRunFileEntity with UUID-sharded disk tree - FileStorageService: write/sha256/expiry, paginated listing, dynamic cleanup cron via SchedulerRegistry - expose getScenarioFiles() and downloadFile() on ScriptContext for use in exec code - wire scenarioId, runId, fileService into ExecContextBuilder and scenario scheduler - add file upload and listing endpoints to ScenarioController - add FILES_DIR, FILE_RUN_EXPIRATION_DAYS, FILE_CLEANUP_CRON to AppConfig
78 lines
1.9 KiB
TypeScript
78 lines
1.9 KiB
TypeScript
import * as https from "https";
|
|
import * as http from "http";
|
|
import { URL } from "url";
|
|
|
|
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
const TIMEOUT_MS = 30000; // 30 seconds
|
|
|
|
export async function downloadFile(
|
|
urlStr: string,
|
|
options?: {
|
|
method?: string;
|
|
headers?: Record<string, string>;
|
|
body?: string;
|
|
},
|
|
): Promise<Buffer> {
|
|
const url = new URL(urlStr);
|
|
const protocol = url.protocol === "https:" ? https : http;
|
|
const method = options?.method ?? "GET";
|
|
const headers = options?.headers ?? {};
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const req = protocol.request(
|
|
{
|
|
hostname: url.hostname,
|
|
port: url.port,
|
|
path: url.pathname + url.search,
|
|
method,
|
|
headers,
|
|
timeout: TIMEOUT_MS,
|
|
},
|
|
(res) => {
|
|
// Follow redirects
|
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
downloadFile(res.headers.location, options).then(resolve).catch(reject);
|
|
return;
|
|
}
|
|
|
|
if (res.statusCode && res.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
return;
|
|
}
|
|
|
|
const chunks: Buffer[] = [];
|
|
let totalSize = 0;
|
|
|
|
res.on("data", (chunk: Buffer) => {
|
|
totalSize += chunk.length;
|
|
if (totalSize > MAX_FILE_SIZE) {
|
|
req.destroy();
|
|
reject(new Error(`File exceeds maximum size of ${MAX_FILE_SIZE} bytes`));
|
|
return;
|
|
}
|
|
chunks.push(chunk);
|
|
});
|
|
|
|
res.on("end", () => {
|
|
resolve(Buffer.concat(chunks));
|
|
});
|
|
|
|
res.on("error", reject);
|
|
},
|
|
);
|
|
|
|
req.on("timeout", () => {
|
|
req.destroy();
|
|
reject(new Error("Download timeout"));
|
|
});
|
|
|
|
req.on("error", reject);
|
|
|
|
if (options?.body && (method === "POST" || method === "PUT")) {
|
|
req.write(options.body);
|
|
}
|
|
|
|
req.end();
|
|
});
|
|
}
|