img-upload-kit 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -41
- package/MIGRATION.md +192 -0
- package/README.md +138 -154
- package/dist/bin.js +1186 -0
- package/dist/bin.js.map +1 -0
- package/dist/blur.cjs +40 -412
- package/dist/blur.cjs.map +1 -1
- package/dist/blur.d.cts +5 -5
- package/dist/blur.d.ts +5 -5
- package/dist/blur.js +39 -393
- package/dist/blur.js.map +1 -1
- package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
- package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
- package/dist/blurService-D5TpMNji.d.ts +83 -0
- package/dist/blurService-DacwylJx.d.cts +83 -0
- package/dist/chunk-2OVNXHTN.cjs +514 -0
- package/dist/chunk-2OVNXHTN.cjs.map +1 -0
- package/dist/chunk-3ICG2SLL.cjs +135 -0
- package/dist/chunk-3ICG2SLL.cjs.map +1 -0
- package/dist/chunk-4ASFG3KB.js +514 -0
- package/dist/chunk-4ASFG3KB.js.map +1 -0
- package/dist/chunk-AI3JJLSA.js +2370 -0
- package/dist/chunk-AI3JJLSA.js.map +1 -0
- package/dist/chunk-CV23O4YU.js +145 -0
- package/dist/chunk-CV23O4YU.js.map +1 -0
- package/dist/chunk-G7LLSKBV.js +158 -0
- package/dist/chunk-G7LLSKBV.js.map +1 -0
- package/dist/chunk-IB3TEDLR.js +294 -0
- package/dist/chunk-IB3TEDLR.js.map +1 -0
- package/dist/chunk-KJVYWKIG.cjs +158 -0
- package/dist/chunk-KJVYWKIG.cjs.map +1 -0
- package/dist/chunk-NEDBIIE3.cjs +226 -0
- package/dist/chunk-NEDBIIE3.cjs.map +1 -0
- package/dist/chunk-QHU7FUO4.cjs +37 -0
- package/dist/chunk-QHU7FUO4.cjs.map +1 -0
- package/dist/chunk-QQHZJKNA.js +135 -0
- package/dist/chunk-QQHZJKNA.js.map +1 -0
- package/dist/chunk-S2IFAWJK.js +37 -0
- package/dist/chunk-S2IFAWJK.js.map +1 -0
- package/dist/chunk-TT5C7K3Q.js +226 -0
- package/dist/chunk-TT5C7K3Q.js.map +1 -0
- package/dist/chunk-U2TWTEBY.cjs +2370 -0
- package/dist/chunk-U2TWTEBY.cjs.map +1 -0
- package/dist/chunk-WISG74VL.cjs +294 -0
- package/dist/chunk-WISG74VL.cjs.map +1 -0
- package/dist/chunk-YRXXQUDE.cjs +145 -0
- package/dist/chunk-YRXXQUDE.cjs.map +1 -0
- package/dist/cli.cjs +273 -76
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +269 -73
- package/dist/cli.js.map +1 -1
- package/dist/headless.cjs +8 -1880
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +3 -1
- package/dist/headless.d.ts +3 -1
- package/dist/headless.js +25 -1886
- package/dist/headless.js.map +1 -1
- package/dist/index-BIdnqtW8.d.ts +234 -0
- package/dist/index-Brk_uHs6.d.cts +234 -0
- package/dist/{index-FVXXKf2v.d.ts → index-CPMlaQsC.d.ts} +76 -153
- package/dist/{index-FVXXKf2v.d.cts → index-DHINe-rA.d.cts} +76 -153
- package/dist/index.cjs +64 -532
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +63 -504
- package/dist/index.js.map +1 -1
- package/dist/{memoryAdapter-CBu7_3Pp.d.ts → memoryAdapter-Ck0ccZbF.d.cts} +3 -2
- package/dist/{memoryAdapter-BJjyLR-1.d.cts → memoryAdapter-DxEKaG0b.d.ts} +3 -2
- package/dist/placeholders-C4ftwaII.d.ts +5 -0
- package/dist/placeholders-x_3rt65M.d.cts +5 -0
- package/dist/react.cjs +1952 -3257
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +188 -50
- package/dist/react.d.ts +188 -50
- package/dist/react.js +1763 -3013
- package/dist/react.js.map +1 -1
- package/dist/server-handler.cjs +14 -0
- package/dist/server-handler.cjs.map +1 -0
- package/dist/server-handler.d.cts +3 -0
- package/dist/server-handler.d.ts +3 -0
- package/dist/server-handler.js +14 -0
- package/dist/server-handler.js.map +1 -0
- package/dist/server.cjs +1096 -532
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +77 -154
- package/dist/server.d.ts +77 -154
- package/dist/server.js +1050 -448
- package/dist/server.js.map +1 -1
- package/dist/storage.cjs +239 -232
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.d.cts +21 -5
- package/dist/storage.d.ts +21 -5
- package/dist/storage.js +227 -207
- package/dist/storage.js.map +1 -1
- package/dist/styles/animated-image.css +112 -0
- package/dist/styles/dropzone-and-tiles.css +247 -0
- package/dist/styles/modals-and-dock.css +248 -0
- package/dist/styles/primitives.css +197 -0
- package/dist/styles/quality-and-inputs.css +184 -0
- package/dist/styles/tokens.css +123 -0
- package/dist/styles.css +39 -24
- package/dist/types-BrP-o9nd.d.cts +85 -0
- package/dist/types-sZ3KoFQY.d.ts +85 -0
- package/dist/types-yShGNQnW.d.cts +61 -0
- package/dist/types-yShGNQnW.d.ts +61 -0
- package/dist/{types-CiLY0DwB.d.cts → upload-C5Trd6Dh.d.cts} +27 -53
- package/dist/{types-CiLY0DwB.d.ts → upload-C5Trd6Dh.d.ts} +27 -53
- package/package.json +92 -32
- package/dist/blurService-DqsnfPTd.d.cts +0 -172
- package/dist/blurService-DqsnfPTd.d.ts +0 -172
package/dist/server.cjs
CHANGED
|
@@ -1,201 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
var
|
|
17
|
-
|
|
18
|
-
// src/server/handler.ts
|
|
19
|
-
var UploadValidationError = class extends Error {
|
|
20
|
-
status = 400;
|
|
21
|
-
constructor(message) {
|
|
22
|
-
super(message);
|
|
23
|
-
this.name = "UploadValidationError";
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
var VALIDATION_ERROR_SUBSTRINGS = [
|
|
27
|
-
"exceeds maximum",
|
|
28
|
-
"file size",
|
|
29
|
-
"too large",
|
|
30
|
-
"is not allowed",
|
|
31
|
-
"not allowed",
|
|
32
|
-
"unsupported image format",
|
|
33
|
-
"unsupported format",
|
|
34
|
-
"invalid format",
|
|
35
|
-
"invalid image",
|
|
36
|
-
"not a valid image",
|
|
37
|
-
"file type",
|
|
38
|
-
"invalid preset",
|
|
39
|
-
"no file provided",
|
|
40
|
-
"file is empty"
|
|
41
|
-
];
|
|
42
|
-
function isClientValidationError(error) {
|
|
43
|
-
if (!error) return false;
|
|
44
|
-
if (error instanceof UploadValidationError) return true;
|
|
45
|
-
if (typeof error === "object" && error !== null && "status" in error && error.status === 400) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
if (error instanceof Error && (error.name === "UploadValidationError" || error.name === "ValidationError")) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
52
|
-
return VALIDATION_ERROR_SUBSTRINGS.some((sub) => msg.includes(sub));
|
|
53
|
-
}
|
|
54
|
-
function jsonResponse(body, status = 200) {
|
|
55
|
-
return new Response(JSON.stringify(body), {
|
|
56
|
-
status,
|
|
57
|
-
headers: { "Content-Type": "application/json" }
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
function parseJsonRecord(raw) {
|
|
61
|
-
if (!raw) return void 0;
|
|
62
|
-
try {
|
|
63
|
-
const parsed = JSON.parse(raw);
|
|
64
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
65
|
-
const clean = {};
|
|
66
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
67
|
-
if (v !== void 0 && v !== null) {
|
|
68
|
-
clean[k] = String(v);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return Object.keys(clean).length > 0 ? clean : void 0;
|
|
72
|
-
}
|
|
73
|
-
} catch {
|
|
74
|
-
}
|
|
75
|
-
return void 0;
|
|
76
|
-
}
|
|
77
|
-
function createUploadHttpHandler(options) {
|
|
78
|
-
const {
|
|
79
|
-
uploader,
|
|
80
|
-
authorize,
|
|
81
|
-
allowedPresets,
|
|
82
|
-
allowClientBucketOverride = false
|
|
83
|
-
} = options;
|
|
84
|
-
async function POST(request) {
|
|
85
|
-
try {
|
|
86
|
-
if (authorize) {
|
|
87
|
-
const allowed = await authorize(request);
|
|
88
|
-
if (!allowed) {
|
|
89
|
-
return jsonResponse({ error: "Unauthorized" }, 401);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const formData = await request.formData();
|
|
93
|
-
const file = formData.get("file");
|
|
94
|
-
if (!file || typeof file === "string" || typeof file.size === "number" && file.size === 0 && !file.name) {
|
|
95
|
-
return jsonResponse({ error: "No file provided" }, 400);
|
|
96
|
-
}
|
|
97
|
-
if (typeof file.size === "number" && file.size === 0) {
|
|
98
|
-
return jsonResponse({ error: "File is empty" }, 400);
|
|
99
|
-
}
|
|
100
|
-
const preset = formData.get("preset") || void 0;
|
|
101
|
-
if (preset) {
|
|
102
|
-
if (allowedPresets && allowedPresets.length > 0 && !allowedPresets.includes(preset)) {
|
|
103
|
-
return jsonResponse(
|
|
104
|
-
{
|
|
105
|
-
error: `Invalid preset "${preset}". Allowed presets: ${allowedPresets.join(", ")}`
|
|
106
|
-
},
|
|
107
|
-
400
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
if (typeof uploader.resolvePreset === "function" && !uploader.resolvePreset(preset)) {
|
|
111
|
-
return jsonResponse(
|
|
112
|
-
{
|
|
113
|
-
error: `Invalid preset "${preset}".`
|
|
114
|
-
},
|
|
115
|
-
400
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const folder = formData.get("folder") || void 0;
|
|
120
|
-
const bucket = allowClientBucketOverride ? formData.get("bucket") || void 0 : void 0;
|
|
121
|
-
const altText = formData.get("altText") || void 0;
|
|
122
|
-
const customFilename = formData.get("customFilename") || void 0;
|
|
123
|
-
const outputFormatRaw = formData.get("outputFormat");
|
|
124
|
-
if (outputFormatRaw && !["jpeg", "png", "webp", "avif"].includes(outputFormatRaw)) {
|
|
125
|
-
return jsonResponse(
|
|
126
|
-
{
|
|
127
|
-
error: `Invalid output format "${outputFormatRaw}". Allowed formats: jpeg, png, webp, avif`
|
|
128
|
-
},
|
|
129
|
-
400
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
const outputFormat = outputFormatRaw || void 0;
|
|
133
|
-
const qualityRaw = formData.get("quality");
|
|
134
|
-
const quality = qualityRaw !== null ? Math.max(1, Math.min(100, parseInt(qualityRaw, 10) || 85)) : void 0;
|
|
135
|
-
const maxWidthRaw = formData.get("maxWidth");
|
|
136
|
-
const maxHeightRaw = formData.get("maxHeight");
|
|
137
|
-
let maxDimensions;
|
|
138
|
-
if (maxWidthRaw && maxHeightRaw) {
|
|
139
|
-
const w = parseInt(maxWidthRaw, 10);
|
|
140
|
-
const h = parseInt(maxHeightRaw, 10);
|
|
141
|
-
if (w > 0 && h > 0) {
|
|
142
|
-
maxDimensions = { width: w, height: h };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const metadata = parseJsonRecord(
|
|
146
|
-
formData.get("metadata")
|
|
147
|
-
);
|
|
148
|
-
const result = await uploader.uploadImage(file, {
|
|
149
|
-
preset,
|
|
150
|
-
folder,
|
|
151
|
-
bucket,
|
|
152
|
-
altText,
|
|
153
|
-
customFilename,
|
|
154
|
-
outputFormat,
|
|
155
|
-
quality,
|
|
156
|
-
maxDimensions,
|
|
157
|
-
metadata
|
|
158
|
-
});
|
|
159
|
-
return jsonResponse(result, 200);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
const errorMessage = error instanceof Error ? error.message : "Internal server error";
|
|
162
|
-
const status = isClientValidationError(error) ? 400 : 500;
|
|
163
|
-
return jsonResponse({ error: errorMessage }, status);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
async function DELETE(request) {
|
|
167
|
-
try {
|
|
168
|
-
if (authorize) {
|
|
169
|
-
const allowed = await authorize(request);
|
|
170
|
-
if (!allowed) {
|
|
171
|
-
return jsonResponse({ error: "Unauthorized" }, 401);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const { searchParams } = new URL(request.url);
|
|
175
|
-
const url = searchParams.get("url");
|
|
176
|
-
if (url) {
|
|
177
|
-
const deleted = await uploader.deleteByUrl(url);
|
|
178
|
-
return jsonResponse({ success: deleted }, deleted ? 200 : 400);
|
|
179
|
-
}
|
|
180
|
-
const storagePath = searchParams.get("path");
|
|
181
|
-
const bucket = allowClientBucketOverride ? searchParams.get("bucket") || void 0 : void 0;
|
|
182
|
-
if (!storagePath) {
|
|
183
|
-
return jsonResponse(
|
|
184
|
-
{ error: 'Missing required "path" or "url" query parameter' },
|
|
185
|
-
400
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
await uploader.deleteImage(storagePath, bucket);
|
|
189
|
-
return jsonResponse({ success: true }, 200);
|
|
190
|
-
} catch (error) {
|
|
191
|
-
const errorMessage = error instanceof Error ? error.message : "Internal server error";
|
|
192
|
-
return jsonResponse({ error: errorMessage }, 500);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return { POST, DELETE };
|
|
196
|
-
}
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunk2OVNXHTNcjs = require('./chunk-2OVNXHTN.cjs');
|
|
7
|
+
require('./chunk-QHU7FUO4.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
var _chunkNEDBIIE3cjs = require('./chunk-NEDBIIE3.cjs');
|
|
197
17
|
|
|
198
18
|
// src/server/optimization/types.ts
|
|
19
|
+
var DEFAULT_LIMIT_INPUT_PIXELS = 5e7;
|
|
199
20
|
var PRIMARY_AVIF_EFFORT = 6;
|
|
200
21
|
var RETRY_AVIF_EFFORT = 5;
|
|
201
22
|
var BLUR_PLACEHOLDER_WIDTH = 128;
|
|
@@ -206,132 +27,252 @@ var EDGE_COLOR_FALLBACK = "#6B7280";
|
|
|
206
27
|
var EDGE_WHITE_FALLBACK = "#F9FAFB";
|
|
207
28
|
var WHITE_DOMINANCE_RATIO = 1.35;
|
|
208
29
|
|
|
209
|
-
// src/
|
|
210
|
-
var
|
|
211
|
-
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
215
|
-
"
|
|
30
|
+
// src/server/security/sanitizeSvg.ts
|
|
31
|
+
var DEFAULT_MAX_SVG_BYTES = 1024 * 1024;
|
|
32
|
+
var FORBIDDEN_ELEMENTS = [
|
|
33
|
+
"script",
|
|
34
|
+
"foreignObject",
|
|
35
|
+
"iframe",
|
|
36
|
+
"embed",
|
|
37
|
+
"object",
|
|
38
|
+
"applet",
|
|
39
|
+
"meta",
|
|
40
|
+
"link",
|
|
41
|
+
"base",
|
|
42
|
+
"audio",
|
|
43
|
+
"video",
|
|
44
|
+
"canvas",
|
|
45
|
+
"animate",
|
|
46
|
+
"animateMotion",
|
|
47
|
+
"animateTransform",
|
|
48
|
+
"set",
|
|
49
|
+
"handler",
|
|
50
|
+
"listener",
|
|
51
|
+
"tbreak"
|
|
216
52
|
];
|
|
217
|
-
var
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
53
|
+
var SAFE_NAMED_ENTITIES = /* @__PURE__ */ new Set(["amp", "lt", "gt", "quot", "apos"]);
|
|
54
|
+
var SAFE_HREF_FRAGMENT_RE = /^#[a-zA-Z0-9_.:-]+$/;
|
|
55
|
+
var SAFE_DATA_IMAGE_RE = /^data:image\/(?:png|jpeg|webp);base64,[a-zA-Z0-9+/=\s]+$/i;
|
|
56
|
+
function stripControlAndSpace(str) {
|
|
57
|
+
let out = "";
|
|
58
|
+
for (let i = 0; i < str.length; i++) {
|
|
59
|
+
if (str.charCodeAt(i) > 32) out += str[i];
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function decodeHtmlEntitiesForCheck(value) {
|
|
64
|
+
const decoded = value.replace(
|
|
65
|
+
/&#x([0-9a-fA-F]+);?/g,
|
|
66
|
+
(_, hex) => String.fromCodePoint(Number.parseInt(hex, 16) || 0)
|
|
67
|
+
).replace(
|
|
68
|
+
/&#(\d+);?/g,
|
|
69
|
+
(_, dec) => String.fromCodePoint(Number.parseInt(dec, 10) || 0)
|
|
70
|
+
);
|
|
71
|
+
return stripControlAndSpace(decoded);
|
|
72
|
+
}
|
|
73
|
+
function isSafeSvgHref(rawValue) {
|
|
74
|
+
const trimmed = rawValue.trim();
|
|
75
|
+
if (SAFE_HREF_FRAGMENT_RE.test(trimmed)) return true;
|
|
76
|
+
const decoded = decodeHtmlEntitiesForCheck(trimmed);
|
|
77
|
+
if (SAFE_HREF_FRAGMENT_RE.test(decoded)) return true;
|
|
78
|
+
if (SAFE_DATA_IMAGE_RE.test(trimmed) && SAFE_DATA_IMAGE_RE.test(decoded)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function sanitizeCssBlock(css) {
|
|
84
|
+
return css.replace(/@import\b[^;{}]*(?:;|$)/gi, "").replace(/expression\s*\([^)]*\)/gi, "").replace(/(?:javascript|vbscript|data)\s*:[^;)"']*/gi, "").replace(/-moz-binding\s*:[^;}"']*/gi, "").replace(/behavior\s*:[^;}"']*/gi, "").replace(/url\s*\(\s*(['"]?)(.*?)\1\s*\)/gi, (full, _q, target) => {
|
|
85
|
+
const cleaned = decodeHtmlEntitiesForCheck(target.trim());
|
|
86
|
+
return SAFE_HREF_FRAGMENT_RE.test(cleaned) ? full : "none";
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function sanitizeTagAttributes(attrs) {
|
|
90
|
+
const attrRe = /([a-zA-Z_:][\w:.-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
91
|
+
const kept = [];
|
|
92
|
+
for (const match of attrs.matchAll(attrRe)) {
|
|
93
|
+
const rawName = match[1];
|
|
94
|
+
const lowerName = rawName.toLowerCase();
|
|
95
|
+
const localName = lowerName.includes(":") ? lowerName.split(":")[1] : lowerName;
|
|
96
|
+
const rawValue = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(match[2], () => ( match[3])), () => ( match[4])), () => ( ""));
|
|
97
|
+
if (lowerName.startsWith("on") || localName.startsWith("on")) {
|
|
98
|
+
continue;
|
|
238
99
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
allowedFormats: RASTER_ALLOWED_FORMATS,
|
|
247
|
-
recommendationNote: {
|
|
248
|
-
en: "Avatars display small \u2014 Medium quality balances sharpness and speed",
|
|
249
|
-
ar: "\u0627\u0644\u0635\u0648\u0631 \u0627\u0644\u0634\u062E\u0635\u064A\u0629 \u0635\u063A\u064A\u0631\u0629 \u2014 \u0627\u0644\u062C\u0648\u062F\u0629 \u0627\u0644\u0645\u062A\u0648\u0633\u0637\u0629 \u062A\u0648\u0627\u0632\u0646 \u0628\u064A\u0646 \u0627\u0644\u0648\u0636\u0648\u062D \u0648\u0627\u0644\u0633\u0631\u0639\u0629"
|
|
100
|
+
if (localName === "href" || lowerName === "xlink:href" || localName === "src" || localName === "action" || localName === "formaction" || localName === "data") {
|
|
101
|
+
if (!isSafeSvgHref(rawValue)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const escaped = rawValue.replace(/"/g, """);
|
|
105
|
+
kept.push(`${rawName}="${escaped}"`);
|
|
106
|
+
continue;
|
|
250
107
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
recommendedQualityLevel: "high",
|
|
258
|
-
allowedFormats: DEFAULT_ALLOWED_FORMATS
|
|
259
|
-
},
|
|
260
|
-
cover: {
|
|
261
|
-
folder: "covers",
|
|
262
|
-
maxDimensions: { width: 1920, height: 1080 },
|
|
263
|
-
maxSizeMB: 10,
|
|
264
|
-
quality: 88,
|
|
265
|
-
recommendedQualityLevel: "high",
|
|
266
|
-
allowedFormats: DEFAULT_ALLOWED_FORMATS
|
|
267
|
-
},
|
|
268
|
-
hero: {
|
|
269
|
-
folder: "hero",
|
|
270
|
-
maxDimensions: { width: 2560, height: 1440 },
|
|
271
|
-
maxSizeMB: 15,
|
|
272
|
-
quality: 92,
|
|
273
|
-
recommendedQualityLevel: "maximum",
|
|
274
|
-
allowedFormats: DEFAULT_ALLOWED_FORMATS,
|
|
275
|
-
recommendationNote: {
|
|
276
|
-
en: "Full-width hero visuals benefit from Maximum quality to preserve fine detail",
|
|
277
|
-
ar: "\u0627\u0644\u0635\u0648\u0631 \u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629 \u0628\u0639\u0631\u0636 \u0643\u0627\u0645\u0644 \u062A\u0633\u062A\u0641\u064A\u062F \u0645\u0646 \u0627\u0644\u062C\u0648\u062F\u0629 \u0627\u0644\u0642\u0635\u0648\u0649 \u0644\u0644\u062D\u0641\u0627\u0638 \u0639\u0644\u0649 \u0627\u0644\u062A\u0641\u0627\u0635\u064A\u0644 \u0627\u0644\u062F\u0642\u064A\u0642\u0629"
|
|
108
|
+
if (localName === "style") {
|
|
109
|
+
const cleanStyle = sanitizeCssBlock(rawValue).replace(/"/g, "'");
|
|
110
|
+
if (cleanStyle.trim()) {
|
|
111
|
+
kept.push(`style="${cleanStyle}"`);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
278
114
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
115
|
+
const decodedVal = decodeHtmlEntitiesForCheck(rawValue).toLowerCase();
|
|
116
|
+
if (decodedVal.includes("javascript:") || decodedVal.includes("vbscript:") || decodedVal.includes("data:text/") || decodedVal.includes("data:image/svg")) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (/url\s*\(/i.test(rawValue)) {
|
|
120
|
+
const sanitizedUrlAttr = sanitizeCssBlock(rawValue).replace(/"/g, """);
|
|
121
|
+
kept.push(`${rawName}="${sanitizedUrlAttr}"`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (match[2] !== void 0 || match[3] !== void 0 || match[4] !== void 0) {
|
|
125
|
+
kept.push(`${rawName}="${rawValue.replace(/"/g, """)}"`);
|
|
126
|
+
} else {
|
|
127
|
+
kept.push(rawName);
|
|
290
128
|
}
|
|
291
|
-
},
|
|
292
|
-
original: {
|
|
293
|
-
folder: "originals",
|
|
294
|
-
maxDimensions: { width: 4096, height: 4096 },
|
|
295
|
-
maxSizeMB: 25,
|
|
296
|
-
quality: 95,
|
|
297
|
-
recommendedQualityLevel: "maximum",
|
|
298
|
-
allowedFormats: DEFAULT_ALLOWED_FORMATS
|
|
299
129
|
}
|
|
300
|
-
};
|
|
130
|
+
return kept.length > 0 ? ` ${kept.join(" ")}` : "";
|
|
131
|
+
}
|
|
132
|
+
function sanitizeSvg(input, options = {}) {
|
|
133
|
+
const maxBytes = _nullishCoalesce(options.maxBytes, () => ( DEFAULT_MAX_SVG_BYTES));
|
|
134
|
+
const byteLength = typeof input === "string" ? Buffer.byteLength(input, "utf8") : input.length;
|
|
135
|
+
if (byteLength > maxBytes) {
|
|
136
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
137
|
+
`SVG file size (${byteLength} bytes) exceeds maximum allowed SVG size of ${maxBytes} bytes`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
let svg = typeof input === "string" ? input : input.toString("utf8");
|
|
141
|
+
svg = svg.replace(/^\uFEFF/, "");
|
|
142
|
+
svg = svg.replace(/<!DOCTYPE[^[>]*(?:\[[\s\S]*?\]\s*)?>/gi, "");
|
|
143
|
+
svg = svg.replace(/<!ENTITY[\s\S]*?>/gi, "");
|
|
144
|
+
svg = svg.replace(/<!--[\s\S]*?-->/g, "");
|
|
145
|
+
svg = svg.replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "");
|
|
146
|
+
svg = svg.replace(/<\?(?!xml\b)[\s\S]*?\?>/gi, "");
|
|
147
|
+
svg = svg.replace(
|
|
148
|
+
/&([a-zA-Z_][\w.-]*);/g,
|
|
149
|
+
(full, name) => SAFE_NAMED_ENTITIES.has(name) ? full : ""
|
|
150
|
+
);
|
|
151
|
+
const tagPattern = FORBIDDEN_ELEMENTS.join("|");
|
|
152
|
+
const pairedRe = new RegExp(
|
|
153
|
+
`<\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\b[^>]*>[\\s\\S]*?<\\s*\\/\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\s*>`,
|
|
154
|
+
"gi"
|
|
155
|
+
);
|
|
156
|
+
const singleRe = new RegExp(
|
|
157
|
+
`<\\s*\\/?\\s*(?:[a-zA-Z0-9_-]+:)?(?:${tagPattern})\\b[^>]*\\/?>`,
|
|
158
|
+
"gi"
|
|
159
|
+
);
|
|
160
|
+
const htmlNamespaceRe = /<\s*\/?\s*(?:xhtml|html):[a-zA-Z0-9_-]+\b[^>]*\/?>/gi;
|
|
161
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
162
|
+
const prev = svg;
|
|
163
|
+
svg = svg.replace(pairedRe, "").replace(singleRe, "").replace(htmlNamespaceRe, "");
|
|
164
|
+
if (svg === prev) break;
|
|
165
|
+
}
|
|
166
|
+
svg = svg.replace(
|
|
167
|
+
/(<\s*style\b[^>]*>)([\s\S]*?)(<\s*\/\s*style\s*>)/gi,
|
|
168
|
+
(_m, open, body, close) => `${open}${sanitizeCssBlock(body)}${close}`
|
|
169
|
+
);
|
|
170
|
+
svg = svg.replace(
|
|
171
|
+
/<([a-zA-Z_:][\w:.-]*)(\s[^<>]*?)?(\s*\/?)>/g,
|
|
172
|
+
(_full, tagName, attrs, selfClose) => {
|
|
173
|
+
const cleanAttrs = attrs ? sanitizeTagAttributes(attrs) : "";
|
|
174
|
+
return `<${tagName}${cleanAttrs}${selfClose}>`;
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
if (!/<svg[\s>/]/i.test(svg)) {
|
|
178
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
179
|
+
"Invalid SVG: missing root <svg> element after sanitization"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (pairedRe.test(svg) || singleRe.test(svg) || /<!(?:DOCTYPE|ENTITY)\b/i.test(svg) || /\bon[a-z]+\s*=/i.test(svg) || /(?:javascript|vbscript)\s*:/i.test(svg)) {
|
|
183
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
184
|
+
"Invalid SVG: unsafe content remained after sanitization"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return Buffer.from(svg.trim(), "utf8");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/server/optimization/loadSharp.ts
|
|
191
|
+
var SHARP_MISSING_MESSAGE = "img-upload-kit/server requires `sharp` \u2014 run `npm i sharp`";
|
|
192
|
+
var sharpPromise = null;
|
|
193
|
+
function unwrapDefault(mod) {
|
|
194
|
+
const record = mod;
|
|
195
|
+
return record && record.default !== void 0 ? record.default : mod;
|
|
196
|
+
}
|
|
197
|
+
function loadSharp() {
|
|
198
|
+
if (!sharpPromise) {
|
|
199
|
+
sharpPromise = Promise.resolve().then(() => _interopRequireWildcard(require("sharp"))).then(
|
|
200
|
+
(mod) => unwrapDefault(mod),
|
|
201
|
+
(error) => {
|
|
202
|
+
sharpPromise = null;
|
|
203
|
+
throw new Error(SHARP_MISSING_MESSAGE, { cause: error });
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return sharpPromise;
|
|
208
|
+
}
|
|
301
209
|
|
|
302
210
|
// src/server/optimization/imageOptimizer.ts
|
|
211
|
+
var PASSTHROUGH_FORMATS = /* @__PURE__ */ new Set(["jpeg", "png", "webp", "avif"]);
|
|
212
|
+
function extractSvgDimensions(svgText, metaWidth, metaHeight) {
|
|
213
|
+
let width = metaWidth;
|
|
214
|
+
let height = metaHeight;
|
|
215
|
+
if (!width || !height) {
|
|
216
|
+
const svgTagMatch = svgText.match(/<svg\b[^>]*>/i);
|
|
217
|
+
if (svgTagMatch) {
|
|
218
|
+
const tag = svgTagMatch[0];
|
|
219
|
+
const wMatch = tag.match(/\bwidth\s*=\s*["']\s*([\d.]+)(?:px)?\s*["']/i);
|
|
220
|
+
const hMatch = tag.match(/\bheight\s*=\s*["']\s*([\d.]+)(?:px)?\s*["']/i);
|
|
221
|
+
if (!width && wMatch) width = Math.round(parseFloat(wMatch[1]));
|
|
222
|
+
if (!height && hMatch) height = Math.round(parseFloat(hMatch[1]));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!width || !height) {
|
|
226
|
+
const viewBoxMatch = svgText.match(
|
|
227
|
+
/viewBox\s*=\s*["']\s*([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)[\s,]+([\d.-]+)\s*["']/i
|
|
228
|
+
);
|
|
229
|
+
if (viewBoxMatch) {
|
|
230
|
+
if (!width) width = Math.round(parseFloat(viewBoxMatch[3]));
|
|
231
|
+
if (!height) height = Math.round(parseFloat(viewBoxMatch[4]));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
width: width && width > 0 ? width : 600,
|
|
236
|
+
height: height && height > 0 ? height : 600
|
|
237
|
+
};
|
|
238
|
+
}
|
|
303
239
|
async function optimizeImage(buffer, options = {}) {
|
|
304
|
-
const
|
|
240
|
+
const sharp = await loadSharp();
|
|
241
|
+
const maxDimensions = options.maxDimensions || _chunkNEDBIIE3cjs.DEFAULT_IMAGE_DIMENSIONS;
|
|
305
242
|
const { outputFormat, quality } = options;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
243
|
+
const limitInputPixels = _nullishCoalesce(options.limitInputPixels, () => ( DEFAULT_LIMIT_INPUT_PIXELS));
|
|
244
|
+
const primaryEffort = _nullishCoalesce(options.effort, () => ( PRIMARY_AVIF_EFFORT));
|
|
245
|
+
const retryEffort = options.effort !== void 0 ? Math.max(1, options.effort - 1) : RETRY_AVIF_EFFORT;
|
|
246
|
+
let pipeline = sharp(buffer, {
|
|
247
|
+
failOn: "error",
|
|
248
|
+
limitInputPixels
|
|
309
249
|
}).rotate();
|
|
310
|
-
|
|
250
|
+
let metadata;
|
|
251
|
+
try {
|
|
252
|
+
metadata = await pipeline.metadata();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
if (msg.toLowerCase().includes("pixel limit") || msg.toLowerCase().includes("exceeds") || msg.toLowerCase().includes("unsupported image format")) {
|
|
256
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(msg);
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
311
260
|
const originalFormat = metadata.format;
|
|
312
261
|
if (originalFormat === "svg") {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const svgText = buffer.toString("utf8");
|
|
317
|
-
const viewBoxMatch = svgText.match(
|
|
318
|
-
/viewBox\s*=\s*["']\s*([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\s*["']/i
|
|
319
|
-
);
|
|
320
|
-
if (viewBoxMatch) {
|
|
321
|
-
width = Math.round(parseFloat(viewBoxMatch[3]));
|
|
322
|
-
height = Math.round(parseFloat(viewBoxMatch[4]));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
262
|
+
const sanitizedBuffer = sanitizeSvg(buffer);
|
|
263
|
+
const svgText = sanitizedBuffer.toString("utf8");
|
|
264
|
+
const dims = extractSvgDimensions(svgText, metadata.width, metadata.height);
|
|
325
265
|
return {
|
|
326
|
-
buffer,
|
|
327
|
-
width:
|
|
328
|
-
height:
|
|
266
|
+
buffer: sanitizedBuffer,
|
|
267
|
+
width: dims.width,
|
|
268
|
+
height: dims.height,
|
|
329
269
|
format: "svg",
|
|
330
|
-
size:
|
|
270
|
+
size: sanitizedBuffer.length,
|
|
271
|
+
...quality !== void 0 ? { quality } : {}
|
|
331
272
|
};
|
|
332
273
|
}
|
|
333
274
|
const isPhoto = originalFormat === "jpeg" || Boolean(metadata.density && metadata.density > 150);
|
|
334
|
-
let baseQuality = quality
|
|
275
|
+
let baseQuality = _nullishCoalesce(quality, () => ( (isPhoto ? 85 : 88)));
|
|
335
276
|
baseQuality = Math.max(1, Math.min(100, baseQuality));
|
|
336
277
|
pipeline = pipeline.resize(maxDimensions.width, maxDimensions.height, {
|
|
337
278
|
fit: "inside",
|
|
@@ -353,25 +294,26 @@ async function optimizeImage(buffer, options = {}) {
|
|
|
353
294
|
width: outputBuffer.info.width || maxDimensions.width,
|
|
354
295
|
height: outputBuffer.info.height || maxDimensions.height,
|
|
355
296
|
format: outputFormat,
|
|
356
|
-
size: outputBuffer.info.size
|
|
297
|
+
size: outputBuffer.info.size,
|
|
298
|
+
quality: baseQuality
|
|
357
299
|
};
|
|
358
300
|
}
|
|
359
301
|
const avifBuffer = await pipeline.clone().avif({
|
|
360
302
|
quality: baseQuality,
|
|
361
|
-
effort:
|
|
303
|
+
effort: primaryEffort,
|
|
362
304
|
chromaSubsampling: "4:2:0",
|
|
363
305
|
bitdepth: 8
|
|
364
306
|
}).toBuffer({ resolveWithObject: true });
|
|
365
307
|
const avifSize = avifBuffer.info.size;
|
|
366
308
|
const originalSize = buffer.length;
|
|
367
309
|
if (avifSize <= originalSize || outputFormat === "avif") {
|
|
368
|
-
const finalMetadata = await sharp__default.default(avifBuffer.data).metadata();
|
|
369
310
|
return {
|
|
370
311
|
buffer: avifBuffer.data,
|
|
371
|
-
width:
|
|
372
|
-
height:
|
|
312
|
+
width: avifBuffer.info.width || maxDimensions.width,
|
|
313
|
+
height: avifBuffer.info.height || maxDimensions.height,
|
|
373
314
|
format: "avif",
|
|
374
|
-
size: avifSize
|
|
315
|
+
size: avifSize,
|
|
316
|
+
quality: baseQuality
|
|
375
317
|
};
|
|
376
318
|
}
|
|
377
319
|
const scaledQuality = Math.max(
|
|
@@ -380,72 +322,91 @@ async function optimizeImage(buffer, options = {}) {
|
|
|
380
322
|
);
|
|
381
323
|
const avifRetry = await pipeline.clone().avif({
|
|
382
324
|
quality: scaledQuality,
|
|
383
|
-
effort:
|
|
325
|
+
effort: retryEffort,
|
|
384
326
|
chromaSubsampling: "4:2:0",
|
|
385
327
|
bitdepth: 8
|
|
386
328
|
}).toBuffer({ resolveWithObject: true });
|
|
387
329
|
if (avifRetry.info.size <= originalSize) {
|
|
388
|
-
const retryMeta = await sharp__default.default(avifRetry.data).metadata();
|
|
389
330
|
return {
|
|
390
331
|
buffer: avifRetry.data,
|
|
391
|
-
width:
|
|
392
|
-
height:
|
|
332
|
+
width: avifRetry.info.width || maxDimensions.width,
|
|
333
|
+
height: avifRetry.info.height || maxDimensions.height,
|
|
393
334
|
format: "avif",
|
|
394
|
-
size: avifRetry.info.size
|
|
335
|
+
size: avifRetry.info.size,
|
|
336
|
+
quality: scaledQuality
|
|
395
337
|
};
|
|
396
338
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
339
|
+
const isRotated90 = typeof metadata.orientation === "number" && metadata.orientation >= 5 && metadata.orientation <= 8;
|
|
340
|
+
const orientedWidth = isRotated90 ? metadata.height : metadata.width;
|
|
341
|
+
const orientedHeight = isRotated90 ? metadata.width : metadata.height;
|
|
342
|
+
const wasDimensionCapped = (_nullishCoalesce(orientedWidth, () => ( 0))) > maxDimensions.width || (_nullishCoalesce(orientedHeight, () => ( 0))) > maxDimensions.height;
|
|
343
|
+
if (!wasDimensionCapped && PASSTHROUGH_FORMATS.has(_nullishCoalesce(originalFormat, () => ( "")))) {
|
|
344
|
+
const srcFmt = originalFormat;
|
|
345
|
+
let passthrough = pipeline.clone();
|
|
346
|
+
if (srcFmt === "jpeg") {
|
|
347
|
+
passthrough = passthrough.jpeg({ quality: baseQuality, mozjpeg: true });
|
|
348
|
+
} else if (srcFmt === "png") {
|
|
349
|
+
passthrough = passthrough.png({ compressionLevel: 9 });
|
|
350
|
+
} else if (srcFmt === "webp") {
|
|
351
|
+
passthrough = passthrough.webp({ quality: baseQuality, effort: 4 });
|
|
352
|
+
} else {
|
|
353
|
+
passthrough = passthrough.avif({
|
|
354
|
+
quality: baseQuality,
|
|
355
|
+
effort: retryEffort
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
let passthroughOut = await passthrough.toBuffer({ resolveWithObject: true });
|
|
359
|
+
if (srcFmt === "jpeg" && passthroughOut.info.size > originalSize) {
|
|
360
|
+
passthroughOut = await pipeline.clone().jpeg({ quality: 10 }).toBuffer({ resolveWithObject: true });
|
|
361
|
+
}
|
|
401
362
|
return {
|
|
402
|
-
buffer,
|
|
403
|
-
width:
|
|
404
|
-
height:
|
|
363
|
+
buffer: passthroughOut.data,
|
|
364
|
+
width: passthroughOut.info.width || orientedWidth || maxDimensions.width,
|
|
365
|
+
height: passthroughOut.info.height || orientedHeight || maxDimensions.height,
|
|
405
366
|
format: srcFmt,
|
|
406
|
-
size:
|
|
367
|
+
size: passthroughOut.info.size,
|
|
368
|
+
quality: baseQuality
|
|
407
369
|
};
|
|
408
370
|
}
|
|
409
371
|
const webpBuffer = await pipeline.clone().webp({ quality: baseQuality, effort: 6, smartSubsample: true }).toBuffer({ resolveWithObject: true });
|
|
410
372
|
const useWebP = webpBuffer.info.size <= avifRetry.info.size;
|
|
411
|
-
const winner = useWebP ? {
|
|
412
|
-
|
|
373
|
+
const winner = useWebP ? {
|
|
374
|
+
buf: webpBuffer.data,
|
|
375
|
+
fmt: "webp",
|
|
376
|
+
sz: webpBuffer.info.size,
|
|
377
|
+
w: webpBuffer.info.width,
|
|
378
|
+
h: webpBuffer.info.height,
|
|
379
|
+
q: baseQuality
|
|
380
|
+
} : {
|
|
381
|
+
buf: avifRetry.data,
|
|
382
|
+
fmt: "avif",
|
|
383
|
+
sz: avifRetry.info.size,
|
|
384
|
+
w: avifRetry.info.width,
|
|
385
|
+
h: avifRetry.info.height,
|
|
386
|
+
q: scaledQuality
|
|
387
|
+
};
|
|
413
388
|
return {
|
|
414
389
|
buffer: winner.buf,
|
|
415
|
-
width:
|
|
416
|
-
height:
|
|
390
|
+
width: winner.w || maxDimensions.width,
|
|
391
|
+
height: winner.h || maxDimensions.height,
|
|
417
392
|
format: winner.fmt,
|
|
418
|
-
size: winner.sz
|
|
393
|
+
size: winner.sz,
|
|
394
|
+
quality: winner.q
|
|
419
395
|
};
|
|
420
396
|
}
|
|
421
397
|
|
|
422
|
-
// src/
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const padded = expanded.padEnd(6, "0").slice(0, 6);
|
|
427
|
-
const value = Number.parseInt(padded, 16);
|
|
428
|
-
if (Number.isNaN(value)) {
|
|
429
|
-
return { r: 245, g: 245, b: 245 };
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
r: value >> 16 & 255,
|
|
433
|
-
g: value >> 8 & 255,
|
|
434
|
-
b: value & 255
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
function rgbToHex(r, g, b) {
|
|
438
|
-
const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
|
|
439
|
-
const value = clamp(r) << 16 | clamp(g) << 8 | clamp(b);
|
|
440
|
-
return `#${value.toString(16).padStart(6, "0").toUpperCase()}`;
|
|
441
|
-
}
|
|
398
|
+
// src/server/optimization/blurGenerator.ts
|
|
399
|
+
var _blurhash = require('blurhash');
|
|
400
|
+
|
|
401
|
+
// src/server/optimization/colorHistogram.ts
|
|
442
402
|
async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
|
|
403
|
+
const sharp = await loadSharp();
|
|
443
404
|
try {
|
|
444
|
-
const metadata = await
|
|
405
|
+
const metadata = await sharp(buffer).metadata();
|
|
445
406
|
const maxSize = 400;
|
|
446
407
|
const needsResize = metadata.width && metadata.height && (metadata.width > maxSize || metadata.height > maxSize);
|
|
447
408
|
if (needsResize) {
|
|
448
|
-
const resized = await
|
|
409
|
+
const resized = await sharp(buffer).resize(maxSize, maxSize, {
|
|
449
410
|
fit: "inside",
|
|
450
411
|
kernel: "lanczos3",
|
|
451
412
|
withoutEnlargement: true
|
|
@@ -457,15 +418,15 @@ async function extractEnhancedColorFromBuffer(buffer, mode = "balanced") {
|
|
|
457
418
|
mode
|
|
458
419
|
);
|
|
459
420
|
}
|
|
460
|
-
const raw = await
|
|
421
|
+
const raw = await sharp(buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
461
422
|
return extractDominantColorHistogramAdvanced(
|
|
462
423
|
raw.data,
|
|
463
424
|
raw.info.width,
|
|
464
425
|
raw.info.height,
|
|
465
426
|
mode
|
|
466
427
|
);
|
|
467
|
-
} catch {
|
|
468
|
-
const stats = await
|
|
428
|
+
} catch (e2) {
|
|
429
|
+
const stats = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
469
430
|
return extractDominantColorHistogram(stats.data);
|
|
470
431
|
}
|
|
471
432
|
}
|
|
@@ -542,7 +503,7 @@ function extractDominantColorHistogramAdvanced(data, width, height, mode) {
|
|
|
542
503
|
return EDGE_WHITE_FALLBACK;
|
|
543
504
|
}
|
|
544
505
|
const [r, g, b] = dominantColor.split(",").map(Number);
|
|
545
|
-
return rgbToHex(r, g, b);
|
|
506
|
+
return _chunkNEDBIIE3cjs.rgbToHex.call(void 0, r, g, b);
|
|
546
507
|
}
|
|
547
508
|
function extractDominantColorHistogram(data) {
|
|
548
509
|
const colorMap = /* @__PURE__ */ new Map();
|
|
@@ -579,13 +540,14 @@ function extractDominantColorHistogram(data) {
|
|
|
579
540
|
return EDGE_WHITE_FALLBACK;
|
|
580
541
|
}
|
|
581
542
|
const [r, g, b] = dominantColor.split(",").map(Number);
|
|
582
|
-
return rgbToHex(r, g, b);
|
|
543
|
+
return _chunkNEDBIIE3cjs.rgbToHex.call(void 0, r, g, b);
|
|
583
544
|
}
|
|
584
545
|
|
|
585
546
|
// src/server/optimization/edgeColorExtractor.ts
|
|
586
547
|
async function extractEdgeColorsFromBuffer(buffer) {
|
|
548
|
+
const sharp = await loadSharp();
|
|
587
549
|
try {
|
|
588
|
-
const sample = await
|
|
550
|
+
const sample = await sharp(buffer).resize(400, 400, {
|
|
589
551
|
fit: "inside",
|
|
590
552
|
kernel: "lanczos3",
|
|
591
553
|
withoutEnlargement: true
|
|
@@ -595,7 +557,7 @@ async function extractEdgeColorsFromBuffer(buffer) {
|
|
|
595
557
|
sample.info.width,
|
|
596
558
|
sample.info.height
|
|
597
559
|
);
|
|
598
|
-
} catch {
|
|
560
|
+
} catch (e3) {
|
|
599
561
|
try {
|
|
600
562
|
const fallback = await extractEnhancedColorFromBuffer(buffer, "edge");
|
|
601
563
|
return {
|
|
@@ -604,7 +566,7 @@ async function extractEdgeColorsFromBuffer(buffer) {
|
|
|
604
566
|
bottom: fallback,
|
|
605
567
|
left: fallback
|
|
606
568
|
};
|
|
607
|
-
} catch {
|
|
569
|
+
} catch (e4) {
|
|
608
570
|
return {
|
|
609
571
|
top: EDGE_COLOR_FALLBACK,
|
|
610
572
|
right: EDGE_COLOR_FALLBACK,
|
|
@@ -703,7 +665,7 @@ function resolveEdgeColor(map, whiteStrength) {
|
|
|
703
665
|
return dominantColor.toUpperCase();
|
|
704
666
|
}
|
|
705
667
|
const [r, g, b] = dominantColor.split(",").map(Number);
|
|
706
|
-
return rgbToHex(r, g, b);
|
|
668
|
+
return _chunkNEDBIIE3cjs.rgbToHex.call(void 0, r, g, b);
|
|
707
669
|
}
|
|
708
670
|
function blendEdgeColors(edgeColors) {
|
|
709
671
|
const colors = Object.values(edgeColors);
|
|
@@ -712,7 +674,7 @@ function blendEdgeColors(edgeColors) {
|
|
|
712
674
|
}
|
|
713
675
|
const totals = colors.reduce(
|
|
714
676
|
(acc, hex) => {
|
|
715
|
-
const { r, g, b } = hexToRgb(hex || EDGE_COLOR_FALLBACK);
|
|
677
|
+
const { r, g, b } = _chunkNEDBIIE3cjs.hexToRgb.call(void 0, hex || EDGE_COLOR_FALLBACK);
|
|
716
678
|
acc.r += r;
|
|
717
679
|
acc.g += g;
|
|
718
680
|
acc.b += b;
|
|
@@ -721,51 +683,78 @@ function blendEdgeColors(edgeColors) {
|
|
|
721
683
|
{ r: 0, g: 0, b: 0 }
|
|
722
684
|
);
|
|
723
685
|
const count = colors.length;
|
|
724
|
-
return rgbToHex(totals.r / count, totals.g / count, totals.b / count);
|
|
686
|
+
return _chunkNEDBIIE3cjs.rgbToHex.call(void 0, totals.r / count, totals.g / count, totals.b / count);
|
|
725
687
|
}
|
|
726
688
|
|
|
727
689
|
// src/server/optimization/blurGenerator.ts
|
|
728
|
-
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
return blurhash.encode(
|
|
690
|
+
function encodeRawBlurHash(data, width, height, componentX = 4, componentY = 3) {
|
|
691
|
+
const clampedX = Math.max(1, Math.min(Math.round(componentX), width, 9));
|
|
692
|
+
const clampedY = Math.max(1, Math.min(Math.round(componentY), height, 9));
|
|
693
|
+
return _blurhash.encode.call(void 0,
|
|
733
694
|
new Uint8ClampedArray(data),
|
|
734
|
-
|
|
735
|
-
|
|
695
|
+
width,
|
|
696
|
+
height,
|
|
736
697
|
clampedX,
|
|
737
698
|
clampedY
|
|
738
699
|
);
|
|
739
700
|
}
|
|
701
|
+
async function encodeBlurHashString(buffer, componentX = 4, componentY = 3) {
|
|
702
|
+
const sharp = await loadSharp();
|
|
703
|
+
const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
704
|
+
return encodeRawBlurHash(
|
|
705
|
+
data,
|
|
706
|
+
info.width,
|
|
707
|
+
info.height,
|
|
708
|
+
componentX,
|
|
709
|
+
componentY
|
|
710
|
+
);
|
|
711
|
+
}
|
|
740
712
|
async function generateBlurPlaceholder(buffer, options = {}) {
|
|
741
|
-
const targetWidth = options.width
|
|
742
|
-
const fallbackWidth = options.fallbackWidth
|
|
743
|
-
const quality = options.quality
|
|
744
|
-
const maxChars = options.maxDataUrlChars
|
|
713
|
+
const targetWidth = _nullishCoalesce(options.width, () => ( BLUR_PLACEHOLDER_WIDTH));
|
|
714
|
+
const fallbackWidth = _nullishCoalesce(options.fallbackWidth, () => ( BLUR_PLACEHOLDER_FALLBACK_WIDTH));
|
|
715
|
+
const quality = _nullishCoalesce(options.quality, () => ( BLUR_PLACEHOLDER_QUALITY));
|
|
716
|
+
const maxChars = _nullishCoalesce(options.maxDataUrlChars, () => ( BLUR_PLACEHOLDER_MAX_CHARS));
|
|
717
|
+
const sharp = await loadSharp();
|
|
745
718
|
try {
|
|
746
719
|
let processBuffer = buffer;
|
|
747
720
|
const isSvg = /<svg[\s>]/i.test(buffer.toString("utf8", 0, 1024));
|
|
748
721
|
if (isSvg) {
|
|
749
722
|
const normalizedSvg = buffer.toString("utf8").replace(/<svg\b/gi, "<svg").replace(/<\/svg\s*>/gi, "</svg>");
|
|
750
|
-
processBuffer =
|
|
723
|
+
processBuffer = Buffer.from(normalizedSvg, "utf8");
|
|
751
724
|
}
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
725
|
+
const baseSize = Math.max(targetWidth, 128);
|
|
726
|
+
const rawSample = await sharp(processBuffer).rotate().resize(baseSize, baseSize, { fit: "inside", withoutEnlargement: true }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
727
|
+
const rawInput = {
|
|
728
|
+
raw: {
|
|
729
|
+
width: rawSample.info.width,
|
|
730
|
+
height: rawSample.info.height,
|
|
731
|
+
channels: 4
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
const buildPlaceholderDataUrl = async (width) => {
|
|
735
|
+
const webpBuffer = await sharp(rawSample.data, rawInput).resize(width, width, { fit: "inside", withoutEnlargement: true }).webp({ quality, effort: 6, smartSubsample: true }).toBuffer();
|
|
736
|
+
return `data:image/webp;base64,${webpBuffer.toString("base64")}`;
|
|
737
|
+
};
|
|
738
|
+
let dataURL = await buildPlaceholderDataUrl(targetWidth);
|
|
739
|
+
if (dataURL.length > maxChars) {
|
|
740
|
+
dataURL = await buildPlaceholderDataUrl(fallbackWidth);
|
|
760
741
|
}
|
|
761
|
-
const
|
|
762
|
-
|
|
742
|
+
const edgeColors = computeEdgeColorSet(
|
|
743
|
+
rawSample.data,
|
|
744
|
+
rawSample.info.width,
|
|
745
|
+
rawSample.info.height
|
|
746
|
+
);
|
|
763
747
|
const color = blendEdgeColors(edgeColors);
|
|
764
748
|
let blurHash;
|
|
765
749
|
if (options.includeBlurHash) {
|
|
766
750
|
try {
|
|
767
|
-
|
|
768
|
-
|
|
751
|
+
const hashSample = rawSample.info.width <= 32 && rawSample.info.height <= 32 ? rawSample : await sharp(rawSample.data, rawInput).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
752
|
+
blurHash = encodeRawBlurHash(
|
|
753
|
+
hashSample.data,
|
|
754
|
+
hashSample.info.width,
|
|
755
|
+
hashSample.info.height
|
|
756
|
+
);
|
|
757
|
+
} catch (e5) {
|
|
769
758
|
blurHash = void 0;
|
|
770
759
|
}
|
|
771
760
|
}
|
|
@@ -774,13 +763,14 @@ async function generateBlurPlaceholder(buffer, options = {}) {
|
|
|
774
763
|
color,
|
|
775
764
|
...blurHash && { blurHash }
|
|
776
765
|
};
|
|
777
|
-
} catch {
|
|
766
|
+
} catch (e6) {
|
|
778
767
|
return generateFallbackBlur(buffer, options.includeBlurHash);
|
|
779
768
|
}
|
|
780
769
|
}
|
|
781
770
|
var generateBlurHash = generateBlurPlaceholder;
|
|
782
771
|
async function generateFallbackBlur(buffer, includeBlurHash) {
|
|
783
|
-
const
|
|
772
|
+
const sharp = await loadSharp();
|
|
773
|
+
const tinyBuffer = await sharp(buffer).resize(64, 64, { fit: "cover", kernel: "lanczos3" }).blur(1).webp({
|
|
784
774
|
quality: 75,
|
|
785
775
|
effort: 6,
|
|
786
776
|
smartSubsample: false
|
|
@@ -792,58 +782,285 @@ async function generateFallbackBlur(buffer, includeBlurHash) {
|
|
|
792
782
|
if (includeBlurHash) {
|
|
793
783
|
try {
|
|
794
784
|
blurHash = await encodeBlurHashString(tinyBuffer);
|
|
795
|
-
} catch {
|
|
785
|
+
} catch (e7) {
|
|
796
786
|
blurHash = void 0;
|
|
797
787
|
}
|
|
798
788
|
}
|
|
799
789
|
return { dataURL, color, ...blurHash && { blurHash } };
|
|
800
790
|
}
|
|
801
791
|
|
|
792
|
+
// src/server/security/sniffFormat.ts
|
|
793
|
+
var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
|
|
794
|
+
var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "mif1", "msf1"]);
|
|
795
|
+
function readAscii(bytes, start, end) {
|
|
796
|
+
let out = "";
|
|
797
|
+
const limit = Math.min(bytes.length, end);
|
|
798
|
+
for (let i = start; i < limit; i++) {
|
|
799
|
+
out += String.fromCharCode(bytes[i]);
|
|
800
|
+
}
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
803
|
+
function sniffIsoBmff(bytes) {
|
|
804
|
+
if (bytes.length < 12) return null;
|
|
805
|
+
if (readAscii(bytes, 4, 8) !== "ftyp") return null;
|
|
806
|
+
const boxSize = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
|
|
807
|
+
const maxOffset = Math.min(
|
|
808
|
+
bytes.length,
|
|
809
|
+
boxSize >= 16 ? Math.min(boxSize, 128) : 64
|
|
810
|
+
);
|
|
811
|
+
const majorBrand = readAscii(bytes, 8, 12).toLowerCase();
|
|
812
|
+
if (AVIF_BRANDS.has(majorBrand)) {
|
|
813
|
+
return { format: "avif", mime: "image/avif" };
|
|
814
|
+
}
|
|
815
|
+
const brands = [majorBrand];
|
|
816
|
+
for (let offset = 16; offset + 4 <= maxOffset; offset += 4) {
|
|
817
|
+
brands.push(readAscii(bytes, offset, offset + 4).toLowerCase());
|
|
818
|
+
}
|
|
819
|
+
if (brands.some((b) => AVIF_BRANDS.has(b))) {
|
|
820
|
+
return { format: "avif", mime: "image/avif" };
|
|
821
|
+
}
|
|
822
|
+
if (brands.some((b) => HEIF_BRANDS.has(b))) {
|
|
823
|
+
return { format: "heif", mime: "image/heif" };
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
function sniffTextSvg(bytes) {
|
|
828
|
+
const sampleLength = Math.min(bytes.length, 4096);
|
|
829
|
+
for (let i = 0; i < Math.min(sampleLength, 512); i++) {
|
|
830
|
+
if (bytes[i] === 0) return null;
|
|
831
|
+
}
|
|
832
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, sampleLength)).replace(/^\uFEFF/, "").trimStart();
|
|
833
|
+
if (!text.startsWith("<")) return null;
|
|
834
|
+
const stripped = text.replace(/^<\?xml[\s\S]*?\?>\s*/i, "").replace(/<!--[\s\S]*?-->\s*/g, "").replace(/<!DOCTYPE\s+svg[\s\S]*?(?:\]>|>)\s*/i, "").replace(/<!--[\s\S]*?-->\s*/g, "").trimStart();
|
|
835
|
+
if (/^<svg[\s>/]/i.test(stripped) || /<svg[\s>/]/i.test(text)) {
|
|
836
|
+
if (/^<!doctype\s+html\b/i.test(text) || /^<html[\s>]/i.test(stripped)) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
return { format: "svg", mime: "image/svg+xml" };
|
|
840
|
+
}
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
function sniffImageFormat(input) {
|
|
844
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
845
|
+
if (bytes.length < 4) return null;
|
|
846
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
|
|
847
|
+
return { format: "jpeg", mime: "image/jpeg" };
|
|
848
|
+
}
|
|
849
|
+
if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
|
|
850
|
+
return { format: "png", mime: "image/png" };
|
|
851
|
+
}
|
|
852
|
+
if (bytes.length >= 6) {
|
|
853
|
+
const header = readAscii(bytes, 0, 6);
|
|
854
|
+
if (header === "GIF87a" || header === "GIF89a") {
|
|
855
|
+
return { format: "gif", mime: "image/gif" };
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (bytes.length >= 12 && readAscii(bytes, 0, 4) === "RIFF" && readAscii(bytes, 8, 12) === "WEBP") {
|
|
859
|
+
return { format: "webp", mime: "image/webp" };
|
|
860
|
+
}
|
|
861
|
+
const bmff = sniffIsoBmff(bytes);
|
|
862
|
+
if (bmff) return bmff;
|
|
863
|
+
if (bytes[0] === 73 && bytes[1] === 73 && bytes[2] === 42 && bytes[3] === 0 || bytes[0] === 77 && bytes[1] === 77 && bytes[2] === 0 && bytes[3] === 42) {
|
|
864
|
+
return { format: "tiff", mime: "image/tiff" };
|
|
865
|
+
}
|
|
866
|
+
if (bytes.length >= 14 && bytes[0] === 66 && bytes[1] === 77) {
|
|
867
|
+
return { format: "bmp", mime: "image/bmp" };
|
|
868
|
+
}
|
|
869
|
+
if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1 && bytes[3] === 0) {
|
|
870
|
+
return { format: "ico", mime: "image/x-icon" };
|
|
871
|
+
}
|
|
872
|
+
return sniffTextSvg(bytes);
|
|
873
|
+
}
|
|
874
|
+
function assertMagicBytesAllowed(buffer, allowedFormats, allowSvg = false) {
|
|
875
|
+
const sniffed = sniffImageFormat(buffer);
|
|
876
|
+
if (!sniffed) {
|
|
877
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
878
|
+
"Unsupported image format: file content is not a recognized image"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (sniffed.format === "svg" && !allowSvg) {
|
|
882
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
883
|
+
"File type image/svg+xml is not allowed unless allowSvg: true is set"
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (allowedFormats.length > 0 && !allowedFormats.includes(sniffed.mime)) {
|
|
887
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(
|
|
888
|
+
`File type ${sniffed.mime} is not allowed. Allowed: ${allowedFormats.join(", ")}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return sniffed;
|
|
892
|
+
}
|
|
893
|
+
|
|
802
894
|
// src/server/semaphore.ts
|
|
803
|
-
var
|
|
804
|
-
|
|
895
|
+
var SemaphoreQueueFullError = (_class = class extends Error {
|
|
896
|
+
__init() {this.status = 503}
|
|
897
|
+
constructor(message = "Server upload queue is full. Please try again later.") {
|
|
898
|
+
super(message);_class.prototype.__init.call(this);;
|
|
899
|
+
this.name = "SemaphoreQueueFullError";
|
|
900
|
+
}
|
|
901
|
+
}, _class);
|
|
902
|
+
function createAbortError(signal) {
|
|
903
|
+
if (_optionalChain([signal, 'optionalAccess', _2 => _2.reason]) instanceof Error) {
|
|
904
|
+
return signal.reason;
|
|
905
|
+
}
|
|
906
|
+
const err = new Error(
|
|
907
|
+
typeof _optionalChain([signal, 'optionalAccess', _3 => _3.reason]) === "string" ? signal.reason : "Operation aborted"
|
|
908
|
+
);
|
|
909
|
+
err.name = "AbortError";
|
|
910
|
+
return err;
|
|
911
|
+
}
|
|
912
|
+
var AsyncSemaphore = (_class2 = class {
|
|
913
|
+
constructor(concurrency, options) {;_class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);
|
|
805
914
|
this.concurrency = concurrency;
|
|
806
915
|
if (concurrency < 1) {
|
|
807
916
|
throw new Error("AsyncSemaphore concurrency must be >= 1");
|
|
808
917
|
}
|
|
918
|
+
if (typeof options === "number") {
|
|
919
|
+
this.maxQueue = options;
|
|
920
|
+
} else if (options && typeof options === "object") {
|
|
921
|
+
this.maxQueue = options.maxQueue;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
__init2() {this.activeCount = 0}
|
|
926
|
+
|
|
927
|
+
__init3() {this.queue = []}
|
|
928
|
+
get active() {
|
|
929
|
+
return this.activeCount;
|
|
809
930
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
async run(task) {
|
|
814
|
-
|
|
931
|
+
get pending() {
|
|
932
|
+
return this.queue.length;
|
|
933
|
+
}
|
|
934
|
+
async run(task, options = {}) {
|
|
935
|
+
const { signal, timeoutMs, label = "Semaphore task" } = options;
|
|
936
|
+
await this.acquire(signal);
|
|
937
|
+
let rawTaskPromise;
|
|
815
938
|
try {
|
|
816
|
-
|
|
817
|
-
}
|
|
939
|
+
rawTaskPromise = Promise.resolve().then(task);
|
|
940
|
+
} catch (syncError) {
|
|
818
941
|
this.release();
|
|
942
|
+
throw syncError;
|
|
819
943
|
}
|
|
944
|
+
let released = false;
|
|
945
|
+
const releaseOnce = () => {
|
|
946
|
+
if (!released) {
|
|
947
|
+
released = true;
|
|
948
|
+
this.release();
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
void rawTaskPromise.then(
|
|
952
|
+
() => releaseOnce(),
|
|
953
|
+
(err) => {
|
|
954
|
+
const pending = err && typeof err === "object" && "pendingTask" in err ? err.pendingTask : void 0;
|
|
955
|
+
if (pending && typeof pending.then === "function") {
|
|
956
|
+
void pending.then(releaseOnce, releaseOnce);
|
|
957
|
+
} else {
|
|
958
|
+
releaseOnce();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
);
|
|
962
|
+
let callerPromise = rawTaskPromise;
|
|
963
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
964
|
+
callerPromise = withTimeout(callerPromise, timeoutMs, label);
|
|
965
|
+
}
|
|
966
|
+
if (signal) {
|
|
967
|
+
callerPromise = raceWithAbort(callerPromise, signal);
|
|
968
|
+
}
|
|
969
|
+
return callerPromise;
|
|
820
970
|
}
|
|
821
|
-
acquire() {
|
|
971
|
+
acquire(signal) {
|
|
972
|
+
if (_optionalChain([signal, 'optionalAccess', _4 => _4.aborted])) {
|
|
973
|
+
return Promise.reject(createAbortError(signal));
|
|
974
|
+
}
|
|
822
975
|
if (this.activeCount < this.concurrency) {
|
|
823
976
|
this.activeCount += 1;
|
|
824
977
|
return Promise.resolve();
|
|
825
978
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
979
|
+
if (this.maxQueue !== void 0 && this.queue.length >= this.maxQueue) {
|
|
980
|
+
return Promise.reject(new SemaphoreQueueFullError());
|
|
981
|
+
}
|
|
982
|
+
return new Promise((resolve, reject) => {
|
|
983
|
+
const onAbort = () => {
|
|
984
|
+
const idx = this.queue.indexOf(entry);
|
|
985
|
+
if (idx !== -1) {
|
|
986
|
+
this.queue.splice(idx, 1);
|
|
987
|
+
}
|
|
988
|
+
entry.cancel(createAbortError(signal));
|
|
989
|
+
};
|
|
990
|
+
const entry = {
|
|
991
|
+
grant: () => {
|
|
992
|
+
_optionalChain([signal, 'optionalAccess', _5 => _5.removeEventListener, 'call', _6 => _6("abort", onAbort)]);
|
|
993
|
+
this.activeCount += 1;
|
|
994
|
+
resolve();
|
|
995
|
+
},
|
|
996
|
+
cancel: (reason) => {
|
|
997
|
+
_optionalChain([signal, 'optionalAccess', _7 => _7.removeEventListener, 'call', _8 => _8("abort", onAbort)]);
|
|
998
|
+
reject(reason);
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
_optionalChain([signal, 'optionalAccess', _9 => _9.addEventListener, 'call', _10 => _10("abort", onAbort, { once: true })]);
|
|
1002
|
+
this.queue.push(entry);
|
|
831
1003
|
});
|
|
832
1004
|
}
|
|
833
1005
|
release() {
|
|
834
1006
|
this.activeCount = Math.max(0, this.activeCount - 1);
|
|
835
1007
|
const next = this.queue.shift();
|
|
836
|
-
next
|
|
1008
|
+
_optionalChain([next, 'optionalAccess', _11 => _11.grant, 'call', _12 => _12()]);
|
|
1009
|
+
}
|
|
1010
|
+
}, _class2);
|
|
1011
|
+
function raceWithAbort(task, signal) {
|
|
1012
|
+
if (signal.aborted) {
|
|
1013
|
+
const err = createAbortError(signal);
|
|
1014
|
+
err.pendingTask = task.then(
|
|
1015
|
+
() => {
|
|
1016
|
+
},
|
|
1017
|
+
() => {
|
|
1018
|
+
}
|
|
1019
|
+
);
|
|
1020
|
+
return Promise.reject(err);
|
|
837
1021
|
}
|
|
838
|
-
|
|
1022
|
+
return new Promise((resolve, reject) => {
|
|
1023
|
+
const onAbort = () => {
|
|
1024
|
+
const err = createAbortError(signal);
|
|
1025
|
+
err.pendingTask = task.then(
|
|
1026
|
+
() => {
|
|
1027
|
+
},
|
|
1028
|
+
() => {
|
|
1029
|
+
}
|
|
1030
|
+
);
|
|
1031
|
+
reject(err);
|
|
1032
|
+
};
|
|
1033
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1034
|
+
task.then(
|
|
1035
|
+
(val) => {
|
|
1036
|
+
signal.removeEventListener("abort", onAbort);
|
|
1037
|
+
resolve(val);
|
|
1038
|
+
},
|
|
1039
|
+
(err) => {
|
|
1040
|
+
signal.removeEventListener("abort", onAbort);
|
|
1041
|
+
reject(err);
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
839
1046
|
async function withTimeout(task, timeoutMs, label) {
|
|
840
1047
|
let timeoutHandle = null;
|
|
1048
|
+
const settledTask = task.then(
|
|
1049
|
+
() => {
|
|
1050
|
+
},
|
|
1051
|
+
() => {
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
841
1054
|
try {
|
|
842
1055
|
return await Promise.race([
|
|
843
1056
|
task,
|
|
844
1057
|
new Promise((_, reject) => {
|
|
845
1058
|
timeoutHandle = setTimeout(() => {
|
|
846
|
-
|
|
1059
|
+
const err = new Error(
|
|
1060
|
+
`${label} timed out after ${timeoutMs}ms`
|
|
1061
|
+
);
|
|
1062
|
+
err.pendingTask = settledTask;
|
|
1063
|
+
reject(err);
|
|
847
1064
|
}, timeoutMs);
|
|
848
1065
|
})
|
|
849
1066
|
]);
|
|
@@ -853,15 +1070,36 @@ async function withTimeout(task, timeoutMs, label) {
|
|
|
853
1070
|
}
|
|
854
1071
|
}
|
|
855
1072
|
}
|
|
1073
|
+
|
|
1074
|
+
// src/server/uploader/bufferProcessor.ts
|
|
1075
|
+
async function computeSha256Hex(buffer) {
|
|
1076
|
+
const bytes = new Uint8Array(buffer.byteLength);
|
|
1077
|
+
bytes.set(buffer);
|
|
1078
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
1079
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1080
|
+
}
|
|
1081
|
+
function throwIfAborted(signal) {
|
|
1082
|
+
if (!_optionalChain([signal, 'optionalAccess', _13 => _13.aborted])) return;
|
|
1083
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
1084
|
+
const err = new Error(
|
|
1085
|
+
typeof signal.reason === "string" ? signal.reason : "Operation aborted"
|
|
1086
|
+
);
|
|
1087
|
+
err.name = "AbortError";
|
|
1088
|
+
throw err;
|
|
1089
|
+
}
|
|
856
1090
|
async function processImageBuffer(originalBuffer, options = {}) {
|
|
857
|
-
const optTimeout = options.optimizationTimeoutMs
|
|
858
|
-
const blurTimeout = options.blurTimeoutMs
|
|
859
|
-
|
|
1091
|
+
const optTimeout = _nullishCoalesce(options.optimizationTimeoutMs, () => ( 6e4));
|
|
1092
|
+
const blurTimeout = _nullishCoalesce(options.blurTimeoutMs, () => ( 3e4));
|
|
1093
|
+
throwIfAborted(options.signal);
|
|
1094
|
+
const contentHash = await computeSha256Hex(originalBuffer);
|
|
1095
|
+
throwIfAborted(options.signal);
|
|
860
1096
|
const optimized = await withTimeout(
|
|
861
1097
|
optimizeImage(originalBuffer, {
|
|
862
1098
|
maxDimensions: options.maxDimensions,
|
|
863
1099
|
outputFormat: options.outputFormat,
|
|
864
|
-
quality: options.quality
|
|
1100
|
+
quality: options.quality,
|
|
1101
|
+
limitInputPixels: options.limitInputPixels,
|
|
1102
|
+
effort: options.effort
|
|
865
1103
|
}),
|
|
866
1104
|
optTimeout,
|
|
867
1105
|
"Image optimization"
|
|
@@ -874,6 +1112,7 @@ async function processImageBuffer(originalBuffer, options = {}) {
|
|
|
874
1112
|
`Image dimensions missing: width=${optimized.width}, height=${optimized.height}`
|
|
875
1113
|
);
|
|
876
1114
|
}
|
|
1115
|
+
throwIfAborted(options.signal);
|
|
877
1116
|
const blurData = await withTimeout(
|
|
878
1117
|
generateBlurPlaceholder(optimized.buffer, {
|
|
879
1118
|
includeBlurHash: options.includeBlurHash
|
|
@@ -889,22 +1128,12 @@ async function processImageBuffer(originalBuffer, options = {}) {
|
|
|
889
1128
|
};
|
|
890
1129
|
}
|
|
891
1130
|
|
|
892
|
-
// src/
|
|
893
|
-
function
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
return {
|
|
897
|
-
valid: false,
|
|
898
|
-
error: `File size (${(file.size / 1024 / 1024).toFixed(2)}MB) exceeds maximum of ${maxSizeMB}MB`
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
if (allowedFormats.length > 0 && !allowedFormats.includes(file.type)) {
|
|
902
|
-
return {
|
|
903
|
-
valid: false,
|
|
904
|
-
error: `File type ${file.type || "unknown"} is not allowed. Allowed: ${allowedFormats.join(", ")}`
|
|
905
|
-
};
|
|
1131
|
+
// src/server/uploader/pathGenerator.ts
|
|
1132
|
+
function randomShortId() {
|
|
1133
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
|
|
1134
|
+
return globalThis.crypto.randomUUID().split("-")[0];
|
|
906
1135
|
}
|
|
907
|
-
return
|
|
1136
|
+
return Math.random().toString(16).slice(2, 10).padEnd(8, "0");
|
|
908
1137
|
}
|
|
909
1138
|
function formatToExtension(format) {
|
|
910
1139
|
switch (format) {
|
|
@@ -923,16 +1152,20 @@ function formatToExtension(format) {
|
|
|
923
1152
|
function sanitizeSegment(value) {
|
|
924
1153
|
return value.replace(/\\/g, "/").split("/").map((part) => part.trim().replace(/[^a-zA-Z0-9._-]/g, "-")).filter((part) => part && part !== "." && part !== "..").join("/");
|
|
925
1154
|
}
|
|
1155
|
+
function stripFilenameExtension(rawFilename) {
|
|
1156
|
+
const leaf = rawFilename.replace(/^.*[\\/]/, "").trim();
|
|
1157
|
+
const dotIdx = leaf.lastIndexOf(".");
|
|
1158
|
+
const base = dotIdx > 0 ? leaf.slice(0, dotIdx) : leaf;
|
|
1159
|
+
const cleaned = base.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^[.-]+|[.-]+$/g, "");
|
|
1160
|
+
return cleaned || "image";
|
|
1161
|
+
}
|
|
926
1162
|
function createDefaultPathGenerator() {
|
|
927
1163
|
let imageCounter = 0;
|
|
928
1164
|
return (ctx) => {
|
|
929
1165
|
const cleanFolder = ctx.folder ? sanitizeSegment(ctx.folder) : "";
|
|
930
|
-
if (ctx.customFilename
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
cleanName = `${cleanName}.${ctx.extension}`;
|
|
934
|
-
}
|
|
935
|
-
cleanName = cleanName.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
1166
|
+
if (_optionalChain([ctx, 'access', _14 => _14.customFilename, 'optionalAccess', _15 => _15.trim, 'call', _16 => _16()])) {
|
|
1167
|
+
const baseName = stripFilenameExtension(ctx.customFilename);
|
|
1168
|
+
const cleanName = `${baseName}.${ctx.extension}`;
|
|
936
1169
|
const fullPath2 = cleanFolder ? `${cleanFolder}/${cleanName}` : cleanName;
|
|
937
1170
|
return {
|
|
938
1171
|
bucket: ctx.bucket,
|
|
@@ -947,7 +1180,7 @@ function createDefaultPathGenerator() {
|
|
|
947
1180
|
imageCounter = (imageCounter + 1) % 1e4;
|
|
948
1181
|
const seq = String(imageCounter).padStart(4, "0");
|
|
949
1182
|
const timestamp = Date.now();
|
|
950
|
-
const randomSuffix =
|
|
1183
|
+
const randomSuffix = randomShortId();
|
|
951
1184
|
const prefix = sanitizeSegment(
|
|
952
1185
|
ctx.preset || cleanFolder.split("/")[0] || "img"
|
|
953
1186
|
);
|
|
@@ -960,10 +1193,21 @@ function createDefaultPathGenerator() {
|
|
|
960
1193
|
};
|
|
961
1194
|
};
|
|
962
1195
|
}
|
|
1196
|
+
|
|
1197
|
+
// src/server/uploader/uploadRetry.ts
|
|
1198
|
+
var _promises = require('fs/promises'); var _promises2 = _interopRequireDefault(_promises);
|
|
1199
|
+
var _path = require('path'); var _path2 = _interopRequireDefault(_path);
|
|
1200
|
+
function randomRetrySuffix() {
|
|
1201
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
|
|
1202
|
+
return globalThis.crypto.randomUUID().split("-")[0];
|
|
1203
|
+
}
|
|
1204
|
+
return Math.random().toString(16).slice(2, 10);
|
|
1205
|
+
}
|
|
963
1206
|
function isCollisionError(e) {
|
|
964
1207
|
if (!e) return false;
|
|
965
1208
|
const code = typeof e === "object" && e !== null && "code" in e ? String(e.code).toUpperCase() : "";
|
|
966
|
-
|
|
1209
|
+
const statusCode = typeof e === "object" && e !== null && "$metadata" in e ? _optionalChain([e, 'access', _17 => _17.$metadata, 'optionalAccess', _18 => _18.httpStatusCode]) : typeof e === "object" && e !== null && "statusCode" in e ? Number(e.statusCode) : void 0;
|
|
1210
|
+
if (code === "EEXIST" || code === "PRECONDITIONFAILED" || statusCode === 412) {
|
|
967
1211
|
return true;
|
|
968
1212
|
}
|
|
969
1213
|
if (code === "ENOENT") {
|
|
@@ -973,7 +1217,7 @@ function isCollisionError(e) {
|
|
|
973
1217
|
if (msg.includes("does not exist") || msg.includes("doesn't exist") || msg.includes("not exist") || msg.includes("enoent")) {
|
|
974
1218
|
return false;
|
|
975
1219
|
}
|
|
976
|
-
return msg.includes("already exists") || msg.includes("already_exists") || msg.includes("eexist") || msg.includes("file exists");
|
|
1220
|
+
return msg.includes("already exists") || msg.includes("already_exists") || msg.includes("eexist") || msg.includes("file exists") || msg.includes("precondition failed");
|
|
977
1221
|
}
|
|
978
1222
|
async function uploadWithCollisionRetry(params) {
|
|
979
1223
|
const {
|
|
@@ -984,6 +1228,7 @@ async function uploadWithCollisionRetry(params) {
|
|
|
984
1228
|
contentType,
|
|
985
1229
|
maxRetries = 3
|
|
986
1230
|
} = params;
|
|
1231
|
+
const isServerCustomName = Boolean(pathContext.customFilename) && !pathContext.isClientFilename;
|
|
987
1232
|
let storageTarget = buildPath(pathContext);
|
|
988
1233
|
let uploadResult;
|
|
989
1234
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
@@ -993,14 +1238,14 @@ async function uploadWithCollisionRetry(params) {
|
|
|
993
1238
|
path: storageTarget.path,
|
|
994
1239
|
data,
|
|
995
1240
|
contentType,
|
|
996
|
-
upsert:
|
|
1241
|
+
upsert: isServerCustomName,
|
|
997
1242
|
contentHash: pathContext.contentHash,
|
|
998
1243
|
metadata: pathContext.metadata
|
|
999
1244
|
});
|
|
1000
1245
|
break;
|
|
1001
1246
|
} catch (e) {
|
|
1002
1247
|
if (isCollisionError(e)) {
|
|
1003
|
-
if (
|
|
1248
|
+
if (isServerCustomName) {
|
|
1004
1249
|
uploadResult = await storage.upload({
|
|
1005
1250
|
bucket: storageTarget.bucket,
|
|
1006
1251
|
path: storageTarget.path,
|
|
@@ -1012,6 +1257,14 @@ async function uploadWithCollisionRetry(params) {
|
|
|
1012
1257
|
});
|
|
1013
1258
|
break;
|
|
1014
1259
|
}
|
|
1260
|
+
if (pathContext.customFilename && pathContext.isClientFilename) {
|
|
1261
|
+
const base = stripFilenameExtension(pathContext.customFilename);
|
|
1262
|
+
storageTarget = buildPath({
|
|
1263
|
+
...pathContext,
|
|
1264
|
+
customFilename: `${base}-${randomRetrySuffix()}`
|
|
1265
|
+
});
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1015
1268
|
storageTarget = buildPath({
|
|
1016
1269
|
...pathContext,
|
|
1017
1270
|
customFilename: void 0
|
|
@@ -1026,23 +1279,233 @@ async function uploadWithCollisionRetry(params) {
|
|
|
1026
1279
|
}
|
|
1027
1280
|
return { uploadResult, storageTarget };
|
|
1028
1281
|
}
|
|
1029
|
-
function mirrorBufferLocally(options) {
|
|
1030
|
-
const {
|
|
1282
|
+
async function mirrorBufferLocally(options) {
|
|
1283
|
+
const {
|
|
1284
|
+
buffer,
|
|
1285
|
+
filename,
|
|
1286
|
+
storagePath,
|
|
1287
|
+
fallbackUrl,
|
|
1288
|
+
mirrorDir,
|
|
1289
|
+
publicUrlBase
|
|
1290
|
+
} = options;
|
|
1031
1291
|
if (!mirrorDir) {
|
|
1032
1292
|
return fallbackUrl;
|
|
1033
1293
|
}
|
|
1294
|
+
const relativeTarget = (storagePath || filename).replace(/^\/+/, "");
|
|
1034
1295
|
try {
|
|
1035
|
-
|
|
1036
|
-
|
|
1296
|
+
const resolvedBase = _path2.default.resolve(mirrorDir);
|
|
1297
|
+
const destPath = _path2.default.resolve(resolvedBase, relativeTarget);
|
|
1298
|
+
const rel = _path2.default.relative(resolvedBase, destPath);
|
|
1299
|
+
if (rel.startsWith("..") || _path2.default.isAbsolute(rel)) {
|
|
1300
|
+
return fallbackUrl;
|
|
1037
1301
|
}
|
|
1038
|
-
|
|
1302
|
+
const destDir = _path2.default.dirname(destPath);
|
|
1303
|
+
await _promises2.default.mkdir(destDir, { recursive: true });
|
|
1304
|
+
await _promises2.default.writeFile(destPath, buffer);
|
|
1039
1305
|
if (publicUrlBase) {
|
|
1040
|
-
return `${publicUrlBase.replace(/\/+$/, "")}/${
|
|
1306
|
+
return `${publicUrlBase.replace(/\/+$/, "")}/${relativeTarget}`;
|
|
1041
1307
|
}
|
|
1042
|
-
} catch {
|
|
1308
|
+
} catch (e8) {
|
|
1043
1309
|
}
|
|
1044
1310
|
return fallbackUrl;
|
|
1045
1311
|
}
|
|
1312
|
+
async function deleteMirroredFiles(mirrorDirs, storagePath) {
|
|
1313
|
+
if (!mirrorDirs.length || !storagePath) return;
|
|
1314
|
+
const relativeTarget = storagePath.replace(/^\/+/, "");
|
|
1315
|
+
if (!relativeTarget) return;
|
|
1316
|
+
await Promise.all(
|
|
1317
|
+
mirrorDirs.map(async (dir) => {
|
|
1318
|
+
try {
|
|
1319
|
+
const resolvedBase = _path2.default.resolve(dir);
|
|
1320
|
+
const destPath = _path2.default.resolve(resolvedBase, relativeTarget);
|
|
1321
|
+
const rel = _path2.default.relative(resolvedBase, destPath);
|
|
1322
|
+
if (rel.startsWith("..") || _path2.default.isAbsolute(rel)) return;
|
|
1323
|
+
await _promises2.default.unlink(destPath);
|
|
1324
|
+
} catch (e9) {
|
|
1325
|
+
}
|
|
1326
|
+
})
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/server/uploader/variantGenerator.ts
|
|
1331
|
+
function buildVariantStoragePath(basePath, width) {
|
|
1332
|
+
const dotIdx = basePath.lastIndexOf(".");
|
|
1333
|
+
const slashIdx = basePath.lastIndexOf("/");
|
|
1334
|
+
if (dotIdx > slashIdx && dotIdx > 0) {
|
|
1335
|
+
return `${basePath.slice(0, dotIdx)}-${width}w${basePath.slice(dotIdx)}`;
|
|
1336
|
+
}
|
|
1337
|
+
return `${basePath}-${width}w`;
|
|
1338
|
+
}
|
|
1339
|
+
function resolveEffectiveAllowedFormats(baseFormats, allowSvg, explicitAllowSvg) {
|
|
1340
|
+
if (!allowSvg) return baseFormats.filter((fmt) => fmt !== _chunkNEDBIIE3cjs.SVG_MIME_TYPE);
|
|
1341
|
+
if (explicitAllowSvg && !baseFormats.includes(_chunkNEDBIIE3cjs.SVG_MIME_TYPE)) {
|
|
1342
|
+
return [...baseFormats, _chunkNEDBIIE3cjs.SVG_MIME_TYPE];
|
|
1343
|
+
}
|
|
1344
|
+
return baseFormats;
|
|
1345
|
+
}
|
|
1346
|
+
async function generateImageVariants(params) {
|
|
1347
|
+
const {
|
|
1348
|
+
sourceBuffer,
|
|
1349
|
+
format,
|
|
1350
|
+
quality,
|
|
1351
|
+
effort,
|
|
1352
|
+
limitInputPixels,
|
|
1353
|
+
mainWidth,
|
|
1354
|
+
mainHeight,
|
|
1355
|
+
mainUrl,
|
|
1356
|
+
baseStoragePath,
|
|
1357
|
+
baseFilename,
|
|
1358
|
+
bucket,
|
|
1359
|
+
storage,
|
|
1360
|
+
variantWidths,
|
|
1361
|
+
upsert = false,
|
|
1362
|
+
contentHash,
|
|
1363
|
+
metadata,
|
|
1364
|
+
mirrorDir,
|
|
1365
|
+
publicUrlBase
|
|
1366
|
+
} = params;
|
|
1367
|
+
if (format === "svg" || !_optionalChain([variantWidths, 'optionalAccess', _19 => _19.length])) {
|
|
1368
|
+
return { variants: [] };
|
|
1369
|
+
}
|
|
1370
|
+
const targetWidths = [
|
|
1371
|
+
...new Set(
|
|
1372
|
+
variantWidths.map((w) => Math.floor(w)).filter((w) => Number.isFinite(w) && w > 0 && w < mainWidth)
|
|
1373
|
+
)
|
|
1374
|
+
].sort((a, b) => a - b);
|
|
1375
|
+
if (targetWidths.length === 0) {
|
|
1376
|
+
return { variants: [] };
|
|
1377
|
+
}
|
|
1378
|
+
const sharp = await loadSharp();
|
|
1379
|
+
const baseQuality = Math.max(1, Math.min(100, _nullishCoalesce(quality, () => ( 85))));
|
|
1380
|
+
const avifEffort = _nullishCoalesce(effort, () => ( PRIMARY_AVIF_EFFORT));
|
|
1381
|
+
const basePipeline = sharp(sourceBuffer, {
|
|
1382
|
+
failOn: "error",
|
|
1383
|
+
limitInputPixels: _nullishCoalesce(limitInputPixels, () => ( DEFAULT_LIMIT_INPUT_PIXELS))
|
|
1384
|
+
}).rotate();
|
|
1385
|
+
const variants = await Promise.all(
|
|
1386
|
+
targetWidths.map(async (w) => {
|
|
1387
|
+
let branch = basePipeline.clone().resize({
|
|
1388
|
+
width: w,
|
|
1389
|
+
withoutEnlargement: true,
|
|
1390
|
+
kernel: "lanczos3"
|
|
1391
|
+
});
|
|
1392
|
+
if (format === "jpeg") {
|
|
1393
|
+
branch = branch.jpeg({ quality: baseQuality, mozjpeg: true });
|
|
1394
|
+
} else if (format === "png") {
|
|
1395
|
+
branch = branch.png({ compressionLevel: 9 });
|
|
1396
|
+
} else if (format === "webp") {
|
|
1397
|
+
branch = branch.webp({ quality: baseQuality, effort: 4 });
|
|
1398
|
+
} else {
|
|
1399
|
+
branch = branch.avif({
|
|
1400
|
+
quality: baseQuality,
|
|
1401
|
+
effort: avifEffort,
|
|
1402
|
+
chromaSubsampling: "4:2:0",
|
|
1403
|
+
bitdepth: 8
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
const out = await branch.toBuffer({ resolveWithObject: true });
|
|
1407
|
+
const variantPath = buildVariantStoragePath(baseStoragePath, w);
|
|
1408
|
+
const variantFilename = buildVariantStoragePath(baseFilename, w);
|
|
1409
|
+
const uploaded = await storage.upload({
|
|
1410
|
+
bucket,
|
|
1411
|
+
path: variantPath,
|
|
1412
|
+
data: out.data,
|
|
1413
|
+
contentType: `image/${format}`,
|
|
1414
|
+
upsert,
|
|
1415
|
+
contentHash,
|
|
1416
|
+
metadata
|
|
1417
|
+
});
|
|
1418
|
+
const resolvedUrl = await mirrorBufferLocally({
|
|
1419
|
+
buffer: out.data,
|
|
1420
|
+
filename: variantFilename,
|
|
1421
|
+
storagePath: uploaded.path,
|
|
1422
|
+
fallbackUrl: uploaded.url,
|
|
1423
|
+
mirrorDir,
|
|
1424
|
+
publicUrlBase
|
|
1425
|
+
});
|
|
1426
|
+
return {
|
|
1427
|
+
width: out.info.width || w,
|
|
1428
|
+
height: out.info.height || Math.max(1, Math.round(mainHeight / Math.max(1, mainWidth) * w)),
|
|
1429
|
+
url: resolvedUrl,
|
|
1430
|
+
size: out.info.size,
|
|
1431
|
+
storagePath: uploaded.path
|
|
1432
|
+
};
|
|
1433
|
+
})
|
|
1434
|
+
);
|
|
1435
|
+
const srcSet = [
|
|
1436
|
+
...variants.map((v) => `${v.url} ${v.width}w`),
|
|
1437
|
+
`${mainUrl} ${mainWidth}w`
|
|
1438
|
+
].join(", ");
|
|
1439
|
+
return { variants, srcSet };
|
|
1440
|
+
}
|
|
1441
|
+
function collectCandidateVariantWidths(presets, extraWidths) {
|
|
1442
|
+
const widths = new Set(extraWidths);
|
|
1443
|
+
for (const preset of Object.values(presets)) {
|
|
1444
|
+
if (_optionalChain([preset, 'optionalAccess', _20 => _20.variantWidths])) {
|
|
1445
|
+
for (const w of preset.variantWidths) {
|
|
1446
|
+
if (Number.isFinite(w) && w > 0) widths.add(Math.floor(w));
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return [...widths];
|
|
1451
|
+
}
|
|
1452
|
+
async function deleteStoredVariants(params) {
|
|
1453
|
+
const {
|
|
1454
|
+
storage,
|
|
1455
|
+
bucket,
|
|
1456
|
+
basePath,
|
|
1457
|
+
mirrorDirs,
|
|
1458
|
+
knownVariantPaths = [],
|
|
1459
|
+
candidateWidths = []
|
|
1460
|
+
} = params;
|
|
1461
|
+
const paths = new Set(knownVariantPaths);
|
|
1462
|
+
for (const w of candidateWidths) {
|
|
1463
|
+
paths.add(buildVariantStoragePath(basePath, w));
|
|
1464
|
+
}
|
|
1465
|
+
if (paths.size === 0) return;
|
|
1466
|
+
await Promise.all(
|
|
1467
|
+
[...paths].map(async (vPath) => {
|
|
1468
|
+
await storage.delete(bucket, vPath).catch(() => {
|
|
1469
|
+
});
|
|
1470
|
+
await deleteMirroredFiles(mirrorDirs, vPath);
|
|
1471
|
+
})
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
async function invokeRecordHookWithRollback(params) {
|
|
1475
|
+
const {
|
|
1476
|
+
onUploadRecord,
|
|
1477
|
+
awaitRecordHook,
|
|
1478
|
+
record,
|
|
1479
|
+
storage,
|
|
1480
|
+
bucket,
|
|
1481
|
+
mainPath,
|
|
1482
|
+
variantPaths,
|
|
1483
|
+
mirrorDirs,
|
|
1484
|
+
onError
|
|
1485
|
+
} = params;
|
|
1486
|
+
if (!onUploadRecord) return;
|
|
1487
|
+
if (awaitRecordHook) {
|
|
1488
|
+
try {
|
|
1489
|
+
await onUploadRecord(record);
|
|
1490
|
+
} catch (hookError) {
|
|
1491
|
+
await storage.delete(bucket, mainPath).catch(() => {
|
|
1492
|
+
});
|
|
1493
|
+
await deleteMirroredFiles(mirrorDirs, mainPath);
|
|
1494
|
+
await deleteStoredVariants({
|
|
1495
|
+
storage,
|
|
1496
|
+
bucket,
|
|
1497
|
+
basePath: mainPath,
|
|
1498
|
+
mirrorDirs,
|
|
1499
|
+
knownVariantPaths: variantPaths
|
|
1500
|
+
});
|
|
1501
|
+
throw hookError;
|
|
1502
|
+
}
|
|
1503
|
+
} else {
|
|
1504
|
+
Promise.resolve(onUploadRecord(record)).catch((err) => {
|
|
1505
|
+
_optionalChain([onError, 'optionalCall', _21 => _21(err, { action: "recordHook" })]);
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1046
1509
|
|
|
1047
1510
|
// src/server/uploader/createImageUploader.ts
|
|
1048
1511
|
function createImageUploader(options) {
|
|
@@ -1051,27 +1514,68 @@ function createImageUploader(options) {
|
|
|
1051
1514
|
defaultBucket = "images",
|
|
1052
1515
|
defaultFolder = "",
|
|
1053
1516
|
defaultMaxSizeMB = 10,
|
|
1054
|
-
defaultAllowedFormats = DEFAULT_ALLOWED_FORMATS,
|
|
1055
|
-
defaultMaxDimensions = DEFAULT_IMAGE_DIMENSIONS,
|
|
1517
|
+
defaultAllowedFormats = _chunkNEDBIIE3cjs.DEFAULT_ALLOWED_FORMATS,
|
|
1518
|
+
defaultMaxDimensions = _chunkNEDBIIE3cjs.DEFAULT_IMAGE_DIMENSIONS,
|
|
1519
|
+
allowSvg: defaultAllowSvg = false,
|
|
1520
|
+
limitInputPixels: defaultLimitInputPixels,
|
|
1521
|
+
effort: defaultEffort,
|
|
1056
1522
|
presets: customPresets,
|
|
1057
1523
|
includeStandardPresets = true,
|
|
1058
1524
|
pathGenerator,
|
|
1059
1525
|
onUploadRecord,
|
|
1060
1526
|
awaitRecordHook = false,
|
|
1527
|
+
onError,
|
|
1061
1528
|
concurrency = 2,
|
|
1529
|
+
maxQueue,
|
|
1062
1530
|
optimizationTimeoutMs = 6e4,
|
|
1063
1531
|
blurTimeoutMs = 3e4,
|
|
1064
1532
|
includeBlurHash: defaultIncludeBlurHash = false
|
|
1065
1533
|
} = options;
|
|
1066
1534
|
const mergedPresets = {
|
|
1067
|
-
...includeStandardPresets ? STANDARD_PRESETS : {},
|
|
1535
|
+
...includeStandardPresets ? _chunkNEDBIIE3cjs.STANDARD_PRESETS : {},
|
|
1068
1536
|
...customPresets || {}
|
|
1069
1537
|
};
|
|
1070
|
-
const semaphore = new AsyncSemaphore(concurrency);
|
|
1538
|
+
const semaphore = new AsyncSemaphore(concurrency, { maxQueue });
|
|
1071
1539
|
const buildPath = pathGenerator || createDefaultPathGenerator();
|
|
1072
|
-
const
|
|
1540
|
+
const trackedVariants = /* @__PURE__ */ new Map();
|
|
1541
|
+
const observedWidths = /* @__PURE__ */ new Set();
|
|
1542
|
+
const resolvePreset = (name) => name ? mergedPresets[name] : void 0;
|
|
1543
|
+
const getAllowedBuckets = () => {
|
|
1544
|
+
const buckets = /* @__PURE__ */ new Set([defaultBucket]);
|
|
1545
|
+
for (const preset of Object.values(mergedPresets)) {
|
|
1546
|
+
if (_optionalChain([preset, 'optionalAccess', _22 => _22.bucket])) buckets.add(preset.bucket);
|
|
1547
|
+
}
|
|
1548
|
+
return [...buckets];
|
|
1549
|
+
};
|
|
1550
|
+
const getMirrorDirs = () => [...new Set(Object.values(mergedPresets).map((p) => _optionalChain([p, 'optionalAccess', _23 => _23.mirrorDir])).filter(Boolean))];
|
|
1551
|
+
const getMaxSizeMB = (allowedPresets) => {
|
|
1552
|
+
let maxMB = defaultMaxSizeMB;
|
|
1553
|
+
const keys = _optionalChain([allowedPresets, 'optionalAccess', _24 => _24.length]) ? allowedPresets : Object.keys(mergedPresets);
|
|
1554
|
+
for (const key of keys) {
|
|
1555
|
+
const p = mergedPresets[key];
|
|
1556
|
+
if (_optionalChain([p, 'optionalAccess', _25 => _25.maxSizeMB]) && p.maxSizeMB > maxMB) maxMB = p.maxSizeMB;
|
|
1557
|
+
}
|
|
1558
|
+
return maxMB;
|
|
1559
|
+
};
|
|
1560
|
+
const cleanupVariantsFor = async (bucket, basePath) => {
|
|
1561
|
+
const key = `${bucket}:${basePath}`;
|
|
1562
|
+
const knownVariantPaths = trackedVariants.get(key);
|
|
1563
|
+
trackedVariants.delete(key);
|
|
1564
|
+
await deleteStoredVariants({
|
|
1565
|
+
storage,
|
|
1566
|
+
bucket,
|
|
1567
|
+
basePath,
|
|
1568
|
+
mirrorDirs: getMirrorDirs(),
|
|
1569
|
+
knownVariantPaths,
|
|
1570
|
+
candidateWidths: collectCandidateVariantWidths(mergedPresets, observedWidths)
|
|
1571
|
+
});
|
|
1572
|
+
};
|
|
1073
1573
|
return {
|
|
1574
|
+
defaultBucket,
|
|
1074
1575
|
resolvePreset,
|
|
1576
|
+
getAllowedBuckets,
|
|
1577
|
+
getMaxSizeMB,
|
|
1578
|
+
parseUrl: (url) => storage.parseUrl ? storage.parseUrl(url) : null,
|
|
1075
1579
|
async processBuffer(buffer, procOptions = {}) {
|
|
1076
1580
|
return semaphore.run(
|
|
1077
1581
|
() => processImageBuffer(buffer, {
|
|
@@ -1079,34 +1583,48 @@ function createImageUploader(options) {
|
|
|
1079
1583
|
optimizationTimeoutMs,
|
|
1080
1584
|
blurTimeoutMs,
|
|
1081
1585
|
includeBlurHash: defaultIncludeBlurHash,
|
|
1586
|
+
limitInputPixels: defaultLimitInputPixels,
|
|
1587
|
+
effort: defaultEffort,
|
|
1082
1588
|
...procOptions
|
|
1083
|
-
})
|
|
1589
|
+
}),
|
|
1590
|
+
{ signal: procOptions.signal }
|
|
1084
1591
|
);
|
|
1085
1592
|
},
|
|
1086
1593
|
async uploadImage(input, uploadOptions = {}) {
|
|
1087
1594
|
const presetConfig = resolvePreset(uploadOptions.preset);
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
const
|
|
1093
|
-
const
|
|
1094
|
-
const
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
maxSizeMB,
|
|
1103
|
-
allowedFormats
|
|
1595
|
+
if (uploadOptions.preset && !presetConfig) {
|
|
1596
|
+
throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(`Invalid preset "${uploadOptions.preset}".`);
|
|
1597
|
+
}
|
|
1598
|
+
const bucket = uploadOptions.bucket || _optionalChain([presetConfig, 'optionalAccess', _26 => _26.bucket]) || defaultBucket;
|
|
1599
|
+
const folder = _nullishCoalesce(_nullishCoalesce(uploadOptions.folder, () => ( _optionalChain([presetConfig, 'optionalAccess', _27 => _27.folder]))), () => ( defaultFolder));
|
|
1600
|
+
const maxSizeMB = _nullishCoalesce(_nullishCoalesce(uploadOptions.maxSizeMB, () => ( _optionalChain([presetConfig, 'optionalAccess', _28 => _28.maxSizeMB]))), () => ( defaultMaxSizeMB));
|
|
1601
|
+
const allowSvg = _nullishCoalesce(_nullishCoalesce(uploadOptions.allowSvg, () => ( _optionalChain([presetConfig, 'optionalAccess', _29 => _29.allowSvg]))), () => ( defaultAllowSvg));
|
|
1602
|
+
const explicitAllowSvg = Boolean(
|
|
1603
|
+
_nullishCoalesce(_nullishCoalesce(uploadOptions.allowSvg, () => ( _optionalChain([presetConfig, 'optionalAccess', _30 => _30.allowSvg]))), () => ( (defaultAllowSvg && !uploadOptions.allowedFormats && !_optionalChain([presetConfig, 'optionalAccess', _31 => _31.allowedFormats]))))
|
|
1604
|
+
);
|
|
1605
|
+
const allowedFormats = resolveEffectiveAllowedFormats(
|
|
1606
|
+
_nullishCoalesce(_nullishCoalesce(uploadOptions.allowedFormats, () => ( _optionalChain([presetConfig, 'optionalAccess', _32 => _32.allowedFormats]))), () => ( defaultAllowedFormats)),
|
|
1607
|
+
allowSvg,
|
|
1608
|
+
explicitAllowSvg
|
|
1104
1609
|
);
|
|
1105
|
-
|
|
1106
|
-
|
|
1610
|
+
const maxDimensions = _nullishCoalesce(_nullishCoalesce(uploadOptions.maxDimensions, () => ( _optionalChain([presetConfig, 'optionalAccess', _33 => _33.maxDimensions]))), () => ( defaultMaxDimensions));
|
|
1611
|
+
const outputFormat = _nullishCoalesce(uploadOptions.outputFormat, () => ( _optionalChain([presetConfig, 'optionalAccess', _34 => _34.outputFormat])));
|
|
1612
|
+
const quality = _nullishCoalesce(uploadOptions.quality, () => ( _optionalChain([presetConfig, 'optionalAccess', _35 => _35.quality])));
|
|
1613
|
+
const includeBlurHash = _nullishCoalesce(_nullishCoalesce(uploadOptions.includeBlurHash, () => ( _optionalChain([presetConfig, 'optionalAccess', _36 => _36.includeBlurHash]))), () => ( defaultIncludeBlurHash));
|
|
1614
|
+
const limitInputPixels = _nullishCoalesce(_nullishCoalesce(uploadOptions.limitInputPixels, () => ( _optionalChain([presetConfig, 'optionalAccess', _37 => _37.limitInputPixels]))), () => ( defaultLimitInputPixels));
|
|
1615
|
+
const variantWidths = _nullishCoalesce(uploadOptions.variantWidths, () => ( _optionalChain([presetConfig, 'optionalAccess', _38 => _38.variantWidths])));
|
|
1616
|
+
const isWebFile = typeof input.arrayBuffer === "function";
|
|
1617
|
+
if (isWebFile) {
|
|
1618
|
+
const pre = _chunkNEDBIIE3cjs.validateImageFileStrict.call(void 0, { size: input.size, type: input.type }, maxSizeMB, allowedFormats);
|
|
1619
|
+
if (!pre.valid) throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(pre.error);
|
|
1107
1620
|
}
|
|
1108
1621
|
try {
|
|
1109
|
-
|
|
1622
|
+
let originalBuffer = isWebFile ? Buffer.from(await input.arrayBuffer()) : input.buffer;
|
|
1623
|
+
if (!originalBuffer || originalBuffer.length === 0) throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)("File is empty");
|
|
1624
|
+
const validation = _chunkNEDBIIE3cjs.validateImageFileStrict.call(void 0, { size: originalBuffer.length, type: input.type }, maxSizeMB, allowedFormats);
|
|
1625
|
+
if (!validation.valid) throw new (0, _chunk2OVNXHTNcjs.UploadValidationError)(validation.error);
|
|
1626
|
+
const sniffed = assertMagicBytesAllowed(originalBuffer, allowedFormats, allowSvg);
|
|
1627
|
+
if (sniffed.format === "svg") originalBuffer = sanitizeSvg(originalBuffer);
|
|
1110
1628
|
const { optimized, blurData, contentHash, originalSize } = await semaphore.run(
|
|
1111
1629
|
() => processImageBuffer(originalBuffer, {
|
|
1112
1630
|
maxDimensions,
|
|
@@ -1114,12 +1632,15 @@ function createImageUploader(options) {
|
|
|
1114
1632
|
quality,
|
|
1115
1633
|
includeBlurHash,
|
|
1116
1634
|
optimizationTimeoutMs,
|
|
1117
|
-
blurTimeoutMs
|
|
1118
|
-
|
|
1635
|
+
blurTimeoutMs,
|
|
1636
|
+
limitInputPixels,
|
|
1637
|
+
effort: defaultEffort,
|
|
1638
|
+
allowSvg,
|
|
1639
|
+
signal: uploadOptions.signal
|
|
1640
|
+
}),
|
|
1641
|
+
{ signal: uploadOptions.signal }
|
|
1119
1642
|
);
|
|
1120
1643
|
const mainFormat = optimized.format;
|
|
1121
|
-
const extension = formatToExtension(mainFormat);
|
|
1122
|
-
const mainContentType = mainFormat === "svg" ? "image/svg+xml" : `image/${mainFormat}`;
|
|
1123
1644
|
const { uploadResult, storageTarget } = await uploadWithCollisionRetry({
|
|
1124
1645
|
storage,
|
|
1125
1646
|
buildPath,
|
|
@@ -1128,23 +1649,52 @@ function createImageUploader(options) {
|
|
|
1128
1649
|
folder,
|
|
1129
1650
|
preset: uploadOptions.preset,
|
|
1130
1651
|
format: mainFormat,
|
|
1131
|
-
extension,
|
|
1652
|
+
extension: formatToExtension(mainFormat),
|
|
1132
1653
|
customFilename: uploadOptions.customFilename,
|
|
1654
|
+
isClientFilename: uploadOptions.isClientFilename,
|
|
1133
1655
|
contentHash,
|
|
1134
|
-
originalName,
|
|
1656
|
+
originalName: input.name,
|
|
1135
1657
|
metadata: uploadOptions.metadata
|
|
1136
1658
|
},
|
|
1137
1659
|
data: optimized.buffer,
|
|
1138
|
-
contentType:
|
|
1660
|
+
contentType: mainFormat === "svg" ? _chunkNEDBIIE3cjs.SVG_MIME_TYPE : `image/${mainFormat}`
|
|
1139
1661
|
});
|
|
1140
|
-
const resolvedUrl = mirrorBufferLocally({
|
|
1662
|
+
const resolvedUrl = await mirrorBufferLocally({
|
|
1141
1663
|
buffer: optimized.buffer,
|
|
1142
1664
|
filename: storageTarget.filename,
|
|
1665
|
+
storagePath: uploadResult.path,
|
|
1143
1666
|
fallbackUrl: uploadResult.url,
|
|
1144
|
-
mirrorDir: presetConfig
|
|
1145
|
-
publicUrlBase: presetConfig
|
|
1667
|
+
mirrorDir: _optionalChain([presetConfig, 'optionalAccess', _39 => _39.mirrorDir]),
|
|
1668
|
+
publicUrlBase: _optionalChain([presetConfig, 'optionalAccess', _40 => _40.publicUrlBase])
|
|
1146
1669
|
});
|
|
1147
|
-
const
|
|
1670
|
+
const { variants, srcSet } = _optionalChain([variantWidths, 'optionalAccess', _41 => _41.length]) && mainFormat !== "svg" ? await semaphore.run(
|
|
1671
|
+
() => generateImageVariants({
|
|
1672
|
+
sourceBuffer: originalBuffer,
|
|
1673
|
+
format: mainFormat,
|
|
1674
|
+
quality: _nullishCoalesce(optimized.quality, () => ( quality)),
|
|
1675
|
+
effort: defaultEffort,
|
|
1676
|
+
limitInputPixels,
|
|
1677
|
+
mainWidth: optimized.width,
|
|
1678
|
+
mainHeight: optimized.height,
|
|
1679
|
+
mainUrl: resolvedUrl,
|
|
1680
|
+
baseStoragePath: uploadResult.path,
|
|
1681
|
+
baseFilename: storageTarget.filename,
|
|
1682
|
+
bucket: storageTarget.bucket,
|
|
1683
|
+
storage,
|
|
1684
|
+
variantWidths,
|
|
1685
|
+
upsert: Boolean(uploadOptions.customFilename) && !uploadOptions.isClientFilename,
|
|
1686
|
+
contentHash,
|
|
1687
|
+
metadata: uploadOptions.metadata,
|
|
1688
|
+
mirrorDir: _optionalChain([presetConfig, 'optionalAccess', _42 => _42.mirrorDir]),
|
|
1689
|
+
publicUrlBase: _optionalChain([presetConfig, 'optionalAccess', _43 => _43.publicUrlBase])
|
|
1690
|
+
}),
|
|
1691
|
+
{ signal: uploadOptions.signal }
|
|
1692
|
+
) : { variants: [], srcSet: void 0 };
|
|
1693
|
+
const variantPaths = variants.map((v) => v.storagePath).filter(Boolean);
|
|
1694
|
+
if (variantPaths.length > 0) {
|
|
1695
|
+
trackedVariants.set(`${storageTarget.bucket}:${uploadResult.path}`, variantPaths);
|
|
1696
|
+
variants.forEach((v) => observedWidths.add(v.width));
|
|
1697
|
+
}
|
|
1148
1698
|
const result = {
|
|
1149
1699
|
url: resolvedUrl,
|
|
1150
1700
|
blurDataUrl: blurData.dataURL,
|
|
@@ -1162,76 +1712,90 @@ function createImageUploader(options) {
|
|
|
1162
1712
|
...uploadOptions.preset && { preset: uploadOptions.preset },
|
|
1163
1713
|
...folder && { folder },
|
|
1164
1714
|
...uploadOptions.metadata && { metadata: uploadOptions.metadata },
|
|
1715
|
+
...variants.length > 0 && { variants },
|
|
1716
|
+
...srcSet && { srcSet },
|
|
1165
1717
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1166
1718
|
};
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1719
|
+
const record = { ...result, quality: _nullishCoalesce(_nullishCoalesce(optimized.quality, () => ( quality)), () => ( 85)) };
|
|
1720
|
+
await invokeRecordHookWithRollback({
|
|
1721
|
+
onUploadRecord,
|
|
1722
|
+
awaitRecordHook,
|
|
1723
|
+
record,
|
|
1724
|
+
storage,
|
|
1725
|
+
bucket: storageTarget.bucket,
|
|
1726
|
+
mainPath: uploadResult.path,
|
|
1727
|
+
variantPaths,
|
|
1728
|
+
mirrorDirs: getMirrorDirs(),
|
|
1729
|
+
onError
|
|
1730
|
+
});
|
|
1179
1731
|
return result;
|
|
1180
1732
|
} catch (error) {
|
|
1181
|
-
if (error instanceof UploadValidationError) {
|
|
1733
|
+
if (error instanceof _chunk2OVNXHTNcjs.UploadValidationError || error instanceof SemaphoreQueueFullError) {
|
|
1182
1734
|
throw error;
|
|
1183
1735
|
}
|
|
1184
|
-
throw new Error(
|
|
1185
|
-
`Image upload failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1186
|
-
);
|
|
1736
|
+
throw new Error(`Image upload failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1187
1737
|
}
|
|
1188
1738
|
},
|
|
1189
1739
|
async deleteImage(targetPath, bucket) {
|
|
1740
|
+
const targetBucket = bucket || defaultBucket;
|
|
1190
1741
|
try {
|
|
1191
|
-
await storage.delete(
|
|
1742
|
+
await storage.delete(targetBucket, targetPath);
|
|
1743
|
+
await deleteMirroredFiles(getMirrorDirs(), targetPath);
|
|
1744
|
+
await cleanupVariantsFor(targetBucket, targetPath);
|
|
1192
1745
|
} catch (error) {
|
|
1193
|
-
throw new Error(
|
|
1194
|
-
`Image deletion failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1195
|
-
);
|
|
1746
|
+
throw new Error(`Image deletion failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1196
1747
|
}
|
|
1197
1748
|
},
|
|
1198
1749
|
async deleteByUrl(url) {
|
|
1199
|
-
|
|
1200
|
-
|
|
1750
|
+
const parsed = _optionalChain([storage, 'access', _44 => _44.parseUrl, 'optionalCall', _45 => _45(url)]);
|
|
1751
|
+
const deleted = storage.deleteByUrl ? await storage.deleteByUrl(url) : false;
|
|
1752
|
+
if (deleted && _optionalChain([parsed, 'optionalAccess', _46 => _46.path])) {
|
|
1753
|
+
await deleteMirroredFiles(getMirrorDirs(), parsed.path);
|
|
1754
|
+
await cleanupVariantsFor(parsed.bucket || defaultBucket, parsed.path);
|
|
1201
1755
|
}
|
|
1202
|
-
return
|
|
1756
|
+
return deleted;
|
|
1203
1757
|
}
|
|
1204
1758
|
};
|
|
1205
1759
|
}
|
|
1206
1760
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
exports.AsyncSemaphore = AsyncSemaphore; exports.BLUR_PLACEHOLDER_FALLBACK_WIDTH = BLUR_PLACEHOLDER_FALLBACK_WIDTH; exports.BLUR_PLACEHOLDER_MAX_CHARS = BLUR_PLACEHOLDER_MAX_CHARS; exports.BLUR_PLACEHOLDER_QUALITY = BLUR_PLACEHOLDER_QUALITY; exports.BLUR_PLACEHOLDER_WIDTH = BLUR_PLACEHOLDER_WIDTH; exports.DEFAULT_LIMIT_INPUT_PIXELS = DEFAULT_LIMIT_INPUT_PIXELS; exports.DEFAULT_MAX_SVG_BYTES = DEFAULT_MAX_SVG_BYTES; exports.EDGE_COLOR_FALLBACK = EDGE_COLOR_FALLBACK; exports.EDGE_WHITE_FALLBACK = EDGE_WHITE_FALLBACK; exports.PRIMARY_AVIF_EFFORT = PRIMARY_AVIF_EFFORT; exports.RETRY_AVIF_EFFORT = RETRY_AVIF_EFFORT; exports.SemaphoreQueueFullError = SemaphoreQueueFullError; exports.UploadValidationError = _chunk2OVNXHTNcjs.UploadValidationError; exports.WHITE_DOMINANCE_RATIO = WHITE_DOMINANCE_RATIO; exports.assertMagicBytesAllowed = assertMagicBytesAllowed; exports.blendEdgeColors = blendEdgeColors; exports.buildVariantStoragePath = buildVariantStoragePath; exports.computeEdgeColorSet = computeEdgeColorSet; exports.createDefaultPathGenerator = createDefaultPathGenerator; exports.createImageUploader = createImageUploader; exports.createUploadHandler = _chunk2OVNXHTNcjs.createUploadHandler; exports.createUploadHttpHandler = _chunk2OVNXHTNcjs.createUploadHttpHandler; exports.encodeBlurHashString = encodeBlurHashString; exports.extractEdgeColorsFromBuffer = extractEdgeColorsFromBuffer; exports.extractEnhancedColorFromBuffer = extractEnhancedColorFromBuffer; exports.formatToExtension = formatToExtension; exports.generateBlurHash = generateBlurHash; exports.generateBlurPlaceholder = generateBlurPlaceholder; exports.generateImageVariants = generateImageVariants; exports.isClientValidationError = _chunk2OVNXHTNcjs.isClientValidationError; exports.isCollisionError = isCollisionError; exports.mirrorBufferLocally = mirrorBufferLocally; exports.optimizeImage = optimizeImage; exports.processImageBuffer = processImageBuffer; exports.sanitizeSegment = sanitizeSegment; exports.sanitizeSvg = sanitizeSvg; exports.sniffImageFormat = sniffImageFormat; exports.uploadWithCollisionRetry = uploadWithCollisionRetry; exports.withTimeout = withTimeout;
|
|
1237
1801
|
//# sourceMappingURL=server.cjs.map
|