payload 3.89.0-internal.4b0e9b6 → 3.89.0-internal.95c9cba
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/dist/uploads/getFileContentRequirement.d.ts +9 -0
- package/dist/uploads/getFileContentRequirement.d.ts.map +1 -0
- package/dist/uploads/getFileContentRequirement.js +24 -0
- package/dist/uploads/getFileContentRequirement.js.map +1 -0
- package/dist/uploads/getFileContentRequirement.spec.js +95 -0
- package/dist/uploads/getFileContentRequirement.spec.js.map +1 -0
- package/dist/uploads/getFileFromClientUpload.d.ts +20 -0
- package/dist/uploads/getFileFromClientUpload.d.ts.map +1 -0
- package/dist/uploads/getFileFromClientUpload.js +199 -0
- package/dist/uploads/getFileFromClientUpload.js.map +1 -0
- package/dist/uploads/getFileFromClientUpload.spec.js +528 -0
- package/dist/uploads/getFileFromClientUpload.spec.js.map +1 -0
- package/dist/uploads/unlinkTempFiles.d.ts.map +1 -1
- package/dist/uploads/unlinkTempFiles.js +12 -2
- package/dist/uploads/unlinkTempFiles.js.map +1 -1
- package/dist/uploads/unlinkTempFiles.spec.js +157 -0
- package/dist/uploads/unlinkTempFiles.spec.js.map +1 -0
- package/dist/utilities/addDataAndFileToRequest.d.ts.map +1 -1
- package/dist/utilities/addDataAndFileToRequest.js +7 -46
- package/dist/utilities/addDataAndFileToRequest.js.map +1 -1
- package/dist/utilities/addDataAndFileToRequest.spec.js +131 -1
- package/dist/utilities/addDataAndFileToRequest.spec.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SanitizedUploadConfig } from './types.js';
|
|
2
|
+
export declare const HEADER_PROBE_BYTE_LENGTH: number;
|
|
3
|
+
export type FileContentRequirement = 'full' | 'header' | 'none';
|
|
4
|
+
export declare function getFileContentRequirement({ hasSizeEdits, mimeType, uploadConfig, }: {
|
|
5
|
+
hasSizeEdits?: boolean;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
uploadConfig: SanitizedUploadConfig;
|
|
8
|
+
}): FileContentRequirement;
|
|
9
|
+
//# sourceMappingURL=getFileContentRequirement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getFileContentRequirement.d.ts","sourceRoot":"","sources":["../../src/uploads/getFileContentRequirement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAMvD,eAAO,MAAM,wBAAwB,QAAc,CAAA;AAEnD,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE/D,wBAAgB,yBAAyB,CAAC,EACxC,YAAY,EACZ,QAAQ,EACR,YAAY,GACb,EAAE;IACD,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,qBAAqB,CAAA;CACpC,GAAG,sBAAsB,CAgCzB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { canResizeImage } from './canResizeImage.js';
|
|
2
|
+
import { isAnimatedImage } from './isAnimatedImage.js';
|
|
3
|
+
import { isImage } from './isImage.js';
|
|
4
|
+
export const HEADER_PROBE_BYTE_LENGTH = 1024 * 1024;
|
|
5
|
+
export function getFileContentRequirement({ hasSizeEdits, mimeType, uploadConfig }) {
|
|
6
|
+
if (!uploadConfig.disableLocalStorage) {
|
|
7
|
+
return 'full';
|
|
8
|
+
}
|
|
9
|
+
const hasMimeTypeAllowList = !uploadConfig.allowRestrictedFileTypes && Array.isArray(uploadConfig.mimeTypes) && uploadConfig.mimeTypes.length > 0;
|
|
10
|
+
if (hasMimeTypeAllowList) {
|
|
11
|
+
return 'full';
|
|
12
|
+
}
|
|
13
|
+
const isResizableImage = canResizeImage(mimeType);
|
|
14
|
+
const hasConfiguredAdjustments = Boolean(uploadConfig.resizeOptions || uploadConfig.formatOptions || uploadConfig.trimOptions || uploadConfig.constructorOptions || Array.isArray(uploadConfig.imageSizes) && uploadConfig.imageSizes.length > 0);
|
|
15
|
+
if (hasSizeEdits || isResizableImage && hasConfiguredAdjustments || isAnimatedImage(mimeType)) {
|
|
16
|
+
return 'full';
|
|
17
|
+
}
|
|
18
|
+
if (isResizableImage || isImage(mimeType)) {
|
|
19
|
+
return 'header';
|
|
20
|
+
}
|
|
21
|
+
return 'none';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=getFileContentRequirement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/uploads/getFileContentRequirement.ts"],"sourcesContent":["import type { SanitizedUploadConfig } from './types.js'\n\nimport { canResizeImage } from './canResizeImage.js'\nimport { isAnimatedImage } from './isAnimatedImage.js'\nimport { isImage } from './isImage.js'\n\nexport const HEADER_PROBE_BYTE_LENGTH = 1024 * 1024\n\nexport type FileContentRequirement = 'full' | 'header' | 'none'\n\nexport function getFileContentRequirement({\n hasSizeEdits,\n mimeType,\n uploadConfig,\n}: {\n hasSizeEdits?: boolean\n mimeType: string\n uploadConfig: SanitizedUploadConfig\n}): FileContentRequirement {\n if (!uploadConfig.disableLocalStorage) {\n return 'full'\n }\n\n const hasMimeTypeAllowList =\n !uploadConfig.allowRestrictedFileTypes &&\n Array.isArray(uploadConfig.mimeTypes) &&\n uploadConfig.mimeTypes.length > 0\n\n if (hasMimeTypeAllowList) {\n return 'full'\n }\n\n const isResizableImage = canResizeImage(mimeType)\n const hasConfiguredAdjustments = Boolean(\n uploadConfig.resizeOptions ||\n uploadConfig.formatOptions ||\n uploadConfig.trimOptions ||\n uploadConfig.constructorOptions ||\n (Array.isArray(uploadConfig.imageSizes) && uploadConfig.imageSizes.length > 0),\n )\n\n if (hasSizeEdits || (isResizableImage && hasConfiguredAdjustments) || isAnimatedImage(mimeType)) {\n return 'full'\n }\n\n if (isResizableImage || isImage(mimeType)) {\n return 'header'\n }\n\n return 'none'\n}\n"],"names":["canResizeImage","isAnimatedImage","isImage","HEADER_PROBE_BYTE_LENGTH","getFileContentRequirement","hasSizeEdits","mimeType","uploadConfig","disableLocalStorage","hasMimeTypeAllowList","allowRestrictedFileTypes","Array","isArray","mimeTypes","length","isResizableImage","hasConfiguredAdjustments","Boolean","resizeOptions","formatOptions","trimOptions","constructorOptions","imageSizes"],"mappings":"AAEA,SAASA,cAAc,QAAQ,sBAAqB;AACpD,SAASC,eAAe,QAAQ,uBAAsB;AACtD,SAASC,OAAO,QAAQ,eAAc;AAEtC,OAAO,MAAMC,2BAA2B,OAAO,KAAI;AAInD,OAAO,SAASC,0BAA0B,EACxCC,YAAY,EACZC,QAAQ,EACRC,YAAY,EAKb;IACC,IAAI,CAACA,aAAaC,mBAAmB,EAAE;QACrC,OAAO;IACT;IAEA,MAAMC,uBACJ,CAACF,aAAaG,wBAAwB,IACtCC,MAAMC,OAAO,CAACL,aAAaM,SAAS,KACpCN,aAAaM,SAAS,CAACC,MAAM,GAAG;IAElC,IAAIL,sBAAsB;QACxB,OAAO;IACT;IAEA,MAAMM,mBAAmBf,eAAeM;IACxC,MAAMU,2BAA2BC,QAC/BV,aAAaW,aAAa,IACxBX,aAAaY,aAAa,IAC1BZ,aAAaa,WAAW,IACxBb,aAAac,kBAAkB,IAC9BV,MAAMC,OAAO,CAACL,aAAae,UAAU,KAAKf,aAAae,UAAU,CAACR,MAAM,GAAG;IAGhF,IAAIT,gBAAiBU,oBAAoBC,4BAA6Bf,gBAAgBK,WAAW;QAC/F,OAAO;IACT;IAEA,IAAIS,oBAAoBb,QAAQI,WAAW;QACzC,OAAO;IACT;IAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getFileContentRequirement } from './getFileContentRequirement.js';
|
|
3
|
+
describe('getFileContentRequirement', ()=>{
|
|
4
|
+
it.each([
|
|
5
|
+
{
|
|
6
|
+
expected: 'full',
|
|
7
|
+
mimeType: 'video/mp4',
|
|
8
|
+
upload: {
|
|
9
|
+
disableLocalStorage: false
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
expected: 'full',
|
|
14
|
+
mimeType: 'video/mp4',
|
|
15
|
+
upload: {
|
|
16
|
+
disableLocalStorage: true,
|
|
17
|
+
mimeTypes: [
|
|
18
|
+
'video/*'
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
expected: 'none',
|
|
24
|
+
mimeType: 'video/mp4',
|
|
25
|
+
upload: {
|
|
26
|
+
disableLocalStorage: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
expected: 'header',
|
|
31
|
+
mimeType: 'image/png',
|
|
32
|
+
upload: {
|
|
33
|
+
disableLocalStorage: true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
expected: 'full',
|
|
38
|
+
mimeType: 'image/png',
|
|
39
|
+
upload: {
|
|
40
|
+
disableLocalStorage: true,
|
|
41
|
+
resizeOptions: {
|
|
42
|
+
width: 100
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
expected: 'full',
|
|
48
|
+
mimeType: 'image/png',
|
|
49
|
+
upload: {
|
|
50
|
+
disableLocalStorage: true,
|
|
51
|
+
imageSizes: [
|
|
52
|
+
{
|
|
53
|
+
name: 'thumb',
|
|
54
|
+
width: 100
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
expected: 'full',
|
|
61
|
+
mimeType: 'image/gif',
|
|
62
|
+
upload: {
|
|
63
|
+
disableLocalStorage: true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
])('returns $expected for $mimeType and $upload', ({ expected, mimeType, upload })=>{
|
|
67
|
+
expect(getFileContentRequirement({
|
|
68
|
+
mimeType,
|
|
69
|
+
uploadConfig: upload
|
|
70
|
+
})).toBe(expected);
|
|
71
|
+
});
|
|
72
|
+
it('requires full content when the request includes crop or size edits', ()=>{
|
|
73
|
+
expect(getFileContentRequirement({
|
|
74
|
+
hasSizeEdits: true,
|
|
75
|
+
mimeType: 'image/png',
|
|
76
|
+
uploadConfig: {
|
|
77
|
+
disableLocalStorage: true
|
|
78
|
+
}
|
|
79
|
+
})).toBe('full');
|
|
80
|
+
});
|
|
81
|
+
it('returns none for a restricted-type-allowed non-image when mimeTypes validation is bypassed', ()=>{
|
|
82
|
+
expect(getFileContentRequirement({
|
|
83
|
+
mimeType: 'video/mp4',
|
|
84
|
+
uploadConfig: {
|
|
85
|
+
allowRestrictedFileTypes: true,
|
|
86
|
+
disableLocalStorage: true,
|
|
87
|
+
mimeTypes: [
|
|
88
|
+
'video/*'
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
})).toBe('none');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=getFileContentRequirement.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/uploads/getFileContentRequirement.spec.ts"],"sourcesContent":["import type { SanitizedUploadConfig } from './types.js'\n\nimport { describe, expect, it } from 'vitest'\n\nimport { getFileContentRequirement } from './getFileContentRequirement.js'\n\ndescribe('getFileContentRequirement', () => {\n it.each([\n { expected: 'full', mimeType: 'video/mp4', upload: { disableLocalStorage: false } },\n {\n expected: 'full',\n mimeType: 'video/mp4',\n upload: { disableLocalStorage: true, mimeTypes: ['video/*'] },\n },\n { expected: 'none', mimeType: 'video/mp4', upload: { disableLocalStorage: true } },\n { expected: 'header', mimeType: 'image/png', upload: { disableLocalStorage: true } },\n {\n expected: 'full',\n mimeType: 'image/png',\n upload: { disableLocalStorage: true, resizeOptions: { width: 100 } },\n },\n {\n expected: 'full',\n mimeType: 'image/png',\n upload: { disableLocalStorage: true, imageSizes: [{ name: 'thumb', width: 100 }] },\n },\n { expected: 'full', mimeType: 'image/gif', upload: { disableLocalStorage: true } },\n ])('returns $expected for $mimeType and $upload', ({ expected, mimeType, upload }) => {\n expect(\n getFileContentRequirement({\n mimeType,\n uploadConfig: upload as SanitizedUploadConfig,\n }),\n ).toBe(expected)\n })\n\n it('requires full content when the request includes crop or size edits', () => {\n expect(\n getFileContentRequirement({\n hasSizeEdits: true,\n mimeType: 'image/png',\n uploadConfig: { disableLocalStorage: true } as SanitizedUploadConfig,\n }),\n ).toBe('full')\n })\n\n it('returns none for a restricted-type-allowed non-image when mimeTypes validation is bypassed', () => {\n expect(\n getFileContentRequirement({\n mimeType: 'video/mp4',\n uploadConfig: {\n allowRestrictedFileTypes: true,\n disableLocalStorage: true,\n mimeTypes: ['video/*'],\n } as SanitizedUploadConfig,\n }),\n ).toBe('none')\n })\n})\n"],"names":["describe","expect","it","getFileContentRequirement","each","expected","mimeType","upload","disableLocalStorage","mimeTypes","resizeOptions","width","imageSizes","name","uploadConfig","toBe","hasSizeEdits","allowRestrictedFileTypes"],"mappings":"AAEA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,yBAAyB,QAAQ,iCAAgC;AAE1EH,SAAS,6BAA6B;IACpCE,GAAGE,IAAI,CAAC;QACN;YAAEC,UAAU;YAAQC,UAAU;YAAaC,QAAQ;gBAAEC,qBAAqB;YAAM;QAAE;QAClF;YACEH,UAAU;YACVC,UAAU;YACVC,QAAQ;gBAAEC,qBAAqB;gBAAMC,WAAW;oBAAC;iBAAU;YAAC;QAC9D;QACA;YAAEJ,UAAU;YAAQC,UAAU;YAAaC,QAAQ;gBAAEC,qBAAqB;YAAK;QAAE;QACjF;YAAEH,UAAU;YAAUC,UAAU;YAAaC,QAAQ;gBAAEC,qBAAqB;YAAK;QAAE;QACnF;YACEH,UAAU;YACVC,UAAU;YACVC,QAAQ;gBAAEC,qBAAqB;gBAAME,eAAe;oBAAEC,OAAO;gBAAI;YAAE;QACrE;QACA;YACEN,UAAU;YACVC,UAAU;YACVC,QAAQ;gBAAEC,qBAAqB;gBAAMI,YAAY;oBAAC;wBAAEC,MAAM;wBAASF,OAAO;oBAAI;iBAAE;YAAC;QACnF;QACA;YAAEN,UAAU;YAAQC,UAAU;YAAaC,QAAQ;gBAAEC,qBAAqB;YAAK;QAAE;KAClF,EAAE,+CAA+C,CAAC,EAAEH,QAAQ,EAAEC,QAAQ,EAAEC,MAAM,EAAE;QAC/EN,OACEE,0BAA0B;YACxBG;YACAQ,cAAcP;QAChB,IACAQ,IAAI,CAACV;IACT;IAEAH,GAAG,sEAAsE;QACvED,OACEE,0BAA0B;YACxBa,cAAc;YACdV,UAAU;YACVQ,cAAc;gBAAEN,qBAAqB;YAAK;QAC5C,IACAO,IAAI,CAAC;IACT;IAEAb,GAAG,8FAA8F;QAC/FD,OACEE,0BAA0B;YACxBG,UAAU;YACVQ,cAAc;gBACZG,0BAA0B;gBAC1BT,qBAAqB;gBACrBC,WAAW;oBAAC;iBAAU;YACxB;QACF,IACAM,IAAI,CAAC;IACT;AACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PayloadRequest } from '../types/index.js';
|
|
2
|
+
export type ClientUploadData = {
|
|
3
|
+
clientUploadContext?: unknown;
|
|
4
|
+
collectionSlug: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
size: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* `req.context` key `unlinkTempFiles` reads to find a materializer-created temp file once
|
|
11
|
+
* `req.file` is gone - plugin-cloud-storage's afterChange hook clears `req.file` after
|
|
12
|
+
* uploading generated image sizes, before cleanup runs, so tracking it only on `req.file`
|
|
13
|
+
* would leak the temp file.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CLIENT_UPLOAD_TEMP_FILE_PATH_CONTEXT_KEY = "payloadClientUploadTempFilePath";
|
|
16
|
+
export declare function getFileFromClientUpload({ file, req, }: {
|
|
17
|
+
file: ClientUploadData;
|
|
18
|
+
req: PayloadRequest;
|
|
19
|
+
}): Promise<NonNullable<PayloadRequest['file']>>;
|
|
20
|
+
//# sourceMappingURL=getFileFromClientUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getFileFromClientUpload.d.ts","sourceRoot":"","sources":["../../src/uploads/getFileFromClientUpload.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAQvD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,oCAAoC,CAAA;AAEzF,wBAAsB,uBAAuB,CAAC,EAC5C,IAAI,EACJ,GAAG,GACJ,EAAE;IACD,IAAI,EAAE,gBAAgB,CAAA;IACtB,GAAG,EAAE,cAAc,CAAA;CACpB,GAAG,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CA8C/C"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { mkdir, rm, writeFile } from 'fs/promises';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { Readable } from 'stream';
|
|
7
|
+
import { pipeline } from 'stream/promises';
|
|
8
|
+
import { APIError } from '../errors/APIError.js';
|
|
9
|
+
import { isolateObjectProperty } from '../utilities/isolateObjectProperty.js';
|
|
10
|
+
import { getFileContentRequirement, HEADER_PROBE_BYTE_LENGTH } from './getFileContentRequirement.js';
|
|
11
|
+
import { getImageSize } from './getImageSize.js';
|
|
12
|
+
/**
|
|
13
|
+
* `req.context` key `unlinkTempFiles` reads to find a materializer-created temp file once
|
|
14
|
+
* `req.file` is gone - plugin-cloud-storage's afterChange hook clears `req.file` after
|
|
15
|
+
* uploading generated image sizes, before cleanup runs, so tracking it only on `req.file`
|
|
16
|
+
* would leak the temp file.
|
|
17
|
+
*/ export const CLIENT_UPLOAD_TEMP_FILE_PATH_CONTEXT_KEY = 'payloadClientUploadTempFilePath';
|
|
18
|
+
export async function getFileFromClientUpload({ file, req }) {
|
|
19
|
+
const uploadConfig = req.payload.collections[file.collectionSlug]?.config.upload;
|
|
20
|
+
if (!uploadConfig || !uploadConfig.handlers) {
|
|
21
|
+
throw new APIError(`uploadConfig.handlers is not present for ${file.collectionSlug}`);
|
|
22
|
+
}
|
|
23
|
+
const contentRequirement = getFileContentRequirement({
|
|
24
|
+
hasSizeEdits: requestHasSizeEdits(req),
|
|
25
|
+
mimeType: file.mimeType,
|
|
26
|
+
uploadConfig
|
|
27
|
+
});
|
|
28
|
+
if (contentRequirement === 'none') {
|
|
29
|
+
return {
|
|
30
|
+
name: file.filename,
|
|
31
|
+
clientUploadContext: file.clientUploadContext,
|
|
32
|
+
data: Buffer.alloc(0),
|
|
33
|
+
mimetype: file.mimeType,
|
|
34
|
+
size: file.size
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (contentRequirement === 'header') {
|
|
38
|
+
const headerFile = await fetchHeaderOnly({
|
|
39
|
+
file,
|
|
40
|
+
req,
|
|
41
|
+
uploadConfig
|
|
42
|
+
});
|
|
43
|
+
if (headerFile) {
|
|
44
|
+
return headerFile;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const response = await fetchUploadResponse({
|
|
48
|
+
file,
|
|
49
|
+
req,
|
|
50
|
+
uploadConfig
|
|
51
|
+
});
|
|
52
|
+
const tempFilePath = await streamResponseToTempFile({
|
|
53
|
+
req,
|
|
54
|
+
response
|
|
55
|
+
});
|
|
56
|
+
if (!req.context) {
|
|
57
|
+
req.context = {};
|
|
58
|
+
}
|
|
59
|
+
req.context[CLIENT_UPLOAD_TEMP_FILE_PATH_CONTEXT_KEY] = tempFilePath;
|
|
60
|
+
return {
|
|
61
|
+
name: file.filename,
|
|
62
|
+
clientUploadContext: file.clientUploadContext,
|
|
63
|
+
data: Buffer.alloc(0),
|
|
64
|
+
mimetype: response.headers.get('Content-Type') || file.mimeType,
|
|
65
|
+
size: file.size,
|
|
66
|
+
tempFilePath
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Only crop and explicit pixel edits require the original bytes - a focal-point-only
|
|
71
|
+
* change re-derives from data already on the document, not the incoming upload.
|
|
72
|
+
*/ function requestHasSizeEdits(req) {
|
|
73
|
+
const uploadEdits = req.query?.uploadEdits && typeof req.query.uploadEdits === 'object' ? req.query.uploadEdits : undefined;
|
|
74
|
+
return Boolean(uploadEdits?.crop || uploadEdits?.heightInPixels || uploadEdits?.widthInPixels);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Runs every configured handler and returns the last one that responds, following a single
|
|
78
|
+
* redirect if that response is one - matching the pre-existing v3 handler contract exactly
|
|
79
|
+
* (v3 always ran every handler; only v4 stops at the first response, via an unrelated PR).
|
|
80
|
+
*/ async function fetchUploadResponse({ file, req, uploadConfig }) {
|
|
81
|
+
let response = null;
|
|
82
|
+
let error;
|
|
83
|
+
for (const handler of uploadConfig.handlers){
|
|
84
|
+
try {
|
|
85
|
+
const result = await handler(req, {
|
|
86
|
+
doc: null,
|
|
87
|
+
params: {
|
|
88
|
+
clientUploadContext: file.clientUploadContext,
|
|
89
|
+
collection: file.collectionSlug,
|
|
90
|
+
filename: file.filename
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
if (result) {
|
|
94
|
+
response = result;
|
|
95
|
+
}
|
|
96
|
+
// If we couldn't get the file from that handler, save the error and try other.
|
|
97
|
+
} catch (err) {
|
|
98
|
+
error = err;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!response) {
|
|
102
|
+
if (error) {
|
|
103
|
+
req.payload.logger.error(error);
|
|
104
|
+
}
|
|
105
|
+
throw new APIError('Expected response from the upload handler.');
|
|
106
|
+
}
|
|
107
|
+
if (response.status >= 300 && response.status < 400) {
|
|
108
|
+
const redirectUrl = response.headers.get('Location');
|
|
109
|
+
if (redirectUrl) {
|
|
110
|
+
// Forward the Range header (if any) so a redirect from the header-probe path still
|
|
111
|
+
// fetches only the bounded slice, instead of the whole object, from the redirect target.
|
|
112
|
+
const rangeHeader = req.headers.get('Range');
|
|
113
|
+
response = rangeHeader ? await fetch(redirectUrl, {
|
|
114
|
+
headers: {
|
|
115
|
+
Range: rangeHeader
|
|
116
|
+
}
|
|
117
|
+
}) : await fetch(redirectUrl);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Requests a bounded byte range from the handlers and probes it for image dimensions, without
|
|
124
|
+
* ever reading past `HEADER_PROBE_BYTE_LENGTH`. Returns `null` when the bounded bytes cannot be
|
|
125
|
+
* probed, so the caller falls back to a full streamed fetch.
|
|
126
|
+
*/ async function fetchHeaderOnly({ file, req, uploadConfig }) {
|
|
127
|
+
const rangedHeaders = new Headers(req.headers);
|
|
128
|
+
rangedHeaders.set('Range', `bytes=0-${HEADER_PROBE_BYTE_LENGTH - 1}`);
|
|
129
|
+
const scopedReq = isolateObjectProperty(req, 'headers');
|
|
130
|
+
scopedReq.headers = rangedHeaders;
|
|
131
|
+
const response = await fetchUploadResponse({
|
|
132
|
+
file,
|
|
133
|
+
req: scopedReq,
|
|
134
|
+
uploadConfig
|
|
135
|
+
});
|
|
136
|
+
if (!response.body) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const reader = response.body.getReader();
|
|
140
|
+
const chunks = [];
|
|
141
|
+
let bytesRead = 0;
|
|
142
|
+
try {
|
|
143
|
+
while(bytesRead < HEADER_PROBE_BYTE_LENGTH){
|
|
144
|
+
const { done, value } = await reader.read();
|
|
145
|
+
if (done || !value) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
const remaining = HEADER_PROBE_BYTE_LENGTH - bytesRead;
|
|
149
|
+
const chunk = value.length > remaining ? value.slice(0, remaining) : value;
|
|
150
|
+
chunks.push(chunk);
|
|
151
|
+
bytesRead += chunk.length;
|
|
152
|
+
}
|
|
153
|
+
} finally{
|
|
154
|
+
await reader.cancel().catch(()=>undefined);
|
|
155
|
+
}
|
|
156
|
+
const probedFile = {
|
|
157
|
+
name: file.filename,
|
|
158
|
+
clientUploadContext: file.clientUploadContext,
|
|
159
|
+
data: Buffer.concat(chunks),
|
|
160
|
+
mimetype: response.headers.get('Content-Type') || file.mimeType,
|
|
161
|
+
size: file.size
|
|
162
|
+
};
|
|
163
|
+
try {
|
|
164
|
+
await getImageSize({
|
|
165
|
+
file: probedFile,
|
|
166
|
+
sharp: req.payload.config.sharp
|
|
167
|
+
});
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return probedFile;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Streams the full response body straight to disk so a cloud object of unbounded size never
|
|
175
|
+
* becomes a single in-memory Buffer.
|
|
176
|
+
*/ async function streamResponseToTempFile({ req, response }) {
|
|
177
|
+
const tempFileDir = req.payload.config.upload?.tempFileDir || os.tmpdir();
|
|
178
|
+
await mkdir(tempFileDir, {
|
|
179
|
+
recursive: true
|
|
180
|
+
});
|
|
181
|
+
const tempFilePath = path.join(tempFileDir, `payload-client-upload-${randomUUID()}`);
|
|
182
|
+
// A null body is a legitimate zero-byte file (the Fetch API allows a Response to omit a
|
|
183
|
+
// body entirely), not a failure - `response.arrayBuffer()` tolerated this the same way.
|
|
184
|
+
if (!response.body) {
|
|
185
|
+
await writeFile(tempFilePath, Buffer.alloc(0));
|
|
186
|
+
return tempFilePath;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(tempFilePath));
|
|
190
|
+
} catch (error) {
|
|
191
|
+
await rm(tempFilePath, {
|
|
192
|
+
force: true
|
|
193
|
+
});
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
return tempFilePath;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
//# sourceMappingURL=getFileFromClientUpload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/uploads/getFileFromClientUpload.ts"],"sourcesContent":["import { randomUUID } from 'crypto'\nimport fs from 'fs'\nimport { mkdir, rm, writeFile } from 'fs/promises'\nimport os from 'os'\nimport path from 'path'\nimport { Readable } from 'stream'\nimport { pipeline } from 'stream/promises'\n\nimport type { PayloadRequest } from '../types/index.js'\nimport type { SanitizedUploadConfig, UploadEdits } from './types.js'\n\nimport { APIError } from '../errors/APIError.js'\nimport { isolateObjectProperty } from '../utilities/isolateObjectProperty.js'\nimport { getFileContentRequirement, HEADER_PROBE_BYTE_LENGTH } from './getFileContentRequirement.js'\nimport { getImageSize } from './getImageSize.js'\n\nexport type ClientUploadData = {\n clientUploadContext?: unknown\n collectionSlug: string\n filename: string\n mimeType: string\n size: number\n}\n\n/**\n * `req.context` key `unlinkTempFiles` reads to find a materializer-created temp file once\n * `req.file` is gone - plugin-cloud-storage's afterChange hook clears `req.file` after\n * uploading generated image sizes, before cleanup runs, so tracking it only on `req.file`\n * would leak the temp file.\n */\nexport const CLIENT_UPLOAD_TEMP_FILE_PATH_CONTEXT_KEY = 'payloadClientUploadTempFilePath'\n\nexport async function getFileFromClientUpload({\n file,\n req,\n}: {\n file: ClientUploadData\n req: PayloadRequest\n}): Promise<NonNullable<PayloadRequest['file']>> {\n const uploadConfig = req.payload.collections[file.collectionSlug]?.config.upload\n\n if (!uploadConfig || !uploadConfig.handlers) {\n throw new APIError(`uploadConfig.handlers is not present for ${file.collectionSlug}`)\n }\n\n const contentRequirement = getFileContentRequirement({\n hasSizeEdits: requestHasSizeEdits(req),\n mimeType: file.mimeType,\n uploadConfig,\n })\n\n if (contentRequirement === 'none') {\n return {\n name: file.filename,\n clientUploadContext: file.clientUploadContext,\n data: Buffer.alloc(0),\n mimetype: file.mimeType,\n size: file.size,\n }\n }\n\n if (contentRequirement === 'header') {\n const headerFile = await fetchHeaderOnly({ file, req, uploadConfig })\n if (headerFile) {\n return headerFile\n }\n }\n\n const response = await fetchUploadResponse({ file, req, uploadConfig })\n const tempFilePath = await streamResponseToTempFile({ req, response })\n\n if (!req.context) {\n req.context = {}\n }\n req.context[CLIENT_UPLOAD_TEMP_FILE_PATH_CONTEXT_KEY] = tempFilePath\n\n return {\n name: file.filename,\n clientUploadContext: file.clientUploadContext,\n data: Buffer.alloc(0),\n mimetype: response.headers.get('Content-Type') || file.mimeType,\n size: file.size,\n tempFilePath,\n }\n}\n\n/**\n * Only crop and explicit pixel edits require the original bytes - a focal-point-only\n * change re-derives from data already on the document, not the incoming upload.\n */\nfunction requestHasSizeEdits(req: PayloadRequest): boolean {\n const uploadEdits =\n req.query?.uploadEdits && typeof req.query.uploadEdits === 'object'\n ? (req.query.uploadEdits as UploadEdits)\n : undefined\n\n return Boolean(uploadEdits?.crop || uploadEdits?.heightInPixels || uploadEdits?.widthInPixels)\n}\n\n/**\n * Runs every configured handler and returns the last one that responds, following a single\n * redirect if that response is one - matching the pre-existing v3 handler contract exactly\n * (v3 always ran every handler; only v4 stops at the first response, via an unrelated PR).\n */\nasync function fetchUploadResponse({\n file,\n req,\n uploadConfig,\n}: {\n file: ClientUploadData\n req: PayloadRequest\n uploadConfig: SanitizedUploadConfig\n}): Promise<Response> {\n let response: null | Response = null\n let error: unknown\n\n for (const handler of uploadConfig.handlers!) {\n try {\n const result = await handler(req, {\n doc: null!,\n params: {\n clientUploadContext: file.clientUploadContext,\n collection: file.collectionSlug,\n filename: file.filename,\n },\n })\n\n if (result) {\n response = result\n }\n // If we couldn't get the file from that handler, save the error and try other.\n } catch (err) {\n error = err\n }\n }\n\n if (!response) {\n if (error) {\n req.payload.logger.error(error)\n }\n\n throw new APIError('Expected response from the upload handler.')\n }\n\n if (response.status >= 300 && response.status < 400) {\n const redirectUrl = response.headers.get('Location')\n if (redirectUrl) {\n // Forward the Range header (if any) so a redirect from the header-probe path still\n // fetches only the bounded slice, instead of the whole object, from the redirect target.\n const rangeHeader = req.headers.get('Range')\n response = rangeHeader\n ? await fetch(redirectUrl, { headers: { Range: rangeHeader } })\n : await fetch(redirectUrl)\n }\n }\n\n return response\n}\n\n/**\n * Requests a bounded byte range from the handlers and probes it for image dimensions, without\n * ever reading past `HEADER_PROBE_BYTE_LENGTH`. Returns `null` when the bounded bytes cannot be\n * probed, so the caller falls back to a full streamed fetch.\n */\nasync function fetchHeaderOnly({\n file,\n req,\n uploadConfig,\n}: {\n file: ClientUploadData\n req: PayloadRequest\n uploadConfig: SanitizedUploadConfig\n}): Promise<NonNullable<PayloadRequest['file']> | null> {\n const rangedHeaders = new Headers(req.headers)\n rangedHeaders.set('Range', `bytes=0-${HEADER_PROBE_BYTE_LENGTH - 1}`)\n\n const scopedReq = isolateObjectProperty(req, 'headers')\n scopedReq.headers = rangedHeaders\n\n const response = await fetchUploadResponse({ file, req: scopedReq, uploadConfig })\n\n if (!response.body) {\n return null\n }\n\n const reader = response.body.getReader()\n const chunks: Uint8Array[] = []\n let bytesRead = 0\n\n try {\n while (bytesRead < HEADER_PROBE_BYTE_LENGTH) {\n const { done, value } = await reader.read()\n if (done || !value) {\n break\n }\n\n const remaining = HEADER_PROBE_BYTE_LENGTH - bytesRead\n const chunk = value.length > remaining ? value.slice(0, remaining) : value\n\n chunks.push(chunk)\n bytesRead += chunk.length\n }\n } finally {\n await reader.cancel().catch(() => undefined)\n }\n\n const probedFile = {\n name: file.filename,\n clientUploadContext: file.clientUploadContext,\n data: Buffer.concat(chunks),\n mimetype: response.headers.get('Content-Type') || file.mimeType,\n size: file.size,\n }\n\n try {\n await getImageSize({ file: probedFile, sharp: req.payload.config.sharp })\n } catch {\n return null\n }\n\n return probedFile\n}\n\n/**\n * Streams the full response body straight to disk so a cloud object of unbounded size never\n * becomes a single in-memory Buffer.\n */\nasync function streamResponseToTempFile({\n req,\n response,\n}: {\n req: PayloadRequest\n response: Response\n}): Promise<string> {\n const tempFileDir = req.payload.config.upload?.tempFileDir || os.tmpdir()\n await mkdir(tempFileDir, { recursive: true })\n const tempFilePath = path.join(tempFileDir, `payload-client-upload-${randomUUID()}`)\n\n // A null body is a legitimate zero-byte file (the Fetch API allows a Response to omit a\n // body entirely), not a failure - `response.arrayBuffer()` tolerated this the same way.\n if (!response.body) {\n await writeFile(tempFilePath, Buffer.alloc(0))\n return tempFilePath\n }\n\n try {\n await pipeline(\n Readable.fromWeb(response.body as ReadableStream<Uint8Array>),\n fs.createWriteStream(tempFilePath),\n )\n } catch (error) {\n await rm(tempFilePath, { force: true })\n throw error\n }\n\n return tempFilePath\n}\n"],"names":["randomUUID","fs","mkdir","rm","writeFile","os","path","Readable","pipeline","APIError","isolateObjectProperty","getFileContentRequirement","HEADER_PROBE_BYTE_LENGTH","getImageSize","CLIENT_UPLOAD_TEMP_FILE_PATH_CONTEXT_KEY","getFileFromClientUpload","file","req","uploadConfig","payload","collections","collectionSlug","config","upload","handlers","contentRequirement","hasSizeEdits","requestHasSizeEdits","mimeType","name","filename","clientUploadContext","data","Buffer","alloc","mimetype","size","headerFile","fetchHeaderOnly","response","fetchUploadResponse","tempFilePath","streamResponseToTempFile","context","headers","get","uploadEdits","query","undefined","Boolean","crop","heightInPixels","widthInPixels","error","handler","result","doc","params","collection","err","logger","status","redirectUrl","rangeHeader","fetch","Range","rangedHeaders","Headers","set","scopedReq","body","reader","getReader","chunks","bytesRead","done","value","read","remaining","chunk","length","slice","push","cancel","catch","probedFile","concat","sharp","tempFileDir","tmpdir","recursive","join","fromWeb","createWriteStream","force"],"mappings":"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"}
|