@valbuild/server 0.72.3 → 0.73.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/README.md +1 -2
- package/dist/declarations/src/ValFSHost.d.ts +1 -0
- package/dist/declarations/src/createFixPatch.d.ts +10 -2
- package/dist/declarations/src/getSettings.d.ts +34 -0
- package/dist/declarations/src/index.d.ts +3 -0
- package/dist/declarations/src/patch/ts/syntax.d.ts +3 -0
- package/dist/declarations/src/personalAccessTokens.d.ts +10 -0
- package/dist/declarations/src/uploadRemoteFile.d.ts +25 -0
- package/dist/valbuild-server.cjs.dev.js +1016 -101
- package/dist/valbuild-server.cjs.prod.js +1016 -101
- package/dist/valbuild-server.esm.js +1012 -103
- package/package.json +4 -4
@@ -1,7 +1,7 @@
|
|
1
1
|
import { newQuickJSWASMModule } from 'quickjs-emscripten';
|
2
2
|
import ts from 'typescript';
|
3
3
|
import { result, pipe } from '@valbuild/core/fp';
|
4
|
-
import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, derefPatch, Internal, Schema, ImageSchema, RichTextSchema, FileSchema,
|
4
|
+
import { FILE_REF_PROP, FILE_REF_SUBTYPE_TAG, VAL_EXTENSION, derefPatch, Internal, Schema, ImageSchema, RichTextSchema, FileSchema, DEFAULT_VAL_REMOTE_HOST } from '@valbuild/core';
|
5
5
|
import { deepEqual, isNotRoot, PatchError, parseAndValidateArrayIndex, applyPatch, JSONOps, deepClone, sourceToPatchPath } from '@valbuild/core/patch';
|
6
6
|
import * as fsPath from 'path';
|
7
7
|
import fsPath__default from 'path';
|
@@ -14,6 +14,8 @@ import crypto$1 from 'crypto';
|
|
14
14
|
import { z } from 'zod';
|
15
15
|
import sizeOf from 'image-size';
|
16
16
|
import { fromError, fromZodError } from 'zod-validation-error';
|
17
|
+
import http from 'http';
|
18
|
+
import https from 'https';
|
17
19
|
|
18
20
|
class ValSyntaxError {
|
19
21
|
constructor(message, node) {
|
@@ -65,7 +67,7 @@ function validateObjectProperties(nodes) {
|
|
65
67
|
* validating its children.
|
66
68
|
*/
|
67
69
|
function shallowValidateExpression(value) {
|
68
|
-
return ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value) || isValFileMethodCall(value) || isValImageMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call c.file", value);
|
70
|
+
return ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) || value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword || value.kind === ts.SyntaxKind.NullKeyword || ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value) || isValFileMethodCall(value) || isValImageMethodCall(value) || isValRemoteMethodCall(value) ? undefined : new ValSyntaxError("Expression must be a literal or call c.file / c.image / c.remote", value);
|
69
71
|
}
|
70
72
|
|
71
73
|
/**
|
@@ -123,8 +125,23 @@ function evaluateExpression(value) {
|
|
123
125
|
});
|
124
126
|
}
|
125
127
|
}));
|
128
|
+
} else if (isValRemoteMethodCall(value)) {
|
129
|
+
return pipe(findValRemoteNodeArg(value), result.flatMap(ref => {
|
130
|
+
if (value.arguments.length === 2) {
|
131
|
+
return pipe(evaluateExpression(value.arguments[1]), result.map(metadata => ({
|
132
|
+
[FILE_REF_PROP]: ref.text,
|
133
|
+
_type: "remote",
|
134
|
+
metadata
|
135
|
+
})));
|
136
|
+
} else {
|
137
|
+
return result.ok({
|
138
|
+
[FILE_REF_PROP]: ref.text,
|
139
|
+
_type: "remote"
|
140
|
+
});
|
141
|
+
}
|
142
|
+
}));
|
126
143
|
} else {
|
127
|
-
return result.err(new ValSyntaxError("Expression must be a literal or call c.file", value));
|
144
|
+
return result.err(new ValSyntaxError("Expression must be a literal or call c.file / c.image / c.remote", value));
|
128
145
|
}
|
129
146
|
}
|
130
147
|
function findObjectPropertyAssignment(value, key) {
|
@@ -144,6 +161,9 @@ function isValFileMethodCall(node) {
|
|
144
161
|
function isValImageMethodCall(node) {
|
145
162
|
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "image";
|
146
163
|
}
|
164
|
+
function isValRemoteMethodCall(node) {
|
165
|
+
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "c" && node.expression.name.text === "remote";
|
166
|
+
}
|
147
167
|
function findValFileNodeArg(node) {
|
148
168
|
if (node.arguments.length === 0) {
|
149
169
|
return result.err(new ValSyntaxError(`Invalid c.file() call: missing ref argument`, node));
|
@@ -155,6 +175,17 @@ function findValFileNodeArg(node) {
|
|
155
175
|
const refNode = node.arguments[0];
|
156
176
|
return result.ok(refNode);
|
157
177
|
}
|
178
|
+
function findValRemoteNodeArg(node) {
|
179
|
+
if (node.arguments.length === 0) {
|
180
|
+
return result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
|
181
|
+
} else if (node.arguments.length > 2) {
|
182
|
+
return result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
|
183
|
+
} else if (!ts.isStringLiteral(node.arguments[0])) {
|
184
|
+
return result.err(new ValSyntaxError(`Invalid c.remote() call: ref must be a string literal`, node));
|
185
|
+
}
|
186
|
+
const refNode = node.arguments[0];
|
187
|
+
return result.ok(refNode);
|
188
|
+
}
|
158
189
|
function findValImageNodeArg(node) {
|
159
190
|
if (node.arguments.length === 0) {
|
160
191
|
return result.err(new ValSyntaxError(`Invalid c.image() call: missing ref argument`, node));
|
@@ -192,6 +223,19 @@ function findValImageMetadataArg(node) {
|
|
192
223
|
}
|
193
224
|
return result.ok(undefined);
|
194
225
|
}
|
226
|
+
function findValRemoteMetadataArg(node) {
|
227
|
+
if (node.arguments.length === 0) {
|
228
|
+
return result.err(new ValSyntaxError(`Invalid c.remote() call: missing ref argument`, node));
|
229
|
+
} else if (node.arguments.length > 2) {
|
230
|
+
return result.err(new ValSyntaxError(`Invalid c.remote() call: too many arguments ${node.arguments.length}}`, node));
|
231
|
+
} else if (node.arguments.length === 2) {
|
232
|
+
if (!ts.isObjectLiteralExpression(node.arguments[1])) {
|
233
|
+
return result.err(new ValSyntaxError(`Invalid c.remote() call: metadata must be a object literal`, node));
|
234
|
+
}
|
235
|
+
return result.ok(node.arguments[1]);
|
236
|
+
}
|
237
|
+
return result.ok(undefined);
|
238
|
+
}
|
195
239
|
|
196
240
|
/**
|
197
241
|
* Given a list of expressions, validates that all the expressions are not
|
@@ -293,6 +337,13 @@ function createValImageReference(value) {
|
|
293
337
|
}
|
294
338
|
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("image")), undefined, args);
|
295
339
|
}
|
340
|
+
function createValRemoteReference(value) {
|
341
|
+
const args = [ts.factory.createStringLiteral(value[FILE_REF_PROP])];
|
342
|
+
if (value.metadata) {
|
343
|
+
args.push(toExpression(value.metadata));
|
344
|
+
}
|
345
|
+
return ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("c"), ts.factory.createIdentifier("remote")), undefined, args);
|
346
|
+
}
|
296
347
|
function toExpression(value) {
|
297
348
|
if (typeof value === "string") {
|
298
349
|
// TODO: Use configuration/heuristics to determine use of single quote or double quote
|
@@ -325,6 +376,9 @@ function toExpression(value) {
|
|
325
376
|
if (isValImageValue(value)) {
|
326
377
|
return createValImageReference(value);
|
327
378
|
}
|
379
|
+
if (isValRemoteValue(value)) {
|
380
|
+
return createValRemoteReference(value);
|
381
|
+
}
|
328
382
|
return ts.factory.createObjectLiteralExpression(Object.entries(value).map(([key, value]) => createPropertyAssignment(key, value)));
|
329
383
|
} else {
|
330
384
|
return ts.factory.createStringLiteral(value);
|
@@ -496,6 +550,25 @@ function replaceInNode(document, node, key, value) {
|
|
496
550
|
ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
|
497
551
|
}));
|
498
552
|
}
|
553
|
+
} else if (isValRemoteMethodCall(node)) {
|
554
|
+
if (key === FILE_REF_PROP) {
|
555
|
+
if (typeof value !== "string") {
|
556
|
+
return result.err(new PatchError("Cannot replace c.image reference with non-string value"));
|
557
|
+
}
|
558
|
+
return pipe(findValRemoteNodeArg(node), result.map(refNode => replaceNodeValue(document, refNode, value)));
|
559
|
+
} else {
|
560
|
+
return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
|
561
|
+
if (!metadataArgNode) {
|
562
|
+
return result.err(new PatchError("Cannot replace in c.remote metadata when it does not exist"));
|
563
|
+
}
|
564
|
+
if (key !== "metadata") {
|
565
|
+
return result.err(new PatchError(`Cannot replace c.remote metadata key ${key} when it does not exist`));
|
566
|
+
}
|
567
|
+
return replaceInNode(document,
|
568
|
+
// TODO: creating a fake object here might not be right - seems to work though
|
569
|
+
ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment(key, metadataArgNode)]), key, value);
|
570
|
+
}));
|
571
|
+
}
|
499
572
|
} else {
|
500
573
|
return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot replace in non-object/array"));
|
501
574
|
}
|
@@ -522,6 +595,11 @@ function getFromNode(node, key) {
|
|
522
595
|
return findValImageNodeArg(node);
|
523
596
|
}
|
524
597
|
return findValImageMetadataArg(node);
|
598
|
+
} else if (isValRemoteMethodCall(node)) {
|
599
|
+
if (key === FILE_REF_PROP) {
|
600
|
+
return findValRemoteNodeArg(node);
|
601
|
+
}
|
602
|
+
return findValRemoteMetadataArg(node);
|
525
603
|
} else {
|
526
604
|
return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot access non-object/array"));
|
527
605
|
}
|
@@ -567,7 +645,7 @@ function removeFromNode(document, node, key) {
|
|
567
645
|
}
|
568
646
|
} else if (isValImageMethodCall(node)) {
|
569
647
|
if (key === FILE_REF_PROP) {
|
570
|
-
return result.err(new PatchError("Cannot remove a ref from c.
|
648
|
+
return result.err(new PatchError("Cannot remove a ref from c.image"));
|
571
649
|
} else {
|
572
650
|
return pipe(findValImageMetadataArg(node), result.flatMap(metadataArgNode => {
|
573
651
|
if (!metadataArgNode) {
|
@@ -576,6 +654,17 @@ function removeFromNode(document, node, key) {
|
|
576
654
|
return removeFromNode(document, metadataArgNode, key);
|
577
655
|
}));
|
578
656
|
}
|
657
|
+
} else if (isValRemoteMethodCall(node)) {
|
658
|
+
if (key === FILE_REF_PROP) {
|
659
|
+
return result.err(new PatchError("Cannot remove a ref from c.remote"));
|
660
|
+
} else {
|
661
|
+
return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
|
662
|
+
if (!metadataArgNode) {
|
663
|
+
return result.err(new PatchError("Cannot remove from c.remote metadata when it does not exist"));
|
664
|
+
}
|
665
|
+
return removeFromNode(document, metadataArgNode, key);
|
666
|
+
}));
|
667
|
+
}
|
579
668
|
} else {
|
580
669
|
return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot remove from non-object/array"));
|
581
670
|
}
|
@@ -584,18 +673,13 @@ function removeAtPath(document, rootNode, path) {
|
|
584
673
|
return pipe(getPointerFromPath(rootNode, path), result.flatMap(([node, key]) => removeFromNode(document, node, key)));
|
585
674
|
}
|
586
675
|
function isValFileValue(value) {
|
587
|
-
return !!(typeof value === "object" && value &&
|
588
|
-
// TODO: replace the below with this:
|
589
|
-
// VAL_EXTENSION in value &&
|
590
|
-
// value[VAL_EXTENSION] === "file" &&
|
591
|
-
FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] !== "image");
|
676
|
+
return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "file" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] !== "image");
|
592
677
|
}
|
593
678
|
function isValImageValue(value) {
|
594
|
-
return !!(typeof value === "object" && value &&
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] === "image");
|
679
|
+
return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "file" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string" && value[FILE_REF_SUBTYPE_TAG] === "image");
|
680
|
+
}
|
681
|
+
function isValRemoteValue(value) {
|
682
|
+
return !!(typeof value === "object" && value && VAL_EXTENSION in value && value[VAL_EXTENSION] === "remote" && FILE_REF_PROP in value && typeof value[FILE_REF_PROP] === "string");
|
599
683
|
}
|
600
684
|
function addToNode(document, node, key, value) {
|
601
685
|
if (ts.isArrayLiteralExpression(node)) {
|
@@ -645,6 +729,23 @@ function addToNode(document, node, key, value) {
|
|
645
729
|
return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
|
646
730
|
}));
|
647
731
|
}
|
732
|
+
} else if (isValRemoteMethodCall(node)) {
|
733
|
+
if (key === FILE_REF_PROP) {
|
734
|
+
if (typeof value !== "string") {
|
735
|
+
return result.err(new PatchError(`Cannot add ${FILE_REF_PROP} key to c.remote with non-string value`));
|
736
|
+
}
|
737
|
+
return pipe(findValRemoteNodeArg(node), result.map(arg => replaceNodeValue(document, arg, value)));
|
738
|
+
} else {
|
739
|
+
return pipe(findValRemoteMetadataArg(node), result.flatMap(metadataArgNode => {
|
740
|
+
if (metadataArgNode) {
|
741
|
+
return result.err(new PatchError("Cannot add metadata to c.remote when it already exists"));
|
742
|
+
}
|
743
|
+
if (key !== "metadata") {
|
744
|
+
return result.err(new PatchError(`Cannot add ${key} key to c.remote: only metadata is allowed`));
|
745
|
+
}
|
746
|
+
return result.ok([insertAt(document, node.arguments, node.arguments.length, toExpression(value))]);
|
747
|
+
}));
|
748
|
+
}
|
648
749
|
} else {
|
649
750
|
return result.err(shallowValidateExpression(node) ?? new PatchError("Cannot add to non-object/array"));
|
650
751
|
}
|
@@ -893,7 +994,7 @@ class ValSourceFileHandler {
|
|
893
994
|
fs.mkdirSync(fsPath__default.dirname(fileName), {
|
894
995
|
recursive: true
|
895
996
|
});
|
896
|
-
fs.writeFileSync(fileName, data, encoding);
|
997
|
+
fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
|
897
998
|
},
|
898
999
|
rmFile: fs.rmSync
|
899
1000
|
}) {
|
@@ -943,7 +1044,7 @@ class ValModuleLoader {
|
|
943
1044
|
fs.mkdirSync(fsPath__default.dirname(fileName), {
|
944
1045
|
recursive: true
|
945
1046
|
});
|
946
|
-
fs.writeFileSync(fileName, data, encoding);
|
1047
|
+
fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
|
947
1048
|
},
|
948
1049
|
rmFile: fs.rmSync
|
949
1050
|
}, disableCache = false) {
|
@@ -1235,7 +1336,7 @@ async function createService(projectRoot, opts, host = {
|
|
1235
1336
|
fs.mkdirSync(fsPath__default.dirname(fileName), {
|
1236
1337
|
recursive: true
|
1237
1338
|
});
|
1238
|
-
fs.writeFileSync(fileName, data, encoding);
|
1339
|
+
fs.writeFileSync(fileName, typeof data === "string" ? data : new Uint8Array(data), encoding);
|
1239
1340
|
},
|
1240
1341
|
rmFile: fs.rmSync
|
1241
1342
|
}, loader) {
|
@@ -1342,7 +1443,7 @@ function encodeJwt(payload, sessionKey) {
|
|
1342
1443
|
}
|
1343
1444
|
|
1344
1445
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
1345
|
-
const textEncoder$
|
1446
|
+
const textEncoder$3 = new TextEncoder();
|
1346
1447
|
const jsonOps = new JSONOps();
|
1347
1448
|
const tsOps = new TSOps(document => {
|
1348
1449
|
return pipe(analyzeValModule(document), result.map(({
|
@@ -1372,12 +1473,12 @@ class ValOps {
|
|
1372
1473
|
if (typeof input === "object") {
|
1373
1474
|
return this.hashObject(input);
|
1374
1475
|
}
|
1375
|
-
return Internal.getSHA256Hash(textEncoder$
|
1476
|
+
return Internal.getSHA256Hash(textEncoder$3.encode(input));
|
1376
1477
|
}
|
1377
1478
|
hashObject(obj) {
|
1378
1479
|
const collector = [];
|
1379
1480
|
this.collectObjectRecursive(obj, collector);
|
1380
|
-
return Internal.getSHA256Hash(textEncoder$
|
1481
|
+
return Internal.getSHA256Hash(textEncoder$3.encode(collector.join("")));
|
1381
1482
|
}
|
1382
1483
|
collectObjectRecursive(item, collector) {
|
1383
1484
|
if (typeof item === "string") {
|
@@ -1546,7 +1647,10 @@ class ValOps {
|
|
1546
1647
|
for (const op of patch.patch) {
|
1547
1648
|
if (op.op === "file") {
|
1548
1649
|
const filePath = op.filePath;
|
1549
|
-
fileLastUpdatedByPatchId[filePath] =
|
1650
|
+
fileLastUpdatedByPatchId[filePath] = {
|
1651
|
+
patchId: patch.patchId,
|
1652
|
+
remote: op.remote
|
1653
|
+
};
|
1550
1654
|
}
|
1551
1655
|
const path = patch.path;
|
1552
1656
|
if (!patchesByModule[path]) {
|
@@ -1682,6 +1786,7 @@ class ValOps {
|
|
1682
1786
|
};
|
1683
1787
|
const errors = {};
|
1684
1788
|
const files = {};
|
1789
|
+
const remoteFiles = {};
|
1685
1790
|
const entries = Object.entries(schemas);
|
1686
1791
|
const modulePathsToValidate = patchesByModule && Object.keys(patchesByModule);
|
1687
1792
|
for (const [pathS, schema] of entries) {
|
@@ -1723,7 +1828,7 @@ class ValOps {
|
|
1723
1828
|
};
|
1724
1829
|
if (validationErrors) {
|
1725
1830
|
for (const validationError of validationErrors) {
|
1726
|
-
var _validationError$fixe;
|
1831
|
+
var _validationError$fixe, _validationError$fixe2, _validationError$fixe3;
|
1727
1832
|
if (isOnlyFileCheckValidationError(validationError)) {
|
1728
1833
|
if (files[sourcePath]) {
|
1729
1834
|
throw new Error("Cannot have multiple files with same path. Path: " + sourcePath + "; Module: " + path);
|
@@ -1732,7 +1837,9 @@ class ValOps {
|
|
1732
1837
|
if (isFileSource(value)) {
|
1733
1838
|
files[sourcePath] = value;
|
1734
1839
|
}
|
1735
|
-
} else if ((_validationError$fixe = validationError.fixes) !== null && _validationError$fixe !== void 0 && _validationError$fixe.includes("
|
1840
|
+
} else if ((_validationError$fixe = validationError.fixes) !== null && _validationError$fixe !== void 0 && _validationError$fixe.includes("image:check-remote") || (_validationError$fixe2 = validationError.fixes) !== null && _validationError$fixe2 !== void 0 && _validationError$fixe2.includes("file:check-remote")) {
|
1841
|
+
remoteFiles[sourcePath] = validationError.value;
|
1842
|
+
} else if ((_validationError$fixe3 = validationError.fixes) !== null && _validationError$fixe3 !== void 0 && _validationError$fixe3.includes("keyof:check-keys")) {
|
1736
1843
|
const TYPE_ERROR_MESSAGE = `This is most likely a Val version mismatch or Val bug.`;
|
1737
1844
|
if (!validationError.value) {
|
1738
1845
|
addError({
|
@@ -1781,9 +1888,14 @@ class ValOps {
|
|
1781
1888
|
}
|
1782
1889
|
return {
|
1783
1890
|
errors,
|
1784
|
-
files
|
1891
|
+
files,
|
1892
|
+
remoteFiles
|
1785
1893
|
};
|
1786
1894
|
}
|
1895
|
+
async validateRemoteFiles(schemas, sources, remoteFiles) {
|
1896
|
+
// TODO: Implement
|
1897
|
+
return {};
|
1898
|
+
}
|
1787
1899
|
|
1788
1900
|
// #region validateFiles
|
1789
1901
|
async validateFiles(schemas, sources, files, fileLastUpdatedByPatchId) {
|
@@ -1831,13 +1943,13 @@ class ValOps {
|
|
1831
1943
|
}
|
1832
1944
|
const type = schemaAtPath instanceof ImageSchema ? "image" : "file";
|
1833
1945
|
const filePath = value[FILE_REF_PROP];
|
1834
|
-
const
|
1946
|
+
const fileData = (fileLastUpdatedByPatchId === null || fileLastUpdatedByPatchId === void 0 ? void 0 : fileLastUpdatedByPatchId[filePath]) || null;
|
1835
1947
|
let metadata;
|
1836
1948
|
let metadataErrors;
|
1837
1949
|
|
1838
1950
|
// TODO: refactor so we call get metadata once instead of iterating like this. Reason: should be a lot faster
|
1839
|
-
if (
|
1840
|
-
const patchFileMetadata = await this.getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId);
|
1951
|
+
if (fileData) {
|
1952
|
+
const patchFileMetadata = await this.getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, fileData.patchId, fileData.remote);
|
1841
1953
|
if (patchFileMetadata.errors) {
|
1842
1954
|
metadataErrors = patchFileMetadata.errors;
|
1843
1955
|
} else {
|
@@ -1857,7 +1969,7 @@ class ValOps {
|
|
1857
1969
|
message: e.message,
|
1858
1970
|
value: {
|
1859
1971
|
filePath,
|
1860
|
-
patchId
|
1972
|
+
patchId: (fileData === null || fileData === void 0 ? void 0 : fileData.patchId) ?? null
|
1861
1973
|
}
|
1862
1974
|
}))
|
1863
1975
|
};
|
@@ -2044,13 +2156,18 @@ class ValOps {
|
|
2044
2156
|
}
|
2045
2157
|
const patchedBinaryFilesDescriptors = {};
|
2046
2158
|
const binaryFilePatchErrors = {};
|
2047
|
-
await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath,
|
2159
|
+
await Promise.all(Object.entries(fileLastUpdatedByPatchId).map(async ([filePath, patchData]) => {
|
2160
|
+
const {
|
2161
|
+
patchId,
|
2162
|
+
remote
|
2163
|
+
} = patchData;
|
2048
2164
|
if (globalAppliedPatches.includes(patchId)) {
|
2049
2165
|
// TODO: do we want to make sure the file is there? Then again, it should be rare that it happens (unless there's a Val bug) so it might be enough to fail later (at commit)
|
2050
2166
|
// TODO: include sha256? This way we can make sure we pick the right file since theoretically there could be multiple files with the same path in the same patch
|
2051
2167
|
// or is that the case? We are picking the latest file by path so, that should be enough?
|
2052
2168
|
patchedBinaryFilesDescriptors[filePath] = {
|
2053
|
-
patchId
|
2169
|
+
patchId,
|
2170
|
+
remote
|
2054
2171
|
};
|
2055
2172
|
} else {
|
2056
2173
|
hasErrors = true;
|
@@ -2145,18 +2262,20 @@ class ValOps {
|
|
2145
2262
|
error: new PatchError("Value is not a string")
|
2146
2263
|
};
|
2147
2264
|
} else {
|
2148
|
-
const sha256 = Internal.getSHA256Hash(textEncoder$
|
2265
|
+
const sha256 = Internal.getSHA256Hash(textEncoder$3.encode(value));
|
2149
2266
|
files[filePath] = {
|
2150
2267
|
value,
|
2151
2268
|
sha256,
|
2152
|
-
path: op.path
|
2269
|
+
path: op.path,
|
2270
|
+
remote: op.remote
|
2153
2271
|
};
|
2154
2272
|
sourceFileOps.push({
|
2155
2273
|
op: "file",
|
2156
2274
|
path: op.path,
|
2157
2275
|
filePath,
|
2158
2276
|
nestedFilePath: op.nestedFilePath,
|
2159
|
-
value: sha256
|
2277
|
+
value: sha256,
|
2278
|
+
remote: op.remote
|
2160
2279
|
});
|
2161
2280
|
}
|
2162
2281
|
}
|
@@ -2239,7 +2358,7 @@ class ValOps {
|
|
2239
2358
|
const MaxRetries = 3;
|
2240
2359
|
let lastRes;
|
2241
2360
|
for (let i = 0; i < MaxRetries; i++) {
|
2242
|
-
lastRes = await this.saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data.value, type, metadataOps.metadata);
|
2361
|
+
lastRes = await this.saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data.value, type, metadataOps.metadata, data.remote);
|
2243
2362
|
if (!lastRes.error) {
|
2244
2363
|
return {
|
2245
2364
|
filePath
|
@@ -2271,8 +2390,8 @@ class ValOps {
|
|
2271
2390
|
// #region abstract ops
|
2272
2391
|
}
|
2273
2392
|
function isOnlyFileCheckValidationError(validationError) {
|
2274
|
-
var _validationError$
|
2275
|
-
if ((_validationError$
|
2393
|
+
var _validationError$fixe4;
|
2394
|
+
if ((_validationError$fixe4 = validationError.fixes) !== null && _validationError$fixe4 !== void 0 && _validationError$fixe4.every(f => f === "file:check-metadata" || f === "image:check-metadata")) {
|
2276
2395
|
return true;
|
2277
2396
|
}
|
2278
2397
|
return false;
|
@@ -2299,7 +2418,7 @@ function createMetadataFromBuffer(type, mimeType, buffer) {
|
|
2299
2418
|
width,
|
2300
2419
|
height,
|
2301
2420
|
type
|
2302
|
-
} = sizeOf(buffer);
|
2421
|
+
} = sizeOf(new Uint8Array(buffer));
|
2303
2422
|
const normalizedType = type === "jpg" ? "jpeg" : type === "svg" ? "svg+xml" : type;
|
2304
2423
|
if (type !== undefined && `image/${normalizedType}` !== mimeType) {
|
2305
2424
|
return {
|
@@ -2408,6 +2527,74 @@ function computeChangedPatchParentRefs(currentPatches, deletePatchIds) {
|
|
2408
2527
|
};
|
2409
2528
|
}
|
2410
2529
|
|
2530
|
+
function getFileExt(filePath) {
|
2531
|
+
// NOTE: We do not import the path module. This code is copied in different projects. We want the same implementation and which means that this might running in browser where path is not available).
|
2532
|
+
return filePath.split(".").pop() || "";
|
2533
|
+
}
|
2534
|
+
|
2535
|
+
const textEncoder$2 = new TextEncoder();
|
2536
|
+
async function uploadRemoteFile(remoteHost, fileBuffer, publicProjectId, bucket, filePath, schema, metadata, auth) {
|
2537
|
+
const fileHash = Internal.remote.getFileHash(fileBuffer);
|
2538
|
+
const coreVersion = Internal.VERSION.core || "unknown";
|
2539
|
+
const fileExt = getFileExt(filePath);
|
2540
|
+
const ref = Internal.remote.createRemoteRef(remoteHost, {
|
2541
|
+
publicProjectId,
|
2542
|
+
coreVersion,
|
2543
|
+
bucket,
|
2544
|
+
validationHash: Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata, fileHash, textEncoder$2),
|
2545
|
+
fileHash,
|
2546
|
+
filePath
|
2547
|
+
});
|
2548
|
+
return uploadRemoteRef(fileBuffer, ref, auth);
|
2549
|
+
}
|
2550
|
+
async function uploadRemoteRef(fileBuffer, ref, auth) {
|
2551
|
+
const authHeader = "apiKey" in auth ? {
|
2552
|
+
Authorization: `Bearer ${auth.apiKey}`
|
2553
|
+
} : {
|
2554
|
+
"x-val-pat": auth.pat
|
2555
|
+
};
|
2556
|
+
const res = await fetch(ref, {
|
2557
|
+
method: "PUT",
|
2558
|
+
headers: {
|
2559
|
+
...authHeader,
|
2560
|
+
"Content-Type": "application/octet-stream"
|
2561
|
+
},
|
2562
|
+
body: fileBuffer
|
2563
|
+
});
|
2564
|
+
if (!res.ok) {
|
2565
|
+
var _res$headers$get;
|
2566
|
+
if (res.status === 409) {
|
2567
|
+
// File already exists
|
2568
|
+
return {
|
2569
|
+
success: true,
|
2570
|
+
ref
|
2571
|
+
};
|
2572
|
+
}
|
2573
|
+
if ((_res$headers$get = res.headers.get("content-type")) !== null && _res$headers$get !== void 0 && _res$headers$get.includes("application/json")) {
|
2574
|
+
const json = await res.json();
|
2575
|
+
if (json.message) {
|
2576
|
+
return {
|
2577
|
+
success: false,
|
2578
|
+
error: `${ref}. Failed to upload file: ${json.message}.`
|
2579
|
+
};
|
2580
|
+
} else {
|
2581
|
+
return {
|
2582
|
+
success: false,
|
2583
|
+
error: `${ref}. Failed to upload file: ${JSON.stringify(json)}.`
|
2584
|
+
};
|
2585
|
+
}
|
2586
|
+
}
|
2587
|
+
return {
|
2588
|
+
success: false,
|
2589
|
+
error: `${ref}. Failed to upload file: ${await res.text()}.`
|
2590
|
+
};
|
2591
|
+
}
|
2592
|
+
return {
|
2593
|
+
success: true,
|
2594
|
+
ref
|
2595
|
+
};
|
2596
|
+
}
|
2597
|
+
|
2411
2598
|
class ValOpsFS extends ValOps {
|
2412
2599
|
static VAL_DIR = ".val";
|
2413
2600
|
constructor(rootDir, valModules, options) {
|
@@ -2849,10 +3036,10 @@ class ValOpsFS extends ValOps {
|
|
2849
3036
|
};
|
2850
3037
|
}
|
2851
3038
|
}
|
2852
|
-
async saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data, _type, metadata) {
|
3039
|
+
async saveBase64EncodedBinaryFileFromPatch(filePath, parentRef, patchId, data, _type, metadata, remote) {
|
2853
3040
|
const patchDir = this.getParentPatchIdFromParentRef(parentRef);
|
2854
|
-
const patchFilePath = this.getBinaryFilePath(filePath, patchDir);
|
2855
|
-
const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir);
|
3041
|
+
const patchFilePath = this.getBinaryFilePath(filePath, patchDir, remote);
|
3042
|
+
const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDir, remote);
|
2856
3043
|
try {
|
2857
3044
|
const buffer = bufferFromDataUrl(data);
|
2858
3045
|
if (!buffer) {
|
@@ -2883,7 +3070,7 @@ class ValOpsFS extends ValOps {
|
|
2883
3070
|
};
|
2884
3071
|
}
|
2885
3072
|
}
|
2886
|
-
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
3073
|
+
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId, remote) {
|
2887
3074
|
const patchDirRes = await this.getParentPatchIdFromPatchId(patchId);
|
2888
3075
|
if (result.isErr(patchDirRes)) {
|
2889
3076
|
return {
|
@@ -2892,7 +3079,7 @@ class ValOpsFS extends ValOps {
|
|
2892
3079
|
}]
|
2893
3080
|
};
|
2894
3081
|
}
|
2895
|
-
const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDirRes.value);
|
3082
|
+
const metadataFilePath = this.getBinaryFileMetadataPath(filePath, patchDirRes.value, remote);
|
2896
3083
|
if (!this.host.fileExists(metadataFilePath)) {
|
2897
3084
|
return {
|
2898
3085
|
errors: [{
|
@@ -2932,7 +3119,10 @@ class ValOpsFS extends ValOps {
|
|
2932
3119
|
if (!result.isOk(patchDirRes)) {
|
2933
3120
|
return null;
|
2934
3121
|
}
|
2935
|
-
const absPath = this.getBinaryFilePath(filePath, patchDirRes.value
|
3122
|
+
const absPath = this.getBinaryFilePath(filePath, patchDirRes.value,
|
3123
|
+
// We save remote remote files using the filepath (so not the remote reference) and we also retrieve them using the filepath. Therefore remote is always false
|
3124
|
+
false // remote = false
|
3125
|
+
);
|
2936
3126
|
if (!this.host.fileExists(absPath)) {
|
2937
3127
|
return null;
|
2938
3128
|
}
|
@@ -2996,43 +3186,90 @@ class ValOpsFS extends ValOps {
|
|
2996
3186
|
}
|
2997
3187
|
}
|
2998
3188
|
}
|
2999
|
-
async
|
3189
|
+
async saveOrUploadFiles(preparedCommit, auth, testMode) {
|
3000
3190
|
const updatedFiles = [];
|
3191
|
+
const uploadedRemoteRefs = [];
|
3001
3192
|
const errors = {};
|
3002
|
-
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3007
|
-
|
3008
|
-
|
3009
|
-
|
3010
|
-
|
3193
|
+
const remoteFileDescriptors = Object.entries(preparedCommit.patchedBinaryFilesDescriptors).filter(([, {
|
3194
|
+
remote
|
3195
|
+
}]) => remote).map(([ref, {
|
3196
|
+
patchId
|
3197
|
+
}]) => [ref, {
|
3198
|
+
patchId
|
3199
|
+
}]);
|
3200
|
+
const localFileDescriptors = Object.entries(preparedCommit.patchedBinaryFilesDescriptors).filter(([, {
|
3201
|
+
remote
|
3202
|
+
}]) => !remote).map(([ref, {
|
3203
|
+
patchId
|
3204
|
+
}]) => [ref, {
|
3205
|
+
patchId
|
3206
|
+
}]);
|
3207
|
+
for (const [ref, {
|
3208
|
+
patchId
|
3209
|
+
}] of remoteFileDescriptors) {
|
3210
|
+
const splitRemoteRefRes = Internal.remote.splitRemoteRef(ref);
|
3211
|
+
if (splitRemoteRefRes.status === "error") {
|
3212
|
+
errors[ref] = {
|
3213
|
+
message: "Failed to split remote ref: " + ref
|
3214
|
+
};
|
3215
|
+
continue;
|
3216
|
+
}
|
3217
|
+
const fileBuffer = await this.getBase64EncodedBinaryFileFromPatch(splitRemoteRefRes.filePath, patchId);
|
3218
|
+
if (!fileBuffer) {
|
3219
|
+
errors[ref] = {
|
3220
|
+
message: "Failed to get binary file from patch. Ref: " + ref + ". PatchId: " + patchId
|
3011
3221
|
};
|
3222
|
+
continue;
|
3223
|
+
}
|
3224
|
+
if (testMode === "test-skip-remote") {
|
3225
|
+
console.log("Skip remote flag enabled. Skipping file upload", ref);
|
3226
|
+
continue;
|
3012
3227
|
}
|
3228
|
+
console.log("Uploading remote file", ref);
|
3229
|
+
const res = await uploadRemoteRef(fileBuffer, ref, auth);
|
3230
|
+
if (!res.success) {
|
3231
|
+
console.error("Failed to upload remote file", ref, res.error);
|
3232
|
+
throw new Error(`Failed to upload remote file: ${ref}. ${res.error}`);
|
3233
|
+
}
|
3234
|
+
console.log("Completed remote file", ref);
|
3235
|
+
uploadedRemoteRefs.push(ref);
|
3013
3236
|
}
|
3014
3237
|
const patchIdToPatchDirMapRes = await this.getParentPatchIdFromPatchIdMap();
|
3015
3238
|
if (result.isErr(patchIdToPatchDirMapRes)) {
|
3016
3239
|
return {
|
3017
3240
|
updatedFiles,
|
3241
|
+
uploadedRemoteRefs,
|
3018
3242
|
errors
|
3019
3243
|
};
|
3020
3244
|
}
|
3021
3245
|
const patchIdToPatchDirMap = patchIdToPatchDirMapRes.value;
|
3022
|
-
for (const [
|
3246
|
+
for (const [ref, {
|
3023
3247
|
patchId
|
3024
|
-
}] of
|
3248
|
+
}] of localFileDescriptors) {
|
3249
|
+
const filePath = ref;
|
3025
3250
|
const absPath = fsPath__default.join(this.rootDir, ...filePath.split("/"));
|
3026
3251
|
try {
|
3027
3252
|
const patchDir = patchIdToPatchDirMap[patchId];
|
3028
3253
|
if (!patchDir) {
|
3029
3254
|
errors[absPath] = {
|
3030
3255
|
message: "Failed to find PatchDir for PatchId " + patchId,
|
3031
|
-
filePath
|
3256
|
+
filePath: filePath
|
3032
3257
|
};
|
3033
3258
|
continue;
|
3034
3259
|
}
|
3035
|
-
this.host.copyFile(this.getBinaryFilePath(filePath, patchDir), absPath);
|
3260
|
+
this.host.copyFile(this.getBinaryFilePath(filePath, patchDir, false), absPath);
|
3261
|
+
updatedFiles.push(absPath);
|
3262
|
+
} catch (err) {
|
3263
|
+
errors[absPath] = {
|
3264
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
3265
|
+
filePath: filePath
|
3266
|
+
};
|
3267
|
+
}
|
3268
|
+
}
|
3269
|
+
for (const [filePath, data] of Object.entries(preparedCommit.patchedSourceFiles)) {
|
3270
|
+
const absPath = fsPath__default.join(this.rootDir, ...filePath.split("/"));
|
3271
|
+
try {
|
3272
|
+
this.host.writeUf8File(absPath, data);
|
3036
3273
|
updatedFiles.push(absPath);
|
3037
3274
|
} catch (err) {
|
3038
3275
|
errors[absPath] = {
|
@@ -3065,6 +3302,7 @@ class ValOpsFS extends ValOps {
|
|
3065
3302
|
}
|
3066
3303
|
return {
|
3067
3304
|
updatedFiles,
|
3305
|
+
uploadedRemoteRefs,
|
3068
3306
|
errors
|
3069
3307
|
};
|
3070
3308
|
}
|
@@ -3133,10 +3371,26 @@ class ValOpsFS extends ValOps {
|
|
3133
3371
|
getFullPatchDir(patchDir) {
|
3134
3372
|
return fsPath__default.join(this.getPatchesDir(), patchDir);
|
3135
3373
|
}
|
3136
|
-
getBinaryFilePath(filePath, patchDir) {
|
3374
|
+
getBinaryFilePath(filePath, patchDir, remote) {
|
3375
|
+
if (remote) {
|
3376
|
+
const res = Internal.remote.splitRemoteRef(filePath);
|
3377
|
+
if (res.status === "error") {
|
3378
|
+
throw new Error("Failed to split remote ref: " + filePath);
|
3379
|
+
}
|
3380
|
+
const actualFilePath = res.filePath;
|
3381
|
+
return fsPath__default.join(this.getFullPatchDir(patchDir), "files", actualFilePath, fsPath__default.basename(actualFilePath));
|
3382
|
+
}
|
3137
3383
|
return fsPath__default.join(this.getFullPatchDir(patchDir), "files", filePath, fsPath__default.basename(filePath));
|
3138
3384
|
}
|
3139
|
-
getBinaryFileMetadataPath(filePath, patchDir) {
|
3385
|
+
getBinaryFileMetadataPath(filePath, patchDir, remote) {
|
3386
|
+
if (remote) {
|
3387
|
+
const res = Internal.remote.splitRemoteRef(filePath);
|
3388
|
+
if (res.status === "error") {
|
3389
|
+
throw new Error("Failed to split remote ref (in metadata path): " + filePath);
|
3390
|
+
}
|
3391
|
+
const actualFilePath = res.filePath;
|
3392
|
+
return fsPath__default.join(this.getFullPatchDir(patchDir), "files", actualFilePath, fsPath__default.basename(actualFilePath));
|
3393
|
+
}
|
3140
3394
|
return fsPath__default.join(this.getFullPatchDir(patchDir), "files", filePath, "metadata.json");
|
3141
3395
|
}
|
3142
3396
|
getPatchFilePath(patchDir) {
|
@@ -3216,7 +3470,7 @@ class FSOpsHost {
|
|
3216
3470
|
fs.mkdirSync(fsPath__default.dirname(path), {
|
3217
3471
|
recursive: true
|
3218
3472
|
});
|
3219
|
-
fs.writeFileSync(path, data, "base64url");
|
3473
|
+
fs.writeFileSync(path, new Uint8Array(data), "base64url");
|
3220
3474
|
}
|
3221
3475
|
copyFile(from, to) {
|
3222
3476
|
fs.mkdirSync(fsPath__default.dirname(to), {
|
@@ -3240,7 +3494,7 @@ const FSPatchBase = z.object({
|
|
3240
3494
|
timestamp: z.string().datetime()
|
3241
3495
|
});
|
3242
3496
|
|
3243
|
-
const textEncoder = new TextEncoder();
|
3497
|
+
const textEncoder$1 = new TextEncoder();
|
3244
3498
|
const PatchId = z.string().refine(s => !!s); // TODO: validate
|
3245
3499
|
const CommitSha = z.string().refine(s => !!s); // TODO: validate
|
3246
3500
|
z.string().refine(s => !!s); // TODO: validate
|
@@ -3287,7 +3541,8 @@ const FilesResponse = z.object({
|
|
3287
3541
|
filePath: z.string(),
|
3288
3542
|
location: z.literal("patch"),
|
3289
3543
|
patchId: PatchId,
|
3290
|
-
value: z.string()
|
3544
|
+
value: z.string(),
|
3545
|
+
remote: z.boolean()
|
3291
3546
|
}), z.object({
|
3292
3547
|
filePath: z.string(),
|
3293
3548
|
location: z.literal("repo"),
|
@@ -3298,7 +3553,8 @@ const FilesResponse = z.object({
|
|
3298
3553
|
filePath: z.string(),
|
3299
3554
|
location: z.literal("patch"),
|
3300
3555
|
patchId: PatchId,
|
3301
|
-
message: z.string()
|
3556
|
+
message: z.string(),
|
3557
|
+
remote: z.boolean()
|
3302
3558
|
}), z.object({
|
3303
3559
|
filePath: z.string(),
|
3304
3560
|
location: z.literal("repo"),
|
@@ -3734,7 +3990,23 @@ class ValOpsHttp extends ValOps {
|
|
3734
3990
|
});
|
3735
3991
|
});
|
3736
3992
|
}
|
3737
|
-
async saveBase64EncodedBinaryFileFromPatch(
|
3993
|
+
async saveBase64EncodedBinaryFileFromPatch(filePathOrRef, _parentRef,
|
3994
|
+
// this is required for the FS implementation, but not for the HTTP implementation (patchId is enough)
|
3995
|
+
patchId, data, type, metadata, remote) {
|
3996
|
+
let filePath;
|
3997
|
+
if (remote) {
|
3998
|
+
const splitRemoteRefDataRes = Internal.remote.splitRemoteRef(filePathOrRef);
|
3999
|
+
if (splitRemoteRefDataRes.status === "error") {
|
4000
|
+
return {
|
4001
|
+
error: {
|
4002
|
+
message: `Could not split remote ref: ${splitRemoteRefDataRes.error}`
|
4003
|
+
}
|
4004
|
+
};
|
4005
|
+
}
|
4006
|
+
filePath = "/" + splitRemoteRefDataRes.filePath;
|
4007
|
+
} else {
|
4008
|
+
filePath = filePathOrRef;
|
4009
|
+
}
|
3738
4010
|
return fetch(`${this.contentUrl}/v1/${this.project}/patches/${patchId}/files`, {
|
3739
4011
|
method: "POST",
|
3740
4012
|
headers: {
|
@@ -3742,10 +4014,11 @@ class ValOpsHttp extends ValOps {
|
|
3742
4014
|
"Content-Type": "application/json"
|
3743
4015
|
},
|
3744
4016
|
body: JSON.stringify({
|
3745
|
-
filePath
|
4017
|
+
filePath,
|
3746
4018
|
data,
|
3747
4019
|
type,
|
3748
|
-
metadata
|
4020
|
+
metadata,
|
4021
|
+
remote
|
3749
4022
|
})
|
3750
4023
|
}).then(async res => {
|
3751
4024
|
if (res.ok) {
|
@@ -3783,7 +4056,7 @@ class ValOpsHttp extends ValOps {
|
|
3783
4056
|
});
|
3784
4057
|
params.set("body_sha",
|
3785
4058
|
// We use this for cache invalidation
|
3786
|
-
Internal.getSHA256Hash(textEncoder.encode(stringifiedFiles)));
|
4059
|
+
Internal.getSHA256Hash(textEncoder$1.encode(stringifiedFiles)));
|
3787
4060
|
return fetch(`${this.contentUrl}/v1/${this.project}/files?${params}`, {
|
3788
4061
|
method: "PUT",
|
3789
4062
|
// Yes, PUT is weird. Weirder to have a body in a GET request.
|
@@ -3843,39 +4116,53 @@ class ValOpsHttp extends ValOps {
|
|
3843
4116
|
}
|
3844
4117
|
async getBinaryFile(filePath) {
|
3845
4118
|
// We could also just get this from public/ on the running server. Current approach feels more clean, but will be slower / puts more server load... We might want to change this
|
3846
|
-
const
|
4119
|
+
const requestFiles = [];
|
4120
|
+
requestFiles.push({
|
3847
4121
|
filePath: filePath,
|
3848
4122
|
location: "repo",
|
3849
4123
|
root: this.root,
|
3850
4124
|
commitSha: this.commitSha
|
3851
|
-
}
|
4125
|
+
});
|
4126
|
+
const filesRes = await this.getHttpFiles(requestFiles);
|
3852
4127
|
if (filesRes.error) {
|
3853
4128
|
return null;
|
3854
4129
|
}
|
3855
|
-
const file = filesRes.files
|
4130
|
+
const file = filesRes.files[0];
|
4131
|
+
if (filesRes.files.length > 1) {
|
4132
|
+
console.error("Expected 1 file, got more:", filesRes.files);
|
4133
|
+
}
|
3856
4134
|
if (!file) {
|
3857
4135
|
return null;
|
3858
4136
|
}
|
3859
4137
|
return Buffer.from(file.value, "base64");
|
3860
4138
|
}
|
3861
|
-
async getBase64EncodedBinaryFileFromPatch(filePath, patchId) {
|
4139
|
+
async getBase64EncodedBinaryFileFromPatch(filePath, patchId, remote) {
|
3862
4140
|
const filesRes = await this.getHttpFiles([{
|
3863
4141
|
filePath: filePath,
|
3864
4142
|
location: "patch",
|
3865
|
-
patchId
|
4143
|
+
patchId,
|
4144
|
+
remote
|
3866
4145
|
}]);
|
3867
4146
|
if (filesRes.error) {
|
4147
|
+
console.error("Error getting file:", filePath, filesRes.error);
|
3868
4148
|
return null;
|
3869
4149
|
}
|
3870
|
-
|
4150
|
+
if (filesRes.errors) {
|
4151
|
+
console.error("Failed while retrieving files", filePath, filesRes.errors);
|
4152
|
+
}
|
4153
|
+
const file = filesRes.files[0];
|
4154
|
+
if (filesRes.files.length > 1) {
|
4155
|
+
console.error("Expected 1 file, got more:", filesRes.files);
|
4156
|
+
}
|
3871
4157
|
if (!file) {
|
3872
4158
|
return null;
|
3873
4159
|
}
|
3874
4160
|
return bufferFromDataUrl(file.value) ?? null;
|
3875
4161
|
}
|
3876
|
-
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId) {
|
4162
|
+
async getBase64EncodedBinaryFileMetadataFromPatch(filePath, type, patchId, remote) {
|
3877
4163
|
const params = new URLSearchParams();
|
3878
4164
|
params.set("file_path", filePath);
|
4165
|
+
params.set("remote", remote.toString());
|
3879
4166
|
try {
|
3880
4167
|
const metadataRes = await fetch(`${this.contentUrl}/v1/${this.project}/patches/${patchId}/files?${params}`, {
|
3881
4168
|
headers: {
|
@@ -4104,8 +4391,112 @@ class ValOpsHttp extends ValOps {
|
|
4104
4391
|
}
|
4105
4392
|
}
|
4106
4393
|
|
4394
|
+
const host = process.env.VAL_CONTENT_URL || "https://content.val.build";
|
4395
|
+
const SettingsSchema = z.object({
|
4396
|
+
publicProjectId: z.string(),
|
4397
|
+
remoteFileBuckets: z.array(z.object({
|
4398
|
+
bucket: z.string()
|
4399
|
+
}))
|
4400
|
+
});
|
4401
|
+
async function getSettings(projectName, auth) {
|
4402
|
+
try {
|
4403
|
+
const response = await fetch(`${host}/v1/${projectName}/settings`, {
|
4404
|
+
headers: "pat" in auth ? {
|
4405
|
+
"x-val-pat": auth.pat,
|
4406
|
+
"Content-Type": "application/json"
|
4407
|
+
} : {
|
4408
|
+
Authorization: `Bearer ${auth.apiKey}`,
|
4409
|
+
"Content-Type": "application/json"
|
4410
|
+
}
|
4411
|
+
});
|
4412
|
+
if (response.status === 404) {
|
4413
|
+
return {
|
4414
|
+
success: false,
|
4415
|
+
message: `Project ${projectName} not found. Verify that user has access to project and that it exists.`
|
4416
|
+
};
|
4417
|
+
}
|
4418
|
+
if (response.status !== 200) {
|
4419
|
+
return {
|
4420
|
+
success: false,
|
4421
|
+
message: `Failed to get project id: ${response.statusText}`
|
4422
|
+
};
|
4423
|
+
}
|
4424
|
+
const json = await response.json();
|
4425
|
+
const parseRes = SettingsSchema.safeParse(json);
|
4426
|
+
if (!parseRes.success) {
|
4427
|
+
return {
|
4428
|
+
success: false,
|
4429
|
+
message: `Failed to parse settings data: ${parseRes.error.message}`
|
4430
|
+
};
|
4431
|
+
}
|
4432
|
+
return {
|
4433
|
+
success: true,
|
4434
|
+
data: parseRes.data
|
4435
|
+
};
|
4436
|
+
} catch (err) {
|
4437
|
+
return {
|
4438
|
+
success: false,
|
4439
|
+
message: `Failed to get project id. Check network connection and try again.`
|
4440
|
+
};
|
4441
|
+
}
|
4442
|
+
}
|
4443
|
+
|
4444
|
+
function getPersonalAccessTokenPath(root) {
|
4445
|
+
return fsPath__default.join(fsPath__default.resolve(root), ".val", "pat.json");
|
4446
|
+
}
|
4447
|
+
function parsePersonalAccessTokenFile(content) {
|
4448
|
+
if (!content) {
|
4449
|
+
return {
|
4450
|
+
success: false,
|
4451
|
+
error: "Invalid content: undefined"
|
4452
|
+
};
|
4453
|
+
}
|
4454
|
+
let patFileContent;
|
4455
|
+
try {
|
4456
|
+
patFileContent = JSON.parse(content);
|
4457
|
+
} catch {
|
4458
|
+
return {
|
4459
|
+
success: false,
|
4460
|
+
error: `Invalid content: file is not a valid JSON file`
|
4461
|
+
};
|
4462
|
+
}
|
4463
|
+
if (typeof patFileContent !== "object") {
|
4464
|
+
return {
|
4465
|
+
success: false,
|
4466
|
+
error: "Invalid content: not an object"
|
4467
|
+
};
|
4468
|
+
}
|
4469
|
+
if (!patFileContent) {
|
4470
|
+
return {
|
4471
|
+
success: false,
|
4472
|
+
error: "Invalid content: null"
|
4473
|
+
};
|
4474
|
+
}
|
4475
|
+
if (!("pat" in patFileContent)) {
|
4476
|
+
return {
|
4477
|
+
success: false,
|
4478
|
+
error: "Invalid content: key 'pat' is missing"
|
4479
|
+
};
|
4480
|
+
}
|
4481
|
+
const patField = patFileContent.pat;
|
4482
|
+
if (typeof patField === "string") {
|
4483
|
+
return {
|
4484
|
+
success: true,
|
4485
|
+
data: {
|
4486
|
+
pat: patField
|
4487
|
+
}
|
4488
|
+
};
|
4489
|
+
} else {
|
4490
|
+
return {
|
4491
|
+
success: false,
|
4492
|
+
error: "Invalid content: pat is not a string"
|
4493
|
+
};
|
4494
|
+
}
|
4495
|
+
}
|
4496
|
+
|
4107
4497
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
4108
4498
|
const ValServer = (valModules, options, callbacks) => {
|
4499
|
+
process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST;
|
4109
4500
|
let serverOps;
|
4110
4501
|
if (options.mode === "fs") {
|
4111
4502
|
serverOps = new ValOpsFS(options.cwd, valModules, {
|
@@ -4259,6 +4650,76 @@ const ValServer = (valModules, options, callbacks) => {
|
|
4259
4650
|
redirectTo: appAuthorizeUrl
|
4260
4651
|
};
|
4261
4652
|
};
|
4653
|
+
let remoteFileAuth = null;
|
4654
|
+
const getRemoteFileAuth = async () => {
|
4655
|
+
if (remoteFileAuth) {
|
4656
|
+
return {
|
4657
|
+
status: 200,
|
4658
|
+
json: {
|
4659
|
+
remoteFileAuth
|
4660
|
+
}
|
4661
|
+
};
|
4662
|
+
}
|
4663
|
+
if (options.apiKey) {
|
4664
|
+
remoteFileAuth = {
|
4665
|
+
apiKey: options.apiKey
|
4666
|
+
};
|
4667
|
+
} else if (serverOps instanceof ValOpsFS) {
|
4668
|
+
const projectRootDir = options.config.root;
|
4669
|
+
if (!projectRootDir) {
|
4670
|
+
return {
|
4671
|
+
status: 400,
|
4672
|
+
json: {
|
4673
|
+
errorCode: "project-not-configured",
|
4674
|
+
message: "Root directory was empty"
|
4675
|
+
}
|
4676
|
+
};
|
4677
|
+
}
|
4678
|
+
const fs = await import('fs');
|
4679
|
+
const patPath = getPersonalAccessTokenPath(fsPath__default.join(process.cwd()));
|
4680
|
+
let patFile;
|
4681
|
+
try {
|
4682
|
+
patFile = await fs.promises.readFile(patPath, "utf-8");
|
4683
|
+
} catch (err) {
|
4684
|
+
return {
|
4685
|
+
status: 400,
|
4686
|
+
json: {
|
4687
|
+
errorCode: "pat-error",
|
4688
|
+
message: "Could not read personal access token file"
|
4689
|
+
}
|
4690
|
+
};
|
4691
|
+
}
|
4692
|
+
const patRes = parsePersonalAccessTokenFile(patFile);
|
4693
|
+
if (patRes.success) {
|
4694
|
+
remoteFileAuth = {
|
4695
|
+
pat: patRes.data.pat
|
4696
|
+
};
|
4697
|
+
} else {
|
4698
|
+
return {
|
4699
|
+
status: 400,
|
4700
|
+
json: {
|
4701
|
+
errorCode: "pat-error",
|
4702
|
+
message: "Could not parse personal access token file"
|
4703
|
+
}
|
4704
|
+
};
|
4705
|
+
}
|
4706
|
+
}
|
4707
|
+
if (!remoteFileAuth) {
|
4708
|
+
return {
|
4709
|
+
status: 400,
|
4710
|
+
json: {
|
4711
|
+
errorCode: "project-not-configured",
|
4712
|
+
message: "Remote file auth is not configured"
|
4713
|
+
}
|
4714
|
+
};
|
4715
|
+
}
|
4716
|
+
return {
|
4717
|
+
status: 200,
|
4718
|
+
json: {
|
4719
|
+
remoteFileAuth
|
4720
|
+
}
|
4721
|
+
};
|
4722
|
+
};
|
4262
4723
|
return {
|
4263
4724
|
"/draft/enable": {
|
4264
4725
|
GET: async req => {
|
@@ -4572,6 +5033,63 @@ const ValServer = (valModules, options, callbacks) => {
|
|
4572
5033
|
};
|
4573
5034
|
}
|
4574
5035
|
},
|
5036
|
+
"/remote/settings": {
|
5037
|
+
GET: async req => {
|
5038
|
+
const cookies = req.cookies;
|
5039
|
+
const auth = getAuth(cookies);
|
5040
|
+
if (auth.error) {
|
5041
|
+
return {
|
5042
|
+
status: 401,
|
5043
|
+
json: {
|
5044
|
+
errorCode: "unauthorized",
|
5045
|
+
message: auth.error
|
5046
|
+
}
|
5047
|
+
};
|
5048
|
+
}
|
5049
|
+
if (serverOps instanceof ValOpsHttp && !("id" in auth)) {
|
5050
|
+
return {
|
5051
|
+
status: 401,
|
5052
|
+
json: {
|
5053
|
+
errorCode: "unauthorized",
|
5054
|
+
message: "Unauthorized"
|
5055
|
+
}
|
5056
|
+
};
|
5057
|
+
}
|
5058
|
+
if (!options.project) {
|
5059
|
+
return {
|
5060
|
+
status: 400,
|
5061
|
+
json: {
|
5062
|
+
errorCode: "project-not-configured",
|
5063
|
+
message: "Project is not configured. Update your val.config file with a valid remote project"
|
5064
|
+
}
|
5065
|
+
};
|
5066
|
+
}
|
5067
|
+
const remoteFileAuthRes = await getRemoteFileAuth();
|
5068
|
+
if (remoteFileAuthRes.status !== 200) {
|
5069
|
+
return remoteFileAuthRes;
|
5070
|
+
}
|
5071
|
+
const remoteFileAuth = remoteFileAuthRes.json.remoteFileAuth;
|
5072
|
+
const settingsRes = await getSettings(options.project, remoteFileAuth);
|
5073
|
+
if (!settingsRes.success) {
|
5074
|
+
console.warn("Could not get public project id: " + settingsRes.message);
|
5075
|
+
return {
|
5076
|
+
status: 400,
|
5077
|
+
json: {
|
5078
|
+
errorCode: "error-could-not-get-settings",
|
5079
|
+
message: `Could not get settings: ${settingsRes.message}`
|
5080
|
+
}
|
5081
|
+
};
|
5082
|
+
}
|
5083
|
+
return {
|
5084
|
+
status: 200,
|
5085
|
+
json: {
|
5086
|
+
publicProjectId: settingsRes.data.publicProjectId,
|
5087
|
+
remoteFileBuckets: settingsRes.data.remoteFileBuckets,
|
5088
|
+
coreVersion: Internal.VERSION.core || "unknown"
|
5089
|
+
}
|
5090
|
+
};
|
5091
|
+
}
|
5092
|
+
},
|
4575
5093
|
//#region stat
|
4576
5094
|
"/stat": {
|
4577
5095
|
POST: async req => {
|
@@ -4931,7 +5449,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
4931
5449
|
};
|
4932
5450
|
let sourcesValidation = {
|
4933
5451
|
errors: {},
|
4934
|
-
files: {}
|
5452
|
+
files: {},
|
5453
|
+
remoteFiles: {}
|
4935
5454
|
};
|
4936
5455
|
if (query.validate_sources || query.validate_binary_files) {
|
4937
5456
|
const schemas = await serverOps.getSchemas();
|
@@ -4940,6 +5459,8 @@ const ValServer = (valModules, options, callbacks) => {
|
|
4940
5459
|
// TODO: send binary files validation errors
|
4941
5460
|
if (query.validate_binary_files) {
|
4942
5461
|
await serverOps.validateFiles(schemas, sourcesRes.sources, sourcesValidation.files);
|
5462
|
+
// TODO : actually do something with this
|
5463
|
+
await serverOps.validateRemoteFiles(schemas, sourcesRes.sources, sourcesValidation.remoteFiles);
|
4943
5464
|
}
|
4944
5465
|
}
|
4945
5466
|
const schemaSha = await serverOps.getSchemaSha();
|
@@ -5111,7 +5632,12 @@ const ValServer = (valModules, options, callbacks) => {
|
|
5111
5632
|
};
|
5112
5633
|
}
|
5113
5634
|
if (serverOps instanceof ValOpsFS) {
|
5114
|
-
await
|
5635
|
+
const remoteFileAuthRes = await getRemoteFileAuth();
|
5636
|
+
if (remoteFileAuthRes.status !== 200) {
|
5637
|
+
return remoteFileAuthRes;
|
5638
|
+
}
|
5639
|
+
const remoteFileAuth = remoteFileAuthRes.json.remoteFileAuth;
|
5640
|
+
await serverOps.saveOrUploadFiles(preparedCommit, remoteFileAuth);
|
5115
5641
|
await serverOps.deletePatches(patchIds);
|
5116
5642
|
return {
|
5117
5643
|
status: 200,
|
@@ -5187,11 +5713,16 @@ const ValServer = (valModules, options, callbacks) => {
|
|
5187
5713
|
let cacheControl;
|
5188
5714
|
let fileBuffer;
|
5189
5715
|
let mimeType;
|
5716
|
+
const remote = query.remote === "true";
|
5190
5717
|
if (query.patch_id) {
|
5191
|
-
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id);
|
5718
|
+
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id, remote);
|
5192
5719
|
mimeType = Internal.filenameToMimeType(filePath);
|
5193
|
-
|
5720
|
+
// TODO: reenable this:
|
5721
|
+
// cacheControl = "public, max-age=20000, immutable";
|
5194
5722
|
} else {
|
5723
|
+
if (serverOps instanceof ValOpsHttp && remote) {
|
5724
|
+
console.error(`Remote file: ${filePath} requested without patch id. This is most likely a bug in Val.`);
|
5725
|
+
}
|
5195
5726
|
fileBuffer = await serverOps.getBinaryFile(filePath);
|
5196
5727
|
}
|
5197
5728
|
if (fileBuffer) {
|
@@ -5938,7 +6469,7 @@ class ValFSHost {
|
|
5938
6469
|
}
|
5939
6470
|
|
5940
6471
|
async function extractImageMetadata(filename, input) {
|
5941
|
-
const imageSize = sizeOf(input);
|
6472
|
+
const imageSize = sizeOf(new Uint8Array(input));
|
5942
6473
|
let mimeType = null;
|
5943
6474
|
if (imageSize.type) {
|
5944
6475
|
const possibleMimeType = `image/${imageSize.type}`;
|
@@ -5988,33 +6519,189 @@ function getValidationErrorFileRef(validationError) {
|
|
5988
6519
|
return maybeRef;
|
5989
6520
|
}
|
5990
6521
|
|
5991
|
-
|
5992
|
-
async function
|
5993
|
-
|
5994
|
-
|
5995
|
-
|
5996
|
-
|
5997
|
-
|
5998
|
-
|
5999
|
-
|
6000
|
-
|
6001
|
-
|
6002
|
-
|
6003
|
-
|
6004
|
-
|
6005
|
-
|
6006
|
-
|
6007
|
-
|
6008
|
-
|
6009
|
-
|
6010
|
-
|
6011
|
-
|
6522
|
+
const textEncoder = new TextEncoder();
|
6523
|
+
async function checkRemoteRef(remoteHost, ref, projectRoot, schema, metadata) {
|
6524
|
+
const remoteRefDataRes = Internal.remote.splitRemoteRef(ref);
|
6525
|
+
if (remoteRefDataRes.status === "error") {
|
6526
|
+
return remoteRefDataRes;
|
6527
|
+
}
|
6528
|
+
const fileHash = remoteRefDataRes.fileHash;
|
6529
|
+
if (!fileHash) {
|
6530
|
+
return {
|
6531
|
+
status: "error",
|
6532
|
+
error: `File hash is missing in remote ref: ${ref}`
|
6533
|
+
};
|
6534
|
+
}
|
6535
|
+
const relativeFilePath = remoteRefDataRes.filePath;
|
6536
|
+
if (!relativeFilePath) {
|
6537
|
+
return {
|
6538
|
+
status: "error",
|
6539
|
+
error: `File path is missing in remote ref: ${ref}`
|
6540
|
+
};
|
6541
|
+
}
|
6542
|
+
if (!relativeFilePath.startsWith("public/val/")) {
|
6543
|
+
return {
|
6544
|
+
status: "error",
|
6545
|
+
error: `File path must be within the public/val/ directory (e.g. public/val/path/to/file.txt). Got: ${relativeFilePath}`
|
6546
|
+
};
|
6547
|
+
}
|
6548
|
+
const coreVersion = Internal.VERSION.core || "unknown";
|
6549
|
+
const fileExt = getFileExt(relativeFilePath);
|
6550
|
+
const currentValidationHash = Internal.remote.getValidationHash(coreVersion, schema, fileExt, metadata,
|
6551
|
+
// TODO: validate
|
6552
|
+
fileHash, textEncoder);
|
6553
|
+
const validationHashFromRemoteRefValues = Internal.remote.getValidationHash(remoteRefDataRes.version, schema, getFileExt(remoteRefDataRes.filePath), metadata,
|
6554
|
+
// TODO: validate
|
6555
|
+
fileHash, textEncoder);
|
6556
|
+
if (
|
6557
|
+
// Current validation hash is the same as the remote validation hash
|
6558
|
+
// We assume the uploaded file exists and is correct
|
6559
|
+
currentValidationHash === remoteRefDataRes.validationHash &&
|
6560
|
+
// This can happen if the version or fileExt has been tampered with
|
6561
|
+
// (or if the way it is computed has changed)
|
6562
|
+
validationHashFromRemoteRefValues === remoteRefDataRes.validationHash) {
|
6563
|
+
return {
|
6564
|
+
status: "success"
|
6565
|
+
};
|
6566
|
+
}
|
6567
|
+
// Validation hash does not match, so we need to re-validate the remote content
|
6568
|
+
const fileBufferRes = await getFileBufferFromRemote(ref, fileExt, remoteRefDataRes.fileHash, projectRoot);
|
6569
|
+
|
6570
|
+
// We could not download the remote file for some reason (for example, the file hash is wrong)
|
6571
|
+
if (fileBufferRes.status === "error") {
|
6572
|
+
return fileBufferRes;
|
6573
|
+
}
|
6574
|
+
|
6575
|
+
// At this point we have the file buffer and we know the correct file hash
|
6576
|
+
// We must create a new ref since something has changed
|
6577
|
+
|
6578
|
+
let currentMetadata;
|
6579
|
+
try {
|
6580
|
+
if (schema.type === "image") {
|
6581
|
+
currentMetadata = await extractImageMetadata(remoteRefDataRes.filePath, fileBufferRes.fileBuffer);
|
6582
|
+
} else if (schema.type === "file") {
|
6583
|
+
currentMetadata = await extractFileMetadata(remoteRefDataRes.filePath, fileBufferRes.fileBuffer);
|
6584
|
+
} else {
|
6585
|
+
const exhaustiveCheck = schema;
|
6586
|
+
return {
|
6587
|
+
status: "error",
|
6588
|
+
error: `Unknown schema type: ${JSON.stringify(exhaustiveCheck)}`
|
6589
|
+
};
|
6590
|
+
}
|
6591
|
+
} catch (err) {
|
6592
|
+
return {
|
6593
|
+
status: "error",
|
6594
|
+
error: `Failed to extract metadata from remote file: ${err instanceof Error ? err.message : "Unknown error"}. Cached file: ${fileBufferRes.cachedFilePath}`
|
6595
|
+
};
|
6596
|
+
}
|
6597
|
+
const updatedMetadata = {
|
6598
|
+
...metadata,
|
6599
|
+
...currentMetadata
|
6600
|
+
};
|
6601
|
+
const nextValidationHash = Internal.remote.getValidationHash(coreVersion, schema, fileExt, updatedMetadata, fileBufferRes.fileHash, textEncoder);
|
6602
|
+
if (!updatedMetadata.mimeType) {
|
6603
|
+
return {
|
6604
|
+
status: "error",
|
6605
|
+
error: `MIME type is missing in metadata: ${JSON.stringify(updatedMetadata)}`
|
6606
|
+
};
|
6607
|
+
}
|
6608
|
+
const newFileExt = Internal.mimeTypeToFileExt(updatedMetadata.mimeType);
|
6609
|
+
const newFilePath = relativeFilePath.slice(0, -fileExt.length) + newFileExt;
|
6610
|
+
return {
|
6611
|
+
status: "fix-required",
|
6612
|
+
metadata: updatedMetadata,
|
6613
|
+
ref: Internal.remote.createRemoteRef(remoteHost, {
|
6614
|
+
publicProjectId: remoteRefDataRes.projectId,
|
6615
|
+
coreVersion,
|
6616
|
+
bucket: remoteRefDataRes.bucket,
|
6617
|
+
validationHash: nextValidationHash,
|
6618
|
+
fileHash: fileBufferRes.fileHash,
|
6619
|
+
filePath: newFilePath
|
6620
|
+
})
|
6621
|
+
};
|
6622
|
+
}
|
6623
|
+
function getCachedRemoteFileDir(projectRoot) {
|
6624
|
+
// store in projectRoot/.val/remote-file-cache
|
6625
|
+
const remoteFileCacheDir = fsPath__default.join(projectRoot, ".val", "remote-file-cache");
|
6626
|
+
return remoteFileCacheDir;
|
6627
|
+
}
|
6628
|
+
function getCachedRemoteFilePath(fileExt, currentFileHash, remoteFileCacheDir) {
|
6629
|
+
const remoteFilePath = fsPath__default.join(remoteFileCacheDir, currentFileHash + "." + fileExt);
|
6630
|
+
return remoteFilePath;
|
6631
|
+
}
|
6632
|
+
async function getFileBufferFromRemote(ref, fileExt, currentFileHash, projectRoot) {
|
6633
|
+
const remoteFileCacheDir = getCachedRemoteFileDir(projectRoot);
|
6634
|
+
const remoteFilePath = getCachedRemoteFilePath(fileExt, currentFileHash, remoteFileCacheDir);
|
6635
|
+
try {
|
6636
|
+
const fileBuffer = await fs.promises.readFile(remoteFilePath);
|
6637
|
+
const computedFileHash = Internal.remote.getFileHash(fileBuffer);
|
6638
|
+
if (computedFileHash !== currentFileHash) {
|
6639
|
+
await fs.promises.unlink(remoteFilePath);
|
6640
|
+
throw Error(`Cached file hash does not match the expected hash: ${computedFileHash} !== ${currentFileHash}`);
|
6641
|
+
}
|
6642
|
+
} catch (err) {
|
6643
|
+
try {
|
6644
|
+
await fs.promises.mkdir(remoteFileCacheDir, {
|
6645
|
+
recursive: true
|
6646
|
+
});
|
6647
|
+
await downloadFileFromRemote(ref, remoteFilePath);
|
6648
|
+
} catch (err) {
|
6649
|
+
try {
|
6650
|
+
// try to delete in case of partial download
|
6651
|
+
await fs.promises.unlink(remoteFilePath);
|
6652
|
+
} catch {
|
6653
|
+
//
|
6654
|
+
}
|
6655
|
+
return {
|
6656
|
+
status: "error",
|
6657
|
+
error: `Failed to download remote file: ${err instanceof Error ? err.message : "Unknown error"}`
|
6658
|
+
};
|
6659
|
+
}
|
6012
6660
|
}
|
6661
|
+
const fileBuffer = await fs.promises.readFile(remoteFilePath);
|
6662
|
+
const computedFileHash = Internal.remote.getFileHash(fileBuffer);
|
6663
|
+
return {
|
6664
|
+
status: "success",
|
6665
|
+
fileBuffer,
|
6666
|
+
fileHash: computedFileHash,
|
6667
|
+
cachedFilePath: remoteFilePath
|
6668
|
+
};
|
6669
|
+
}
|
6670
|
+
async function downloadFileFromRemote(ref, filePath) {
|
6671
|
+
return new Promise((resolve, reject) => {
|
6672
|
+
const url = new URL(ref);
|
6673
|
+
const client = url.protocol === "https:" ? https : http;
|
6674
|
+
const request = client.get(url, response => {
|
6675
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
6676
|
+
// Handle redirects
|
6677
|
+
return downloadFileFromRemote(response.headers.location, filePath).then(resolve).catch(reject);
|
6678
|
+
}
|
6679
|
+
if (response.statusCode == 404) {
|
6680
|
+
return reject(new Error(`${ref}. File not found. The URL is most likely incorrect.`));
|
6681
|
+
}
|
6682
|
+
if (response.statusCode !== 200) {
|
6683
|
+
return reject(new Error(`${ref}. Failed to download file. HTTP Status: ${response.statusCode}`));
|
6684
|
+
}
|
6685
|
+
const writeStream = fs.createWriteStream(filePath);
|
6686
|
+
response.pipe(writeStream);
|
6687
|
+
writeStream.on("finish", () => resolve({
|
6688
|
+
status: "success",
|
6689
|
+
filePath
|
6690
|
+
}));
|
6691
|
+
writeStream.on("error", err => reject(new Error(`${ref}. Error writing file: ${err.message}`)));
|
6692
|
+
});
|
6693
|
+
request.on("error", err => reject(new Error(`${ref}. Download error: ${err.message}`)));
|
6694
|
+
request.end();
|
6695
|
+
});
|
6696
|
+
}
|
6697
|
+
|
6698
|
+
// TODO: find a better name? transformFixesToPatch?
|
6699
|
+
async function createFixPatch(config, apply, sourcePath, validationError, remoteFiles, moduleSource, moduleSchema) {
|
6013
6700
|
const remainingErrors = [];
|
6014
6701
|
const patch = [];
|
6015
6702
|
for (const fix of validationError.fixes || []) {
|
6016
6703
|
if (fix === "image:check-metadata" || fix === "image:add-metadata") {
|
6017
|
-
const imageMetadata = await getImageMetadata();
|
6704
|
+
const imageMetadata = await getImageMetadata(config.projectRoot, validationError);
|
6018
6705
|
if (imageMetadata.width === undefined || imageMetadata.height === undefined) {
|
6019
6706
|
remainingErrors.push({
|
6020
6707
|
...validationError,
|
@@ -6087,7 +6774,7 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
6087
6774
|
});
|
6088
6775
|
}
|
6089
6776
|
} else if (fix === "file:add-metadata" || fix === "file:check-metadata") {
|
6090
|
-
const fileMetadata = await getFileMetadata();
|
6777
|
+
const fileMetadata = await getFileMetadata(config.projectRoot, validationError);
|
6091
6778
|
if (fileMetadata === undefined) {
|
6092
6779
|
remainingErrors.push({
|
6093
6780
|
...validationError,
|
@@ -6144,6 +6831,161 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
6144
6831
|
}
|
6145
6832
|
});
|
6146
6833
|
}
|
6834
|
+
} else if (fix === "image:upload-remote" || fix === "file:upload-remote") {
|
6835
|
+
const remoteFile = remoteFiles[sourcePath];
|
6836
|
+
if (!remoteFile) {
|
6837
|
+
remainingErrors.push({
|
6838
|
+
...validationError,
|
6839
|
+
message: "Cannot fix local to remote image: remote image was not uploaded",
|
6840
|
+
fixes: undefined
|
6841
|
+
});
|
6842
|
+
} else {
|
6843
|
+
patch.push({
|
6844
|
+
op: "replace",
|
6845
|
+
value: {
|
6846
|
+
_type: "remote",
|
6847
|
+
_ref: remoteFile.ref,
|
6848
|
+
metadata: remoteFile.metadata
|
6849
|
+
},
|
6850
|
+
path: sourceToPatchPath(sourcePath)
|
6851
|
+
});
|
6852
|
+
}
|
6853
|
+
} else if (fix === "image:download-remote" || fix === "file:download-remote") {
|
6854
|
+
const v = getRemoteValueFromValidationError(validationError);
|
6855
|
+
if (!v.success) {
|
6856
|
+
remainingErrors.push({
|
6857
|
+
...validationError,
|
6858
|
+
message: v.message,
|
6859
|
+
fixes: undefined
|
6860
|
+
});
|
6861
|
+
continue;
|
6862
|
+
}
|
6863
|
+
const splitRemoteRefDataRes = Internal.remote.splitRemoteRef(v._ref);
|
6864
|
+
if (splitRemoteRefDataRes.status === "error") {
|
6865
|
+
remainingErrors.push({
|
6866
|
+
...validationError,
|
6867
|
+
message: splitRemoteRefDataRes.error,
|
6868
|
+
fixes: undefined
|
6869
|
+
});
|
6870
|
+
continue;
|
6871
|
+
}
|
6872
|
+
const url = v._ref;
|
6873
|
+
const filePath = splitRemoteRefDataRes.filePath;
|
6874
|
+
if (!filePath) {
|
6875
|
+
remainingErrors.push({
|
6876
|
+
...validationError,
|
6877
|
+
message: "Unexpected error while downloading remote (no filePath)",
|
6878
|
+
fixes: undefined
|
6879
|
+
});
|
6880
|
+
continue;
|
6881
|
+
}
|
6882
|
+
if (!filePath.startsWith("public/val/")) {
|
6883
|
+
remainingErrors.push({
|
6884
|
+
...validationError,
|
6885
|
+
message: "Unexpected error while downloading remote (invalid file path - must start with public/val/)",
|
6886
|
+
fixes: undefined
|
6887
|
+
});
|
6888
|
+
continue;
|
6889
|
+
}
|
6890
|
+
const absoluteFilePath = fsPath__default.join(config.projectRoot, splitRemoteRefDataRes.filePath);
|
6891
|
+
await fs.promises.mkdir(fsPath__default.dirname(absoluteFilePath), {
|
6892
|
+
recursive: true
|
6893
|
+
});
|
6894
|
+
const res = await downloadFileFromRemote(url, absoluteFilePath);
|
6895
|
+
if (res.status === "error") {
|
6896
|
+
remainingErrors.push({
|
6897
|
+
...validationError,
|
6898
|
+
message: res.error,
|
6899
|
+
fixes: undefined
|
6900
|
+
});
|
6901
|
+
continue;
|
6902
|
+
}
|
6903
|
+
const value = {
|
6904
|
+
[VAL_EXTENSION]: "file",
|
6905
|
+
[FILE_REF_PROP]: `/${filePath}`,
|
6906
|
+
...(fix === "image:download-remote" ? {
|
6907
|
+
[FILE_REF_SUBTYPE_TAG]: "image"
|
6908
|
+
} : {})
|
6909
|
+
};
|
6910
|
+
patch.push({
|
6911
|
+
op: "replace",
|
6912
|
+
path: sourceToPatchPath(sourcePath),
|
6913
|
+
value: v.metadata ? {
|
6914
|
+
...value,
|
6915
|
+
metadata: v.metadata
|
6916
|
+
} : value
|
6917
|
+
});
|
6918
|
+
} else if (fix === "file:check-remote" || fix === "image:check-remote") {
|
6919
|
+
const v = getRemoteValueFromValidationError(validationError);
|
6920
|
+
if (!v.success) {
|
6921
|
+
remainingErrors.push({
|
6922
|
+
...validationError,
|
6923
|
+
message: v.message,
|
6924
|
+
fixes: undefined
|
6925
|
+
});
|
6926
|
+
continue;
|
6927
|
+
}
|
6928
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
|
6929
|
+
if (moduleSource === undefined) {
|
6930
|
+
remainingErrors.push({
|
6931
|
+
...validationError,
|
6932
|
+
message: "Unexpected error while checking remote (no moduleSource)",
|
6933
|
+
fixes: undefined
|
6934
|
+
});
|
6935
|
+
continue;
|
6936
|
+
}
|
6937
|
+
if (moduleSchema === undefined) {
|
6938
|
+
remainingErrors.push({
|
6939
|
+
...validationError,
|
6940
|
+
message: "Unexpected error while checking remote (no moduleSchema)",
|
6941
|
+
fixes: undefined
|
6942
|
+
});
|
6943
|
+
continue;
|
6944
|
+
}
|
6945
|
+
const {
|
6946
|
+
schema: schemaAtPath
|
6947
|
+
} = Internal.resolvePath(modulePath, moduleSource, moduleSchema);
|
6948
|
+
if (schemaAtPath.type === "image" || schemaAtPath.type === "file") {
|
6949
|
+
const res = await checkRemoteRef(config.remoteHost, v._ref, config.projectRoot, schemaAtPath, v.metadata);
|
6950
|
+
if (res.status === "success") ; else if (res.status === "error") {
|
6951
|
+
remainingErrors.push({
|
6952
|
+
...validationError,
|
6953
|
+
message: res.error,
|
6954
|
+
fixes: undefined
|
6955
|
+
});
|
6956
|
+
} else if (res.status === "fix-required") {
|
6957
|
+
if (apply) {
|
6958
|
+
patch.push({
|
6959
|
+
op: "replace",
|
6960
|
+
path: sourceToPatchPath(sourcePath),
|
6961
|
+
value: {
|
6962
|
+
_type: "remote",
|
6963
|
+
_ref: res.ref,
|
6964
|
+
metadata: res.metadata
|
6965
|
+
}
|
6966
|
+
});
|
6967
|
+
} else {
|
6968
|
+
remainingErrors.push({
|
6969
|
+
...validationError,
|
6970
|
+
message: `Remote ref: ${res.ref} is not valid. Use the --fix flag to fix this issue.`,
|
6971
|
+
fixes: undefined
|
6972
|
+
});
|
6973
|
+
}
|
6974
|
+
} else {
|
6975
|
+
const exhaustiveCheck = res;
|
6976
|
+
remainingErrors.push({
|
6977
|
+
...validationError,
|
6978
|
+
message: `Internal error found found unexpected status: ${JSON.stringify(exhaustiveCheck)}`,
|
6979
|
+
fixes: undefined
|
6980
|
+
});
|
6981
|
+
}
|
6982
|
+
} else {
|
6983
|
+
remainingErrors.push({
|
6984
|
+
...validationError,
|
6985
|
+
message: "Could not check remote ref: schema type is not image or file: " + (schemaAtPath === null || schemaAtPath === void 0 ? void 0 : schemaAtPath.type),
|
6986
|
+
fixes: undefined
|
6987
|
+
});
|
6988
|
+
}
|
6147
6989
|
}
|
6148
6990
|
}
|
6149
6991
|
if (!validationError.fixes || validationError.fixes.length === 0) {
|
@@ -6154,5 +6996,72 @@ async function createFixPatch(config, apply, sourcePath, validationError) {
|
|
6154
6996
|
remainingErrors
|
6155
6997
|
};
|
6156
6998
|
}
|
6999
|
+
function getRemoteValueFromValidationError(v) {
|
7000
|
+
if (v.value && typeof v.value !== "object") {
|
7001
|
+
return {
|
7002
|
+
success: false,
|
7003
|
+
message: "Unexpected error while checking remote (not an object)"
|
7004
|
+
};
|
7005
|
+
}
|
7006
|
+
if (!v.value) {
|
7007
|
+
return {
|
7008
|
+
success: false,
|
7009
|
+
message: "Unexpected error while checking remote (no value)"
|
7010
|
+
};
|
7011
|
+
}
|
7012
|
+
if (typeof v.value !== "object" || v.value === null || !(FILE_REF_PROP in v.value)) {
|
7013
|
+
return {
|
7014
|
+
success: false,
|
7015
|
+
message: "Unexpected error while checking remote (no _ref in value)"
|
7016
|
+
};
|
7017
|
+
}
|
7018
|
+
if (typeof v.value._ref !== "string") {
|
7019
|
+
return {
|
7020
|
+
success: false,
|
7021
|
+
message: "Unexpected error while checking remote (_ref is not a string)"
|
7022
|
+
};
|
7023
|
+
}
|
7024
|
+
let metadata;
|
7025
|
+
if ("metadata" in v.value && typeof v.value.metadata === "object") {
|
7026
|
+
if (v.value.metadata === null) {
|
7027
|
+
return {
|
7028
|
+
success: false,
|
7029
|
+
message: "Unexpected error while checking remote (metadata is null)"
|
7030
|
+
};
|
7031
|
+
}
|
7032
|
+
if (Array.isArray(v.value.metadata)) {
|
7033
|
+
return {
|
7034
|
+
success: false,
|
7035
|
+
message: "Unexpected error while checking remote (metadata is an array)"
|
7036
|
+
};
|
7037
|
+
}
|
7038
|
+
metadata = v.value.metadata;
|
7039
|
+
}
|
7040
|
+
return {
|
7041
|
+
success: true,
|
7042
|
+
_ref: v.value._ref,
|
7043
|
+
metadata
|
7044
|
+
};
|
7045
|
+
}
|
7046
|
+
async function getImageMetadata(projectRoot, validationError) {
|
7047
|
+
const fileRef = getValidationErrorFileRef(validationError);
|
7048
|
+
if (!fileRef) {
|
7049
|
+
// TODO:
|
7050
|
+
throw Error("Cannot fix image without a file reference");
|
7051
|
+
}
|
7052
|
+
const filename = fsPath__default.join(projectRoot, fileRef);
|
7053
|
+
const buffer = fs.readFileSync(filename);
|
7054
|
+
return extractImageMetadata(filename, buffer);
|
7055
|
+
}
|
7056
|
+
async function getFileMetadata(projectRoot, validationError) {
|
7057
|
+
const fileRef = getValidationErrorFileRef(validationError);
|
7058
|
+
if (!fileRef) {
|
7059
|
+
// TODO:
|
7060
|
+
throw Error("Cannot fix file without a file reference");
|
7061
|
+
}
|
7062
|
+
const filename = fsPath__default.join(projectRoot, fileRef);
|
7063
|
+
fs.readFileSync(filename);
|
7064
|
+
return extractFileMetadata(fileRef);
|
7065
|
+
}
|
6157
7066
|
|
6158
|
-
export { Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, patchSourceFile, safeReadGit };
|
7067
|
+
export { Service, ValFSHost, ValModuleLoader, ValSourceFileHandler, createFixPatch, createService, createValApiRouter, createValServer, decodeJwt, encodeJwt, formatSyntaxErrorTree, getCompilerOptions, getExpire, getPersonalAccessTokenPath, getSettings, parsePersonalAccessTokenFile, patchSourceFile, safeReadGit, uploadRemoteFile };
|