Files
liqa/server/src/common/file-downloader.ts
T
ars9 6a91ce30e3 feat(files): add file storage subsystem with scenario and run artifact support
- 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
2026-04-21 13:01:09 +03:00

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();
});
}