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:
2026-04-21 13:01:09 +03:00
parent aa3bae9020
commit 6a91ce30e3
15 changed files with 864 additions and 0 deletions
@@ -6,6 +6,7 @@ import { Effect } from "effect";
import type { Browser, BrowserContext, Page } from "playwright";
import { chromium } from "playwright";
import { Repository } from "typeorm";
import { FileStorageService } from "../file/file-storage.service";
import type { ScriptLogger } from "../code-executor/code-executor.service";
import { CodeExecutorService } from "../code-executor/code-executor.service";
import { ExecContextBuilder } from "../code-executor/exec-context.builder";
@@ -43,6 +44,8 @@ export class ScenarioSchedulerService implements OnModuleInit {
private readonly runEnvironments = new Map<string, EnvironmentData>();
// Cache scenario-level timeout (seconds) per run
private readonly runScenarioTimeouts = new Map<string, number | null>();
// Cache scenarioId per run
private readonly runScenarioIds = new Map<string, string>();
private static readonly DEFAULT_SCENARIO_TIMEOUT_SEC = 600;
private static readonly DEFAULT_STEP_TIMEOUT_SEC = 60;
@@ -62,6 +65,7 @@ export class ScenarioSchedulerService implements OnModuleInit {
private readonly snippetService: SnippetService,
private readonly sessionService: SessionService,
private readonly sessionContextService: SessionContextService,
private readonly fileStorageService: FileStorageService,
) {}
async onModuleInit(): Promise<void> {
@@ -198,6 +202,7 @@ export class ScenarioSchedulerService implements OnModuleInit {
.findOne(run.scenarioId)
.catch(() => null);
this.runScenarioTimeouts.set(run.id, scenario?.timeoutSeconds ?? null);
this.runScenarioIds.set(run.id, run.scenarioId);
const traceId = crypto.randomUUID();
void traceStorage.run({ traceId }, () =>
this.processRunToCompletion(run.id),
@@ -210,6 +215,7 @@ export class ScenarioSchedulerService implements OnModuleInit {
this.runSnippets.delete(run.id);
this.runEnvironments.delete(run.id);
this.runScenarioTimeouts.delete(run.id);
this.runScenarioIds.delete(run.id);
this.logger.error(
`Run #${run.id}: setup failed — ${String(err)}`,
);
@@ -272,6 +278,7 @@ export class ScenarioSchedulerService implements OnModuleInit {
this.runSnippets.delete(runId);
this.runEnvironments.delete(runId);
this.runScenarioTimeouts.delete(runId);
this.runScenarioIds.delete(runId);
await this.maybePreserveSession(runId);
}
}
@@ -349,6 +356,7 @@ export class ScenarioSchedulerService implements OnModuleInit {
const creds = this.runCredentials.get(stepRun.runId);
const snips = this.runSnippets.get(stepRun.runId);
const env = this.runEnvironments.get(stepRun.runId);
const scenarioId = this.runScenarioIds.get(stepRun.runId);
const execCtx = new ExecContextBuilder()
.page(page)
.browser(context)
@@ -358,6 +366,9 @@ export class ScenarioSchedulerService implements OnModuleInit {
.credentials(creds)
.environment(env)
.snippets(snips)
.scenarioId(scenarioId)
.runId(stepRun.runId)
.fileService(this.fileStorageService)
.build();
const scenarioTimeoutSec = this.runScenarioTimeouts.get(stepRun.runId) ?? null;