@modelprofile.com/browser-runtime 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +34 -0
- package/changelog.md +11 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/actions.d.ts +3 -0
- package/dist_ts/actions.js +215 -0
- package/dist_ts/classes.artifactstore.d.ts +38 -0
- package/dist_ts/classes.artifactstore.js +344 -0
- package/dist_ts/classes.egressproxy.d.ts +67 -0
- package/dist_ts/classes.egressproxy.js +830 -0
- package/dist_ts/classes.flexprovider.d.ts +9 -0
- package/dist_ts/classes.flexprovider.js +117 -0
- package/dist_ts/classes.framed.d.ts +52 -0
- package/dist_ts/classes.framed.js +557 -0
- package/dist_ts/classes.runtime.d.ts +202 -0
- package/dist_ts/classes.runtime.js +1667 -0
- package/dist_ts/confinement.d.ts +2 -0
- package/dist_ts/confinement.js +63 -0
- package/dist_ts/errors.d.ts +7 -0
- package/dist_ts/errors.js +40 -0
- package/dist_ts/index.d.ts +11 -0
- package/dist_ts/index.js +9 -0
- package/dist_ts/interfaces.d.ts +287 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/internal.testing.d.ts +9 -0
- package/dist_ts/internal.testing.js +2 -0
- package/dist_ts/mcp.d.ts +4 -0
- package/dist_ts/mcp.js +196 -0
- package/dist_ts/plugins.d.ts +20 -0
- package/dist_ts/plugins.js +24 -0
- package/dist_ts/utils.d.ts +25 -0
- package/dist_ts/utils.js +143 -0
- package/license.md +21 -0
- package/package.json +59 -0
- package/readme.hints.md +35 -0
- package/readme.md +181 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/actions.ts +241 -0
- package/ts/classes.artifactstore.ts +432 -0
- package/ts/classes.egressproxy.ts +1005 -0
- package/ts/classes.flexprovider.ts +134 -0
- package/ts/classes.framed.ts +649 -0
- package/ts/classes.runtime.ts +2135 -0
- package/ts/confinement.ts +90 -0
- package/ts/errors.ts +63 -0
- package/ts/index.ts +52 -0
- package/ts/interfaces.ts +375 -0
- package/ts/internal.testing.ts +18 -0
- package/ts/mcp.ts +230 -0
- package/ts/plugins.ts +28 -0
- package/ts/utils.ts +188 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IBrowserArtifactMetadata,
|
|
4
|
+
IBrowserArtifactStoreOptions,
|
|
5
|
+
} from './interfaces.js';
|
|
6
|
+
import { BrowserRuntimeError } from './errors.js';
|
|
7
|
+
import {
|
|
8
|
+
TransitionMutex,
|
|
9
|
+
randomId,
|
|
10
|
+
validateBoundedString,
|
|
11
|
+
validateOptionalInteger,
|
|
12
|
+
} from './utils.js';
|
|
13
|
+
|
|
14
|
+
interface IStoredArtifact {
|
|
15
|
+
metadata: IBrowserArtifactMetadata;
|
|
16
|
+
digest: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface IProjectArtifacts {
|
|
20
|
+
directoryName: string;
|
|
21
|
+
totalBytes: number;
|
|
22
|
+
artifacts: Map<string, IStoredArtifact>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class BrowserArtifactStore {
|
|
26
|
+
private readonly rootDirectory: string;
|
|
27
|
+
private readonly maxFileBytes: number;
|
|
28
|
+
private readonly maxArtifactsPerProject: number;
|
|
29
|
+
private readonly maxProjectBytes: number;
|
|
30
|
+
private readonly maxProjects: number;
|
|
31
|
+
private readonly maxTotalArtifacts: number;
|
|
32
|
+
private readonly maxTotalBytes: number;
|
|
33
|
+
private readonly artifactTtlMs: number;
|
|
34
|
+
private readonly now: () => number;
|
|
35
|
+
private readonly directoryKey = plugins.crypto.randomBytes(32);
|
|
36
|
+
private readonly projects = new Map<string, IProjectArtifacts>();
|
|
37
|
+
private readonly mutex = new TransitionMutex();
|
|
38
|
+
private totalArtifacts = 0;
|
|
39
|
+
private totalBytes = 0;
|
|
40
|
+
private started = false;
|
|
41
|
+
private startPromise?: Promise<void>;
|
|
42
|
+
private stopPromise?: Promise<void>;
|
|
43
|
+
private sweepTimer?: ReturnType<typeof setInterval>;
|
|
44
|
+
private sweepRunning = false;
|
|
45
|
+
|
|
46
|
+
constructor(options: IBrowserArtifactStoreOptions) {
|
|
47
|
+
if (!options || typeof options !== 'object' || !plugins.path.isAbsolute(options.rootDirectory)) {
|
|
48
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'rootDirectory must be absolute');
|
|
49
|
+
}
|
|
50
|
+
this.rootDirectory = options.rootDirectory;
|
|
51
|
+
this.maxFileBytes = validateOptionalInteger(
|
|
52
|
+
options.maxFileBytes,
|
|
53
|
+
'maxFileBytes',
|
|
54
|
+
1024,
|
|
55
|
+
64 * 1024 * 1024,
|
|
56
|
+
8 * 1024 * 1024,
|
|
57
|
+
);
|
|
58
|
+
this.maxArtifactsPerProject = validateOptionalInteger(
|
|
59
|
+
options.maxArtifactsPerProject,
|
|
60
|
+
'maxArtifactsPerProject',
|
|
61
|
+
1,
|
|
62
|
+
4096,
|
|
63
|
+
256,
|
|
64
|
+
);
|
|
65
|
+
this.maxProjectBytes = validateOptionalInteger(
|
|
66
|
+
options.maxProjectBytes,
|
|
67
|
+
'maxProjectBytes',
|
|
68
|
+
this.maxFileBytes,
|
|
69
|
+
1024 * 1024 * 1024,
|
|
70
|
+
64 * 1024 * 1024,
|
|
71
|
+
);
|
|
72
|
+
this.maxProjects = validateOptionalInteger(
|
|
73
|
+
options.maxProjects,
|
|
74
|
+
'maxProjects',
|
|
75
|
+
1,
|
|
76
|
+
256,
|
|
77
|
+
32,
|
|
78
|
+
);
|
|
79
|
+
this.maxTotalArtifacts = validateOptionalInteger(
|
|
80
|
+
options.maxTotalArtifacts,
|
|
81
|
+
'maxTotalArtifacts',
|
|
82
|
+
this.maxArtifactsPerProject,
|
|
83
|
+
65_536,
|
|
84
|
+
Math.max(4096, this.maxArtifactsPerProject),
|
|
85
|
+
);
|
|
86
|
+
this.maxTotalBytes = validateOptionalInteger(
|
|
87
|
+
options.maxTotalBytes,
|
|
88
|
+
'maxTotalBytes',
|
|
89
|
+
this.maxProjectBytes,
|
|
90
|
+
8 * 1024 * 1024 * 1024,
|
|
91
|
+
Math.max(512 * 1024 * 1024, this.maxProjectBytes),
|
|
92
|
+
);
|
|
93
|
+
this.artifactTtlMs = validateOptionalInteger(
|
|
94
|
+
options.artifactTtlMs,
|
|
95
|
+
'artifactTtlMs',
|
|
96
|
+
1000,
|
|
97
|
+
24 * 60 * 60 * 1000,
|
|
98
|
+
60 * 60 * 1000,
|
|
99
|
+
);
|
|
100
|
+
this.now = options.now ?? Date.now;
|
|
101
|
+
if (typeof this.now !== 'function') throw new BrowserRuntimeError('INVALID_INPUT');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public start(): Promise<void> {
|
|
105
|
+
if (this.started) return Promise.resolve();
|
|
106
|
+
if (this.startPromise) return this.startPromise;
|
|
107
|
+
this.startPromise = this.startInternal().finally(() => {
|
|
108
|
+
this.startPromise = undefined;
|
|
109
|
+
});
|
|
110
|
+
return this.startPromise;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public stop(): Promise<void> {
|
|
114
|
+
if (this.stopPromise) return this.stopPromise;
|
|
115
|
+
if (!this.started && !this.startPromise) return Promise.resolve();
|
|
116
|
+
this.stopPromise = (async () => {
|
|
117
|
+
await this.startPromise?.catch(() => undefined);
|
|
118
|
+
if (this.sweepTimer) clearInterval(this.sweepTimer);
|
|
119
|
+
this.sweepTimer = undefined;
|
|
120
|
+
const release = await this.mutex.acquire();
|
|
121
|
+
try {
|
|
122
|
+
await plugins.fsPromises.rm(this.rootDirectory, { recursive: true, force: true });
|
|
123
|
+
this.projects.clear();
|
|
124
|
+
this.totalArtifacts = 0;
|
|
125
|
+
this.totalBytes = 0;
|
|
126
|
+
this.started = false;
|
|
127
|
+
} finally {
|
|
128
|
+
release();
|
|
129
|
+
}
|
|
130
|
+
})().finally(() => {
|
|
131
|
+
this.stopPromise = undefined;
|
|
132
|
+
});
|
|
133
|
+
return this.stopPromise;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async store(
|
|
137
|
+
projectIdArg: string,
|
|
138
|
+
mimeTypeArg: string,
|
|
139
|
+
data: Uint8Array,
|
|
140
|
+
): Promise<IBrowserArtifactMetadata> {
|
|
141
|
+
this.requireStarted();
|
|
142
|
+
const projectId = validateBoundedString(projectIdArg, 'projectId', 1, 128);
|
|
143
|
+
const mimeType = validateBoundedString(mimeTypeArg, 'mimeType', 1, 128);
|
|
144
|
+
if (!(data instanceof Uint8Array)) {
|
|
145
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'data must be Uint8Array');
|
|
146
|
+
}
|
|
147
|
+
if (data.byteLength > this.maxFileBytes) throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
148
|
+
const bytes = plugins.Buffer.from(data);
|
|
149
|
+
const release = await this.mutex.acquire();
|
|
150
|
+
try {
|
|
151
|
+
this.requireStarted();
|
|
152
|
+
await this.purgeExpiredInternal(projectId);
|
|
153
|
+
const existingProject = this.projects.get(projectId);
|
|
154
|
+
if (!existingProject && this.projects.size >= this.maxProjects) {
|
|
155
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
this.totalArtifacts >= this.maxTotalArtifacts
|
|
159
|
+
|| this.totalBytes + bytes.byteLength > this.maxTotalBytes
|
|
160
|
+
) throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
161
|
+
const project = existingProject ?? await this.createProject(projectId);
|
|
162
|
+
if (
|
|
163
|
+
project.artifacts.size >= this.maxArtifactsPerProject
|
|
164
|
+
|| project.totalBytes + bytes.byteLength > this.maxProjectBytes
|
|
165
|
+
) throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
166
|
+
|
|
167
|
+
const artifactId = randomId();
|
|
168
|
+
const createdAt = this.now();
|
|
169
|
+
const metadata: IBrowserArtifactMetadata = {
|
|
170
|
+
artifactId,
|
|
171
|
+
projectId,
|
|
172
|
+
mimeType,
|
|
173
|
+
size: bytes.byteLength,
|
|
174
|
+
createdAt,
|
|
175
|
+
expiresAt: createdAt + this.artifactTtlMs,
|
|
176
|
+
};
|
|
177
|
+
const stored: IStoredArtifact = {
|
|
178
|
+
metadata,
|
|
179
|
+
digest: plugins.crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
180
|
+
};
|
|
181
|
+
const projectDirectory = plugins.path.join(this.rootDirectory, project.directoryName);
|
|
182
|
+
const finalPath = plugins.path.join(projectDirectory, artifactId);
|
|
183
|
+
const temporaryPath = plugins.path.join(projectDirectory, `.${randomId(12)}.tmp`);
|
|
184
|
+
let handle: plugins.fsPromises.FileHandle | undefined;
|
|
185
|
+
let renamed = false;
|
|
186
|
+
try {
|
|
187
|
+
handle = await plugins.fsPromises.open(temporaryPath, 'wx', 0o600);
|
|
188
|
+
await handle.writeFile(bytes);
|
|
189
|
+
await handle.sync();
|
|
190
|
+
await handle.close();
|
|
191
|
+
handle = undefined;
|
|
192
|
+
await plugins.fsPromises.rename(temporaryPath, finalPath);
|
|
193
|
+
renamed = true;
|
|
194
|
+
await plugins.fsPromises.chmod(finalPath, 0o600);
|
|
195
|
+
project.artifacts.set(artifactId, stored);
|
|
196
|
+
project.totalBytes += bytes.byteLength;
|
|
197
|
+
this.totalArtifacts += 1;
|
|
198
|
+
this.totalBytes += bytes.byteLength;
|
|
199
|
+
return { ...metadata };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
await handle?.close().catch(() => undefined);
|
|
202
|
+
await plugins.fsPromises.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
203
|
+
if (renamed) await plugins.fsPromises.rm(finalPath, { force: true }).catch(() => undefined);
|
|
204
|
+
if (project.artifacts.size === 0) await this.removeEmptyProject(projectId, project);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
release();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public async read(projectIdArg: string, artifactIdArg: string): Promise<Uint8Array> {
|
|
213
|
+
this.requireStarted();
|
|
214
|
+
const projectId = validateBoundedString(projectIdArg, 'projectId', 1, 128);
|
|
215
|
+
const artifactId = validateBoundedString(artifactIdArg, 'artifactId', 1, 128);
|
|
216
|
+
const release = await this.mutex.acquire();
|
|
217
|
+
let handle: plugins.fsPromises.FileHandle | undefined;
|
|
218
|
+
try {
|
|
219
|
+
const { project, stored, filePath } = this.resolveArtifact(projectId, artifactId);
|
|
220
|
+
if (stored.metadata.expiresAt <= this.now()) {
|
|
221
|
+
await this.deleteArtifact(projectId, project, stored, filePath);
|
|
222
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'artifact is unavailable');
|
|
223
|
+
}
|
|
224
|
+
handle = await plugins.fsPromises.open(
|
|
225
|
+
filePath,
|
|
226
|
+
plugins.fs.constants.O_RDONLY | plugins.fs.constants.O_NOFOLLOW,
|
|
227
|
+
);
|
|
228
|
+
const stat = await handle.stat();
|
|
229
|
+
if (!stat.isFile() || stat.size !== stored.metadata.size || stat.size > this.maxFileBytes) {
|
|
230
|
+
await handle.close();
|
|
231
|
+
handle = undefined;
|
|
232
|
+
await this.deleteArtifact(projectId, project, stored, filePath);
|
|
233
|
+
throw new BrowserRuntimeError('FENCED', 'artifact integrity check failed');
|
|
234
|
+
}
|
|
235
|
+
const data = plugins.Buffer.allocUnsafe(stat.size);
|
|
236
|
+
const { bytesRead } = await handle.read(data, 0, stat.size, 0);
|
|
237
|
+
await handle.close();
|
|
238
|
+
handle = undefined;
|
|
239
|
+
const digest = plugins.crypto.createHash('sha256').update(data).digest('hex');
|
|
240
|
+
if (bytesRead !== stat.size || digest !== stored.digest) {
|
|
241
|
+
await this.deleteArtifact(projectId, project, stored, filePath);
|
|
242
|
+
throw new BrowserRuntimeError('FENCED', 'artifact integrity check failed');
|
|
243
|
+
}
|
|
244
|
+
return new Uint8Array(data);
|
|
245
|
+
} finally {
|
|
246
|
+
await handle?.close().catch(() => undefined);
|
|
247
|
+
release();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public async delete(projectIdArg: string, artifactIdArg: string): Promise<void> {
|
|
252
|
+
this.requireStarted();
|
|
253
|
+
const projectId = validateBoundedString(projectIdArg, 'projectId', 1, 128);
|
|
254
|
+
const artifactId = validateBoundedString(artifactIdArg, 'artifactId', 1, 128);
|
|
255
|
+
const release = await this.mutex.acquire();
|
|
256
|
+
try {
|
|
257
|
+
const { project, stored, filePath } = this.resolveArtifact(projectId, artifactId);
|
|
258
|
+
await this.deleteArtifact(projectId, project, stored, filePath);
|
|
259
|
+
} finally {
|
|
260
|
+
release();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public async purgeExpired(projectIdArg?: string): Promise<number> {
|
|
265
|
+
this.requireStarted();
|
|
266
|
+
const projectId = projectIdArg === undefined
|
|
267
|
+
? undefined
|
|
268
|
+
: validateBoundedString(projectIdArg, 'projectId', 1, 128);
|
|
269
|
+
const release = await this.mutex.acquire();
|
|
270
|
+
try {
|
|
271
|
+
return await this.purgeExpiredInternal(projectId);
|
|
272
|
+
} finally {
|
|
273
|
+
release();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public async purgeProject(projectIdArg: string): Promise<void> {
|
|
278
|
+
this.requireStarted();
|
|
279
|
+
const projectId = validateBoundedString(projectIdArg, 'projectId', 1, 128);
|
|
280
|
+
const release = await this.mutex.acquire();
|
|
281
|
+
try {
|
|
282
|
+
const project = this.projects.get(projectId);
|
|
283
|
+
if (!project) return;
|
|
284
|
+
await plugins.fsPromises.rm(
|
|
285
|
+
plugins.path.join(this.rootDirectory, project.directoryName),
|
|
286
|
+
{ recursive: true, force: true },
|
|
287
|
+
);
|
|
288
|
+
this.totalArtifacts -= project.artifacts.size;
|
|
289
|
+
this.totalBytes -= project.totalBytes;
|
|
290
|
+
this.projects.delete(projectId);
|
|
291
|
+
} finally {
|
|
292
|
+
release();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
public getMetadata(projectIdArg: string, artifactIdArg: string): IBrowserArtifactMetadata {
|
|
297
|
+
this.requireStarted();
|
|
298
|
+
const projectId = validateBoundedString(projectIdArg, 'projectId', 1, 128);
|
|
299
|
+
const artifactId = validateBoundedString(artifactIdArg, 'artifactId', 1, 128);
|
|
300
|
+
const project = this.projects.get(projectId);
|
|
301
|
+
const stored = project?.artifacts.get(artifactId);
|
|
302
|
+
if (!stored || stored.metadata.expiresAt <= this.now()) {
|
|
303
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'artifact is unavailable');
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const stat = plugins.fs.lstatSync(
|
|
307
|
+
plugins.path.join(this.rootDirectory, project!.directoryName, artifactId),
|
|
308
|
+
);
|
|
309
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size !== stored.metadata.size) {
|
|
310
|
+
throw new BrowserRuntimeError('FENCED', 'artifact integrity check failed');
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (error instanceof BrowserRuntimeError) throw error;
|
|
314
|
+
throw new BrowserRuntimeError('FENCED', 'artifact integrity check failed');
|
|
315
|
+
}
|
|
316
|
+
return { ...stored.metadata };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private async startInternal(): Promise<void> {
|
|
320
|
+
try {
|
|
321
|
+
await plugins.fsPromises.mkdir(this.rootDirectory, { recursive: false, mode: 0o700 });
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
324
|
+
const entries = await plugins.fsPromises.readdir(this.rootDirectory).catch(() => []);
|
|
325
|
+
throw new BrowserRuntimeError(
|
|
326
|
+
'FENCED',
|
|
327
|
+
entries.length > 0 ? 'artifact root is not empty' : 'artifact root must not exist',
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
const stat = await plugins.fsPromises.lstat(this.rootDirectory);
|
|
333
|
+
const uid = process.getuid?.();
|
|
334
|
+
if (
|
|
335
|
+
!stat.isDirectory()
|
|
336
|
+
|| stat.isSymbolicLink()
|
|
337
|
+
|| (uid !== undefined && stat.uid !== uid)
|
|
338
|
+
|| (stat.mode & 0o777) !== 0o700
|
|
339
|
+
) throw new BrowserRuntimeError('FENCED', 'artifact root is not private');
|
|
340
|
+
const entries = await plugins.fsPromises.readdir(this.rootDirectory);
|
|
341
|
+
if (entries.length > 0) throw new BrowserRuntimeError('FENCED', 'artifact root is not empty');
|
|
342
|
+
await plugins.fsPromises.chmod(this.rootDirectory, 0o700);
|
|
343
|
+
this.started = true;
|
|
344
|
+
const sweepIntervalMs = Math.min(60_000, Math.max(1000, Math.floor(this.artifactTtlMs / 2)));
|
|
345
|
+
this.sweepTimer = setInterval(() => {
|
|
346
|
+
if (this.sweepRunning) return;
|
|
347
|
+
this.sweepRunning = true;
|
|
348
|
+
void this.purgeExpired().catch(() => undefined).finally(() => {
|
|
349
|
+
this.sweepRunning = false;
|
|
350
|
+
});
|
|
351
|
+
}, sweepIntervalMs);
|
|
352
|
+
this.sweepTimer.unref();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private requireStarted(): void {
|
|
356
|
+
if (!this.started) throw new BrowserRuntimeError('NOT_RUNNING');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private async createProject(projectId: string): Promise<IProjectArtifacts> {
|
|
360
|
+
const directoryName = plugins.crypto
|
|
361
|
+
.createHmac('sha256', this.directoryKey)
|
|
362
|
+
.update(projectId, 'utf8')
|
|
363
|
+
.digest('hex');
|
|
364
|
+
const projectDirectory = plugins.path.join(this.rootDirectory, directoryName);
|
|
365
|
+
await plugins.fsPromises.mkdir(projectDirectory, { recursive: false, mode: 0o700 });
|
|
366
|
+
const project = { directoryName, totalBytes: 0, artifacts: new Map() };
|
|
367
|
+
this.projects.set(projectId, project);
|
|
368
|
+
return project;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async purgeExpiredInternal(projectId?: string): Promise<number> {
|
|
372
|
+
const projectIds = projectId === undefined ? [...this.projects.keys()] : [projectId];
|
|
373
|
+
let purged = 0;
|
|
374
|
+
for (const currentProjectId of projectIds) {
|
|
375
|
+
const project = this.projects.get(currentProjectId);
|
|
376
|
+
if (!project) continue;
|
|
377
|
+
for (const stored of [...project.artifacts.values()]) {
|
|
378
|
+
if (stored.metadata.expiresAt <= this.now()) {
|
|
379
|
+
const filePath = plugins.path.join(
|
|
380
|
+
this.rootDirectory,
|
|
381
|
+
project.directoryName,
|
|
382
|
+
stored.metadata.artifactId,
|
|
383
|
+
);
|
|
384
|
+
await this.deleteArtifact(currentProjectId, project, stored, filePath);
|
|
385
|
+
purged += 1;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return purged;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async deleteArtifact(
|
|
393
|
+
projectId: string,
|
|
394
|
+
project: IProjectArtifacts,
|
|
395
|
+
stored: IStoredArtifact,
|
|
396
|
+
filePath: string,
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
await plugins.fsPromises.rm(filePath, { force: true });
|
|
399
|
+
if (project.artifacts.delete(stored.metadata.artifactId)) {
|
|
400
|
+
project.totalBytes = Math.max(0, project.totalBytes - stored.metadata.size);
|
|
401
|
+
this.totalArtifacts = Math.max(0, this.totalArtifacts - 1);
|
|
402
|
+
this.totalBytes = Math.max(0, this.totalBytes - stored.metadata.size);
|
|
403
|
+
}
|
|
404
|
+
await this.removeEmptyProject(projectId, project);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private async removeEmptyProject(projectId: string, project: IProjectArtifacts): Promise<void> {
|
|
408
|
+
if (project.artifacts.size > 0 || this.projects.get(projectId) !== project) return;
|
|
409
|
+
await plugins.fsPromises.rm(
|
|
410
|
+
plugins.path.join(this.rootDirectory, project.directoryName),
|
|
411
|
+
{ recursive: true, force: true },
|
|
412
|
+
);
|
|
413
|
+
this.projects.delete(projectId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private resolveArtifact(projectId: string, artifactId: string): {
|
|
417
|
+
project: IProjectArtifacts;
|
|
418
|
+
stored: IStoredArtifact;
|
|
419
|
+
filePath: string;
|
|
420
|
+
} {
|
|
421
|
+
const project = this.projects.get(projectId);
|
|
422
|
+
const stored = project?.artifacts.get(artifactId);
|
|
423
|
+
if (!project || !stored) {
|
|
424
|
+
throw new BrowserRuntimeError('INVALID_INPUT', 'artifact is unavailable');
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
project,
|
|
428
|
+
stored,
|
|
429
|
+
filePath: plugins.path.join(this.rootDirectory, project.directoryName, artifactId),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|