diff --git a/.env b/.env new file mode 100644 index 0000000..0883d88 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +# S3 data storage +S3_BUCKET_NAME="flockstr" +MY_AWS_ACCESS_KEY="AKIAT2UDOJMC6K25RFFN" +MY_AWS_SECRET_KEY="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" +REGION="us-east-1" +S3_BUCKET_URL="https://flockstr.s3.amazonaws.com" +NEXT_PUBLIC_S3_BUCKET_URL="https://flockstr.s3.amazonaws.com" \ No newline at end of file diff --git a/app/api/upload/route.ts b/app/api/upload/route.ts new file mode 100644 index 0000000..75dd052 --- /dev/null +++ b/app/api/upload/route.ts @@ -0,0 +1,28 @@ +import { nanoid } from "nanoid"; +import { NextResponse } from "next/server"; +import { generateV4UploadSignedUrl } from "@/lib/actions/upload"; +import { z } from "zod"; + +const BodySchema = z.object({ + folderName: z.string().optional(), + fileType: z.string(), +}); + +// export const runtime = "edge"; +async function handler(req: Request) { + // const session = await getSession(); + // if (!session?.user.id) { + // return new Response("Unauthorized", { + // status: 401, + // }); + // } + const rawJson = await req.json(); + const body = BodySchema.parse(rawJson); + const { folderName, fileType } = body; + const filename = (`${folderName}/` ?? "") + nanoid(); + const signedUrl = await generateV4UploadSignedUrl(filename, fileType); + + return NextResponse.json({ ...signedUrl, fileName: filename }); +} + +export { handler as POST }; diff --git a/bun.lockb b/bun.lockb index ed569d7..bf5558d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/lib/actions/upload.ts b/lib/actions/upload.ts new file mode 100644 index 0000000..b4b3046 --- /dev/null +++ b/lib/actions/upload.ts @@ -0,0 +1,19 @@ +import { s3Client } from "@/lib/clients/s3"; + +export async function generateV4UploadSignedUrl( + fileName: string, + fileType: string, +) { + const preSignedUrl = await s3Client.getSignedUrl("putObject", { + Bucket: process.env.S3_BUCKET_NAME, + Key: fileName, + ContentType: fileType, + Expires: 5 * 60, + }); + + console.log("PresignedUrl", preSignedUrl); + + return { url: preSignedUrl }; +} + +export default generateV4UploadSignedUrl; diff --git a/lib/clients/s3.ts b/lib/clients/s3.ts new file mode 100644 index 0000000..9538639 --- /dev/null +++ b/lib/clients/s3.ts @@ -0,0 +1,9 @@ +import S3 from "aws-sdk/clients/s3"; +const s3Client = new S3({ + signatureVersion: "v4", + region: process.env.REGION, + accessKeyId: process.env.MY_AWS_ACCESS_KEY, + secretAccessKey: process.env.MY_AWS_SECRET_KEY, +}); + +export { s3Client }; diff --git a/lib/hooks/useImageUpload.tsx b/lib/hooks/useImageUpload.tsx new file mode 100644 index 0000000..4da4767 --- /dev/null +++ b/lib/hooks/useImageUpload.tsx @@ -0,0 +1,110 @@ +"use client"; +import { useState, ReactNode, useRef } from "react"; +import { z } from "zod"; +import { createZodFetcher } from "zod-fetch"; + +const fetchWithZod = createZodFetcher(); + +const PresignedPostSchema = z.object({ + url: z.string(), + fileName: z.string(), +}); + +const useImageUpload = (folderName?: string) => { + const [status, setStatus] = useState< + "empty" | "uploading" | "success" | "error" + >("empty"); + const [imageUrl, setImageUrl] = useState(); + const [imagePreview, setImagePreview] = useState(); + + const uploadImage = async (file: File, folderName?: string) => { + if (!file) return; + try { + const presignedPost = await fetchWithZod( + // The schema you want to validate with + PresignedPostSchema, + // Any parameters you would usually pass to fetch + "/api/upload", + { + method: "POST", + body: JSON.stringify({ folderName, fileType: file.type }), + }, + ); + + const { url, fileName } = presignedPost; + + if (!url) return; + + const result = await fetch(url, { + method: "PUT", + body: file, + headers: { + "Content-Type": file.type, + }, + }); + + if (result.ok) { + setStatus("success"); + const imageUrl = `${process.env.NEXT_PUBLIC_S3_BUCKET_URL}/${fileName}`; + setImageUrl(imageUrl); + setImagePreview(imageUrl); + return imageUrl; + } + return; + } catch (err) { + setStatus("error"); + console.log("ERROR", err); + } + }; + + const onImageChange = (e: React.FormEvent) => { + const file = e.currentTarget.files?.[0]; + if (!file) return; + setStatus("uploading"); + uploadImage(file, folderName); + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = async (readerEvent) => { + setImagePreview(readerEvent?.target?.result as string); + }; + }; + + const ImageUploadButton = ({ children }: { children: ReactNode }) => { + const inputFileRef = useRef(null); + function onButtonClick() { + if (inputFileRef.current) { + inputFileRef.current!.click(); + } + } + return ( + <> + + + + ); + }; + + const clear = () => { + setStatus("empty"); + setImageUrl(null); + setImagePreview(null); + }; + + return { + imagePreview, + status, + imageUrl, + ImageUploadButton, + clear, + }; +}; + +export default useImageUpload; diff --git a/package.json b/package.json index 8f32124..5165ca3 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@tailwindcss/container-queries": "^0.1.1", + "aws-sdk": "^2.1475.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0",