diff --git a/package.json b/package.json index 2f919920..ea02cf5d 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@ariakit/react-core": "0.3.1", "@headlessui/react": "1.7.13", "@heroicons/react": "1.0.6", + "@img/sharp-linux-x64": "0.33.1", "@sendgrid/mail": "^8.0.0", "@sentry/nextjs": "^7.73.0", "@supabase/auth-helpers-nextjs": "0.5.6", @@ -107,6 +108,7 @@ "date-fns": "2.29.3", "form-data": "^4.0.0", "formidable": "^3.0.0", + "image-size": "1.0.2", "jsonwebtoken": "9.0.0", "jwt-decode": "3.1.2", "lodash": "4.17.21", @@ -125,6 +127,7 @@ "react-spinners": "0.13.8", "react-stately": "3.21.0", "react-toastify": "9.1.2", + "sharp": "0.32.6", "slugify": "1.6.6", "tailwind-merge": "1.12.0", "uniqid": "^5.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0965342..330bd2ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: '@heroicons/react': specifier: 1.0.6 version: 1.0.6(react@18.2.0) + '@img/sharp-linux-x64': + specifier: 0.33.1 + version: 0.33.1 '@sendgrid/mail': specifier: ^8.0.0 version: 8.1.0 @@ -80,6 +83,9 @@ dependencies: formidable: specifier: ^3.0.0 version: 3.5.1 + image-size: + specifier: 1.0.2 + version: 1.0.2 jsonwebtoken: specifier: 9.0.0 version: 9.0.0 @@ -134,6 +140,9 @@ dependencies: react-toastify: specifier: 9.1.2 version: 9.1.2(react-dom@18.2.0)(react@18.2.0) + sharp: + specifier: 0.32.6 + version: 0.32.6 slugify: specifier: 1.6.6 version: 1.6.6 @@ -307,9 +316,6 @@ devDependencies: release-it: specifier: 17.0.0 version: 17.0.0(typescript@5.0.4) - sharp: - specifier: 0.32.6 - version: 0.32.6 source-map-explorer: specifier: 2.5.3 version: 2.5.3 @@ -1735,6 +1741,24 @@ packages: resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} dev: true + /@img/sharp-libvips-linux-x64@1.0.0: + resolution: {integrity: sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-linux-x64@0.33.1: + resolution: {integrity: sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.0 + dev: false + /@internationalized/date@3.0.0: resolution: {integrity: sha512-mvlQCeYNg7JgO+QtIIx1zFJx10CJNVnt8cOjW2pYab1Ap/c7BYybS37Z5oTCdmRZbvnpmWtjhOEH324zlgWHLw==} dependencies: @@ -4962,7 +4986,7 @@ packages: /b4a@1.6.4: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} - dev: true + dev: false /babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -5366,6 +5390,7 @@ packages: /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false /ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -5540,6 +5565,7 @@ packages: dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 + dev: false /color@4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} @@ -5547,6 +5573,7 @@ packages: dependencies: color-convert: 2.0.1 color-string: 1.9.1 + dev: false /colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -6377,6 +6404,7 @@ packages: /detect-libc@2.0.2: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} + dev: false /detect-newline@4.0.1: resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} @@ -7557,6 +7585,7 @@ packages: /expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + dev: false /ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -7624,7 +7653,7 @@ packages: /fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - dev: true + dev: false /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} @@ -7890,6 +7919,7 @@ packages: /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false /fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} @@ -8142,6 +8172,7 @@ packages: /github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -8831,6 +8862,7 @@ packages: /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false /is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} @@ -10219,6 +10251,7 @@ packages: /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} @@ -10265,6 +10298,7 @@ packages: /napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -10371,6 +10405,7 @@ packages: engines: {node: '>=10'} dependencies: semver: 7.5.4 + dev: false /node-addon-api@5.1.0: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} @@ -10378,7 +10413,7 @@ packages: /node-addon-api@6.1.0: resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - dev: true + dev: false /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} @@ -11165,6 +11200,7 @@ packages: simple-get: 4.0.1 tar-fs: 2.1.1 tunnel-agent: 0.6.0 + dev: false /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -11405,7 +11441,7 @@ packages: /queue-tick@1.0.1: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} - dev: true + dev: false /queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} @@ -12231,7 +12267,7 @@ packages: simple-get: 4.0.1 tar-fs: 3.0.4 tunnel-agent: 0.6.0 - dev: true + dev: false /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -12278,6 +12314,7 @@ packages: /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false /simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} @@ -12285,11 +12322,13 @@ packages: decompress-response: 6.0.0 once: 1.4.0 simple-concat: 1.0.1 + dev: false /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: is-arrayish: 0.3.2 + dev: false /sirv@1.0.19: resolution: {integrity: sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==} @@ -12511,7 +12550,7 @@ packages: dependencies: fast-fifo: 1.3.2 queue-tick: 1.0.1 - dev: true + dev: false /string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} @@ -12905,6 +12944,7 @@ packages: mkdirp-classic: 0.5.3 pump: 3.0.0 tar-stream: 2.2.0 + dev: false /tar-fs@3.0.4: resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} @@ -12912,7 +12952,7 @@ packages: mkdirp-classic: 0.5.3 pump: 3.0.0 tar-stream: 3.1.6 - dev: true + dev: false /tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} @@ -12923,6 +12963,7 @@ packages: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 + dev: false /tar-stream@3.1.6: resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} @@ -12930,7 +12971,7 @@ packages: b4a: 1.6.4 fast-fifo: 1.3.2 streamx: 2.15.5 - dev: true + dev: false /temp@0.9.4: resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} diff --git a/src/pages/api/file/upload-file.ts b/src/pages/api/file/upload-file.ts index 8ce609d7..98b25331 100644 --- a/src/pages/api/file/upload-file.ts +++ b/src/pages/api/file/upload-file.ts @@ -4,17 +4,16 @@ import { log } from "console"; import fs, { promises as fileSystem } from "fs"; import { type NextApiRequest, type NextApiResponse } from "next"; import { decode } from "base64-arraybuffer"; -import { blurhashFromURL } from "blurhash-from-url"; import { IncomingForm } from "formidable"; import jwtDecode from "jwt-decode"; import isNil from "lodash/isNil"; -import fetch from "node-fetch"; import { type ImgMetadataType, type UploadFileApiResponse, } from "../../../types/apiTypes"; import { FILES_STORAGE_NAME, MAIN_TABLE_NAME } from "../../../utils/constants"; +import { blurhashFromURL } from "../../../utils/getBlurHash"; import { isUserInACategory } from "../../../utils/helpers"; import { apiSupabaseClient, @@ -71,20 +70,24 @@ export default async ( })) as { fields: { access_token?: string; category_id?: string }; files: { - file?: { filepath?: string; mimetype: string; originalFilename?: string }; + file?: Array<{ + filepath?: string; + mimetype: string; + originalFilename?: string; + }>; }; }; - const { error: _error } = verifyAuthToken( - data?.fields?.access_token as string, - ); + const accessToken = data?.fields?.access_token?.[0]; + + const { error: _error } = verifyAuthToken(accessToken as string); if (_error) { response.status(500).json({ success: false, error: _error }); - throw new Error("ERROR: token error"); + throw new Error(`ERROR: token error ${_error.message}`, _error); } - const categoryId = data?.fields?.category_id; + const categoryId = data?.fields?.category_id?.[0]; const categoryIdLogic = categoryId ? isUserInACategory(categoryId) @@ -92,21 +95,19 @@ export default async ( : 0 : 0; - const tokenDecode: { sub: string } = jwtDecode( - data?.fields?.access_token as string, - ); + const tokenDecode: { sub: string } = jwtDecode(accessToken as string); const userId = tokenDecode?.sub; let contents; - if (data?.files?.file?.filepath) { - contents = await fileSystem.readFile(data?.files?.file?.filepath, { + if (data?.files?.file && data?.files?.file[0]?.filepath) { + contents = await fileSystem.readFile(data?.files?.file[0]?.filepath, { encoding: "base64", }); } - const fileName = data?.files?.file?.originalFilename; - const fileType = data?.files?.file?.mimetype; + const fileName = data?.files?.file?.[0]?.originalFilename; + const fileType = data?.files?.file?.[0]?.mimetype; if (contents) { const storagePath = `public/${userId}/${fileName}`; @@ -134,7 +135,9 @@ export default async ( const isVideo = fileType?.includes("video"); if (!isVideo) { - const imageCaption = await query(data?.files?.file?.filepath as string); + const imageCaption = await query( + data?.files?.file?.[0]?.filepath as string, + ); const jsonResponse = (await imageCaption?.json()) as Array<{ generated_text: string; diff --git a/src/utils/getBlurHash.ts b/src/utils/getBlurHash.ts new file mode 100644 index 00000000..0e02c441 --- /dev/null +++ b/src/utils/getBlurHash.ts @@ -0,0 +1,89 @@ +import { encode } from "blurhash"; +import sizeOf from "image-size"; +import fetch from "node-fetch"; +import sharp from "sharp"; + +export type IOptions = { + offline?: boolean; + size?: number; +}; + +export type IOutput = { + encoded: string; + height: number | undefined; + width: number | undefined; +}; + +/** + * Generate a Blurhash string from a given image URL or local path. + * + * @param {string} source - The image URL or local path to the image file. + * @param {IOptions} [options] - The optional configuration options. + * @param {number} [options.size] - The desired size of the image for encoding the Blurhash. + * @param {boolean} [options.offline] - Set to `true` if the image source is a local path, `false` if it's a URL. + * @returns {Promise} The Promise that resolves to the encoded Blurhash string, along with the image width and height. + * @default + * @default + * @example + * ```js + * import { blurhashFromURL } from "blurhash-from-url"; + * + * const output = await blurhashFromURL("https://i.imgur.com/NhfEdg2.png", { + * size: 32, + * }); + * + * console.log(output); + * ``` + */ +export const blurhashFromURL = async ( + source: string, + options: IOptions = {}, +): Promise => { + const { size = 32, offline = false } = options; + + let height; + let returnedBuffer; + let width; + + if (offline) { + const fs = await import("fs"); + const { width: localWidth, height: localHeight } = sizeOf(source); + width = localWidth; + height = localHeight; + returnedBuffer = await sharp(fs.readFileSync(source)).toBuffer(); + } else { + const response = await fetch(source); + const arrayBuffer = await response.arrayBuffer(); + returnedBuffer = Buffer.from(arrayBuffer); + + const { width: remoteWidth, height: remoteHeight } = sizeOf(returnedBuffer); + width = remoteWidth; + height = remoteHeight; + } + + const { info, data } = await sharp(returnedBuffer) + .resize(size, size, { + fit: "inside", + }) + .ensureAlpha() + .raw() + .toBuffer({ + resolveWithObject: true, + }); + + const encoded = encode( + new Uint8ClampedArray(data), + info.width, + info.height, + 4, + 4, + ); + + const output: IOutput = { + encoded, + width, + height, + }; + + return output; +};