UNPKG

pixel-serve-server

Version:

A robust Node.js utility for handling and processing images. This package provides features like resizing, format conversion and etc.

1 lines 18.5 kB
{"version":3,"sources":["../src/pixel.ts","../src/variables.ts","../src/functions.ts","../src/renders.ts"],"sourcesContent":["import path from \"node:path\";\r\nimport sharp, { FormatEnum, ResizeOptions } from \"sharp\";\r\nimport type { Request, Response, NextFunction } from \"express\";\r\nimport type { Options, UserData, ImageFormat, ImageType } from \"./types\";\r\nimport { allowedFormats, mimeTypes } from \"./variables\";\r\nimport { fetchImage, readLocalImage } from \"./functions\";\r\nimport { renderOptions, renderUserData } from \"./renders\";\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @function serveImage\r\n * @description Processes and serves an image based on user data and options.\r\n * @param {Request} req - The Express request object.\r\n * @param {Response} res - The Express response object.\r\n * @param {NextFunction} next - The Express next function.\r\n * @param {Options} options - The options object for image processing.\r\n * @returns {Promise<void>}\r\n */\r\nconst serveImage = async (\r\n req: Request,\r\n res: Response,\r\n next: NextFunction,\r\n options: Options\r\n) => {\r\n try {\r\n const userData = renderUserData(req.query as UserData);\r\n const parsedOptions = renderOptions(options);\r\n\r\n let imageBuffer;\r\n let baseDir = parsedOptions.baseDir;\r\n let parsedUserId;\r\n\r\n if (userData.userId) {\r\n const userIdStr =\r\n typeof userData.userId === \"object\"\r\n ? String(Object.values(userData.userId)[0])\r\n : String(userData.userId);\r\n if (parsedOptions.idHandler) {\r\n parsedUserId = parsedOptions.idHandler(userIdStr);\r\n } else {\r\n parsedUserId = userIdStr;\r\n }\r\n }\r\n\r\n if (userData.folder === \"private\") {\r\n const dir = await parsedOptions?.getUserFolder?.(req, parsedUserId);\r\n if (dir) {\r\n baseDir = dir;\r\n }\r\n }\r\n\r\n const outputFormat = allowedFormats.includes(\r\n userData?.format?.toLowerCase() as ImageFormat\r\n )\r\n ? userData?.format?.toLowerCase()\r\n : \"jpeg\";\r\n\r\n if (userData?.src?.startsWith(\"http\")) {\r\n imageBuffer = await fetchImage(\r\n userData?.src ?? \"\",\r\n baseDir,\r\n parsedOptions?.websiteURL ?? \"\",\r\n userData?.type as ImageType,\r\n parsedOptions?.apiRegex,\r\n parsedOptions?.allowedNetworkList\r\n );\r\n } else {\r\n imageBuffer = await readLocalImage(\r\n userData?.src ?? \"\",\r\n baseDir,\r\n userData?.type as ImageType\r\n );\r\n }\r\n\r\n let image = sharp(imageBuffer);\r\n\r\n if (userData?.width || userData?.height) {\r\n const resizeOptions = {\r\n width: userData?.width ?? undefined,\r\n height: userData?.height ?? undefined,\r\n fit: sharp.fit.cover,\r\n };\r\n image = image.resize(resizeOptions as ResizeOptions);\r\n }\r\n\r\n const processedImage = await image\r\n .toFormat(outputFormat as keyof FormatEnum, {\r\n quality: userData?.quality ? Number(userData?.quality) : 80,\r\n })\r\n .toBuffer();\r\n\r\n const processedFileName = `${path.basename(\r\n userData?.src ?? \"\",\r\n path.extname(userData?.src ?? \"\")\r\n )}.${outputFormat}`;\r\n\r\n res.type(mimeTypes[outputFormat]);\r\n res.setHeader(\r\n \"Content-Disposition\",\r\n `inline; filename=\"${processedFileName}\"`\r\n );\r\n res.send(processedImage);\r\n } catch (error) {\r\n next(error);\r\n }\r\n};\r\n\r\n/**\r\n * @function registerServe\r\n * @description A function to register the serveImage function as middleware for Express.\r\n * @param {Options} options - The options object for image processing.\r\n * @returns {function(Request, Response, NextFunction): Promise<void>} The middleware function.\r\n */\r\nconst registerServe = (options: Options) => {\r\n return async (req: Request, res: Response, next: NextFunction) =>\r\n serveImage(req, res, next, options);\r\n};\r\n\r\nexport default registerServe;\r\n","import type { ImageFormat } from \"./types\";\r\nimport { readFile } from \"node:fs/promises\";\r\n\r\nconst NOT_FOUND_IMAGE = new URL(\"./assets/noimage.jpg\", import.meta.url)\r\n .pathname;\r\n\r\nconst NOT_FOUND_AVATAR = new URL(\"./assets/noavatar.png\", import.meta.url)\r\n .pathname;\r\n\r\nexport const FALLBACKIMAGES = {\r\n normal: async () => readFile(NOT_FOUND_IMAGE),\r\n avatar: async () => readFile(NOT_FOUND_AVATAR),\r\n};\r\n\r\nexport const API_REGEX: RegExp = /^\\/api\\/v1\\//;\r\n\r\nexport const allowedFormats: ImageFormat[] = [\r\n \"jpeg\",\r\n \"jpg\",\r\n \"png\",\r\n \"webp\",\r\n \"gif\",\r\n \"tiff\",\r\n \"avif\",\r\n \"svg\",\r\n];\r\n\r\nexport const mimeTypes: Readonly<Record<string, string>> = {\r\n jpeg: \"image/jpeg\",\r\n jpg: \"image/jpeg\",\r\n png: \"image/png\",\r\n webp: \"image/webp\",\r\n gif: \"image/gif\",\r\n tiff: \"image/tiff\",\r\n avif: \"image/avif\",\r\n svg: \"image/svg+xml\",\r\n};\r\n","import path from \"node:path\";\r\nimport * as fs from \"node:fs/promises\";\r\nimport axios from \"axios\";\r\nimport { mimeTypes, API_REGEX, FALLBACKIMAGES } from \"./variables\";\r\nimport type { ImageType } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * Checks if a specified path is valid within a base path.\r\n *\r\n * @param {string} basePath - The base directory to resolve paths.\r\n * @param {string} specifiedPath - The path to check.\r\n * @returns {boolean} True if the path is valid, false otherwise.\r\n */\r\nexport const isValidPath = async (\r\n basePath: string,\r\n specifiedPath: string\r\n): Promise<boolean> => {\r\n try {\r\n if (!basePath || !specifiedPath) return false;\r\n if (specifiedPath.includes(\"\\0\")) return false;\r\n if (path.isAbsolute(specifiedPath)) return false;\r\n if (!/^[^\\x00-\\x1F]+$/.test(specifiedPath)) return false;\r\n\r\n const resolvedBase = path.resolve(basePath);\r\n const resolvedPath = path.resolve(resolvedBase, specifiedPath);\r\n\r\n const [realBase, realPath] = await Promise.all([\r\n fs.realpath(resolvedBase),\r\n fs.realpath(resolvedPath),\r\n ]);\r\n\r\n const baseStats = await fs.stat(realBase);\r\n if (!baseStats.isDirectory()) return false;\r\n\r\n const normalizedBase = realBase + path.sep;\r\n const normalizedPath = realPath + path.sep;\r\n\r\n const isInside =\r\n normalizedPath.startsWith(normalizedBase) || realPath === realBase;\r\n\r\n const relative = path.relative(realBase, realPath);\r\n return !relative.startsWith(\"..\") && !path.isAbsolute(relative) && isInside;\r\n } catch {\r\n return false;\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from a network source.\r\n *\r\n * @param {string} src - The URL of the image.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image in case of an error.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nconst fetchFromNetwork = async (\r\n src: string,\r\n type: ImageType = \"normal\"\r\n): Promise<Buffer> => {\r\n try {\r\n const response = await axios.get(src, {\r\n responseType: \"arraybuffer\",\r\n timeout: 5000,\r\n });\r\n\r\n const contentType = response.headers[\"content-type\"]?.toLowerCase();\r\n const allowedMimeTypes = Object.values(mimeTypes);\r\n\r\n if (allowedMimeTypes.includes(contentType ?? \"\")) {\r\n return Buffer.from(response.data);\r\n }\r\n return await FALLBACKIMAGES[type]();\r\n } catch (error) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Reads an image from the local file system.\r\n *\r\n * @param {string} filePath - Path to the image file.\r\n * @param {string} baseDir - Base directory to resolve paths.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @returns {Promise<Buffer>} A buffer containing the image data.\r\n */\r\nexport const readLocalImage = async (\r\n filePath: string,\r\n baseDir: string,\r\n type: ImageType = \"normal\"\r\n) => {\r\n const isValid = await isValidPath(baseDir, filePath);\r\n if (!isValid) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n try {\r\n return await fs.readFile(path.resolve(baseDir, filePath));\r\n } catch (error) {\r\n return await FALLBACKIMAGES[type]();\r\n }\r\n};\r\n\r\n/**\r\n * Fetches an image from either a local file or a network source.\r\n *\r\n * @param {string} src - The URL or local path of the image.\r\n * @param {string} baseDir - Base directory to resolve local paths.\r\n * @param {string} websiteURL - The URL of the website.\r\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\r\n * @param {RegExp} [apiRegex=API_REGEX] - Regular expression to match API routes.\r\n * @param {string[]} [allowedNetworkList=[]] - List of allowed network hosts.\r\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\r\n */\r\nexport const fetchImage = (\r\n src: string,\r\n baseDir: string,\r\n websiteURL: string,\r\n type: ImageType = \"normal\",\r\n apiRegex: RegExp = API_REGEX,\r\n allowedNetworkList: string[] = []\r\n) => {\r\n const url = new URL(src);\r\n const isInternal = [websiteURL, `www.${websiteURL}`].includes(url.host);\r\n if (isInternal) {\r\n const localPath = url.pathname.replace(apiRegex, \"\");\r\n return readLocalImage(localPath, baseDir, type);\r\n } else {\r\n const allowedCondition = allowedNetworkList.includes(url.host);\r\n if (!allowedCondition) {\r\n return FALLBACKIMAGES[type]();\r\n }\r\n return fetchFromNetwork(src, type);\r\n }\r\n};\r\n","import { API_REGEX } from \"./variables\";\r\nimport type { Options, UserData } from \"./types\";\r\n\r\n/**\r\n * @typedef {(\"avatar\" | \"normal\")} ImageType\r\n * @description Defines the type of image being processed.\r\n */\r\n\r\n/**\r\n * @typedef {(\"jpeg\" | \"jpg\" | \"png\" | \"webp\" | \"gif\" | \"tiff\" | \"avif\" | \"svg\")} ImageFormat\r\n * @description Supported formats for image processing.\r\n */\r\n\r\n/**\r\n * @typedef {Object} Options\r\n * @property {string} baseDir - The base directory for public image files.\r\n * @property {function(string): string} idHandler - A function to handle user IDs.\r\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\r\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\r\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\r\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\r\n */\r\n\r\n/**\r\n * @typedef {Object} UserData\r\n * @property {number|string} quality - Quality of the image (1–100).\r\n * @property {ImageFormat} format - Desired format of the image.\r\n * @property {string} [src] - Source path or URL for the image.\r\n * @property {string} [folder] - The folder type (\"public\" or \"private\").\r\n * @property {ImageType} [type] - Type of the image (\"avatar\" or \"normal\").\r\n * @property {string|null} [userId] - Optional user identifier.\r\n * @property {number|string} [width] - Desired image width.\r\n * @property {number|string} [height] - Desired image height.\r\n */\r\n\r\n/**\r\n * Renders the options object with default values and user-provided values.\r\n *\r\n * @param {Partial<Options>} options - The user-provided options.\r\n * @returns {Options} The rendered options object.\r\n */\r\nexport const renderOptions = (options: Partial<Options>): Options => {\r\n const initialOptions: Options = {\r\n baseDir: \"\",\r\n idHandler: (id: string) => id,\r\n getUserFolder: async () => \"\",\r\n websiteURL: \"\",\r\n apiRegex: API_REGEX,\r\n allowedNetworkList: [],\r\n };\r\n return {\r\n ...initialOptions,\r\n ...options,\r\n };\r\n};\r\n\r\n/**\r\n * Renders the user data object with default values and user-provided values.\r\n *\r\n * @param {Partial<UserData>} userData - The user-provided data.\r\n * @returns {UserData} The rendered user data object.\r\n */\r\nexport const renderUserData = (userData: Partial<UserData>): UserData => {\r\n const initialUserData: UserData = {\r\n quality: 80,\r\n format: \"jpeg\",\r\n src: \"/placeholder/noimage.jpg\",\r\n folder: \"public\",\r\n type: \"normal\",\r\n width: undefined,\r\n height: undefined,\r\n userId: undefined,\r\n };\r\n return {\r\n ...initialUserData,\r\n ...userData,\r\n quality: userData.quality\r\n ? Math.min(Math.max(Number(userData.quality) || 80, 1), 100)\r\n : 100,\r\n width: userData.width\r\n ? Math.min(Math.max(Number(userData.width), 50), 2000)\r\n : undefined,\r\n height: userData.height\r\n ? Math.min(Math.max(Number(userData.height), 50), 2000)\r\n : undefined,\r\n };\r\n};\r\n"],"mappings":"AAAA,OAAOA,MAAU,YACjB,OAAOC,MAA0C,QCAjD,OAAS,YAAAC,MAAgB,mBAEzB,IAAMC,EAAkB,IAAI,IAAI,uBAAwB,YAAY,GAAG,EACpE,SAEGC,EAAmB,IAAI,IAAI,wBAAyB,YAAY,GAAG,EACtE,SAEUC,EAAiB,CAC5B,OAAQ,SAAYH,EAASC,CAAe,EAC5C,OAAQ,SAAYD,EAASE,CAAgB,CAC/C,EAEaE,EAAoB,eAEpBC,EAAgC,CAC3C,OACA,MACA,MACA,OACA,MACA,OACA,OACA,KACF,EAEaC,EAA8C,CACzD,KAAM,aACN,IAAK,aACL,IAAK,YACL,KAAM,aACN,IAAK,YACL,KAAM,aACN,KAAM,aACN,IAAK,eACP,ECpCA,OAAOC,MAAU,YACjB,UAAYC,MAAQ,mBACpB,OAAOC,MAAW,QAgBX,IAAMC,EAAc,MACzBC,EACAC,IACqB,CACrB,GAAI,CAIF,GAHI,CAACD,GAAY,CAACC,GACdA,EAAc,SAAS,IAAI,GAC3BC,EAAK,WAAWD,CAAa,GAC7B,CAAC,kBAAkB,KAAKA,CAAa,EAAG,MAAO,GAEnD,IAAME,EAAeD,EAAK,QAAQF,CAAQ,EACpCI,EAAeF,EAAK,QAAQC,EAAcF,CAAa,EAEvD,CAACI,EAAUC,CAAQ,EAAI,MAAM,QAAQ,IAAI,CAC1C,WAASH,CAAY,EACrB,WAASC,CAAY,CAC1B,CAAC,EAGD,GAAI,EADc,MAAS,OAAKC,CAAQ,GACzB,YAAY,EAAG,MAAO,GAErC,IAAME,EAAiBF,EAAWH,EAAK,IAGjCM,GAFiBF,EAAWJ,EAAK,KAGtB,WAAWK,CAAc,GAAKD,IAAaD,EAEtDI,EAAWP,EAAK,SAASG,EAAUC,CAAQ,EACjD,MAAO,CAACG,EAAS,WAAW,IAAI,GAAK,CAACP,EAAK,WAAWO,CAAQ,GAAKD,CACrE,MAAQ,CACN,MAAO,EACT,CACF,EASME,EAAmB,MACvBC,EACAC,EAAkB,WACE,CACpB,GAAI,CACF,IAAMC,EAAW,MAAMC,EAAM,IAAIH,EAAK,CACpC,aAAc,cACd,QAAS,GACX,CAAC,EAEKI,EAAcF,EAAS,QAAQ,cAAc,GAAG,YAAY,EAGlE,OAFyB,OAAO,OAAOG,CAAS,EAE3B,SAASD,GAAe,EAAE,EACtC,OAAO,KAAKF,EAAS,IAAI,EAE3B,MAAMI,EAAeL,CAAI,EAAE,CACpC,MAAgB,CACd,OAAO,MAAMK,EAAeL,CAAI,EAAE,CACpC,CACF,EAUaM,EAAiB,MAC5BC,EACAC,EACAR,EAAkB,WACf,CAEH,GAAI,CADY,MAAMb,EAAYqB,EAASD,CAAQ,EAEjD,OAAO,MAAMF,EAAeL,CAAI,EAAE,EAEpC,GAAI,CACF,OAAO,MAAS,WAASV,EAAK,QAAQkB,EAASD,CAAQ,CAAC,CAC1D,MAAgB,CACd,OAAO,MAAMF,EAAeL,CAAI,EAAE,CACpC,CACF,EAaaS,EAAa,CACxBV,EACAS,EACAE,EACAV,EAAkB,SAClBW,EAAmBC,EACnBC,EAA+B,CAAC,IAC7B,CACH,IAAMC,EAAM,IAAI,IAAIf,CAAG,EAEvB,GADmB,CAACW,EAAY,OAAOA,CAAU,EAAE,EAAE,SAASI,EAAI,IAAI,EACtD,CACd,IAAMC,EAAYD,EAAI,SAAS,QAAQH,EAAU,EAAE,EACnD,OAAOL,EAAeS,EAAWP,EAASR,CAAI,CAChD,KAEE,QADyBa,EAAmB,SAASC,EAAI,IAAI,EAItDhB,EAAiBC,EAAKC,CAAI,EAFxBK,EAAeL,CAAI,EAAE,CAIlC,EC/FO,IAAMgB,EAAiBC,IASrB,CACL,GAT8B,CAC9B,QAAS,GACT,UAAYC,GAAeA,EAC3B,cAAe,SAAY,GAC3B,WAAY,GACZ,SAAUC,EACV,mBAAoB,CAAC,CACvB,EAGE,GAAGF,CACL,GASWG,EAAkBC,IAWtB,CACL,GAXgC,CAChC,QAAS,GACT,OAAQ,OACR,IAAK,2BACL,OAAQ,SACR,KAAM,SACN,MAAO,OACP,OAAQ,OACR,OAAQ,MACV,EAGE,GAAGA,EACH,QAASA,EAAS,QACd,KAAK,IAAI,KAAK,IAAI,OAAOA,EAAS,OAAO,GAAK,GAAI,CAAC,EAAG,GAAG,EACzD,IACJ,MAAOA,EAAS,MACZ,KAAK,IAAI,KAAK,IAAI,OAAOA,EAAS,KAAK,EAAG,EAAE,EAAG,GAAI,EACnD,OACJ,OAAQA,EAAS,OACb,KAAK,IAAI,KAAK,IAAI,OAAOA,EAAS,MAAM,EAAG,EAAE,EAAG,GAAI,EACpD,MACN,GH1DF,IAAMC,EAAa,MACjBC,EACAC,EACAC,EACAC,IACG,CACH,GAAI,CACF,IAAMC,EAAWC,EAAeL,EAAI,KAAiB,EAC/CM,EAAgBC,EAAcJ,CAAO,EAEvCK,EACAC,EAAUH,EAAc,QACxBI,EAEJ,GAAIN,EAAS,OAAQ,CACnB,IAAMO,EACJ,OAAOP,EAAS,QAAW,SACvB,OAAO,OAAO,OAAOA,EAAS,MAAM,EAAE,CAAC,CAAC,EACxC,OAAOA,EAAS,MAAM,EACxBE,EAAc,UAChBI,EAAeJ,EAAc,UAAUK,CAAS,EAEhDD,EAAeC,CAEnB,CAEA,GAAIP,EAAS,SAAW,UAAW,CACjC,IAAMQ,EAAM,MAAMN,GAAe,gBAAgBN,EAAKU,CAAY,EAC9DE,IACFH,EAAUG,EAEd,CAEA,IAAMC,EAAeC,EAAe,SAClCV,GAAU,QAAQ,YAAY,CAChC,EACIA,GAAU,QAAQ,YAAY,EAC9B,OAEAA,GAAU,KAAK,WAAW,MAAM,EAClCI,EAAc,MAAMO,EAClBX,GAAU,KAAO,GACjBK,EACAH,GAAe,YAAc,GAC7BF,GAAU,KACVE,GAAe,SACfA,GAAe,kBACjB,EAEAE,EAAc,MAAMQ,EAClBZ,GAAU,KAAO,GACjBK,EACAL,GAAU,IACZ,EAGF,IAAIa,EAAQC,EAAMV,CAAW,EAE7B,GAAIJ,GAAU,OAASA,GAAU,OAAQ,CACvC,IAAMe,EAAgB,CACpB,MAAOf,GAAU,OAAS,OAC1B,OAAQA,GAAU,QAAU,OAC5B,IAAKc,EAAM,IAAI,KACjB,EACAD,EAAQA,EAAM,OAAOE,CAA8B,CACrD,CAEA,IAAMC,EAAiB,MAAMH,EAC1B,SAASJ,EAAkC,CAC1C,QAAST,GAAU,QAAU,OAAOA,GAAU,OAAO,EAAI,EAC3D,CAAC,EACA,SAAS,EAENiB,EAAoB,GAAGC,EAAK,SAChClB,GAAU,KAAO,GACjBkB,EAAK,QAAQlB,GAAU,KAAO,EAAE,CAClC,CAAC,IAAIS,CAAY,GAEjBZ,EAAI,KAAKsB,EAAUV,CAAY,CAAC,EAChCZ,EAAI,UACF,sBACA,qBAAqBoB,CAAiB,GACxC,EACApB,EAAI,KAAKmB,CAAc,CACzB,OAASI,EAAO,CACdtB,EAAKsB,CAAK,CACZ,CACF,EAQMC,EAAiBtB,GACd,MAAOH,EAAcC,EAAeC,IACzCH,EAAWC,EAAKC,EAAKC,EAAMC,CAAO,EAG/BuB,EAAQD","names":["path","sharp","readFile","NOT_FOUND_IMAGE","NOT_FOUND_AVATAR","FALLBACKIMAGES","API_REGEX","allowedFormats","mimeTypes","path","fs","axios","isValidPath","basePath","specifiedPath","path","resolvedBase","resolvedPath","realBase","realPath","normalizedBase","isInside","relative","fetchFromNetwork","src","type","response","axios","contentType","mimeTypes","FALLBACKIMAGES","readLocalImage","filePath","baseDir","fetchImage","websiteURL","apiRegex","API_REGEX","allowedNetworkList","url","localPath","renderOptions","options","id","API_REGEX","renderUserData","userData","serveImage","req","res","next","options","userData","renderUserData","parsedOptions","renderOptions","imageBuffer","baseDir","parsedUserId","userIdStr","dir","outputFormat","allowedFormats","fetchImage","readLocalImage","image","sharp","resizeOptions","processedImage","processedFileName","path","mimeTypes","error","registerServe","pixel_default"]}