From e0cc078ddfbd79745cf2c47d8db776df12c96f21 Mon Sep 17 00:00:00 2001 From: Bigint <69431456+bigint@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:50:04 +0530 Subject: [PATCH] refactor: Replace StorageNode upload with IPFS upload --- .../components/Composer/ChooseThumbnail.tsx | 4 +- .../components/Shared/Audio/CoverImage.tsx | 4 +- apps/web/src/helpers/accountPictureUtils.ts | 6 +- apps/web/src/helpers/uploadToIPFS.ts | 104 ++++++++++++++++++ apps/web/src/helpers/uploadToStorageNode.ts | 45 -------- apps/web/src/hooks/useUploadAttachments.tsx | 5 +- packages/helpers/sanitizeDStorageUrl.ts | 13 ++- 7 files changed, 124 insertions(+), 57 deletions(-) create mode 100644 apps/web/src/helpers/uploadToIPFS.ts delete mode 100644 apps/web/src/helpers/uploadToStorageNode.ts diff --git a/apps/web/src/components/Composer/ChooseThumbnail.tsx b/apps/web/src/components/Composer/ChooseThumbnail.tsx index d7c74eb196c..3c6de6c9df2 100644 --- a/apps/web/src/components/Composer/ChooseThumbnail.tsx +++ b/apps/web/src/components/Composer/ChooseThumbnail.tsx @@ -1,5 +1,5 @@ import ThumbnailsShimmer from "@components/Shared/Shimmer/ThumbnailsShimmer"; -import { uploadFileToStorageNode } from "@helpers/uploadToStorageNode"; +import { uploadFileToIPFS } from "@helpers/uploadToIPFS"; import { CheckCircleIcon, PhotoIcon } from "@heroicons/react/24/outline"; import { generateVideoThumbnails } from "@hey/helpers/generateVideoThumbnails"; import getFileFromDataURL from "@hey/helpers/getFileFromDataURL"; @@ -28,7 +28,7 @@ const ChooseThumbnail: FC = () => { const uploadThumbnailToStorageNode = async (fileToUpload: File) => { setVideoThumbnail({ ...videoThumbnail, uploading: true }); - const result = await uploadFileToStorageNode(fileToUpload); + const result = await uploadFileToIPFS(fileToUpload); if (!result.uri) { toast.error("Failed to upload thumbnail"); } diff --git a/apps/web/src/components/Shared/Audio/CoverImage.tsx b/apps/web/src/components/Shared/Audio/CoverImage.tsx index 4b80faf1c79..b81fdd8da84 100644 --- a/apps/web/src/components/Shared/Audio/CoverImage.tsx +++ b/apps/web/src/components/Shared/Audio/CoverImage.tsx @@ -1,5 +1,5 @@ import errorToast from "@helpers/errorToast"; -import { uploadFileToStorageNode } from "@helpers/uploadToStorageNode"; +import { uploadFileToIPFS } from "@helpers/uploadToIPFS"; import { PhotoIcon } from "@heroicons/react/24/outline"; import { ATTACHMENT } from "@hey/data/constants"; import imageKit from "@hey/helpers/imageKit"; @@ -36,7 +36,7 @@ const CoverImage: FC = ({ try { setIsSubmitting(true); const file = event.target.files[0]; - const attachment = await uploadFileToStorageNode(file); + const attachment = await uploadFileToIPFS(file); setCover( URL.createObjectURL(file), attachment.uri, diff --git a/apps/web/src/helpers/accountPictureUtils.ts b/apps/web/src/helpers/accountPictureUtils.ts index a2274a5e7ca..efd692ba753 100644 --- a/apps/web/src/helpers/accountPictureUtils.ts +++ b/apps/web/src/helpers/accountPictureUtils.ts @@ -1,5 +1,5 @@ import imageCompression from "browser-image-compression"; -import { uploadFileToStorageNode } from "./uploadToStorageNode"; +import { uploadFileToIPFS } from "./uploadToIPFS"; /** * Read a file as a base64 string @@ -36,10 +36,10 @@ const uploadCroppedImage = async ( maxWidthOrHeight: 3000, useWebWorker: true }); - const attachment = await uploadFileToStorageNode(cleanedFile); + const attachment = await uploadFileToIPFS(cleanedFile); const decentralizedUrl = attachment.uri; if (!decentralizedUrl) { - throw new Error("uploadFileToStorageNode failed"); + throw new Error("uploadFileToIPFS failed"); } return decentralizedUrl; diff --git a/apps/web/src/helpers/uploadToIPFS.ts b/apps/web/src/helpers/uploadToIPFS.ts new file mode 100644 index 00000000000..832ebf2f4ee --- /dev/null +++ b/apps/web/src/helpers/uploadToIPFS.ts @@ -0,0 +1,104 @@ +import { S3 } from "@aws-sdk/client-s3"; +import { Upload } from "@aws-sdk/lib-storage"; +import { + EVER_API, + EVER_BUCKET, + EVER_REGION, + HEY_API_URL +} from "@hey/data/constants"; +import axios from "axios"; +import { v4 as uuid } from "uuid"; + +const FALLBACK_TYPE = "image/jpeg"; + +/** + * Returns an S3 client with temporary credentials obtained from the STS service. + * + * @returns S3 client instance. + */ +const getS3Client = async (): Promise => { + const { data } = await axios.get(`${HEY_API_URL}/sts/token`); + const client = new S3({ + credentials: { + accessKeyId: data?.accessKeyId, + secretAccessKey: data?.secretAccessKey, + sessionToken: data?.sessionToken + }, + endpoint: EVER_API, + maxAttempts: 10, + region: EVER_REGION + }); + + return client; +}; + +/** + * Uploads a set of files to the IPFS network via S3 and returns an array of MediaSet objects. + * + * @param data Files to upload to IPFS. + * @param onProgress Callback to be called when the upload progress changes. + * @returns Array of MediaSet objects. + */ +const uploadToIPFS = async ( + data: any, + onProgress?: (percentage: number) => void +): Promise<{ mimeType: string; uri: string }[]> => { + try { + const files = Array.from(data); + const client = await getS3Client(); + const currentDate = new Date() + .toLocaleDateString("en-GB") + .replace(/\//g, "-"); + + const attachments = await Promise.all( + files.map(async (_: any, i: number) => { + const file = data[i]; + const params = { + Body: file, + Bucket: EVER_BUCKET, + ContentType: file.type, + Key: `${currentDate}/${uuid()}` + }; + const task = new Upload({ client, params }); + task.on("httpUploadProgress", (e) => { + const loaded = e.loaded || 0; + const total = e.total || 0; + const progress = (loaded / total) * 100; + onProgress?.(Math.round(progress)); + }); + await task.done(); + const result = await client.headObject(params); + const metadata = result.Metadata; + const cid = metadata?.["ipfs-hash"]; + + return { mimeType: file.type || FALLBACK_TYPE, uri: `ipfs://${cid}` }; + }) + ); + + return attachments; + } catch { + return []; + } +}; + +/** + * Uploads a file to the IPFS network via S3 and returns a MediaSet object. + * + * @param file File to upload to IPFS. + * @returns MediaSet object or null if the upload fails. + */ +export const uploadFileToIPFS = async ( + file: File, + onProgress?: (percentage: number) => void +): Promise<{ mimeType: string; uri: string }> => { + try { + const ipfsResponse = await uploadToIPFS([file], onProgress); + const metadata = ipfsResponse[0]; + + return { mimeType: file.type || FALLBACK_TYPE, uri: metadata.uri }; + } catch { + return { mimeType: file.type || FALLBACK_TYPE, uri: "" }; + } +}; + +export default uploadToIPFS; diff --git a/apps/web/src/helpers/uploadToStorageNode.ts b/apps/web/src/helpers/uploadToStorageNode.ts deleted file mode 100644 index 592564634fb..00000000000 --- a/apps/web/src/helpers/uploadToStorageNode.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { StorageNodeResponse } from "@hey/types/misc"; -import { storageClient } from "./storageClient"; - -/** - * Uploads a set of files to the Lens Storage Node and returns an array of MediaSet objects. - * - * @param data Files to upload to Lens Storage Node. - * @param onProgress Callback to be called when the upload progress changes. - * @returns Array of MediaSet objects. - */ -const uploadToStorageNode = async ( - data: File[] -): Promise => { - try { - const { files } = await storageClient.uploadFolder(data); - const attachments = files.map(({ gatewayUrl }, index) => { - return { mimeType: data[index].type || "image/jpeg", uri: gatewayUrl }; - }); - - return attachments; - } catch { - return []; - } -}; - -/** - * Uploads a file to the Lens Storage Node and returns a MediaSet object. - * - * @param file File to upload to Lens Storage Node. - * @returns MediaSet object or null if the upload fails. - */ -export const uploadFileToStorageNode = async ( - file: File -): Promise => { - try { - const response = await uploadToStorageNode([file]); - const { uri, mimeType } = response[0]; - - return { mimeType, uri }; - } catch { - return { mimeType: "", uri: "" }; - } -}; - -export default uploadToStorageNode; diff --git a/apps/web/src/hooks/useUploadAttachments.tsx b/apps/web/src/hooks/useUploadAttachments.tsx index e4ca5b2bd4b..a77f142c015 100644 --- a/apps/web/src/hooks/useUploadAttachments.tsx +++ b/apps/web/src/hooks/useUploadAttachments.tsx @@ -1,4 +1,4 @@ -import uploadToStorageNode from "@helpers/uploadToStorageNode"; +import uploadToIPFS from "@helpers/uploadToIPFS"; import type { NewAttachment } from "@hey/types/misc"; import imageCompression from "browser-image-compression"; import { useCallback } from "react"; @@ -92,8 +92,7 @@ const useUploadAttachments = () => { addAttachments(previewAttachments); try { - const attachmentsUploaded = - await uploadToStorageNode(compressedFiles); + const attachmentsUploaded = await uploadToIPFS(compressedFiles); const attachments = attachmentsUploaded.map((uploaded, index) => ({ ...previewAttachments[index], mimeType: uploaded.mimeType, diff --git a/packages/helpers/sanitizeDStorageUrl.ts b/packages/helpers/sanitizeDStorageUrl.ts index f0c331a409b..909dc14a25a 100644 --- a/packages/helpers/sanitizeDStorageUrl.ts +++ b/packages/helpers/sanitizeDStorageUrl.ts @@ -1,4 +1,4 @@ -import { STORAGE_NODE_URL } from "@hey/data/constants"; +import { IPFS_GATEWAY, STORAGE_NODE_URL } from "@hey/data/constants"; /** * Returns the decentralized storage link for a given hash. @@ -11,7 +11,16 @@ const sanitizeDStorageUrl = (hash?: string): string => { return ""; } - return hash.replace("lens://", `${STORAGE_NODE_URL}/`); + const ipfsGateway = `${IPFS_GATEWAY}/`; + + let link = hash.replace(/^Qm[1-9A-Za-z]{44}/gm, `${IPFS_GATEWAY}/${hash}`); + link = link.replace("https://ipfs.io/ipfs/", ipfsGateway); + link = link.replace("ipfs://ipfs/", ipfsGateway); + link = link.replace("ipfs://", ipfsGateway); + link = link.replace("lens://", `${STORAGE_NODE_URL}/`); + link = link.replace("ar://", "https://gateway.arweave.net/"); + + return link; }; export default sanitizeDStorageUrl;