ipx 0.7.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -21
- package/bin/ipx.mjs +2 -0
- package/dist/{cli.js → chunks/middleware.cjs} +59 -51
- package/dist/chunks/middleware.mjs +527 -0
- package/dist/cli.cjs +33 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +27 -0
- package/dist/index.cjs +19 -518
- package/dist/index.mjs +13 -503
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
High performance, secure and easy to use image proxy based on [sharp](https://github.com/lovell/sharp) and [libvips](https://github.com/libvips/libvips).
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Usage
|
|
10
10
|
|
|
11
11
|
### Quick Start
|
|
12
12
|
|
|
@@ -52,25 +52,27 @@ Resize to `200x200px` using `embed` method and change format to `webp`:
|
|
|
52
52
|
|
|
53
53
|
### Modifiers
|
|
54
54
|
|
|
55
|
-
| Property
|
|
56
|
-
|
|
|
57
|
-
| width
|
|
58
|
-
| height
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
55
|
+
| Property | Docs | Example | Comments |
|
|
56
|
+
| ----------- | :-------------------------------------------------------------- | :------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
+
| width / w | \_ | `http://localhost:3000/width_200/buffalo.png` |
|
|
58
|
+
| height / h | \_ | `http://localhost:3000/height_200/buffalo.png` |
|
|
59
|
+
| resize / s | \_ | `http://localhost:3000/s_200x200/buffalo.png` |
|
|
60
|
+
| trim | [Docs](https://sharp.pixelplumbing.com/api-resize#trim) | `http://localhost:3000/trim_100/buffalo.png` |
|
|
61
|
+
| format | [Docs](https://sharp.pixelplumbing.com/api-output#toformat) | `http://localhost:3000/format_webp/buffalo.png` | Supported format: `jpg`, `jpeg`, `png`, `webp`, `avif`, `gif`, `heif` |
|
|
62
|
+
| quality / q | \_ | `http://localhost:3000/quality_50/buffalo.png` | Accepted values: 0 to 100 |
|
|
63
|
+
| rotate | [Docs](https://sharp.pixelplumbing.com/api-operation#rotate) | `http://localhost:3000/rotate_45/buffalo.png` |
|
|
64
|
+
| enlarge | \_ | `http://localhost:3000/enlarge,s_2000x2000/buffalo.png` | Allow the image to be upscaled. By default the returned image will never be larger than the source in any dimension, while preserving the requested aspect ratio. |
|
|
65
|
+
| flip | [Docs](https://sharp.pixelplumbing.com/api-operation#flip) | `http://localhost:3000/flip/buffalo.png` |
|
|
66
|
+
| flop | [Docs](https://sharp.pixelplumbing.com/api-operation#flop) | `http://localhost:3000/flop/buffalo.png` |
|
|
67
|
+
| sharpen | [Docs](https://sharp.pixelplumbing.com/api-operation#sharpen) | `http://localhost:3000/sharpen_30/buffalo.png` |
|
|
68
|
+
| median | [Docs](https://sharp.pixelplumbing.com/api-operation#median) | `http://localhost:3000/median_10/buffalo.png` |
|
|
69
|
+
| gamma | [Docs](https://sharp.pixelplumbing.com/api-operation#gamma) | `http://localhost:3000/gamma_3/buffalo.png` |
|
|
70
|
+
| negate | [Docs](https://sharp.pixelplumbing.com/api-operation#negate) | `http://localhost:3000/negate/buffalo.png` |
|
|
71
|
+
| normalize | [Docs](https://sharp.pixelplumbing.com/api-operation#normalize) | `http://localhost:3000/normalize/buffalo.png` |
|
|
72
|
+
| threshold | [Docs](https://sharp.pixelplumbing.com/api-operation#threshold) | `http://localhost:3000/threshold_10/buffalo.png` |
|
|
73
|
+
| tint | [Docs](https://sharp.pixelplumbing.com/api-colour#tint) | `http://localhost:3000/tint_1098123/buffalo.png` |
|
|
74
|
+
| grayscale | [Docs](https://sharp.pixelplumbing.com/api-colour#grayscale) | `http://localhost:3000/grayscale/buffalo.png` |
|
|
75
|
+
| animated | - | `http://localhost:3000/animated/buffalo.gif` | Experimental |
|
|
74
76
|
|
|
75
77
|
### Config
|
|
76
78
|
|
|
@@ -83,6 +85,6 @@ Config can be customized using `IPX_*` environment variables.
|
|
|
83
85
|
- `IPX_DOMAINS`
|
|
84
86
|
- Default: `[]`
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
## License
|
|
87
89
|
|
|
88
90
|
MIT
|
package/bin/ipx.mjs
ADDED
|
@@ -1,42 +1,34 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
'use strict';
|
|
4
2
|
|
|
5
|
-
const consola = require('consola');
|
|
6
|
-
const listhen = require('listhen');
|
|
7
|
-
const Sharp = require('sharp');
|
|
8
3
|
const defu = require('defu');
|
|
9
4
|
const imageMeta = require('image-meta');
|
|
10
5
|
const ufo = require('ufo');
|
|
11
|
-
const
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const pathe = require('pathe');
|
|
12
8
|
const isValidPath = require('is-valid-path');
|
|
13
|
-
const fsExtra = require('fs-extra');
|
|
14
|
-
const destr = require('destr');
|
|
15
9
|
const http = require('http');
|
|
16
10
|
const https = require('https');
|
|
17
|
-
const
|
|
11
|
+
const ohmyfetch = require('ohmyfetch');
|
|
12
|
+
const destr = require('destr');
|
|
18
13
|
const getEtag = require('etag');
|
|
19
14
|
const xss = require('xss');
|
|
20
15
|
|
|
21
|
-
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e :
|
|
16
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
|
|
22
17
|
|
|
23
|
-
const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
|
|
24
|
-
const Sharp__default = /*#__PURE__*/_interopDefaultLegacy(Sharp);
|
|
25
18
|
const defu__default = /*#__PURE__*/_interopDefaultLegacy(defu);
|
|
26
|
-
const imageMeta__default = /*#__PURE__*/_interopDefaultLegacy(imageMeta);
|
|
27
19
|
const isValidPath__default = /*#__PURE__*/_interopDefaultLegacy(isValidPath);
|
|
28
|
-
const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
|
|
29
20
|
const http__default = /*#__PURE__*/_interopDefaultLegacy(http);
|
|
30
21
|
const https__default = /*#__PURE__*/_interopDefaultLegacy(https);
|
|
31
|
-
const
|
|
22
|
+
const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
|
|
32
23
|
const getEtag__default = /*#__PURE__*/_interopDefaultLegacy(getEtag);
|
|
33
24
|
const xss__default = /*#__PURE__*/_interopDefaultLegacy(xss);
|
|
34
25
|
|
|
35
|
-
const Handlers =
|
|
26
|
+
const Handlers = {
|
|
36
27
|
__proto__: null,
|
|
37
28
|
get quality () { return quality; },
|
|
38
29
|
get fit () { return fit; },
|
|
39
30
|
get background () { return background; },
|
|
31
|
+
get enlarge () { return enlarge; },
|
|
40
32
|
get width () { return width; },
|
|
41
33
|
get height () { return height; },
|
|
42
34
|
get resize () { return resize; },
|
|
@@ -63,11 +55,10 @@ const Handlers = /*#__PURE__*/Object.freeze({
|
|
|
63
55
|
get w () { return w; },
|
|
64
56
|
get h () { return h; },
|
|
65
57
|
get s () { return s; }
|
|
66
|
-
}
|
|
58
|
+
};
|
|
67
59
|
|
|
68
60
|
function getEnv(name, defaultValue) {
|
|
69
|
-
|
|
70
|
-
return (_a = destr__default['default'](process.env[name])) != null ? _a : defaultValue;
|
|
61
|
+
return destr__default(process.env[name]) ?? defaultValue;
|
|
71
62
|
}
|
|
72
63
|
function cachedPromise(fn) {
|
|
73
64
|
let p;
|
|
@@ -89,15 +80,15 @@ function createError(message, statusCode) {
|
|
|
89
80
|
}
|
|
90
81
|
|
|
91
82
|
const createFilesystemSource = (options) => {
|
|
92
|
-
const rootDir =
|
|
83
|
+
const rootDir = pathe.resolve(options.dir);
|
|
93
84
|
return async (id) => {
|
|
94
|
-
const fsPath =
|
|
95
|
-
if (!isValidPath__default
|
|
85
|
+
const fsPath = pathe.resolve(pathe.join(rootDir, id));
|
|
86
|
+
if (!isValidPath__default(id) || id.includes("..") || !fsPath.startsWith(rootDir)) {
|
|
96
87
|
throw createError("Forbidden path:" + id, 403);
|
|
97
88
|
}
|
|
98
89
|
let stats;
|
|
99
90
|
try {
|
|
100
|
-
stats = await
|
|
91
|
+
stats = await fs.promises.stat(fsPath);
|
|
101
92
|
} catch (err) {
|
|
102
93
|
if (err.code === "ENOENT") {
|
|
103
94
|
throw createError("File not found: " + fsPath, 404);
|
|
@@ -111,14 +102,14 @@ const createFilesystemSource = (options) => {
|
|
|
111
102
|
return {
|
|
112
103
|
mtime: stats.mtime,
|
|
113
104
|
maxAge: options.maxAge || 300,
|
|
114
|
-
getData: cachedPromise(() =>
|
|
105
|
+
getData: cachedPromise(() => fs.promises.readFile(fsPath))
|
|
115
106
|
};
|
|
116
107
|
};
|
|
117
108
|
};
|
|
118
109
|
|
|
119
110
|
const createHTTPSource = (options) => {
|
|
120
|
-
const httpsAgent = new https__default
|
|
121
|
-
const httpAgent = new http__default
|
|
111
|
+
const httpsAgent = new https__default.Agent({ keepAlive: true });
|
|
112
|
+
const httpAgent = new http__default.Agent({ keepAlive: true });
|
|
122
113
|
let domains = options.domains || [];
|
|
123
114
|
if (typeof domains === "string") {
|
|
124
115
|
domains = domains.split(",").map((s) => s.trim());
|
|
@@ -129,10 +120,10 @@ const createHTTPSource = (options) => {
|
|
|
129
120
|
if (!parsedUrl.host) {
|
|
130
121
|
throw createError("Hostname is missing: " + id, 403);
|
|
131
122
|
}
|
|
132
|
-
if (!
|
|
123
|
+
if (!reqOptions?.bypassDomain && !hosts.find((host) => parsedUrl.host === host)) {
|
|
133
124
|
throw createError("Forbidden host: " + parsedUrl.host, 403);
|
|
134
125
|
}
|
|
135
|
-
const response = await
|
|
126
|
+
const response = await ohmyfetch.fetch(id, {
|
|
136
127
|
agent: id.startsWith("https") ? httpsAgent : httpAgent
|
|
137
128
|
});
|
|
138
129
|
if (!response.ok) {
|
|
@@ -160,7 +151,7 @@ const createHTTPSource = (options) => {
|
|
|
160
151
|
};
|
|
161
152
|
|
|
162
153
|
function VArg(arg) {
|
|
163
|
-
return destr__default
|
|
154
|
+
return destr__default(arg);
|
|
164
155
|
}
|
|
165
156
|
function parseArgs(args, mappers) {
|
|
166
157
|
const vargs = args.split("_");
|
|
@@ -173,6 +164,19 @@ function applyHandler(ctx, pipe, handler, argsStr) {
|
|
|
173
164
|
const args = handler.args ? parseArgs(argsStr, handler.args) : [];
|
|
174
165
|
return handler.apply(ctx, pipe, ...args);
|
|
175
166
|
}
|
|
167
|
+
function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
|
|
168
|
+
const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
|
|
169
|
+
let { width, height } = desiredDimensions;
|
|
170
|
+
if (width > sourceDimensions.width) {
|
|
171
|
+
width = sourceDimensions.width;
|
|
172
|
+
height = Math.round(sourceDimensions.width / desiredAspectRatio);
|
|
173
|
+
}
|
|
174
|
+
if (height > sourceDimensions.height) {
|
|
175
|
+
height = sourceDimensions.height;
|
|
176
|
+
width = Math.round(sourceDimensions.height * desiredAspectRatio);
|
|
177
|
+
}
|
|
178
|
+
return { width, height };
|
|
179
|
+
}
|
|
176
180
|
|
|
177
181
|
const quality = {
|
|
178
182
|
args: [VArg],
|
|
@@ -201,22 +205,33 @@ const background = {
|
|
|
201
205
|
context.background = background2;
|
|
202
206
|
}
|
|
203
207
|
};
|
|
208
|
+
const enlarge = {
|
|
209
|
+
args: [],
|
|
210
|
+
apply: (context) => {
|
|
211
|
+
context.enlarge = true;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
204
214
|
const width = {
|
|
205
215
|
args: [VArg],
|
|
206
|
-
apply: (
|
|
207
|
-
return pipe.resize(width2, null);
|
|
216
|
+
apply: (context, pipe, width2) => {
|
|
217
|
+
return pipe.resize(width2, null, { withoutEnlargement: !context.enlarge });
|
|
208
218
|
}
|
|
209
219
|
};
|
|
210
220
|
const height = {
|
|
211
221
|
args: [VArg],
|
|
212
|
-
apply: (
|
|
213
|
-
return pipe.resize(null, height2);
|
|
222
|
+
apply: (context, pipe, height2) => {
|
|
223
|
+
return pipe.resize(null, height2, { withoutEnlargement: !context.enlarge });
|
|
214
224
|
}
|
|
215
225
|
};
|
|
216
226
|
const resize = {
|
|
217
227
|
args: [VArg, VArg, VArg],
|
|
218
228
|
apply: (context, pipe, size) => {
|
|
219
|
-
|
|
229
|
+
let [width2, height2] = String(size).split("x").map((v) => Number(v));
|
|
230
|
+
if (!context.enlarge) {
|
|
231
|
+
const clamped = clampDimensionsPreservingAspectRatio(context.meta, { width: width2, height: height2 });
|
|
232
|
+
width2 = clamped.width;
|
|
233
|
+
height2 = clamped.height;
|
|
234
|
+
}
|
|
220
235
|
return pipe.resize(width2, height2, {
|
|
221
236
|
fit: context.fit,
|
|
222
237
|
background: context.background
|
|
@@ -360,7 +375,7 @@ function createIPX(userOptions) {
|
|
|
360
375
|
alias: getEnv("IPX_ALIAS", {}),
|
|
361
376
|
sharp: {}
|
|
362
377
|
};
|
|
363
|
-
const options = defu__default
|
|
378
|
+
const options = defu__default(userOptions, defaults);
|
|
364
379
|
options.alias = Object.fromEntries(Object.entries(options.alias).map((e) => [ufo.withLeadingSlash(e[0]), e[1]]));
|
|
365
380
|
const ctx = {
|
|
366
381
|
sources: {}
|
|
@@ -395,7 +410,7 @@ function createIPX(userOptions) {
|
|
|
395
410
|
const getData = cachedPromise(async () => {
|
|
396
411
|
const src = await getSrc();
|
|
397
412
|
const data = await src.getData();
|
|
398
|
-
const meta =
|
|
413
|
+
const meta = imageMeta.imageMeta(data);
|
|
399
414
|
const mFormat = modifiers.f || modifiers.format;
|
|
400
415
|
let format = mFormat || meta.type;
|
|
401
416
|
if (format === "jpg") {
|
|
@@ -412,14 +427,15 @@ function createIPX(userOptions) {
|
|
|
412
427
|
if (animated) {
|
|
413
428
|
format = "webp";
|
|
414
429
|
}
|
|
415
|
-
|
|
430
|
+
const Sharp = await import('sharp').then((r) => r.default || r);
|
|
431
|
+
let sharp = Sharp(data, { animated });
|
|
416
432
|
Object.assign(sharp.options, options.sharp);
|
|
417
433
|
const handlers = Object.entries(modifiers).map(([name, args]) => ({ handler: getHandler(name), name, args })).filter((h) => h.handler).sort((a, b) => {
|
|
418
434
|
const aKey = (a.handler.order || a.name || "").toString();
|
|
419
435
|
const bKey = (b.handler.order || b.name || "").toString();
|
|
420
436
|
return aKey.localeCompare(bKey);
|
|
421
437
|
});
|
|
422
|
-
const handlerCtx = {};
|
|
438
|
+
const handlerCtx = { meta };
|
|
423
439
|
for (const h of handlers) {
|
|
424
440
|
sharp = applyHandler(handlerCtx, sharp, h.handler, h.args) || sharp;
|
|
425
441
|
}
|
|
@@ -480,7 +496,7 @@ async function _handleRequest(req, ipx) {
|
|
|
480
496
|
res.headers["Cache-Control"] = `max-age=${+src.maxAge}, public, s-maxage=${+src.maxAge}`;
|
|
481
497
|
}
|
|
482
498
|
const { data, format } = await img.data();
|
|
483
|
-
const etag = getEtag__default
|
|
499
|
+
const etag = getEtag__default(data);
|
|
484
500
|
res.headers.ETag = etag;
|
|
485
501
|
if (etag && req.headers["if-none-match"] === etag) {
|
|
486
502
|
res.statusCode = 304;
|
|
@@ -495,7 +511,7 @@ async function _handleRequest(req, ipx) {
|
|
|
495
511
|
function handleRequest(req, ipx) {
|
|
496
512
|
return _handleRequest(req, ipx).catch((err) => {
|
|
497
513
|
const statusCode = parseInt(err.statusCode) || 500;
|
|
498
|
-
const statusMessage = err.statusMessage ? xss__default
|
|
514
|
+
const statusMessage = err.statusMessage ? xss__default(err.statusMessage) : `IPX Error (${statusCode})`;
|
|
499
515
|
if (process.env.NODE_ENV !== "production" && statusCode === 500) {
|
|
500
516
|
console.error(err);
|
|
501
517
|
}
|
|
@@ -520,14 +536,6 @@ function createIPXMiddleware(ipx) {
|
|
|
520
536
|
};
|
|
521
537
|
}
|
|
522
538
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
await listhen.listen(middleware, {
|
|
527
|
-
clipboard: false
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
main().catch((err) => {
|
|
531
|
-
consola__default['default'].error(err);
|
|
532
|
-
process.exit(1);
|
|
533
|
-
});
|
|
539
|
+
exports.createIPX = createIPX;
|
|
540
|
+
exports.createIPXMiddleware = createIPXMiddleware;
|
|
541
|
+
exports.handleRequest = handleRequest;
|