@websolutespa/bom-mixer-image 1.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 +7 -0
- package/README.md +11 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +186 -0
- package/dist/index.mjs +149 -0
- package/package.json +58 -0
- package/src/__tests/example.test.ts +8 -0
- package/src/api/api.handler.ts +43 -0
- package/src/image/image.handler.ts +189 -0
- package/src/index.ts +2 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @websolutespa/bom-mixer-image
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40websolutespa%2Fbom-mixer-image)
|
|
4
|
+
|
|
5
|
+
[](https://shields.io/)
|
|
6
|
+
|
|
7
|
+
Mixer Image module of the [BOM Repository](https://github.com/websolutespa/bom) by [websolute](https://www.websolute.com/).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
##### *this library is for internal usage and not production ready*
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
imageHandler: () => imageHandler
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(src_exports);
|
|
36
|
+
|
|
37
|
+
// src/image/image.handler.ts
|
|
38
|
+
var fs = __toESM(require("fs"));
|
|
39
|
+
var path = __toESM(require("path"));
|
|
40
|
+
var import_sharp = __toESM(require("sharp"));
|
|
41
|
+
|
|
42
|
+
// src/api/api.handler.ts
|
|
43
|
+
function apiHandler(handler) {
|
|
44
|
+
return async (request, response) => {
|
|
45
|
+
const method = (request.method || "GET").toLowerCase();
|
|
46
|
+
if (!handler[method]) {
|
|
47
|
+
return response.status(405).end(`Method ${method} Not Allowed`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await handler[method](request, response);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return errorHandler(error, response);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function errorHandler(error, response) {
|
|
57
|
+
if (typeof error === "string") {
|
|
58
|
+
const is404 = error.toLowerCase().endsWith("not found");
|
|
59
|
+
const statusCode = is404 ? 404 : 400;
|
|
60
|
+
return response.status(statusCode).json({ message: error });
|
|
61
|
+
}
|
|
62
|
+
if (error.name === "UnauthorizedError") {
|
|
63
|
+
return response.status(401).json({ message: "Invalid Token" });
|
|
64
|
+
}
|
|
65
|
+
console.error(error);
|
|
66
|
+
return response.status(500).json({ message: error.message });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/image/image.handler.ts
|
|
70
|
+
var IMAGE_MAX_AGE = 604800;
|
|
71
|
+
function imageHandler({ domains = [] }) {
|
|
72
|
+
return apiHandler({
|
|
73
|
+
// Defining the 'get' method for handling GET requests
|
|
74
|
+
get: async (request, response) => {
|
|
75
|
+
try {
|
|
76
|
+
const { url, w, h, q } = request.query;
|
|
77
|
+
if (typeof url !== "string") {
|
|
78
|
+
return response.status(400).send("missing url parameter");
|
|
79
|
+
}
|
|
80
|
+
const width = typeof w === "string" ? parseInt(w) : void 0;
|
|
81
|
+
if (!width) {
|
|
82
|
+
return response.status(400).send("missing w parameter");
|
|
83
|
+
}
|
|
84
|
+
if (width > 2048) {
|
|
85
|
+
return response.status(400).send("invalid w parameter");
|
|
86
|
+
}
|
|
87
|
+
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
88
|
+
const quality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : 80));
|
|
89
|
+
const cachedName = urlToCachedName(url, width, height, quality);
|
|
90
|
+
if (fs.existsSync(cachedName)) {
|
|
91
|
+
const cachedImage = await getCachedImage(cachedName);
|
|
92
|
+
response.setHeader("Content-Type", "image/webp");
|
|
93
|
+
response.setHeader("Expires", new Date(Date.now() + IMAGE_MAX_AGE * 1e3).toUTCString());
|
|
94
|
+
response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
|
|
95
|
+
return response.send(cachedImage);
|
|
96
|
+
}
|
|
97
|
+
const originalImage = await getOriginalImage(url, domains);
|
|
98
|
+
if (originalImage) {
|
|
99
|
+
const resizedImage = (0, import_sharp.default)(originalImage).resize({
|
|
100
|
+
width,
|
|
101
|
+
height: height ? height : void 0,
|
|
102
|
+
fit: import_sharp.default.fit.cover,
|
|
103
|
+
position: import_sharp.default.strategy.entropy
|
|
104
|
+
}).webp({ quality });
|
|
105
|
+
resizedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("error caching image", cachedName, error));
|
|
106
|
+
const data = await resizedImage.toBuffer();
|
|
107
|
+
return response.end(data);
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("imageHandler.error", error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function getOriginalImage(url, domains) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
if (url.indexOf("http") === 0) {
|
|
119
|
+
if (!checkUrlValidity(url, domains)) {
|
|
120
|
+
console.error("imageHandler.getOriginalImage.error invalid url");
|
|
121
|
+
return reject({ status: 401, message: "invalid url" });
|
|
122
|
+
}
|
|
123
|
+
fetch(url, {
|
|
124
|
+
method: "GET",
|
|
125
|
+
mode: "cors",
|
|
126
|
+
headers: {
|
|
127
|
+
"Access-Control-Allow-Origin": "*"
|
|
128
|
+
}
|
|
129
|
+
}).then((response) => {
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
return reject({ status: response.status, message: response.statusText });
|
|
132
|
+
}
|
|
133
|
+
response.arrayBuffer().then((buffer) => {
|
|
134
|
+
return resolve(Buffer.from(buffer));
|
|
135
|
+
}).catch((error) => {
|
|
136
|
+
return reject(error);
|
|
137
|
+
});
|
|
138
|
+
}).catch((error) => {
|
|
139
|
+
return reject(error);
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
fs.readFile(url, (error, data) => {
|
|
143
|
+
if (error) {
|
|
144
|
+
return reject(error);
|
|
145
|
+
}
|
|
146
|
+
return resolve(data);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function getCachedImage(pathname) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
fs.readFile(pathname, (error, data) => {
|
|
154
|
+
if (error) {
|
|
155
|
+
return reject(error);
|
|
156
|
+
}
|
|
157
|
+
return resolve(data);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function urlToFileName(url) {
|
|
162
|
+
let uniqueName = url.replace("://", "_").replace(".", "_").replace("/", "_");
|
|
163
|
+
uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf("_")) + "." + uniqueName.substring(uniqueName.lastIndexOf("_") + 1, uniqueName.length);
|
|
164
|
+
return uniqueName;
|
|
165
|
+
}
|
|
166
|
+
function isVercel() {
|
|
167
|
+
return process && process.env && String(process.env.VERCEL) === "1";
|
|
168
|
+
}
|
|
169
|
+
function urlToCachedName(url, width, height, quality) {
|
|
170
|
+
const fileName = urlToFileName(url);
|
|
171
|
+
const parsedName = path.parse(fileName);
|
|
172
|
+
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
173
|
+
if (!fs.existsSync(cacheDir)) {
|
|
174
|
+
fs.mkdirSync(cacheDir);
|
|
175
|
+
}
|
|
176
|
+
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_q_${quality}${parsedName.ext}`);
|
|
177
|
+
return cachedName;
|
|
178
|
+
}
|
|
179
|
+
function checkUrlValidity(src, domains) {
|
|
180
|
+
const url = new URL(src);
|
|
181
|
+
return domains.includes(url.hostname);
|
|
182
|
+
}
|
|
183
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
184
|
+
0 && (module.exports = {
|
|
185
|
+
imageHandler
|
|
186
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/image/image.handler.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import sharp from "sharp";
|
|
5
|
+
|
|
6
|
+
// src/api/api.handler.ts
|
|
7
|
+
function apiHandler(handler) {
|
|
8
|
+
return async (request, response) => {
|
|
9
|
+
const method = (request.method || "GET").toLowerCase();
|
|
10
|
+
if (!handler[method]) {
|
|
11
|
+
return response.status(405).end(`Method ${method} Not Allowed`);
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await handler[method](request, response);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return errorHandler(error, response);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function errorHandler(error, response) {
|
|
21
|
+
if (typeof error === "string") {
|
|
22
|
+
const is404 = error.toLowerCase().endsWith("not found");
|
|
23
|
+
const statusCode = is404 ? 404 : 400;
|
|
24
|
+
return response.status(statusCode).json({ message: error });
|
|
25
|
+
}
|
|
26
|
+
if (error.name === "UnauthorizedError") {
|
|
27
|
+
return response.status(401).json({ message: "Invalid Token" });
|
|
28
|
+
}
|
|
29
|
+
console.error(error);
|
|
30
|
+
return response.status(500).json({ message: error.message });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/image/image.handler.ts
|
|
34
|
+
var IMAGE_MAX_AGE = 604800;
|
|
35
|
+
function imageHandler({ domains = [] }) {
|
|
36
|
+
return apiHandler({
|
|
37
|
+
// Defining the 'get' method for handling GET requests
|
|
38
|
+
get: async (request, response) => {
|
|
39
|
+
try {
|
|
40
|
+
const { url, w, h, q } = request.query;
|
|
41
|
+
if (typeof url !== "string") {
|
|
42
|
+
return response.status(400).send("missing url parameter");
|
|
43
|
+
}
|
|
44
|
+
const width = typeof w === "string" ? parseInt(w) : void 0;
|
|
45
|
+
if (!width) {
|
|
46
|
+
return response.status(400).send("missing w parameter");
|
|
47
|
+
}
|
|
48
|
+
if (width > 2048) {
|
|
49
|
+
return response.status(400).send("invalid w parameter");
|
|
50
|
+
}
|
|
51
|
+
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
52
|
+
const quality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : 80));
|
|
53
|
+
const cachedName = urlToCachedName(url, width, height, quality);
|
|
54
|
+
if (fs.existsSync(cachedName)) {
|
|
55
|
+
const cachedImage = await getCachedImage(cachedName);
|
|
56
|
+
response.setHeader("Content-Type", "image/webp");
|
|
57
|
+
response.setHeader("Expires", new Date(Date.now() + IMAGE_MAX_AGE * 1e3).toUTCString());
|
|
58
|
+
response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
|
|
59
|
+
return response.send(cachedImage);
|
|
60
|
+
}
|
|
61
|
+
const originalImage = await getOriginalImage(url, domains);
|
|
62
|
+
if (originalImage) {
|
|
63
|
+
const resizedImage = sharp(originalImage).resize({
|
|
64
|
+
width,
|
|
65
|
+
height: height ? height : void 0,
|
|
66
|
+
fit: sharp.fit.cover,
|
|
67
|
+
position: sharp.strategy.entropy
|
|
68
|
+
}).webp({ quality });
|
|
69
|
+
resizedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("error caching image", cachedName, error));
|
|
70
|
+
const data = await resizedImage.toBuffer();
|
|
71
|
+
return response.end(data);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("imageHandler.error", error);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function getOriginalImage(url, domains) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
if (url.indexOf("http") === 0) {
|
|
83
|
+
if (!checkUrlValidity(url, domains)) {
|
|
84
|
+
console.error("imageHandler.getOriginalImage.error invalid url");
|
|
85
|
+
return reject({ status: 401, message: "invalid url" });
|
|
86
|
+
}
|
|
87
|
+
fetch(url, {
|
|
88
|
+
method: "GET",
|
|
89
|
+
mode: "cors",
|
|
90
|
+
headers: {
|
|
91
|
+
"Access-Control-Allow-Origin": "*"
|
|
92
|
+
}
|
|
93
|
+
}).then((response) => {
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
return reject({ status: response.status, message: response.statusText });
|
|
96
|
+
}
|
|
97
|
+
response.arrayBuffer().then((buffer) => {
|
|
98
|
+
return resolve(Buffer.from(buffer));
|
|
99
|
+
}).catch((error) => {
|
|
100
|
+
return reject(error);
|
|
101
|
+
});
|
|
102
|
+
}).catch((error) => {
|
|
103
|
+
return reject(error);
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
fs.readFile(url, (error, data) => {
|
|
107
|
+
if (error) {
|
|
108
|
+
return reject(error);
|
|
109
|
+
}
|
|
110
|
+
return resolve(data);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function getCachedImage(pathname) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
fs.readFile(pathname, (error, data) => {
|
|
118
|
+
if (error) {
|
|
119
|
+
return reject(error);
|
|
120
|
+
}
|
|
121
|
+
return resolve(data);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function urlToFileName(url) {
|
|
126
|
+
let uniqueName = url.replace("://", "_").replace(".", "_").replace("/", "_");
|
|
127
|
+
uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf("_")) + "." + uniqueName.substring(uniqueName.lastIndexOf("_") + 1, uniqueName.length);
|
|
128
|
+
return uniqueName;
|
|
129
|
+
}
|
|
130
|
+
function isVercel() {
|
|
131
|
+
return process && process.env && String(process.env.VERCEL) === "1";
|
|
132
|
+
}
|
|
133
|
+
function urlToCachedName(url, width, height, quality) {
|
|
134
|
+
const fileName = urlToFileName(url);
|
|
135
|
+
const parsedName = path.parse(fileName);
|
|
136
|
+
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
137
|
+
if (!fs.existsSync(cacheDir)) {
|
|
138
|
+
fs.mkdirSync(cacheDir);
|
|
139
|
+
}
|
|
140
|
+
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_q_${quality}${parsedName.ext}`);
|
|
141
|
+
return cachedName;
|
|
142
|
+
}
|
|
143
|
+
function checkUrlValidity(src, domains) {
|
|
144
|
+
const url = new URL(src);
|
|
145
|
+
return domains.includes(url.hostname);
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
imageHandler
|
|
149
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@websolutespa/bom-mixer-image",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mixer Image module of the BOM Repository",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bom",
|
|
7
|
+
"mixer",
|
|
8
|
+
"image"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git://github.com/websolutespa/bom.git"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"lint": "eslint *.ts*",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test-watch": "npm test -- --watch",
|
|
19
|
+
"coverage:integration": "npm run test -- --coverage",
|
|
20
|
+
"coverage": "run-s -c --silent coverage:*",
|
|
21
|
+
"compile": "tsup ./src/index.ts --format esm,cjs --dts",
|
|
22
|
+
"prepack": "bom prepack",
|
|
23
|
+
"postpack": "bom postpack"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"next": ">= 13.4.19",
|
|
28
|
+
"sharp": ">= 0.30.4"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@websolutespa/bom-cli": "*",
|
|
32
|
+
"@websolutespa/test": "*",
|
|
33
|
+
"@websolutespa/tsconfig": "*",
|
|
34
|
+
"eslint": "^8.46.0",
|
|
35
|
+
"eslint-config-websolute": "*",
|
|
36
|
+
"npm-run-all": "^4.1.5",
|
|
37
|
+
"sharp": "0.30.4",
|
|
38
|
+
"ts-node": "^10.9.1",
|
|
39
|
+
"tsup": "^6.7.0",
|
|
40
|
+
"typescript": "^5.1.6"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"prepack": {
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"main": "./dist/index.js",
|
|
49
|
+
"source": "./src/index.ts"
|
|
50
|
+
},
|
|
51
|
+
"postpack": {
|
|
52
|
+
"types": "./src/index.ts",
|
|
53
|
+
"main": "./src/index.ts"
|
|
54
|
+
},
|
|
55
|
+
"types": "./dist/index.d.ts",
|
|
56
|
+
"main": "./dist/index.js",
|
|
57
|
+
"source": "./src/index.ts"
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
|
|
3
|
+
export type ICrudMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | string;
|
|
4
|
+
|
|
5
|
+
export type IHandler = (request: NextApiRequest, response: NextApiResponse) => Promise<any>;
|
|
6
|
+
|
|
7
|
+
export type IApiHandler = Record<ICrudMethod, IHandler>;
|
|
8
|
+
|
|
9
|
+
export function apiHandler(handler: IApiHandler): any {
|
|
10
|
+
return async (request: NextApiRequest, response: NextApiResponse): Promise<any> => {
|
|
11
|
+
const method: ICrudMethod = (request.method || 'GET').toLowerCase() as ICrudMethod;
|
|
12
|
+
// check handler supports HTTP method
|
|
13
|
+
if (!handler[method]) {
|
|
14
|
+
return response.status(405).end(`Method ${method} Not Allowed`);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
// global middleware
|
|
18
|
+
// await jwtMiddleware(request, response);
|
|
19
|
+
// route handler
|
|
20
|
+
await handler[method](request, response);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// console.log('error', error);
|
|
23
|
+
// global error handler
|
|
24
|
+
return errorHandler(error, response);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function errorHandler(error: any, response: NextApiResponse): Promise<any> | void {
|
|
30
|
+
if (typeof (error) === 'string') {
|
|
31
|
+
// custom application error
|
|
32
|
+
const is404 = error.toLowerCase().endsWith('not found');
|
|
33
|
+
const statusCode = is404 ? 404 : 400;
|
|
34
|
+
return response.status(statusCode).json({ message: error });
|
|
35
|
+
}
|
|
36
|
+
if (error.name === 'UnauthorizedError') {
|
|
37
|
+
// jwt authentication error
|
|
38
|
+
return response.status(401).json({ message: 'Invalid Token' });
|
|
39
|
+
}
|
|
40
|
+
// default to 500 server error
|
|
41
|
+
console.error(error);
|
|
42
|
+
return response.status(500).json({ message: error.message });
|
|
43
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Importing the 'fs' module to work with the file system
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
// Importing the 'NextApiRequest' and 'NextApiResponse' from 'next' for handling API requests and responses
|
|
4
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
5
|
+
// Importing the 'path' module to work with file and directory paths
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
// Importing 'sharp' for image processing
|
|
8
|
+
import sharp from 'sharp';
|
|
9
|
+
// Importing 'apiHandler' from '../api/api.handler'
|
|
10
|
+
import { apiHandler } from '../api/api.handler';
|
|
11
|
+
|
|
12
|
+
const IMAGE_MAX_AGE = 604800;
|
|
13
|
+
|
|
14
|
+
// Exporting the 'imageHandler' function
|
|
15
|
+
export function imageHandler({ domains = [] }: { domains: string[] }) {
|
|
16
|
+
// Returning the 'apiHandler' function with a configuration object
|
|
17
|
+
return apiHandler({
|
|
18
|
+
// Defining the 'get' method for handling GET requests
|
|
19
|
+
get: async (request: NextApiRequest, response: NextApiResponse) => {
|
|
20
|
+
try {
|
|
21
|
+
// Destructuring 'url', 'w', 'h', 'q' from the request query
|
|
22
|
+
const { url, w, h, q } = request.query;
|
|
23
|
+
// Checking if 'url' is not a string
|
|
24
|
+
if (typeof url !== 'string') {
|
|
25
|
+
// Logging an error message and returning a 400 status code with a message
|
|
26
|
+
// console.error('imageHandler', 'missing url parameter');
|
|
27
|
+
return response.status(400).send('missing url parameter');
|
|
28
|
+
}
|
|
29
|
+
// Parsing 'w' to an integer and assigning it to 'width'
|
|
30
|
+
const width = typeof w === 'string' ? parseInt(w) : undefined;
|
|
31
|
+
// Checking if 'width' is not defined
|
|
32
|
+
if (!width) {
|
|
33
|
+
// Logging an error message and returning a 400 status code with a message
|
|
34
|
+
// console.error('imageHandler', 'missing w parameter');
|
|
35
|
+
return response.status(400).send('missing w parameter');
|
|
36
|
+
}
|
|
37
|
+
if (width > 2048) {
|
|
38
|
+
return response.status(400).send('invalid w parameter');
|
|
39
|
+
}
|
|
40
|
+
// Parsing 'h' to an integer and assigning it to 'height'
|
|
41
|
+
const height = typeof h === 'string' ? parseInt(h) : undefined;
|
|
42
|
+
// Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'quality'
|
|
43
|
+
const quality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : 80)));
|
|
44
|
+
// Generating a cached name for the image
|
|
45
|
+
const cachedName = urlToCachedName(url, width, height, quality);
|
|
46
|
+
// Checking if the cached image exists
|
|
47
|
+
if (fs.existsSync(cachedName)) {
|
|
48
|
+
// Reading the cached image
|
|
49
|
+
const cachedImage = await getCachedImage(cachedName);
|
|
50
|
+
// Setting the 'Content-Type' header to 'image/webp'
|
|
51
|
+
response.setHeader('Content-Type', 'image/webp');
|
|
52
|
+
// Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)
|
|
53
|
+
response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());
|
|
54
|
+
// Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image
|
|
55
|
+
response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);
|
|
56
|
+
// Ending the response and sending the cached image
|
|
57
|
+
return response.send(cachedImage);
|
|
58
|
+
}
|
|
59
|
+
// Reading the original image
|
|
60
|
+
const originalImage = await getOriginalImage(url, domains);
|
|
61
|
+
// Checking if the original image exists
|
|
62
|
+
if (originalImage) {
|
|
63
|
+
// Resizing the original image and converting it to WebP format
|
|
64
|
+
const resizedImage = sharp(originalImage)
|
|
65
|
+
.resize({
|
|
66
|
+
width,
|
|
67
|
+
height: height ? height : undefined,
|
|
68
|
+
fit: sharp.fit.cover,
|
|
69
|
+
position: sharp.strategy.entropy,
|
|
70
|
+
})
|
|
71
|
+
.webp({ quality });
|
|
72
|
+
// Saving the resized image to the cache
|
|
73
|
+
resizedImage.toFile(cachedName)
|
|
74
|
+
.then(() => console.log('imageHandler.sharp.cached', cachedName))
|
|
75
|
+
.catch(error => console.error('error caching image', cachedName, error));
|
|
76
|
+
// Converting the resized image to a buffer and sending it in the response
|
|
77
|
+
const data = await resizedImage.toBuffer();
|
|
78
|
+
return response.end(data);
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Logging an error message and throwing the error
|
|
82
|
+
console.error('imageHandler.error', error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Defining the 'getOriginalImage' function
|
|
90
|
+
function getOriginalImage(url: string, domains: string[]): Promise<Buffer> {
|
|
91
|
+
// Returning a new promise
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
// Checking if 'url' starts with 'http'
|
|
94
|
+
if (url.indexOf('http') === 0) {
|
|
95
|
+
if (!checkUrlValidity(url, domains)) {
|
|
96
|
+
console.error('imageHandler.getOriginalImage.error invalid url');
|
|
97
|
+
return reject({ status: 401, message: 'invalid url' });
|
|
98
|
+
}
|
|
99
|
+
// Fetching the image from the URL
|
|
100
|
+
fetch(url, {
|
|
101
|
+
method: 'GET',
|
|
102
|
+
mode: 'cors',
|
|
103
|
+
headers: {
|
|
104
|
+
'Access-Control-Allow-Origin': '*',
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
.then(response => {
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
return reject({ status: response.status, message: response.statusText });
|
|
110
|
+
}
|
|
111
|
+
response.arrayBuffer().then(buffer => {
|
|
112
|
+
return resolve(Buffer.from(buffer));
|
|
113
|
+
}).catch(error => {
|
|
114
|
+
return reject(error);
|
|
115
|
+
});
|
|
116
|
+
})
|
|
117
|
+
.catch(error => {
|
|
118
|
+
// Rejecting the promise with the error
|
|
119
|
+
return reject(error);
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
// Reading the image from the file system
|
|
123
|
+
fs.readFile(url, (error, data) => {
|
|
124
|
+
if (error) {
|
|
125
|
+
return reject(error);
|
|
126
|
+
}
|
|
127
|
+
// Resolving the promise with the image data
|
|
128
|
+
return resolve(data);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Defining the 'getCachedImage' function
|
|
135
|
+
function getCachedImage(pathname: string): Promise<Buffer> {
|
|
136
|
+
// Returning a new promise
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
// Reading the image from the file system
|
|
139
|
+
fs.readFile(pathname, (error, data) => {
|
|
140
|
+
if (error) {
|
|
141
|
+
// Rejecting the promise with the error
|
|
142
|
+
return reject(error);
|
|
143
|
+
}
|
|
144
|
+
// Resolving the promise with the image data
|
|
145
|
+
return resolve(data);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Defining the 'urlToFileName' function
|
|
151
|
+
function urlToFileName(url: string): string {
|
|
152
|
+
// Replacing useless characters with UNDERSCORE
|
|
153
|
+
let uniqueName = url.replace('://', '_').replace('.', '_').replace('/', '_');
|
|
154
|
+
// Replacing last UNDERSCORE with a DOT
|
|
155
|
+
uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf('_'))
|
|
156
|
+
+ '.' + uniqueName.substring(uniqueName.lastIndexOf('_') + 1, uniqueName.length);
|
|
157
|
+
// Returning the unique name
|
|
158
|
+
return uniqueName;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isVercel(): boolean {
|
|
162
|
+
return process && process.env && String(process.env.VERCEL) === '1';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Defining the 'urlToCachedName' function
|
|
166
|
+
function urlToCachedName(url: string, width: number, height: number | undefined, quality: number): string {
|
|
167
|
+
// Converting the URL to a file name
|
|
168
|
+
const fileName = urlToFileName(url);
|
|
169
|
+
// Parsing the file name
|
|
170
|
+
const parsedName = path.parse(fileName);
|
|
171
|
+
// Defining the cache directory
|
|
172
|
+
const cacheDir = isVercel() ?
|
|
173
|
+
path.join('/tmp', '.cache') :
|
|
174
|
+
path.join(process.cwd(), '.cache');
|
|
175
|
+
// Checking if the cache directory exists
|
|
176
|
+
if (!fs.existsSync(cacheDir)) {
|
|
177
|
+
// Creating the cache directory
|
|
178
|
+
fs.mkdirSync(cacheDir);
|
|
179
|
+
}
|
|
180
|
+
// Generating the cached name
|
|
181
|
+
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_q_${quality}${parsedName.ext}`);
|
|
182
|
+
// Returning the cached name
|
|
183
|
+
return cachedName;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function checkUrlValidity(src: string, domains: string[]): boolean {
|
|
187
|
+
const url = new URL(src);
|
|
188
|
+
return domains.includes(url.hostname);
|
|
189
|
+
}
|
package/src/index.ts
ADDED