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
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user