nextjs-cms 0.5.9 → 0.5.11
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/api/axios/axiosInstance.d.ts +1 -1
- package/dist/api/axios/axiosInstance.js +8 -8
- package/dist/api/index.d.ts +855 -855
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +12 -12
- package/dist/api/lib/serverActions.d.ts +239 -239
- package/dist/api/lib/serverActions.d.ts.map +1 -1
- package/dist/api/lib/serverActions.js +834 -834
- package/dist/api/root.d.ts +828 -828
- package/dist/api/root.js +30 -30
- package/dist/api/routers/accountSettings.d.ts +60 -60
- package/dist/api/routers/accountSettings.js +108 -108
- package/dist/api/routers/admins.d.ts +105 -105
- package/dist/api/routers/admins.js +219 -219
- package/dist/api/routers/auth.d.ts +47 -47
- package/dist/api/routers/auth.js +25 -25
- package/dist/api/routers/categorySection.d.ts +103 -103
- package/dist/api/routers/categorySection.js +38 -38
- package/dist/api/routers/cmsSettings.d.ts +48 -48
- package/dist/api/routers/cmsSettings.js +51 -51
- package/dist/api/routers/cpanel.d.ts +83 -83
- package/dist/api/routers/cpanel.js +216 -216
- package/dist/api/routers/files.d.ts +47 -47
- package/dist/api/routers/files.js +23 -23
- package/dist/api/routers/gallery.d.ts +35 -35
- package/dist/api/routers/gallery.js +62 -62
- package/dist/api/routers/googleAnalytics.d.ts +30 -30
- package/dist/api/routers/googleAnalytics.js +7 -7
- package/dist/api/routers/hasItemsSection.d.ts +139 -139
- package/dist/api/routers/hasItemsSection.js +34 -34
- package/dist/api/routers/navigation.d.ts +51 -51
- package/dist/api/routers/navigation.js +11 -11
- package/dist/api/routers/simpleSection.d.ts +57 -57
- package/dist/api/routers/simpleSection.js +12 -12
- package/dist/api/trpc.d.ts +106 -106
- package/dist/api/trpc.js +72 -72
- package/dist/auth/axios/axiosInstance.d.ts +1 -1
- package/dist/auth/axios/axiosInstance.js +8 -8
- package/dist/auth/csrf.d.ts +29 -29
- package/dist/auth/csrf.js +76 -76
- package/dist/auth/hooks/index.d.ts +3 -3
- package/dist/auth/hooks/index.d.ts.map +1 -1
- package/dist/auth/hooks/index.js +3 -3
- package/dist/auth/hooks/useAxiosPrivate.d.ts +4 -4
- package/dist/auth/hooks/useAxiosPrivate.js +74 -74
- package/dist/auth/hooks/useRefreshToken.d.ts +6 -6
- package/dist/auth/hooks/useRefreshToken.js +79 -79
- package/dist/auth/index.d.ts +22 -22
- package/dist/auth/index.js +44 -44
- package/dist/auth/jwt.d.ts +5 -5
- package/dist/auth/jwt.js +25 -25
- package/dist/auth/lib/actions.d.ts +32 -32
- package/dist/auth/lib/actions.d.ts.map +1 -1
- package/dist/auth/lib/actions.js +209 -209
- package/dist/auth/lib/client.d.ts +3 -3
- package/dist/auth/lib/client.js +46 -46
- package/dist/auth/lib/index.d.ts +2 -2
- package/dist/auth/lib/index.d.ts.map +1 -1
- package/dist/auth/lib/index.js +2 -2
- package/dist/auth/react.d.ts +105 -105
- package/dist/auth/react.d.ts.map +1 -1
- package/dist/auth/react.js +347 -347
- package/dist/auth/trpc.d.ts +5 -5
- package/dist/auth/trpc.d.ts.map +1 -1
- package/dist/auth/trpc.js +81 -81
- package/dist/core/config/config-loader.d.ts +91 -91
- package/dist/core/config/config-loader.js +230 -230
- package/dist/core/config/index.d.ts +2 -2
- package/dist/core/config/index.d.ts.map +1 -1
- package/dist/core/config/index.js +1 -1
- package/dist/core/config/loader.d.ts +1 -1
- package/dist/core/config/loader.js +42 -42
- package/dist/core/db/index.d.ts +1 -1
- package/dist/core/db/index.d.ts.map +1 -1
- package/dist/core/db/index.js +1 -1
- package/dist/core/db/table-checker/DbTable.d.ts +5 -5
- package/dist/core/db/table-checker/DbTable.js +5 -5
- package/dist/core/db/table-checker/MysqlTable.d.ts +33 -33
- package/dist/core/db/table-checker/MysqlTable.d.ts.map +1 -1
- package/dist/core/db/table-checker/MysqlTable.js +94 -94
- package/dist/core/db/table-checker/index.d.ts +1 -1
- package/dist/core/db/table-checker/index.d.ts.map +1 -1
- package/dist/core/db/table-checker/index.js +1 -1
- package/dist/core/factories/FieldFactory.d.ts +123 -123
- package/dist/core/factories/FieldFactory.d.ts.map +1 -1
- package/dist/core/factories/FieldFactory.js +411 -411
- package/dist/core/factories/SectionFactory.d.ts +109 -109
- package/dist/core/factories/SectionFactory.d.ts.map +1 -1
- package/dist/core/factories/SectionFactory.js +415 -415
- package/dist/core/factories/index.d.ts +2 -2
- package/dist/core/factories/index.d.ts.map +1 -1
- package/dist/core/factories/index.js +2 -2
- package/dist/core/fields/checkbox.d.ts +62 -62
- package/dist/core/fields/checkbox.d.ts.map +1 -1
- package/dist/core/fields/checkbox.js +62 -62
- package/dist/core/fields/color.d.ts +83 -83
- package/dist/core/fields/color.d.ts.map +1 -1
- package/dist/core/fields/color.js +91 -91
- package/dist/core/fields/date.d.ts +99 -99
- package/dist/core/fields/date.d.ts.map +1 -1
- package/dist/core/fields/date.js +108 -108
- package/dist/core/fields/document.d.ts +179 -179
- package/dist/core/fields/document.d.ts.map +1 -1
- package/dist/core/fields/document.js +277 -277
- package/dist/core/fields/field-group.d.ts +17 -17
- package/dist/core/fields/field-group.d.ts.map +1 -1
- package/dist/core/fields/field-group.js +6 -6
- package/dist/core/fields/field.d.ts +125 -125
- package/dist/core/fields/field.d.ts.map +1 -1
- package/dist/core/fields/field.js +148 -148
- package/dist/core/fields/fileField.d.ts +14 -14
- package/dist/core/fields/fileField.d.ts.map +1 -1
- package/dist/core/fields/fileField.js +5 -5
- package/dist/core/fields/index.d.ts +64 -64
- package/dist/core/fields/index.d.ts.map +1 -1
- package/dist/core/fields/index.js +18 -18
- package/dist/core/fields/map.d.ts +166 -166
- package/dist/core/fields/map.d.ts.map +1 -1
- package/dist/core/fields/map.js +152 -152
- package/dist/core/fields/number.d.ts +185 -185
- package/dist/core/fields/number.d.ts.map +1 -1
- package/dist/core/fields/number.js +241 -241
- package/dist/core/fields/password.d.ts +108 -108
- package/dist/core/fields/password.d.ts.map +1 -1
- package/dist/core/fields/password.js +133 -133
- package/dist/core/fields/photo.d.ts +288 -288
- package/dist/core/fields/photo.d.ts.map +1 -1
- package/dist/core/fields/photo.js +410 -410
- package/dist/core/fields/richText.d.ts +294 -294
- package/dist/core/fields/richText.d.ts.map +1 -1
- package/dist/core/fields/richText.js +338 -338
- package/dist/core/fields/select.d.ts +365 -365
- package/dist/core/fields/select.d.ts.map +1 -1
- package/dist/core/fields/select.js +499 -499
- package/dist/core/fields/selectMultiple.d.ts +235 -235
- package/dist/core/fields/selectMultiple.d.ts.map +1 -1
- package/dist/core/fields/selectMultiple.js +417 -417
- package/dist/core/fields/tags.d.ts +130 -130
- package/dist/core/fields/tags.d.ts.map +1 -1
- package/dist/core/fields/tags.js +105 -105
- package/dist/core/fields/text.d.ts +135 -135
- package/dist/core/fields/text.d.ts.map +1 -1
- package/dist/core/fields/text.js +157 -157
- package/dist/core/fields/textArea.d.ts +106 -106
- package/dist/core/fields/textArea.d.ts.map +1 -1
- package/dist/core/fields/textArea.js +126 -126
- package/dist/core/fields/video.d.ts +147 -147
- package/dist/core/fields/video.d.ts.map +1 -1
- package/dist/core/fields/video.js +248 -248
- package/dist/core/helpers/entity.d.ts +7 -7
- package/dist/core/helpers/entity.js +27 -27
- package/dist/core/helpers/index.d.ts +4 -4
- package/dist/core/helpers/index.d.ts.map +1 -1
- package/dist/core/helpers/index.js +3 -3
- package/dist/core/index.d.ts +7 -7
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -7
- package/dist/core/sections/category.d.ts +282 -282
- package/dist/core/sections/category.d.ts.map +1 -1
- package/dist/core/sections/category.js +147 -147
- package/dist/core/sections/hasItems.d.ts +631 -631
- package/dist/core/sections/hasItems.d.ts.map +1 -1
- package/dist/core/sections/hasItems.js +144 -144
- package/dist/core/sections/index.d.ts +4 -4
- package/dist/core/sections/index.d.ts.map +1 -1
- package/dist/core/sections/index.js +4 -4
- package/dist/core/sections/section.d.ts +225 -225
- package/dist/core/sections/section.d.ts.map +1 -1
- package/dist/core/sections/section.js +341 -341
- package/dist/core/sections/simple.d.ts +98 -98
- package/dist/core/sections/simple.d.ts.map +1 -1
- package/dist/core/sections/simple.js +95 -95
- package/dist/core/security/dom.d.ts +10 -10
- package/dist/core/security/dom.js +92 -92
- package/dist/core/submit/ItemEditSubmit.d.ts +75 -75
- package/dist/core/submit/ItemEditSubmit.js +186 -186
- package/dist/core/submit/NewItemSubmit.d.ts +13 -13
- package/dist/core/submit/NewItemSubmit.js +93 -93
- package/dist/core/submit/SimpleSectionSubmit.d.ts +12 -12
- package/dist/core/submit/SimpleSectionSubmit.js +93 -93
- package/dist/core/submit/index.d.ts +4 -4
- package/dist/core/submit/index.js +4 -4
- package/dist/core/submit/submit.d.ts +115 -115
- package/dist/core/submit/submit.js +479 -479
- package/dist/core/types/index.d.ts +279 -279
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +1 -1
- package/dist/db/client.d.ts +8 -8
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +19 -19
- package/dist/db/config.d.ts +5 -5
- package/dist/db/config.js +22 -22
- package/dist/db/drizzle.config.d.ts +5 -5
- package/dist/db/drizzle.config.js +18 -18
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +3 -3
- package/dist/db/schema.d.ts +638 -638
- package/dist/db/schema.js +73 -73
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -7
- package/dist/translations/index.d.ts +2 -2
- package/dist/translations/index.js +15 -15
- package/dist/utils/CpanelApi.d.ts +24 -24
- package/dist/utils/CpanelApi.js +64 -64
- package/dist/utils/constants.d.ts +13 -13
- package/dist/utils/constants.js +61 -61
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -4
- package/dist/utils/utils.d.ts +59 -59
- package/dist/utils/utils.js +132 -132
- package/dist/validators/checkbox.d.ts +3 -3
- package/dist/validators/checkbox.d.ts.map +1 -1
- package/dist/validators/checkbox.js +12 -12
- package/dist/validators/color.d.ts +3 -3
- package/dist/validators/color.d.ts.map +1 -1
- package/dist/validators/color.js +7 -7
- package/dist/validators/date.d.ts +3 -3
- package/dist/validators/date.d.ts.map +1 -1
- package/dist/validators/date.js +5 -5
- package/dist/validators/document.d.ts +3 -3
- package/dist/validators/document.d.ts.map +1 -1
- package/dist/validators/document.js +57 -57
- package/dist/validators/index.d.ts +14 -14
- package/dist/validators/index.d.ts.map +1 -1
- package/dist/validators/index.js +14 -14
- package/dist/validators/map.d.ts +3 -3
- package/dist/validators/map.d.ts.map +1 -1
- package/dist/validators/map.js +5 -5
- package/dist/validators/number.d.ts +3 -3
- package/dist/validators/number.d.ts.map +1 -1
- package/dist/validators/number.js +20 -20
- package/dist/validators/password.d.ts +3 -3
- package/dist/validators/password.d.ts.map +1 -1
- package/dist/validators/password.js +11 -11
- package/dist/validators/photo.d.ts +3 -3
- package/dist/validators/photo.d.ts.map +1 -1
- package/dist/validators/photo.js +100 -100
- package/dist/validators/richText.d.ts +3 -3
- package/dist/validators/richText.d.ts.map +1 -1
- package/dist/validators/richText.js +8 -8
- package/dist/validators/select-multiple.d.ts +9 -9
- package/dist/validators/select-multiple.d.ts.map +1 -1
- package/dist/validators/select-multiple.js +20 -20
- package/dist/validators/select.d.ts +3 -3
- package/dist/validators/select.d.ts.map +1 -1
- package/dist/validators/select.js +5 -5
- package/dist/validators/text.d.ts +3 -3
- package/dist/validators/text.d.ts.map +1 -1
- package/dist/validators/text.js +7 -7
- package/dist/validators/textarea.d.ts +3 -3
- package/dist/validators/textarea.d.ts.map +1 -1
- package/dist/validators/textarea.js +7 -7
- package/dist/validators/video.d.ts +3 -3
- package/dist/validators/video.d.ts.map +1 -1
- package/dist/validators/video.js +57 -57
- package/package.json +4 -5
|
@@ -1,410 +1,410 @@
|
|
|
1
|
-
import { baseFieldConfigSchema } from
|
|
2
|
-
import { entityKind } from
|
|
3
|
-
import * as z from 'zod';
|
|
4
|
-
import sharp from 'sharp';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import { customAlphabet } from 'nanoid';
|
|
8
|
-
import { FileField } from
|
|
9
|
-
import { humanReadableFileSize } from
|
|
10
|
-
import { getCMSConfig } from
|
|
11
|
-
const cmsConfig = getCMSConfig();
|
|
12
|
-
const sizeSchema = z.strictObject({
|
|
13
|
-
width: z.number().describe('Image width in pixels'),
|
|
14
|
-
height: z.number().describe('Image height in pixels'),
|
|
15
|
-
/**
|
|
16
|
-
* @true The image will not have dimensions constraints,
|
|
17
|
-
* and will be cropped to fit the specified dimensions
|
|
18
|
-
*
|
|
19
|
-
* @false The user will be forced to upload an image with the specified dimensions
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* size: {
|
|
23
|
-
* width: 1200,
|
|
24
|
-
* height: 450,
|
|
25
|
-
* crop: true, // No constraints
|
|
26
|
-
* }
|
|
27
|
-
*/
|
|
28
|
-
crop: z.boolean().describe('Whether to crop the image to fit dimensions'),
|
|
29
|
-
});
|
|
30
|
-
const thumbnailSchema = z.strictObject({
|
|
31
|
-
width: z.number().describe('Thumbnail width in pixels'),
|
|
32
|
-
height: z.number().describe('Thumbnail height in pixels'),
|
|
33
|
-
crop: z.boolean().describe('Whether to crop the thumbnail'),
|
|
34
|
-
quality: z.number().optional().describe('Thumbnail quality (0-100)'),
|
|
35
|
-
});
|
|
36
|
-
const maxFileSizeSchema = z.strictObject({
|
|
37
|
-
size: z.number().describe('Maximum file size'),
|
|
38
|
-
unit: z.enum(['kb', 'mb']).describe('Size unit'),
|
|
39
|
-
});
|
|
40
|
-
const configSchema = z.strictObject({
|
|
41
|
-
/** Whether to add watermark to images */
|
|
42
|
-
watermark: z.boolean().nullable().optional(),
|
|
43
|
-
blurPlaceholder: z.boolean().nullable().optional(),
|
|
44
|
-
size: sizeSchema.optional(),
|
|
45
|
-
thumbnail: thumbnailSchema.optional(),
|
|
46
|
-
/**
|
|
47
|
-
* Maximum file size
|
|
48
|
-
* @example
|
|
49
|
-
* maxFileSize: {
|
|
50
|
-
* size: 512,
|
|
51
|
-
* unit: 'kb',
|
|
52
|
-
* }
|
|
53
|
-
*/
|
|
54
|
-
maxFileSize: maxFileSizeSchema.optional(),
|
|
55
|
-
/**
|
|
56
|
-
* Allowed image types
|
|
57
|
-
* @example
|
|
58
|
-
* type: ['jpeg', 'png', 'webp']
|
|
59
|
-
* @default ['jpeg']
|
|
60
|
-
* @link https://sharp.pixelplumbing.com/api-output#toformat
|
|
61
|
-
* @hint 'jpg' is an alias for 'jpeg'
|
|
62
|
-
*/
|
|
63
|
-
type: z.array(z.enum(['jpeg', 'jpg', 'png', 'webp'])).optional(),
|
|
64
|
-
/**
|
|
65
|
-
* Remove the extension from the file name
|
|
66
|
-
* @default true
|
|
67
|
-
*/
|
|
68
|
-
removeExtension: z.boolean().optional(),
|
|
69
|
-
});
|
|
70
|
-
export class PhotoField extends FileField {
|
|
71
|
-
static [entityKind] = 'PhotoField';
|
|
72
|
-
watermark;
|
|
73
|
-
blurPlaceholder;
|
|
74
|
-
size;
|
|
75
|
-
maxFileSize;
|
|
76
|
-
mimeType;
|
|
77
|
-
extensions;
|
|
78
|
-
thumbnail;
|
|
79
|
-
removeExtension;
|
|
80
|
-
uploadsFolder = cmsConfig.files.upload.uploadPath;
|
|
81
|
-
/**
|
|
82
|
-
* _file is the file object if it's present
|
|
83
|
-
* Whereas the value is the path to the file
|
|
84
|
-
*/
|
|
85
|
-
_file = undefined;
|
|
86
|
-
_sharpImage = undefined;
|
|
87
|
-
_folder;
|
|
88
|
-
_allowedExtensions;
|
|
89
|
-
constructor(config, file) {
|
|
90
|
-
super(config, 'photo');
|
|
91
|
-
if (file) {
|
|
92
|
-
this._file = file;
|
|
93
|
-
}
|
|
94
|
-
this.watermark = config.watermark;
|
|
95
|
-
this.blurPlaceholder = config.blurPlaceholder;
|
|
96
|
-
this.size = config.size;
|
|
97
|
-
this.maxFileSize = config.maxFileSize ?? { size: 2, unit: 'mb' };
|
|
98
|
-
this.thumbnail = config.thumbnail ?? cmsConfig.files.images.thumbnail;
|
|
99
|
-
this.removeExtension = config.removeExtension ?? true;
|
|
100
|
-
this.extensions = config.type ?? ['jpeg'];
|
|
101
|
-
/**
|
|
102
|
-
* Replace 'jpg' with 'jpeg'
|
|
103
|
-
*/
|
|
104
|
-
this.extensions = this.extensions.map((e) => (e === 'jpg' ? 'jpeg' : e));
|
|
105
|
-
/**
|
|
106
|
-
* Extract the mime types from the extensions
|
|
107
|
-
*/
|
|
108
|
-
this.mimeType = this.extensions.map((e) => {
|
|
109
|
-
if (e === 'jpeg')
|
|
110
|
-
return 'image/jpeg';
|
|
111
|
-
if (e === 'png')
|
|
112
|
-
return 'image/png';
|
|
113
|
-
if (e === 'webp')
|
|
114
|
-
return 'image/webp';
|
|
115
|
-
throw new Error(`Invalid image extension provided: ${e}`);
|
|
116
|
-
});
|
|
117
|
-
/**
|
|
118
|
-
* Set the allowed extensions, add jpg if jpeg is present
|
|
119
|
-
*/
|
|
120
|
-
this._allowedExtensions = this.extensions;
|
|
121
|
-
if (this.extensions.includes('jpeg')) {
|
|
122
|
-
this._allowedExtensions.push('jpg');
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
exportForClient() {
|
|
126
|
-
return {
|
|
127
|
-
...super.exportForClient(),
|
|
128
|
-
thumbnail: this.thumbnail,
|
|
129
|
-
// watermark: this.watermark,
|
|
130
|
-
// blurPlaceholder: this.blurPlaceholder,
|
|
131
|
-
size: this.size,
|
|
132
|
-
maxFileSize: this.maxFileSize,
|
|
133
|
-
extensions: this._allowedExtensions,
|
|
134
|
-
mimeType: this.mimeType,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Write the file to the disk
|
|
139
|
-
*/
|
|
140
|
-
async writeToFile() {
|
|
141
|
-
if (!this._folder) {
|
|
142
|
-
throw new Error(`${this.label}: Folder is not set. Make sure to set the folder by calling postSubmit() before writing the file to disk`);
|
|
143
|
-
}
|
|
144
|
-
if (!this._sharpImage) {
|
|
145
|
-
throw new Error(`${this.label}: Image is not set. Make sure to call prepareForSubmission() before writing the file to disk`);
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
/**
|
|
149
|
-
* If .photos, and 'sectionName' folders don't exist, create them
|
|
150
|
-
*/
|
|
151
|
-
const photosFolder = path.join(this.uploadsFolder, '.photos', this._folder);
|
|
152
|
-
const thumbsFolder = path.join(this.uploadsFolder, '.thumbs', this._folder);
|
|
153
|
-
if (!fs.existsSync(photosFolder)) {
|
|
154
|
-
fs.mkdirSync(photosFolder, { recursive: true });
|
|
155
|
-
}
|
|
156
|
-
if (!fs.existsSync(thumbsFolder)) {
|
|
157
|
-
fs.mkdirSync(thumbsFolder, { recursive: true });
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Check if the image needs to be resized and write the file to disk
|
|
161
|
-
*/
|
|
162
|
-
if (this.size && this.size.crop) {
|
|
163
|
-
await this._sharpImage
|
|
164
|
-
.clone()
|
|
165
|
-
.resize({
|
|
166
|
-
width: this.size.width,
|
|
167
|
-
height: this.size.height,
|
|
168
|
-
fit: 'cover',
|
|
169
|
-
})
|
|
170
|
-
.webp()
|
|
171
|
-
.toFile(path.join(this.uploadsFolder, '.photos', this._folder, this.value));
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
await this._sharpImage
|
|
175
|
-
.clone()
|
|
176
|
-
.toFile(path.join(this.uploadsFolder, '.photos', this._folder, this.value));
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Also, write a thumbnail
|
|
180
|
-
*/
|
|
181
|
-
await this._sharpImage
|
|
182
|
-
.clone()
|
|
183
|
-
.resize({
|
|
184
|
-
width: this.thumbnail.width,
|
|
185
|
-
height: this.thumbnail.height,
|
|
186
|
-
fit: this.thumbnail.crop ? 'cover' : 'contain',
|
|
187
|
-
})
|
|
188
|
-
.webp({
|
|
189
|
-
quality: this.thumbnail.quality || 80,
|
|
190
|
-
})
|
|
191
|
-
.toFile(path.join(this.uploadsFolder, '.thumbs', this._folder, this.value));
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
throw new Error(`${this.label}: Error writing file to disk ${error.message}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
async postSubmit(folder) {
|
|
198
|
-
if (!this._file)
|
|
199
|
-
return;
|
|
200
|
-
this._folder = folder;
|
|
201
|
-
await this.writeToFile();
|
|
202
|
-
}
|
|
203
|
-
async postSubmitRollback() {
|
|
204
|
-
if (!this._file)
|
|
205
|
-
return;
|
|
206
|
-
if (!this._folder) {
|
|
207
|
-
throw new Error(`${this.label}: Folder is not set. Make sure to set the folder before writing the file to disk`);
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
const pathToFile = path.join(this.uploadsFolder, '.photos', this._folder, this.value);
|
|
211
|
-
await fs.promises.unlink(pathToFile);
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
throw new Error(`${this.label}: Error deleting file from disk`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Get the value of the field
|
|
219
|
-
*/
|
|
220
|
-
getValue() {
|
|
221
|
-
return this.value;
|
|
222
|
-
}
|
|
223
|
-
setFileName(value) {
|
|
224
|
-
this.value = value;
|
|
225
|
-
}
|
|
226
|
-
setValue(value) {
|
|
227
|
-
if (typeof value === 'string') {
|
|
228
|
-
this.setFileName(value);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
this.setFile(value);
|
|
232
|
-
}
|
|
233
|
-
setFile(file) {
|
|
234
|
-
if (!file || file.size === 0 || file.name?.trim() === '')
|
|
235
|
-
return;
|
|
236
|
-
this._file = file;
|
|
237
|
-
}
|
|
238
|
-
checkRequired() {
|
|
239
|
-
/**
|
|
240
|
-
* Check if the field is required
|
|
241
|
-
* If it is, check if the file is present
|
|
242
|
-
* If it's not, throw an error
|
|
243
|
-
*/
|
|
244
|
-
if (this.required) {
|
|
245
|
-
if (!this._file?.type || !this._file?.name || !this._file?.size) {
|
|
246
|
-
throw new Error(`Field ${this.label} is required`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Prepare the field for submission
|
|
252
|
-
*/
|
|
253
|
-
async prepareForSubmission() {
|
|
254
|
-
/**
|
|
255
|
-
* Check if the file is present
|
|
256
|
-
*/
|
|
257
|
-
if (!this._file)
|
|
258
|
-
return;
|
|
259
|
-
/**
|
|
260
|
-
* Check extension
|
|
261
|
-
*/
|
|
262
|
-
let ext = this._file.name.split('.').pop();
|
|
263
|
-
// Treat jpg as jpeg
|
|
264
|
-
if (ext === 'jpg')
|
|
265
|
-
ext = 'jpeg';
|
|
266
|
-
if (!ext || !this.extensions.includes(ext)) {
|
|
267
|
-
throw new Error(`${this.label}: Invalid file type or extension. Allowed extensions: ${this.extensions.join(', ')}`);
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Construct the image
|
|
271
|
-
*/
|
|
272
|
-
const arrayBuffer = await this._file.arrayBuffer();
|
|
273
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
274
|
-
/**
|
|
275
|
-
* Check mime type
|
|
276
|
-
*/
|
|
277
|
-
if (!this.mimeType.includes(this._file.type)) {
|
|
278
|
-
throw new Error(`${this.label}: Invalid file type or extension. Allowed extensions: ${this.extensions.join(', ')}`);
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Check actual mime type
|
|
282
|
-
*/
|
|
283
|
-
const { fileTypeFromBuffer } = await import('file-type');
|
|
284
|
-
const actualMimeType = await fileTypeFromBuffer(buffer);
|
|
285
|
-
if (!actualMimeType ||
|
|
286
|
-
!this.extensions.includes(actualMimeType.ext) ||
|
|
287
|
-
!this.mimeType.includes(actualMimeType.mime)) {
|
|
288
|
-
throw new Error(`${this.label}: Invalid file type or extension. Allowed extensions: ${this.extensions.join(', ')}`);
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Disable caching for the image to avoid unlink issues
|
|
292
|
-
*/
|
|
293
|
-
sharp.cache({ files: 0 });
|
|
294
|
-
sharp.cache(false);
|
|
295
|
-
const image = sharp(buffer);
|
|
296
|
-
/**
|
|
297
|
-
* Get the metadata
|
|
298
|
-
* Fast access to (uncached) image metadata without decoding any compressed pixel data.
|
|
299
|
-
* This is read from the header of the input image.
|
|
300
|
-
* It does not take into consideration any operations to be applied to the output image, such as resize or rotate.
|
|
301
|
-
* @link: https://sharp.pixelplumbing.com/api-input#metadata
|
|
302
|
-
*/
|
|
303
|
-
const metadata = await image.metadata();
|
|
304
|
-
/**
|
|
305
|
-
* Check the actual file size (buffer size)
|
|
306
|
-
*/
|
|
307
|
-
const fileSize = buffer.length;
|
|
308
|
-
if (!fileSize || !metadata.size) {
|
|
309
|
-
throw new Error(`Field ${this.label} is required`);
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Check the file size
|
|
313
|
-
*/
|
|
314
|
-
if (fileSize > this.maxFileSize.size * (this.maxFileSize.unit === 'kb' ? 1024 : 1024 * 1024)) {
|
|
315
|
-
throw new Error(`${this.label}: File size (${humanReadableFileSize(fileSize)}) exceeds the maximum allowed size of ${this.maxFileSize.size} ${this.maxFileSize.unit}`);
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Don't just trust the file extension
|
|
319
|
-
* Check the format
|
|
320
|
-
*/
|
|
321
|
-
if (!metadata.format || !this.extensions.includes(metadata.format)) {
|
|
322
|
-
throw new Error(`${this.label}: Invalid file type or extension. Allowed extensions: ${this.extensions.join(', ')}`);
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Check stat
|
|
326
|
-
*/
|
|
327
|
-
try {
|
|
328
|
-
await image.stats();
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
throw new Error(`File is corrupted`);
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Convert the image to webp
|
|
335
|
-
*/
|
|
336
|
-
try {
|
|
337
|
-
image.toFormat('webp').withExif({});
|
|
338
|
-
}
|
|
339
|
-
catch (error) {
|
|
340
|
-
throw new Error(`File is corrupted`);
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Check the size
|
|
344
|
-
*/
|
|
345
|
-
if (this.size) {
|
|
346
|
-
/**
|
|
347
|
-
* Check if the image does not need to be cropped (dimensions are constrained)
|
|
348
|
-
*/
|
|
349
|
-
if (!this.size.crop) {
|
|
350
|
-
/**
|
|
351
|
-
* Just resize the image?
|
|
352
|
-
*/
|
|
353
|
-
/**
|
|
354
|
-
* Check if the size matches the required size
|
|
355
|
-
*/
|
|
356
|
-
if (metadata.width !== this.size.width || metadata.height !== this.size.height) {
|
|
357
|
-
throw new Error(`${this.label}: Uploaded image size (${metadata.width}x${metadata.height} pixels) does not match the required size: ${this.size.width}x${this.size.height} pixels`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Generate a random name for the file
|
|
363
|
-
*/
|
|
364
|
-
this.value = customAlphabet('1234567890abcdef', 21)();
|
|
365
|
-
this.value = this.removeExtension ? this.value : this.value + '.webp';
|
|
366
|
-
/*if (saveFilesWithExtensions) {
|
|
367
|
-
/!**
|
|
368
|
-
* Currently we're forcing webp output.
|
|
369
|
-
* I've to add the ability for developers to choose the output format
|
|
370
|
-
*!/
|
|
371
|
-
this.value += '.webp'
|
|
372
|
-
}*/
|
|
373
|
-
/**
|
|
374
|
-
* Set the sharp image
|
|
375
|
-
*/
|
|
376
|
-
this._sharpImage = image;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
const optionsSchema = z.strictObject({
|
|
380
|
-
...baseFieldConfigSchema.shape,
|
|
381
|
-
...configSchema.shape,
|
|
382
|
-
});
|
|
383
|
-
const photoFieldConfigSchema = z.strictObject({
|
|
384
|
-
...optionsSchema.shape,
|
|
385
|
-
type: z.literal('photo').describe('The type of the field'),
|
|
386
|
-
build: z.function().output(z.instanceof(PhotoField)).describe('Build a PhotoField instance from this config'),
|
|
387
|
-
});
|
|
388
|
-
/**
|
|
389
|
-
* Helper function to create a photo field configuration
|
|
390
|
-
* Returns a config object with a build() method that can be serialized and used anywhere
|
|
391
|
-
* @param field
|
|
392
|
-
*/
|
|
393
|
-
export function photoField(field) {
|
|
394
|
-
/**
|
|
395
|
-
* Validate the field config
|
|
396
|
-
*/
|
|
397
|
-
const result = optionsSchema.safeParse(field);
|
|
398
|
-
if (!result.success) {
|
|
399
|
-
throw new Error(`[Field: ${field.name}]: ${z.prettifyError(result.error)}`);
|
|
400
|
-
}
|
|
401
|
-
const config = {
|
|
402
|
-
...field,
|
|
403
|
-
type: 'photo',
|
|
404
|
-
build() {
|
|
405
|
-
// Use the original field config directly (it doesn't have build() method)
|
|
406
|
-
return new PhotoField(field);
|
|
407
|
-
},
|
|
408
|
-
};
|
|
409
|
-
return config;
|
|
410
|
-
}
|
|
1
|
+
import { baseFieldConfigSchema } from './field.js';
|
|
2
|
+
import { entityKind } from '../helpers/index.js';
|
|
3
|
+
import * as z from 'zod';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { customAlphabet } from 'nanoid';
|
|
8
|
+
import { FileField } from './fileField.js';
|
|
9
|
+
import { humanReadableFileSize } from '../../utils/index.js';
|
|
10
|
+
import { getCMSConfig } from '../config/index.js';
|
|
11
|
+
const cmsConfig = getCMSConfig();
|
|
12
|
+
const sizeSchema = z.strictObject({
|
|
13
|
+
width: z.number().describe('Image width in pixels'),
|
|
14
|
+
height: z.number().describe('Image height in pixels'),
|
|
15
|
+
/**
|
|
16
|
+
* @true The image will not have dimensions constraints,
|
|
17
|
+
* and will be cropped to fit the specified dimensions
|
|
18
|
+
*
|
|
19
|
+
* @false The user will be forced to upload an image with the specified dimensions
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* size: {
|
|
23
|
+
* width: 1200,
|
|
24
|
+
* height: 450,
|
|
25
|
+
* crop: true, // No constraints
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
crop: z.boolean().describe('Whether to crop the image to fit dimensions'),
|
|
29
|
+
});
|
|
30
|
+
const thumbnailSchema = z.strictObject({
|
|
31
|
+
width: z.number().describe('Thumbnail width in pixels'),
|
|
32
|
+
height: z.number().describe('Thumbnail height in pixels'),
|
|
33
|
+
crop: z.boolean().describe('Whether to crop the thumbnail'),
|
|
34
|
+
quality: z.number().optional().describe('Thumbnail quality (0-100)'),
|
|
35
|
+
});
|
|
36
|
+
const maxFileSizeSchema = z.strictObject({
|
|
37
|
+
size: z.number().describe('Maximum file size'),
|
|
38
|
+
unit: z.enum(['kb', 'mb']).describe('Size unit'),
|
|
39
|
+
});
|
|
40
|
+
const configSchema = z.strictObject({
|
|
41
|
+
/** Whether to add watermark to images */
|
|
42
|
+
watermark: z.boolean().nullable().optional(),
|
|
43
|
+
blurPlaceholder: z.boolean().nullable().optional(),
|
|
44
|
+
size: sizeSchema.optional(),
|
|
45
|
+
thumbnail: thumbnailSchema.optional(),
|
|
46
|
+
/**
|
|
47
|
+
* Maximum file size
|
|
48
|
+
* @example
|
|
49
|
+
* maxFileSize: {
|
|
50
|
+
* size: 512,
|
|
51
|
+
* unit: 'kb',
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
maxFileSize: maxFileSizeSchema.optional(),
|
|
55
|
+
/**
|
|
56
|
+
* Allowed image types
|
|
57
|
+
* @example
|
|
58
|
+
* type: ['jpeg', 'png', 'webp']
|
|
59
|
+
* @default ['jpeg']
|
|
60
|
+
* @link https://sharp.pixelplumbing.com/api-output#toformat
|
|
61
|
+
* @hint 'jpg' is an alias for 'jpeg'
|
|
62
|
+
*/
|
|
63
|
+
type: z.array(z.enum(['jpeg', 'jpg', 'png', 'webp'])).optional(),
|
|
64
|
+
/**
|
|
65
|
+
* Remove the extension from the file name
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
removeExtension: z.boolean().optional(),
|
|
69
|
+
});
|
|
70
|
+
export class PhotoField extends FileField {
|
|
71
|
+
static [entityKind] = 'PhotoField';
|
|
72
|
+
watermark;
|
|
73
|
+
blurPlaceholder;
|
|
74
|
+
size;
|
|
75
|
+
maxFileSize;
|
|
76
|
+
mimeType;
|
|
77
|
+
extensions;
|
|
78
|
+
thumbnail;
|
|
79
|
+
removeExtension;
|
|
80
|
+
uploadsFolder = cmsConfig.files.upload.uploadPath;
|
|
81
|
+
/**
|
|
82
|
+
* _file is the file object if it's present
|
|
83
|
+
* Whereas the value is the path to the file
|
|
84
|
+
*/
|
|
85
|
+
_file = undefined;
|
|
86
|
+
_sharpImage = undefined;
|
|
87
|
+
_folder;
|
|
88
|
+
_allowedExtensions;
|
|
89
|
+
constructor(config, file) {
|
|
90
|
+
super(config, 'photo');
|
|
91
|
+
if (file) {
|
|
92
|
+
this._file = file;
|
|
93
|
+
}
|
|
94
|
+
this.watermark = config.watermark;
|
|
95
|
+
this.blurPlaceholder = config.blurPlaceholder;
|
|
96
|
+
this.size = config.size;
|
|
97
|
+
this.maxFileSize = config.maxFileSize ?? { size: 2, unit: 'mb' };
|
|
98
|
+
this.thumbnail = config.thumbnail ?? cmsConfig.files.images.thumbnail;
|
|
99
|
+
this.removeExtension = config.removeExtension ?? true;
|
|
100
|
+
this.extensions = config.type ?? ['jpeg'];
|
|
101
|
+
/**
|
|
102
|
+
* Replace 'jpg' with 'jpeg'
|
|
103
|
+
*/
|
|
104
|
+
this.extensions = this.extensions.map((e) => (e === 'jpg' ? 'jpeg' : e));
|
|
105
|
+
/**
|
|
106
|
+
* Extract the mime types from the extensions
|
|
107
|
+
*/
|
|
108
|
+
this.mimeType = this.extensions.map((e) => {
|
|
109
|
+
if (e === 'jpeg')
|
|
110
|
+
return 'image/jpeg';
|
|
111
|
+
if (e === 'png')
|
|
112
|
+
return 'image/png';
|
|
113
|
+
if (e === 'webp')
|
|
114
|
+
return 'image/webp';
|
|
115
|
+
throw new Error(`Invalid image extension provided: ${e}`);
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* Set the allowed extensions, add jpg if jpeg is present
|
|
119
|
+
*/
|
|
120
|
+
this._allowedExtensions = this.extensions;
|
|
121
|
+
if (this.extensions.includes('jpeg')) {
|
|
122
|
+
this._allowedExtensions.push('jpg');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exportForClient() {
|
|
126
|
+
return {
|
|
127
|
+
...super.exportForClient(),
|
|
128
|
+
thumbnail: this.thumbnail,
|
|
129
|
+
// watermark: this.watermark,
|
|
130
|
+
// blurPlaceholder: this.blurPlaceholder,
|
|
131
|
+
size: this.size,
|
|
132
|
+
maxFileSize: this.maxFileSize,
|
|
133
|
+
extensions: this._allowedExtensions,
|
|
134
|
+
mimeType: this.mimeType,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Write the file to the disk
|
|
139
|
+
*/
|
|
140
|
+
async writeToFile() {
|
|
141
|
+
if (!this._folder) {
|
|
142
|
+
throw new Error(`${this.label}: Folder is not set. Make sure to set the folder by calling postSubmit() before writing the file to disk`);
|
|
143
|
+
}
|
|
144
|
+
if (!this._sharpImage) {
|
|
145
|
+
throw new Error(`${this.label}: Image is not set. Make sure to call prepareForSubmission() before writing the file to disk`);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
/**
|
|
149
|
+
* If .photos, and 'sectionName' folders don't exist, create them
|
|
150
|
+
*/
|
|
151
|
+
const photosFolder = path.join(this.uploadsFolder, '.photos', this._folder);
|
|
152
|
+
const thumbsFolder = path.join(this.uploadsFolder, '.thumbs', this._folder);
|
|
153
|
+
if (!fs.existsSync(photosFolder)) {
|
|
154
|
+
fs.mkdirSync(photosFolder, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
if (!fs.existsSync(thumbsFolder)) {
|
|
157
|
+
fs.mkdirSync(thumbsFolder, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if the image needs to be resized and write the file to disk
|
|
161
|
+
*/
|
|
162
|
+
if (this.size && this.size.crop) {
|
|
163
|
+
await this._sharpImage
|
|
164
|
+
.clone()
|
|
165
|
+
.resize({
|
|
166
|
+
width: this.size.width,
|
|
167
|
+
height: this.size.height,
|
|
168
|
+
fit: 'cover',
|
|
169
|
+
})
|
|
170
|
+
.webp()
|
|
171
|
+
.toFile(path.join(this.uploadsFolder, '.photos', this._folder, this.value));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await this._sharpImage
|
|
175
|
+
.clone()
|
|
176
|
+
.toFile(path.join(this.uploadsFolder, '.photos', this._folder, this.value));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Also, write a thumbnail
|
|
180
|
+
*/
|
|
181
|
+
await this._sharpImage
|
|
182
|
+
.clone()
|
|
183
|
+
.resize({
|
|
184
|
+
width: this.thumbnail.width,
|
|
185
|
+
height: this.thumbnail.height,
|
|
186
|
+
fit: this.thumbnail.crop ? 'cover' : 'contain',
|
|
187
|
+
})
|
|
188
|
+
.webp({
|
|
189
|
+
quality: this.thumbnail.quality || 80,
|
|
190
|
+
})
|
|
191
|
+
.toFile(path.join(this.uploadsFolder, '.thumbs', this._folder, this.value));
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw new Error(`${this.label}: Error writing file to disk ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async postSubmit(folder) {
|
|
198
|
+
if (!this._file)
|
|
199
|
+
return;
|
|
200
|
+
this._folder = folder;
|
|
201
|
+
await this.writeToFile();
|
|
202
|
+
}
|
|
203
|
+
async postSubmitRollback() {
|
|
204
|
+
if (!this._file)
|
|
205
|
+
return;
|
|
206
|
+
if (!this._folder) {
|
|
207
|
+
throw new Error(`${this.label}: Folder is not set. Make sure to set the folder before writing the file to disk`);
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const pathToFile = path.join(this.uploadsFolder, '.photos', this._folder, this.value);
|
|
211
|
+
await fs.promises.unlink(pathToFile);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
throw new Error(`${this.label}: Error deleting file from disk`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get the value of the field
|
|
219
|
+
*/
|
|
220
|
+
getValue() {
|
|
221
|
+
return this.value;
|
|
222
|
+
}
|
|
223
|
+
setFileName(value) {
|
|
224
|
+
this.value = value;
|
|
225
|
+
}
|
|
226
|
+
setValue(value) {
|
|
227
|
+
if (typeof value === 'string') {
|
|
228
|
+
this.setFileName(value);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.setFile(value);
|
|
232
|
+
}
|
|
233
|
+
setFile(file) {
|
|
234
|
+
if (!file || file.size === 0 || file.name?.trim() === '')
|
|
235
|
+
return;
|
|
236
|
+
this._file = file;
|
|
237
|
+
}
|
|
238
|
+
checkRequired() {
|
|
239
|
+
/**
|
|
240
|
+
* Check if the field is required
|
|
241
|
+
* If it is, check if the file is present
|
|
242
|
+
* If it's not, throw an error
|
|
243
|
+
*/
|
|
244
|
+
if (this.required) {
|
|
245
|
+
if (!this._file?.type || !this._file?.name || !this._file?.size) {
|
|
246
|
+
throw new Error(`Field ${this.label} is required`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Prepare the field for submission
|
|
252
|
+
*/
|
|
253
|
+
async prepareForSubmission() {
|
|
254
|
+
/**
|
|
255
|
+
* Check if the file is present
|
|
256
|
+
*/
|
|
257
|
+
if (!this._file)
|
|
258
|
+
return;
|
|
259
|
+
/**
|
|
260
|
+
* Check extension
|
|
261
|
+
*/
|
|
262
|
+
let ext = this._file.name.split('.').pop();
|
|
263
|
+
// Treat jpg as jpeg
|
|
264
|
+
if (ext === 'jpg')
|
|
265
|
+
ext = 'jpeg';
|
|
266
|
+
if (!ext || !this.extensions.includes(ext)) {
|
|
267
|
+
throw new Error(`${this.label}: Invalid file type or extension. Allowed extensions: ${this.extensions.join(', ')}`);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Construct the image
|
|
271
|
+
*/
|
|
272
|
+
const arrayBuffer = await this._file.arrayBuffer();
|
|
273
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
274
|
+
/**
|
|
275
|
+
* Check mime type
|
|
276
|
+
*/
|
|
277
|
+
if (!this.mimeType.includes(this._file.type)) {
|
|
278
|
+
throw new Error(`${this.label}: Invalid file type or extension. Allowed extensions: ${this.extensions.join(', ')}`);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Check actual mime type
|
|
282
|
+
*/
|
|
283
|
+
const { fileTypeFromBuffer } = await import('file-type');
|
|
284
|
+
const actualMimeType = await fileTypeFromBuffer(buffer);
|
|
285
|
+
if (!actualMimeType ||
|
|
286
|
+
!this.extensions.includes(actualMimeType.ext) ||
|
|
287
|
+
!this.mimeType.includes(actualMimeType.mime)) {
|
|
288
|
+
throw new Error(`${this.label}: Invalid file type or extension. Allowed extensions: ${this.extensions.join(', ')}`);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Disable caching for the image to avoid unlink issues
|
|
292
|
+
*/
|
|
293
|
+
sharp.cache({ files: 0 });
|
|
294
|
+
sharp.cache(false);
|
|
295
|
+
const image = sharp(buffer);
|
|
296
|
+
/**
|
|
297
|
+
* Get the metadata
|
|
298
|
+
* Fast access to (uncached) image metadata without decoding any compressed pixel data.
|
|
299
|
+
* This is read from the header of the input image.
|
|
300
|
+
* It does not take into consideration any operations to be applied to the output image, such as resize or rotate.
|
|
301
|
+
* @link: https://sharp.pixelplumbing.com/api-input#metadata
|
|
302
|
+
*/
|
|
303
|
+
const metadata = await image.metadata();
|
|
304
|
+
/**
|
|
305
|
+
* Check the actual file size (buffer size)
|
|
306
|
+
*/
|
|
307
|
+
const fileSize = buffer.length;
|
|
308
|
+
if (!fileSize || !metadata.size) {
|
|
309
|
+
throw new Error(`Field ${this.label} is required`);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Check the file size
|
|
313
|
+
*/
|
|
314
|
+
if (fileSize > this.maxFileSize.size * (this.maxFileSize.unit === 'kb' ? 1024 : 1024 * 1024)) {
|
|
315
|
+
throw new Error(`${this.label}: File size (${humanReadableFileSize(fileSize)}) exceeds the maximum allowed size of ${this.maxFileSize.size} ${this.maxFileSize.unit}`);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Don't just trust the file extension
|
|
319
|
+
* Check the format
|
|
320
|
+
*/
|
|
321
|
+
if (!metadata.format || !this.extensions.includes(metadata.format)) {
|
|
322
|
+
throw new Error(`${this.label}: Invalid file type or extension. Allowed extensions: ${this.extensions.join(', ')}`);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Check stat
|
|
326
|
+
*/
|
|
327
|
+
try {
|
|
328
|
+
await image.stats();
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
throw new Error(`File is corrupted`);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Convert the image to webp
|
|
335
|
+
*/
|
|
336
|
+
try {
|
|
337
|
+
image.toFormat('webp').withExif({});
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
throw new Error(`File is corrupted`);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Check the size
|
|
344
|
+
*/
|
|
345
|
+
if (this.size) {
|
|
346
|
+
/**
|
|
347
|
+
* Check if the image does not need to be cropped (dimensions are constrained)
|
|
348
|
+
*/
|
|
349
|
+
if (!this.size.crop) {
|
|
350
|
+
/**
|
|
351
|
+
* Just resize the image?
|
|
352
|
+
*/
|
|
353
|
+
/**
|
|
354
|
+
* Check if the size matches the required size
|
|
355
|
+
*/
|
|
356
|
+
if (metadata.width !== this.size.width || metadata.height !== this.size.height) {
|
|
357
|
+
throw new Error(`${this.label}: Uploaded image size (${metadata.width}x${metadata.height} pixels) does not match the required size: ${this.size.width}x${this.size.height} pixels`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Generate a random name for the file
|
|
363
|
+
*/
|
|
364
|
+
this.value = customAlphabet('1234567890abcdef', 21)();
|
|
365
|
+
this.value = this.removeExtension ? this.value : this.value + '.webp';
|
|
366
|
+
/*if (saveFilesWithExtensions) {
|
|
367
|
+
/!**
|
|
368
|
+
* Currently we're forcing webp output.
|
|
369
|
+
* I've to add the ability for developers to choose the output format
|
|
370
|
+
*!/
|
|
371
|
+
this.value += '.webp'
|
|
372
|
+
}*/
|
|
373
|
+
/**
|
|
374
|
+
* Set the sharp image
|
|
375
|
+
*/
|
|
376
|
+
this._sharpImage = image;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const optionsSchema = z.strictObject({
|
|
380
|
+
...baseFieldConfigSchema.shape,
|
|
381
|
+
...configSchema.shape,
|
|
382
|
+
});
|
|
383
|
+
const photoFieldConfigSchema = z.strictObject({
|
|
384
|
+
...optionsSchema.shape,
|
|
385
|
+
type: z.literal('photo').describe('The type of the field'),
|
|
386
|
+
build: z.function().output(z.instanceof(PhotoField)).describe('Build a PhotoField instance from this config'),
|
|
387
|
+
});
|
|
388
|
+
/**
|
|
389
|
+
* Helper function to create a photo field configuration
|
|
390
|
+
* Returns a config object with a build() method that can be serialized and used anywhere
|
|
391
|
+
* @param field
|
|
392
|
+
*/
|
|
393
|
+
export function photoField(field) {
|
|
394
|
+
/**
|
|
395
|
+
* Validate the field config
|
|
396
|
+
*/
|
|
397
|
+
const result = optionsSchema.safeParse(field);
|
|
398
|
+
if (!result.success) {
|
|
399
|
+
throw new Error(`[Field: ${field.name}]: ${z.prettifyError(result.error)}`);
|
|
400
|
+
}
|
|
401
|
+
const config = {
|
|
402
|
+
...field,
|
|
403
|
+
type: 'photo',
|
|
404
|
+
build() {
|
|
405
|
+
// Use the original field config directly (it doesn't have build() method)
|
|
406
|
+
return new PhotoField(field);
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
return config;
|
|
410
|
+
}
|