@valbuild/cli 0.98.0 → 0.99.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/cli/dist/valbuild-cli-cli.cjs.dev.js +149 -50
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +149 -50
- package/cli/dist/valbuild-cli-cli.esm.js +149 -50
- package/package.json +5 -5
- package/src/__fixtures__/basic/content/basic-gallery-remote-existing.val.ts +20 -0
- package/src/__fixtures__/basic/content/basic-gallery-remote.val.ts +19 -0
- package/src/__fixtures__/basic/public/val/images-remote/image.png +0 -0
- package/src/__fixtures__/basic/public/val/images-remote2/image.png +0 -0
- package/src/__fixtures__/basic/val.config.ts +3 -1
- package/src/__fixtures__/basic/val.modules.ts +2 -0
- package/src/runValidation.test.ts +101 -0
- package/src/runValidation.ts +200 -73
|
@@ -100,32 +100,13 @@ async function handleFileMetadata(ctx) {
|
|
|
100
100
|
shouldApplyPatch: true
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
async function handleRemoteFileUpload(ctx) {
|
|
104
|
-
var _resolvedRemoteFileAt;
|
|
105
|
-
if (!ctx.fix) {
|
|
106
|
-
return {
|
|
107
|
-
success: false,
|
|
108
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
112
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
113
|
-
return {
|
|
114
|
-
success: false,
|
|
115
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
119
103
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
const filePath = path__default["default"].join(ctx.projectRoot, fileRefProp);
|
|
104
|
+
// Shared upload core used by both the single-field (handleRemoteFileUpload)
|
|
105
|
+
// and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
|
|
106
|
+
// how they derive the local file ref, metadata and serialized image/file
|
|
107
|
+
// schema; everything from auth through upload is identical.
|
|
108
|
+
async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
|
|
109
|
+
const filePath = path__default["default"].join(ctx.projectRoot, fileRef);
|
|
129
110
|
if (!ctx.fs.fileExists(filePath)) {
|
|
130
111
|
return {
|
|
131
112
|
success: false,
|
|
@@ -165,21 +146,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
165
146
|
}]
|
|
166
147
|
};
|
|
167
148
|
}
|
|
168
|
-
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
175
|
-
const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
176
|
-
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
177
|
-
if (!resolveRemoteFileSchema) {
|
|
178
|
-
return {
|
|
179
|
-
success: false,
|
|
180
|
-
errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
149
|
const projectName = ctx.project;
|
|
184
150
|
let publicProjectId = ctx.publicProjectId;
|
|
185
151
|
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
@@ -215,12 +181,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
215
181
|
errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
|
|
216
182
|
};
|
|
217
183
|
}
|
|
218
|
-
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
219
|
-
return {
|
|
220
|
-
success: false,
|
|
221
|
-
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
184
|
remoteFilesCounter += 1;
|
|
225
185
|
const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
226
186
|
if (!bucket) {
|
|
@@ -246,8 +206,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
246
206
|
const fileHash = core.Internal.remote.getFileHash(fileBuffer);
|
|
247
207
|
const coreVersion = core.Internal.VERSION.core || "unknown";
|
|
248
208
|
const fileExt = getFileExt(filePath);
|
|
249
|
-
const schema = resolveRemoteFileSchema;
|
|
250
|
-
const metadata = fileSourceMetadata;
|
|
251
209
|
const ref = core.Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
|
|
252
210
|
publicProjectId,
|
|
253
211
|
coreVersion,
|
|
@@ -267,7 +225,7 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
267
225
|
}
|
|
268
226
|
ctx.remoteFiles[ctx.sourcePath] = {
|
|
269
227
|
ref,
|
|
270
|
-
metadata
|
|
228
|
+
metadata
|
|
271
229
|
};
|
|
272
230
|
return {
|
|
273
231
|
success: true,
|
|
@@ -284,6 +242,131 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
284
242
|
}]
|
|
285
243
|
};
|
|
286
244
|
}
|
|
245
|
+
async function handleRemoteFileUpload(ctx) {
|
|
246
|
+
var _resolvedRemoteFileAt;
|
|
247
|
+
if (!ctx.fix) {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
251
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
255
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
262
|
+
|
|
263
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
264
|
+
const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP];
|
|
265
|
+
if (!fileRefProp) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
272
|
+
if (!resolveRemoteFileSchema) {
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
285
|
+
const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
286
|
+
return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
|
|
290
|
+
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
291
|
+
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
292
|
+
// ref from the key and synthesize the image/file schema from the record's
|
|
293
|
+
// media options.
|
|
294
|
+
async function handleRemoteGalleryFileUpload(ctx) {
|
|
295
|
+
var _ctx$validationError$;
|
|
296
|
+
if (!ctx.fix) {
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
300
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
|
|
304
|
+
const mediaType = fix === "files:upload-remote" ? "file" : "image";
|
|
305
|
+
|
|
306
|
+
// The gallery entry is keyed by its local file path; validateMediaKey carries
|
|
307
|
+
// that key as the error value.
|
|
308
|
+
const fileRef = ctx.validationError.value;
|
|
309
|
+
if (typeof fileRef !== "string") {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
316
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
317
|
+
return {
|
|
318
|
+
success: false,
|
|
319
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const resolved = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
323
|
+
const entrySource = resolved.source;
|
|
324
|
+
const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
|
|
325
|
+
|
|
326
|
+
// The gallery item schema is an ObjectSchema, not an image/file schema, so
|
|
327
|
+
// synthesize the serialized image/file schema (matching how single fields
|
|
328
|
+
// serialize) for the remote ref's validation hash.
|
|
329
|
+
//
|
|
330
|
+
// `accept`/`directory` come from the RECORD that holds the entry, resolved
|
|
331
|
+
// from the entry's PARENT path - not from the module root, which is only the
|
|
332
|
+
// record when the gallery is the whole module. A nested gallery
|
|
333
|
+
// (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
|
|
334
|
+
// with no options and bake a validation hash into the remote ref that can
|
|
335
|
+
// never validate, so a mismatch fails fast instead of uploading.
|
|
336
|
+
const [, parentModulePath] = core.Internal.splitModuleFilePathAndModulePath(core.Internal.parentOfSourcePath(ctx.sourcePath));
|
|
337
|
+
const recordSchema = core.Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
|
|
338
|
+
if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
|
|
339
|
+
return {
|
|
340
|
+
success: false,
|
|
341
|
+
errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const {
|
|
345
|
+
accept,
|
|
346
|
+
directory
|
|
347
|
+
} = recordSchema;
|
|
348
|
+
const schema = mediaType === "image" ? {
|
|
349
|
+
type: "image",
|
|
350
|
+
opt: false,
|
|
351
|
+
options: {
|
|
352
|
+
...(accept ? {
|
|
353
|
+
accept
|
|
354
|
+
} : {}),
|
|
355
|
+
...(directory ? {
|
|
356
|
+
directory
|
|
357
|
+
} : {})
|
|
358
|
+
}
|
|
359
|
+
} : {
|
|
360
|
+
type: "file",
|
|
361
|
+
opt: false,
|
|
362
|
+
options: {
|
|
363
|
+
...(accept ? {
|
|
364
|
+
accept
|
|
365
|
+
} : {})
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
|
|
369
|
+
}
|
|
287
370
|
async function handleRemoteFileDownload(ctx) {
|
|
288
371
|
if (ctx.fix) {
|
|
289
372
|
return {
|
|
@@ -341,6 +424,17 @@ async function handleUniqueFolderCheck(ctx) {
|
|
|
341
424
|
success: true
|
|
342
425
|
};
|
|
343
426
|
}
|
|
427
|
+
|
|
428
|
+
// Maps a gallery key to its on-disk local path. Remote galleries key uploaded
|
|
429
|
+
// entries by a remote URL while keeping the file on disk; everything else is
|
|
430
|
+
// already a local path and is returned unchanged.
|
|
431
|
+
function remoteKeyToLocalPath(key) {
|
|
432
|
+
const remoteRefRes = core.Internal.remote.splitRemoteRef(key);
|
|
433
|
+
if (remoteRefRes.status === "success") {
|
|
434
|
+
return `/${remoteRefRes.filePath}`;
|
|
435
|
+
}
|
|
436
|
+
return key;
|
|
437
|
+
}
|
|
344
438
|
async function handleCheckAllFiles(ctx) {
|
|
345
439
|
const value = ctx.validationError.value;
|
|
346
440
|
if (!value || typeof value.directory !== "string") {
|
|
@@ -359,7 +453,10 @@ async function handleCheckAllFiles(ctx) {
|
|
|
359
453
|
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
360
454
|
};
|
|
361
455
|
}
|
|
362
|
-
|
|
456
|
+
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
457
|
+
// entries by a remote URL, but the file is kept on disk at its local path, so
|
|
458
|
+
// normalize remote-URL keys back to that local path for the on-disk checks.
|
|
459
|
+
const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
|
|
363
460
|
|
|
364
461
|
// Check that all tracked files exist on disk
|
|
365
462
|
const missingTrackedFiles = [...trackedFiles].filter(f => {
|
|
@@ -414,6 +511,8 @@ const currentFixHandlers = {
|
|
|
414
511
|
"file:add-metadata": handleFileMetadata,
|
|
415
512
|
"image:upload-remote": handleRemoteFileUpload,
|
|
416
513
|
"file:upload-remote": handleRemoteFileUpload,
|
|
514
|
+
"images:upload-remote": handleRemoteGalleryFileUpload,
|
|
515
|
+
"files:upload-remote": handleRemoteGalleryFileUpload,
|
|
417
516
|
"image:download-remote": handleRemoteFileDownload,
|
|
418
517
|
"file:download-remote": handleRemoteFileDownload,
|
|
419
518
|
"image:check-remote": handleRemoteFileCheck,
|
|
@@ -100,32 +100,13 @@ async function handleFileMetadata(ctx) {
|
|
|
100
100
|
shouldApplyPatch: true
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
async function handleRemoteFileUpload(ctx) {
|
|
104
|
-
var _resolvedRemoteFileAt;
|
|
105
|
-
if (!ctx.fix) {
|
|
106
|
-
return {
|
|
107
|
-
success: false,
|
|
108
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
112
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
113
|
-
return {
|
|
114
|
-
success: false,
|
|
115
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
119
103
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
const filePath = path__default["default"].join(ctx.projectRoot, fileRefProp);
|
|
104
|
+
// Shared upload core used by both the single-field (handleRemoteFileUpload)
|
|
105
|
+
// and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
|
|
106
|
+
// how they derive the local file ref, metadata and serialized image/file
|
|
107
|
+
// schema; everything from auth through upload is identical.
|
|
108
|
+
async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
|
|
109
|
+
const filePath = path__default["default"].join(ctx.projectRoot, fileRef);
|
|
129
110
|
if (!ctx.fs.fileExists(filePath)) {
|
|
130
111
|
return {
|
|
131
112
|
success: false,
|
|
@@ -165,21 +146,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
165
146
|
}]
|
|
166
147
|
};
|
|
167
148
|
}
|
|
168
|
-
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
175
|
-
const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
176
|
-
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
177
|
-
if (!resolveRemoteFileSchema) {
|
|
178
|
-
return {
|
|
179
|
-
success: false,
|
|
180
|
-
errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
149
|
const projectName = ctx.project;
|
|
184
150
|
let publicProjectId = ctx.publicProjectId;
|
|
185
151
|
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
@@ -215,12 +181,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
215
181
|
errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
|
|
216
182
|
};
|
|
217
183
|
}
|
|
218
|
-
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
219
|
-
return {
|
|
220
|
-
success: false,
|
|
221
|
-
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
184
|
remoteFilesCounter += 1;
|
|
225
185
|
const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
226
186
|
if (!bucket) {
|
|
@@ -246,8 +206,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
246
206
|
const fileHash = core.Internal.remote.getFileHash(fileBuffer);
|
|
247
207
|
const coreVersion = core.Internal.VERSION.core || "unknown";
|
|
248
208
|
const fileExt = getFileExt(filePath);
|
|
249
|
-
const schema = resolveRemoteFileSchema;
|
|
250
|
-
const metadata = fileSourceMetadata;
|
|
251
209
|
const ref = core.Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
|
|
252
210
|
publicProjectId,
|
|
253
211
|
coreVersion,
|
|
@@ -267,7 +225,7 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
267
225
|
}
|
|
268
226
|
ctx.remoteFiles[ctx.sourcePath] = {
|
|
269
227
|
ref,
|
|
270
|
-
metadata
|
|
228
|
+
metadata
|
|
271
229
|
};
|
|
272
230
|
return {
|
|
273
231
|
success: true,
|
|
@@ -284,6 +242,131 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
284
242
|
}]
|
|
285
243
|
};
|
|
286
244
|
}
|
|
245
|
+
async function handleRemoteFileUpload(ctx) {
|
|
246
|
+
var _resolvedRemoteFileAt;
|
|
247
|
+
if (!ctx.fix) {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
251
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
255
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const resolvedRemoteFileAtSourcePath = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
262
|
+
|
|
263
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
264
|
+
const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[core.FILE_REF_PROP];
|
|
265
|
+
if (!fileRefProp) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
272
|
+
if (!resolveRemoteFileSchema) {
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
285
|
+
const fileSourceMetadata = core.Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
286
|
+
return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
|
|
290
|
+
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
291
|
+
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
292
|
+
// ref from the key and synthesize the image/file schema from the record's
|
|
293
|
+
// media options.
|
|
294
|
+
async function handleRemoteGalleryFileUpload(ctx) {
|
|
295
|
+
var _ctx$validationError$;
|
|
296
|
+
if (!ctx.fix) {
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
300
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
|
|
304
|
+
const mediaType = fix === "files:upload-remote" ? "file" : "image";
|
|
305
|
+
|
|
306
|
+
// The gallery entry is keyed by its local file path; validateMediaKey carries
|
|
307
|
+
// that key as the error value.
|
|
308
|
+
const fileRef = ctx.validationError.value;
|
|
309
|
+
if (typeof fileRef !== "string") {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
316
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
317
|
+
return {
|
|
318
|
+
success: false,
|
|
319
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const resolved = core.Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
323
|
+
const entrySource = resolved.source;
|
|
324
|
+
const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
|
|
325
|
+
|
|
326
|
+
// The gallery item schema is an ObjectSchema, not an image/file schema, so
|
|
327
|
+
// synthesize the serialized image/file schema (matching how single fields
|
|
328
|
+
// serialize) for the remote ref's validation hash.
|
|
329
|
+
//
|
|
330
|
+
// `accept`/`directory` come from the RECORD that holds the entry, resolved
|
|
331
|
+
// from the entry's PARENT path - not from the module root, which is only the
|
|
332
|
+
// record when the gallery is the whole module. A nested gallery
|
|
333
|
+
// (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
|
|
334
|
+
// with no options and bake a validation hash into the remote ref that can
|
|
335
|
+
// never validate, so a mismatch fails fast instead of uploading.
|
|
336
|
+
const [, parentModulePath] = core.Internal.splitModuleFilePathAndModulePath(core.Internal.parentOfSourcePath(ctx.sourcePath));
|
|
337
|
+
const recordSchema = core.Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
|
|
338
|
+
if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
|
|
339
|
+
return {
|
|
340
|
+
success: false,
|
|
341
|
+
errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const {
|
|
345
|
+
accept,
|
|
346
|
+
directory
|
|
347
|
+
} = recordSchema;
|
|
348
|
+
const schema = mediaType === "image" ? {
|
|
349
|
+
type: "image",
|
|
350
|
+
opt: false,
|
|
351
|
+
options: {
|
|
352
|
+
...(accept ? {
|
|
353
|
+
accept
|
|
354
|
+
} : {}),
|
|
355
|
+
...(directory ? {
|
|
356
|
+
directory
|
|
357
|
+
} : {})
|
|
358
|
+
}
|
|
359
|
+
} : {
|
|
360
|
+
type: "file",
|
|
361
|
+
opt: false,
|
|
362
|
+
options: {
|
|
363
|
+
...(accept ? {
|
|
364
|
+
accept
|
|
365
|
+
} : {})
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
|
|
369
|
+
}
|
|
287
370
|
async function handleRemoteFileDownload(ctx) {
|
|
288
371
|
if (ctx.fix) {
|
|
289
372
|
return {
|
|
@@ -341,6 +424,17 @@ async function handleUniqueFolderCheck(ctx) {
|
|
|
341
424
|
success: true
|
|
342
425
|
};
|
|
343
426
|
}
|
|
427
|
+
|
|
428
|
+
// Maps a gallery key to its on-disk local path. Remote galleries key uploaded
|
|
429
|
+
// entries by a remote URL while keeping the file on disk; everything else is
|
|
430
|
+
// already a local path and is returned unchanged.
|
|
431
|
+
function remoteKeyToLocalPath(key) {
|
|
432
|
+
const remoteRefRes = core.Internal.remote.splitRemoteRef(key);
|
|
433
|
+
if (remoteRefRes.status === "success") {
|
|
434
|
+
return `/${remoteRefRes.filePath}`;
|
|
435
|
+
}
|
|
436
|
+
return key;
|
|
437
|
+
}
|
|
344
438
|
async function handleCheckAllFiles(ctx) {
|
|
345
439
|
const value = ctx.validationError.value;
|
|
346
440
|
if (!value || typeof value.directory !== "string") {
|
|
@@ -359,7 +453,10 @@ async function handleCheckAllFiles(ctx) {
|
|
|
359
453
|
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
360
454
|
};
|
|
361
455
|
}
|
|
362
|
-
|
|
456
|
+
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
457
|
+
// entries by a remote URL, but the file is kept on disk at its local path, so
|
|
458
|
+
// normalize remote-URL keys back to that local path for the on-disk checks.
|
|
459
|
+
const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
|
|
363
460
|
|
|
364
461
|
// Check that all tracked files exist on disk
|
|
365
462
|
const missingTrackedFiles = [...trackedFiles].filter(f => {
|
|
@@ -414,6 +511,8 @@ const currentFixHandlers = {
|
|
|
414
511
|
"file:add-metadata": handleFileMetadata,
|
|
415
512
|
"image:upload-remote": handleRemoteFileUpload,
|
|
416
513
|
"file:upload-remote": handleRemoteFileUpload,
|
|
514
|
+
"images:upload-remote": handleRemoteGalleryFileUpload,
|
|
515
|
+
"files:upload-remote": handleRemoteGalleryFileUpload,
|
|
417
516
|
"image:download-remote": handleRemoteFileDownload,
|
|
418
517
|
"file:download-remote": handleRemoteFileDownload,
|
|
419
518
|
"image:check-remote": handleRemoteFileCheck,
|
|
@@ -68,32 +68,13 @@ async function handleFileMetadata(ctx) {
|
|
|
68
68
|
shouldApplyPatch: true
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
async function handleRemoteFileUpload(ctx) {
|
|
72
|
-
var _resolvedRemoteFileAt;
|
|
73
|
-
if (!ctx.fix) {
|
|
74
|
-
return {
|
|
75
|
-
success: false,
|
|
76
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
80
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
81
|
-
return {
|
|
82
|
-
success: false,
|
|
83
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
87
71
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const filePath = path.join(ctx.projectRoot, fileRefProp);
|
|
72
|
+
// Shared upload core used by both the single-field (handleRemoteFileUpload)
|
|
73
|
+
// and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
|
|
74
|
+
// how they derive the local file ref, metadata and serialized image/file
|
|
75
|
+
// schema; everything from auth through upload is identical.
|
|
76
|
+
async function uploadRemoteFileCore(ctx, fileRef, metadata, schema) {
|
|
77
|
+
const filePath = path.join(ctx.projectRoot, fileRef);
|
|
97
78
|
if (!ctx.fs.fileExists(filePath)) {
|
|
98
79
|
return {
|
|
99
80
|
success: false,
|
|
@@ -133,21 +114,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
133
114
|
}]
|
|
134
115
|
};
|
|
135
116
|
}
|
|
136
|
-
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
137
|
-
return {
|
|
138
|
-
success: false,
|
|
139
|
-
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
143
|
-
const fileSourceMetadata = Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
144
|
-
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
145
|
-
if (!resolveRemoteFileSchema) {
|
|
146
|
-
return {
|
|
147
|
-
success: false,
|
|
148
|
-
errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
117
|
const projectName = ctx.project;
|
|
152
118
|
let publicProjectId = ctx.publicProjectId;
|
|
153
119
|
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
@@ -183,12 +149,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
183
149
|
errorMessage: `Could not get project. Check that your val.config has the 'project' field set, or set it using the VAL_PROJECT environment variable`
|
|
184
150
|
};
|
|
185
151
|
}
|
|
186
|
-
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
187
|
-
return {
|
|
188
|
-
success: false,
|
|
189
|
-
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
152
|
remoteFilesCounter += 1;
|
|
193
153
|
const bucket = remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
194
154
|
if (!bucket) {
|
|
@@ -214,8 +174,6 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
214
174
|
const fileHash = Internal.remote.getFileHash(fileBuffer);
|
|
215
175
|
const coreVersion = Internal.VERSION.core || "unknown";
|
|
216
176
|
const fileExt = getFileExt(filePath);
|
|
217
|
-
const schema = resolveRemoteFileSchema;
|
|
218
|
-
const metadata = fileSourceMetadata;
|
|
219
177
|
const ref = Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
|
|
220
178
|
publicProjectId,
|
|
221
179
|
coreVersion,
|
|
@@ -235,7 +193,7 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
235
193
|
}
|
|
236
194
|
ctx.remoteFiles[ctx.sourcePath] = {
|
|
237
195
|
ref,
|
|
238
|
-
metadata
|
|
196
|
+
metadata
|
|
239
197
|
};
|
|
240
198
|
return {
|
|
241
199
|
success: true,
|
|
@@ -252,6 +210,131 @@ async function handleRemoteFileUpload(ctx) {
|
|
|
252
210
|
}]
|
|
253
211
|
};
|
|
254
212
|
}
|
|
213
|
+
async function handleRemoteFileUpload(ctx) {
|
|
214
|
+
var _resolvedRemoteFileAt;
|
|
215
|
+
if (!ctx.fix) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
219
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
223
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
230
|
+
|
|
231
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
232
|
+
const fileRefProp = (_resolvedRemoteFileAt = resolvedRemoteFileAtSourcePath.source) === null || _resolvedRemoteFileAt === void 0 ? void 0 : _resolvedRemoteFileAt[FILE_REF_PROP];
|
|
233
|
+
if (!fileRefProp) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
240
|
+
if (!resolveRemoteFileSchema) {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (resolveRemoteFileSchema.type !== "image" && resolveRemoteFileSchema.type !== "file") {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
253
|
+
const fileSourceMetadata = Internal.isFile(actualRemoteFileSource) ? actualRemoteFileSource.metadata : undefined;
|
|
254
|
+
return uploadRemoteFileCore(ctx, fileRefProp, fileSourceMetadata, resolveRemoteFileSchema);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
|
|
258
|
+
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
259
|
+
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
260
|
+
// ref from the key and synthesize the image/file schema from the record's
|
|
261
|
+
// media options.
|
|
262
|
+
async function handleRemoteGalleryFileUpload(ctx) {
|
|
263
|
+
var _ctx$validationError$;
|
|
264
|
+
if (!ctx.fix) {
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
268
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const fix = (_ctx$validationError$ = ctx.validationError.fixes) === null || _ctx$validationError$ === void 0 ? void 0 : _ctx$validationError$[0];
|
|
272
|
+
const mediaType = fix === "files:upload-remote" ? "file" : "image";
|
|
273
|
+
|
|
274
|
+
// The gallery entry is keyed by its local file path; validateMediaKey carries
|
|
275
|
+
// that key as the error value.
|
|
276
|
+
const fileRef = ctx.validationError.value;
|
|
277
|
+
if (typeof fileRef !== "string") {
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(ctx.sourcePath);
|
|
284
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const resolved = Internal.resolvePath(modulePath, ctx.valModule.source, ctx.valModule.schema);
|
|
291
|
+
const entrySource = resolved.source;
|
|
292
|
+
const metadata = entrySource && typeof entrySource === "object" && !Array.isArray(entrySource) ? entrySource : undefined;
|
|
293
|
+
|
|
294
|
+
// The gallery item schema is an ObjectSchema, not an image/file schema, so
|
|
295
|
+
// synthesize the serialized image/file schema (matching how single fields
|
|
296
|
+
// serialize) for the remote ref's validation hash.
|
|
297
|
+
//
|
|
298
|
+
// `accept`/`directory` come from the RECORD that holds the entry, resolved
|
|
299
|
+
// from the entry's PARENT path - not from the module root, which is only the
|
|
300
|
+
// record when the gallery is the whole module. A nested gallery
|
|
301
|
+
// (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
|
|
302
|
+
// with no options and bake a validation hash into the remote ref that can
|
|
303
|
+
// never validate, so a mismatch fails fast instead of uploading.
|
|
304
|
+
const [, parentModulePath] = Internal.splitModuleFilePathAndModulePath(Internal.parentOfSourcePath(ctx.sourcePath));
|
|
305
|
+
const recordSchema = Internal.resolvePath(parentModulePath, ctx.valModule.source, ctx.valModule.schema).schema;
|
|
306
|
+
if ((recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) !== "record") {
|
|
307
|
+
return {
|
|
308
|
+
success: false,
|
|
309
|
+
errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${(recordSchema === null || recordSchema === void 0 ? void 0 : recordSchema.type) ?? "nothing"}`
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const {
|
|
313
|
+
accept,
|
|
314
|
+
directory
|
|
315
|
+
} = recordSchema;
|
|
316
|
+
const schema = mediaType === "image" ? {
|
|
317
|
+
type: "image",
|
|
318
|
+
opt: false,
|
|
319
|
+
options: {
|
|
320
|
+
...(accept ? {
|
|
321
|
+
accept
|
|
322
|
+
} : {}),
|
|
323
|
+
...(directory ? {
|
|
324
|
+
directory
|
|
325
|
+
} : {})
|
|
326
|
+
}
|
|
327
|
+
} : {
|
|
328
|
+
type: "file",
|
|
329
|
+
opt: false,
|
|
330
|
+
options: {
|
|
331
|
+
...(accept ? {
|
|
332
|
+
accept
|
|
333
|
+
} : {})
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
|
|
337
|
+
}
|
|
255
338
|
async function handleRemoteFileDownload(ctx) {
|
|
256
339
|
if (ctx.fix) {
|
|
257
340
|
return {
|
|
@@ -309,6 +392,17 @@ async function handleUniqueFolderCheck(ctx) {
|
|
|
309
392
|
success: true
|
|
310
393
|
};
|
|
311
394
|
}
|
|
395
|
+
|
|
396
|
+
// Maps a gallery key to its on-disk local path. Remote galleries key uploaded
|
|
397
|
+
// entries by a remote URL while keeping the file on disk; everything else is
|
|
398
|
+
// already a local path and is returned unchanged.
|
|
399
|
+
function remoteKeyToLocalPath(key) {
|
|
400
|
+
const remoteRefRes = Internal.remote.splitRemoteRef(key);
|
|
401
|
+
if (remoteRefRes.status === "success") {
|
|
402
|
+
return `/${remoteRefRes.filePath}`;
|
|
403
|
+
}
|
|
404
|
+
return key;
|
|
405
|
+
}
|
|
312
406
|
async function handleCheckAllFiles(ctx) {
|
|
313
407
|
const value = ctx.validationError.value;
|
|
314
408
|
if (!value || typeof value.directory !== "string") {
|
|
@@ -327,7 +421,10 @@ async function handleCheckAllFiles(ctx) {
|
|
|
327
421
|
errorMessage: `Could not get source for ${ctx.sourcePath}`
|
|
328
422
|
};
|
|
329
423
|
}
|
|
330
|
-
|
|
424
|
+
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
425
|
+
// entries by a remote URL, but the file is kept on disk at its local path, so
|
|
426
|
+
// normalize remote-URL keys back to that local path for the on-disk checks.
|
|
427
|
+
const trackedFiles = new Set(Object.keys(source).map(remoteKeyToLocalPath));
|
|
331
428
|
|
|
332
429
|
// Check that all tracked files exist on disk
|
|
333
430
|
const missingTrackedFiles = [...trackedFiles].filter(f => {
|
|
@@ -382,6 +479,8 @@ const currentFixHandlers = {
|
|
|
382
479
|
"file:add-metadata": handleFileMetadata,
|
|
383
480
|
"image:upload-remote": handleRemoteFileUpload,
|
|
384
481
|
"file:upload-remote": handleRemoteFileUpload,
|
|
482
|
+
"images:upload-remote": handleRemoteGalleryFileUpload,
|
|
483
|
+
"files:upload-remote": handleRemoteGalleryFileUpload,
|
|
385
484
|
"image:download-remote": handleRemoteFileDownload,
|
|
386
485
|
"file:download-remote": handleRemoteFileDownload,
|
|
387
486
|
"image:check-remote": handleRemoteFileCheck,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/cli",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.99.0",
|
|
5
5
|
"description": "Val CLI tools",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"open": "^9.1.0",
|
|
28
28
|
"picocolors": "^1.1.1",
|
|
29
29
|
"zod": "^4.3.5",
|
|
30
|
-
"@valbuild/core": "0.
|
|
30
|
+
"@valbuild/core": "0.99.0",
|
|
31
31
|
"@valbuild/eslint-plugin": "0.93.0",
|
|
32
|
-
"@valbuild/language-server": "0.
|
|
33
|
-
"@valbuild/server": "0.
|
|
34
|
-
"@valbuild/shared": "0.
|
|
32
|
+
"@valbuild/language-server": "0.99.0",
|
|
33
|
+
"@valbuild/server": "0.99.0",
|
|
34
|
+
"@valbuild/shared": "0.99.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"prettier": "*",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { c, s } from "../val.config";
|
|
2
|
+
|
|
3
|
+
export default c.define(
|
|
4
|
+
"/content/basic-gallery-remote-existing.val.ts",
|
|
5
|
+
s.images({
|
|
6
|
+
directory: "/public/val/images-remote2",
|
|
7
|
+
accept: "image/*",
|
|
8
|
+
remote: true,
|
|
9
|
+
}),
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"https://remote.val.build/file/p/proj123/b/01/v/1.0.0/h/abc1/f/def4/p/public/val/images-remote2/image.png":
|
|
13
|
+
{
|
|
14
|
+
width: 1,
|
|
15
|
+
height: 1,
|
|
16
|
+
mimeType: "image/png",
|
|
17
|
+
alt: null,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { c, s } from "../val.config";
|
|
2
|
+
|
|
3
|
+
export default c.define(
|
|
4
|
+
"/content/basic-gallery-remote.val.ts",
|
|
5
|
+
s.images({
|
|
6
|
+
directory: "/public/val/images-remote",
|
|
7
|
+
accept: "image/*",
|
|
8
|
+
remote: true,
|
|
9
|
+
}),
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"/public/val/images-remote/image.png": {
|
|
13
|
+
width: 1,
|
|
14
|
+
height: 1,
|
|
15
|
+
mimeType: "image/png",
|
|
16
|
+
alt: null,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
);
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { initVal } from "@valbuild/core";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// NOTE: initVal() must be called with an options object for `config` to be
|
|
4
|
+
// defined - the CLI requires val.config to export a config object.
|
|
5
|
+
const { s, c, config } = initVal({});
|
|
4
6
|
|
|
5
7
|
export { s, c, config };
|
|
@@ -13,4 +13,6 @@ export default modules(config, [
|
|
|
13
13
|
{ def: () => import("./content/basic-gallery-fail-on-non-unique-dir.val") },
|
|
14
14
|
{ def: () => import("./content/basic-gallery-missing-tracked.val") },
|
|
15
15
|
{ def: () => import("./content/basic-gallery-wrong-metadata.val") },
|
|
16
|
+
{ def: () => import("./content/basic-gallery-remote.val") },
|
|
17
|
+
{ def: () => import("./content/basic-gallery-remote-existing.val") },
|
|
16
18
|
]);
|
|
@@ -383,4 +383,105 @@ describe("runValidation", () => {
|
|
|
383
383
|
service.dispose();
|
|
384
384
|
}
|
|
385
385
|
});
|
|
386
|
+
|
|
387
|
+
test("reports upload-remote error for local entry in a remote gallery", async () => {
|
|
388
|
+
const events: ValidationEvent[] = [];
|
|
389
|
+
|
|
390
|
+
for await (const event of runValidation({
|
|
391
|
+
root: tmpDir,
|
|
392
|
+
fix: false,
|
|
393
|
+
valFiles: ["content/basic-gallery-remote.val.ts"],
|
|
394
|
+
project: undefined,
|
|
395
|
+
remote: mockRemote,
|
|
396
|
+
fs: createDefaultValFSHost(),
|
|
397
|
+
})) {
|
|
398
|
+
events.push(event);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
expect(events.at(-1)).toEqual({
|
|
402
|
+
type: "summary-errors",
|
|
403
|
+
count: expect.any(Number),
|
|
404
|
+
});
|
|
405
|
+
const errors = events.filter((e) => e.type === "validation-error");
|
|
406
|
+
expect(
|
|
407
|
+
errors.some(
|
|
408
|
+
(e) =>
|
|
409
|
+
"message" in e &&
|
|
410
|
+
(e.message as string).includes("needs to be uploaded"),
|
|
411
|
+
),
|
|
412
|
+
).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("uploads local entry in a remote gallery and rewrites the key when fix is true", async () => {
|
|
416
|
+
const uploadRemote: IValRemote = {
|
|
417
|
+
remoteHost: DEFAULT_VAL_REMOTE_HOST,
|
|
418
|
+
getSettings: async () => ({
|
|
419
|
+
success: true,
|
|
420
|
+
data: {
|
|
421
|
+
publicProjectId: "pubproj",
|
|
422
|
+
remoteFileBuckets: [{ bucket: "01" }],
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
uploadFile: async () => ({ success: true }),
|
|
426
|
+
};
|
|
427
|
+
// Upload requires a personal access token on disk.
|
|
428
|
+
fs.mkdirSync(path.join(tmpDir, ".val"), { recursive: true });
|
|
429
|
+
fs.writeFileSync(
|
|
430
|
+
path.join(tmpDir, ".val", "pat.json"),
|
|
431
|
+
JSON.stringify({ pat: "test-pat" }),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const events: ValidationEvent[] = [];
|
|
435
|
+
for await (const event of runValidation({
|
|
436
|
+
root: tmpDir,
|
|
437
|
+
fix: true,
|
|
438
|
+
valFiles: ["content/basic-gallery-remote.val.ts"],
|
|
439
|
+
project: "test/project",
|
|
440
|
+
remote: uploadRemote,
|
|
441
|
+
fs: createDefaultValFSHost(),
|
|
442
|
+
})) {
|
|
443
|
+
events.push(event);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
expect(events.some((e) => e.type === "remote-uploaded")).toBe(true);
|
|
447
|
+
expect(events.some((e) => e.type === "fix-applied")).toBe(true);
|
|
448
|
+
|
|
449
|
+
const service = await createService(tmpDir, createDefaultValFSHost());
|
|
450
|
+
try {
|
|
451
|
+
const result = await service.get(
|
|
452
|
+
"/content/basic-gallery-remote.val.ts" as ModuleFilePath,
|
|
453
|
+
"" as ModulePath,
|
|
454
|
+
{ validate: false },
|
|
455
|
+
);
|
|
456
|
+
const source = result.source as Record<string, unknown>;
|
|
457
|
+
// The local-path key is replaced by a remote URL key (file kept on disk).
|
|
458
|
+
expect(source).not.toHaveProperty("/public/val/images-remote/image.png");
|
|
459
|
+
const keys = Object.keys(source);
|
|
460
|
+
expect(keys).toHaveLength(1);
|
|
461
|
+
expect(keys[0]).toContain("pubproj");
|
|
462
|
+
expect(keys[0]).toContain("public/val/images-remote/image.png");
|
|
463
|
+
} finally {
|
|
464
|
+
service.dispose();
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("does not flag a kept-on-disk file behind a remote gallery key", async () => {
|
|
469
|
+
const events: ValidationEvent[] = [];
|
|
470
|
+
|
|
471
|
+
for await (const event of runValidation({
|
|
472
|
+
root: tmpDir,
|
|
473
|
+
fix: false,
|
|
474
|
+
valFiles: ["content/basic-gallery-remote-existing.val.ts"],
|
|
475
|
+
project: undefined,
|
|
476
|
+
remote: mockRemote,
|
|
477
|
+
fs: createDefaultValFSHost(),
|
|
478
|
+
})) {
|
|
479
|
+
events.push(event);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// The file kept on disk under the remote URL key must not be reported as
|
|
483
|
+
// untracked, and the remote URL key must not be reported as missing.
|
|
484
|
+
expect(events.at(-1)).toEqual({ type: "summary-success" });
|
|
485
|
+
expect(events.filter((e) => e.type === "validation-error")).toHaveLength(0);
|
|
486
|
+
});
|
|
386
487
|
});
|
package/src/runValidation.ts
CHANGED
|
@@ -181,45 +181,17 @@ export async function handleFileMetadata(
|
|
|
181
181
|
return { success: true, shouldApplyPatch: true };
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
// Shared upload core used by both the single-field (handleRemoteFileUpload)
|
|
185
|
+
// and gallery (handleRemoteGalleryFileUpload) handlers. The two differ only in
|
|
186
|
+
// how they derive the local file ref, metadata and serialized image/file
|
|
187
|
+
// schema; everything from auth through upload is identical.
|
|
188
|
+
async function uploadRemoteFileCore(
|
|
185
189
|
ctx: FixHandlerContext,
|
|
190
|
+
fileRef: string,
|
|
191
|
+
metadata: Record<string, unknown> | undefined,
|
|
192
|
+
schema: SerializedImageSchema | SerializedFileSchema,
|
|
186
193
|
): Promise<FixHandlerResult> {
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
success: false,
|
|
190
|
-
errorMessage: `Remote file ${ctx.sourcePath} needs to be uploaded (use --fix to upload)`,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(
|
|
195
|
-
ctx.sourcePath,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
199
|
-
return {
|
|
200
|
-
success: false,
|
|
201
|
-
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(
|
|
206
|
-
modulePath,
|
|
207
|
-
ctx.valModule.source,
|
|
208
|
-
ctx.valModule.schema,
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
-
const fileRefProp = (resolvedRemoteFileAtSourcePath.source as any)?.[
|
|
213
|
-
FILE_REF_PROP
|
|
214
|
-
];
|
|
215
|
-
if (!fileRefProp) {
|
|
216
|
-
return {
|
|
217
|
-
success: false,
|
|
218
|
-
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const filePath = path.join(ctx.projectRoot, fileRefProp);
|
|
194
|
+
const filePath = path.join(ctx.projectRoot, fileRef);
|
|
223
195
|
if (!ctx.fs.fileExists(filePath)) {
|
|
224
196
|
return {
|
|
225
197
|
success: false,
|
|
@@ -259,26 +231,6 @@ export async function handleRemoteFileUpload(
|
|
|
259
231
|
};
|
|
260
232
|
}
|
|
261
233
|
|
|
262
|
-
if (!resolvedRemoteFileAtSourcePath.schema) {
|
|
263
|
-
return {
|
|
264
|
-
success: false,
|
|
265
|
-
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
270
|
-
const fileSourceMetadata = Internal.isFile(actualRemoteFileSource)
|
|
271
|
-
? actualRemoteFileSource.metadata
|
|
272
|
-
: undefined;
|
|
273
|
-
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
274
|
-
|
|
275
|
-
if (!resolveRemoteFileSchema) {
|
|
276
|
-
return {
|
|
277
|
-
success: false,
|
|
278
|
-
errorMessage: `Could not resolve schema for remote file: ${ctx.sourcePath}`,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
234
|
const projectName = ctx.project;
|
|
283
235
|
let publicProjectId = ctx.publicProjectId;
|
|
284
236
|
let remoteFileBuckets = ctx.remoteFileBuckets;
|
|
@@ -317,16 +269,6 @@ export async function handleRemoteFileUpload(
|
|
|
317
269
|
};
|
|
318
270
|
}
|
|
319
271
|
|
|
320
|
-
if (
|
|
321
|
-
resolveRemoteFileSchema.type !== "image" &&
|
|
322
|
-
resolveRemoteFileSchema.type !== "file"
|
|
323
|
-
) {
|
|
324
|
-
return {
|
|
325
|
-
success: false,
|
|
326
|
-
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
272
|
remoteFilesCounter += 1;
|
|
331
273
|
const bucket =
|
|
332
274
|
remoteFileBuckets[remoteFilesCounter % remoteFileBuckets.length];
|
|
@@ -361,10 +303,6 @@ export async function handleRemoteFileUpload(
|
|
|
361
303
|
const fileHash = Internal.remote.getFileHash(fileBuffer);
|
|
362
304
|
const coreVersion = Internal.VERSION.core || "unknown";
|
|
363
305
|
const fileExt = getFileExt(filePath);
|
|
364
|
-
const schema = resolveRemoteFileSchema as
|
|
365
|
-
| SerializedImageSchema
|
|
366
|
-
| SerializedFileSchema;
|
|
367
|
-
const metadata = fileSourceMetadata;
|
|
368
306
|
const ref = Internal.remote.createRemoteRef(ctx.remote.remoteHost, {
|
|
369
307
|
publicProjectId,
|
|
370
308
|
coreVersion,
|
|
@@ -399,7 +337,7 @@ export async function handleRemoteFileUpload(
|
|
|
399
337
|
|
|
400
338
|
ctx.remoteFiles[ctx.sourcePath] = {
|
|
401
339
|
ref,
|
|
402
|
-
metadata
|
|
340
|
+
metadata,
|
|
403
341
|
};
|
|
404
342
|
|
|
405
343
|
return {
|
|
@@ -415,6 +353,177 @@ export async function handleRemoteFileUpload(
|
|
|
415
353
|
};
|
|
416
354
|
}
|
|
417
355
|
|
|
356
|
+
export async function handleRemoteFileUpload(
|
|
357
|
+
ctx: FixHandlerContext,
|
|
358
|
+
): Promise<FixHandlerResult> {
|
|
359
|
+
if (!ctx.fix) {
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
363
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(
|
|
368
|
+
ctx.sourcePath,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const resolvedRemoteFileAtSourcePath = Internal.resolvePath(
|
|
379
|
+
modulePath,
|
|
380
|
+
ctx.valModule.source,
|
|
381
|
+
ctx.valModule.schema,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
385
|
+
const fileRefProp = (resolvedRemoteFileAtSourcePath.source as any)?.[
|
|
386
|
+
FILE_REF_PROP
|
|
387
|
+
];
|
|
388
|
+
if (!fileRefProp) {
|
|
389
|
+
return {
|
|
390
|
+
success: false,
|
|
391
|
+
errorMessage: `Expected file to be defined at: ${ctx.sourcePath} but no file was found`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const resolveRemoteFileSchema = resolvedRemoteFileAtSourcePath.schema;
|
|
396
|
+
if (!resolveRemoteFileSchema) {
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
errorMessage: `Cannot upload remote file: schema not found for ${ctx.sourcePath}`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
resolveRemoteFileSchema.type !== "image" &&
|
|
405
|
+
resolveRemoteFileSchema.type !== "file"
|
|
406
|
+
) {
|
|
407
|
+
return {
|
|
408
|
+
success: false,
|
|
409
|
+
errorMessage: `The schema is the remote is neither image nor file: ${ctx.sourcePath}`,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const actualRemoteFileSource = resolvedRemoteFileAtSourcePath.source;
|
|
414
|
+
const fileSourceMetadata = Internal.isFile(actualRemoteFileSource)
|
|
415
|
+
? actualRemoteFileSource.metadata
|
|
416
|
+
: undefined;
|
|
417
|
+
|
|
418
|
+
return uploadRemoteFileCore(
|
|
419
|
+
ctx,
|
|
420
|
+
fileRefProp,
|
|
421
|
+
fileSourceMetadata,
|
|
422
|
+
resolveRemoteFileSchema,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Gallery (s.images({ remote: true }) / s.files({ remote: true })) upload.
|
|
427
|
+
// Unlike a single image/file field, a gallery entry is keyed by its local file
|
|
428
|
+
// path and the value is bare metadata (no FileSource), so we derive the file
|
|
429
|
+
// ref from the key and synthesize the image/file schema from the record's
|
|
430
|
+
// media options.
|
|
431
|
+
export async function handleRemoteGalleryFileUpload(
|
|
432
|
+
ctx: FixHandlerContext,
|
|
433
|
+
): Promise<FixHandlerResult> {
|
|
434
|
+
if (!ctx.fix) {
|
|
435
|
+
return {
|
|
436
|
+
success: false,
|
|
437
|
+
// No sourcePath in the message: the reported location already points at it.
|
|
438
|
+
errorMessage: `Remote file needs to be uploaded (use --fix to upload)`,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const fix = ctx.validationError.fixes?.[0];
|
|
443
|
+
const mediaType = fix === "files:upload-remote" ? "file" : "image";
|
|
444
|
+
|
|
445
|
+
// The gallery entry is keyed by its local file path; validateMediaKey carries
|
|
446
|
+
// that key as the error value.
|
|
447
|
+
const fileRef = ctx.validationError.value;
|
|
448
|
+
if (typeof fileRef !== "string") {
|
|
449
|
+
return {
|
|
450
|
+
success: false,
|
|
451
|
+
errorMessage: `Expected a local file path for gallery entry at ${ctx.sourcePath}`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(
|
|
456
|
+
ctx.sourcePath,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (!ctx.valModule.source || !ctx.valModule.schema) {
|
|
460
|
+
return {
|
|
461
|
+
success: false,
|
|
462
|
+
errorMessage: `Could not resolve source or schema for ${ctx.sourcePath}`,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const resolved = Internal.resolvePath(
|
|
467
|
+
modulePath,
|
|
468
|
+
ctx.valModule.source,
|
|
469
|
+
ctx.valModule.schema,
|
|
470
|
+
);
|
|
471
|
+
const entrySource = resolved.source;
|
|
472
|
+
const metadata =
|
|
473
|
+
entrySource &&
|
|
474
|
+
typeof entrySource === "object" &&
|
|
475
|
+
!Array.isArray(entrySource)
|
|
476
|
+
? (entrySource as Record<string, unknown>)
|
|
477
|
+
: undefined;
|
|
478
|
+
|
|
479
|
+
// The gallery item schema is an ObjectSchema, not an image/file schema, so
|
|
480
|
+
// synthesize the serialized image/file schema (matching how single fields
|
|
481
|
+
// serialize) for the remote ref's validation hash.
|
|
482
|
+
//
|
|
483
|
+
// `accept`/`directory` come from the RECORD that holds the entry, resolved
|
|
484
|
+
// from the entry's PARENT path - not from the module root, which is only the
|
|
485
|
+
// record when the gallery is the whole module. A nested gallery
|
|
486
|
+
// (s.object({ gallery: s.images(...) })) would otherwise synthesize a schema
|
|
487
|
+
// with no options and bake a validation hash into the remote ref that can
|
|
488
|
+
// never validate, so a mismatch fails fast instead of uploading.
|
|
489
|
+
const [, parentModulePath] = Internal.splitModuleFilePathAndModulePath(
|
|
490
|
+
Internal.parentOfSourcePath(ctx.sourcePath),
|
|
491
|
+
);
|
|
492
|
+
const recordSchema = Internal.resolvePath(
|
|
493
|
+
parentModulePath,
|
|
494
|
+
ctx.valModule.source,
|
|
495
|
+
ctx.valModule.schema,
|
|
496
|
+
).schema;
|
|
497
|
+
if (recordSchema?.type !== "record") {
|
|
498
|
+
return {
|
|
499
|
+
success: false,
|
|
500
|
+
errorMessage: `Expected a gallery record at the parent of ${ctx.sourcePath}, got ${
|
|
501
|
+
recordSchema?.type ?? "nothing"
|
|
502
|
+
}`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const { accept, directory } = recordSchema;
|
|
506
|
+
const schema: SerializedImageSchema | SerializedFileSchema =
|
|
507
|
+
mediaType === "image"
|
|
508
|
+
? {
|
|
509
|
+
type: "image",
|
|
510
|
+
opt: false,
|
|
511
|
+
options: {
|
|
512
|
+
...(accept ? { accept } : {}),
|
|
513
|
+
...(directory ? { directory } : {}),
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
: {
|
|
517
|
+
type: "file",
|
|
518
|
+
opt: false,
|
|
519
|
+
options: {
|
|
520
|
+
...(accept ? { accept } : {}),
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
return uploadRemoteFileCore(ctx, fileRef, metadata, schema);
|
|
525
|
+
}
|
|
526
|
+
|
|
418
527
|
export async function handleRemoteFileDownload(
|
|
419
528
|
ctx: FixHandlerContext,
|
|
420
529
|
): Promise<FixHandlerResult> {
|
|
@@ -479,6 +588,17 @@ export async function handleUniqueFolderCheck(
|
|
|
479
588
|
return { success: true };
|
|
480
589
|
}
|
|
481
590
|
|
|
591
|
+
// Maps a gallery key to its on-disk local path. Remote galleries key uploaded
|
|
592
|
+
// entries by a remote URL while keeping the file on disk; everything else is
|
|
593
|
+
// already a local path and is returned unchanged.
|
|
594
|
+
function remoteKeyToLocalPath(key: string): string {
|
|
595
|
+
const remoteRefRes = Internal.remote.splitRemoteRef(key);
|
|
596
|
+
if (remoteRefRes.status === "success") {
|
|
597
|
+
return `/${remoteRefRes.filePath}`;
|
|
598
|
+
}
|
|
599
|
+
return key;
|
|
600
|
+
}
|
|
601
|
+
|
|
482
602
|
export async function handleCheckAllFiles(
|
|
483
603
|
ctx: FixHandlerContext,
|
|
484
604
|
): Promise<FixHandlerResult> {
|
|
@@ -500,7 +620,12 @@ export async function handleCheckAllFiles(
|
|
|
500
620
|
errorMessage: `Could not get source for ${ctx.sourcePath}`,
|
|
501
621
|
};
|
|
502
622
|
}
|
|
503
|
-
|
|
623
|
+
// Gallery entries are keyed by their file path. Remote galleries key uploaded
|
|
624
|
+
// entries by a remote URL, but the file is kept on disk at its local path, so
|
|
625
|
+
// normalize remote-URL keys back to that local path for the on-disk checks.
|
|
626
|
+
const trackedFiles = new Set(
|
|
627
|
+
Object.keys(source as Record<string, unknown>).map(remoteKeyToLocalPath),
|
|
628
|
+
);
|
|
504
629
|
|
|
505
630
|
// Check that all tracked files exist on disk
|
|
506
631
|
const missingTrackedFiles = [...trackedFiles].filter((f) => {
|
|
@@ -558,6 +683,8 @@ export const currentFixHandlers: Record<
|
|
|
558
683
|
"file:add-metadata": handleFileMetadata,
|
|
559
684
|
"image:upload-remote": handleRemoteFileUpload,
|
|
560
685
|
"file:upload-remote": handleRemoteFileUpload,
|
|
686
|
+
"images:upload-remote": handleRemoteGalleryFileUpload,
|
|
687
|
+
"files:upload-remote": handleRemoteGalleryFileUpload,
|
|
561
688
|
"image:download-remote": handleRemoteFileDownload,
|
|
562
689
|
"file:download-remote": handleRemoteFileDownload,
|
|
563
690
|
"image:check-remote": handleRemoteFileCheck,
|