Skip to content

Commit

Permalink
feat: #23 MD5 + refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
mateopresacastro committed Dec 16, 2024
1 parent 1bdf9cc commit 12b418a
Show file tree
Hide file tree
Showing 18 changed files with 116 additions and 35 deletions.
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"babel-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"framer-motion": "^12.0.0-alpha.1",
"grammy": "^1.32.0",
"howler": "^2.2.4",
Expand Down Expand Up @@ -80,6 +81,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/crypto-js": "^4.2.2",
"@types/howler": "^2.2.12",
"@types/jest": "^29.5.14",
"@types/node": "^20",
Expand Down
52 changes: 35 additions & 17 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,35 +240,53 @@ export async function persistSamplePackDataAction(
}
}

export async function createPreSignedUrlAction(numOfSamples: number) {
const fileDataSchema = z.object({
hash: z.string(),
fileType: z.string(),
});

const createPresignedUrlActionSchema = z.object({
img: fileDataSchema,
zip: fileDataSchema,
samples: z.array(fileDataSchema),
});

type CreatePreSignedUrlActionSchema = z.infer<
typeof createPresignedUrlActionSchema
>;

export async function createPreSignedUrlAction({
img,
zip,
samples,
}: CreatePreSignedUrlActionSchema) {
try {
const user = await currentUser();
if (!user) throw new Error();

if (!numOfSamples || typeof numOfSamples !== "number") {
throw new TypeError(
`Invalid number of samples. Expected a number, got: ${numOfSamples}`
);
}
createPresignedUrlActionSchema.parse({ img, zip, samples });

const zipFileSignedUrlPromise = createPresignedUrl({
bucketName: AWS_PRIVATE_BUCKET_NAME,
fileType: "zip",
type: "zip",
hash: zip.hash,
fileType: zip.fileType,
});

const imageSignedUrlPromise = createPresignedUrl({
bucketName: AWS_PUBLIC_BUCKET_NAME,
fileType: "image",
type: "image",
hash: img.hash,
fileType: img.fileType,
});

const samplesSignedUrlsPromises = new Array(numOfSamples)
.fill(null)
.map(() =>
createPresignedUrl({
bucketName: AWS_PUBLIC_BUCKET_NAME,
fileType: "samples",
})
);
const samplesSignedUrlsPromises = samples.map(({ hash, fileType }) =>
createPresignedUrl({
bucketName: AWS_PUBLIC_BUCKET_NAME,
type: "samples",
hash,
fileType,
})
);

const [zipFileSignedUrl, imageSignedUrl, ...samplesSignedUrls] =
await Promise.all([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import NumberFlow from "@number-flow/react";
import { cn } from "@/utils";
import { Button } from "@/components/ui/button";
import { UseFormReturn } from "react-hook-form";
import { UploadFormSchema } from "@/components/upload-form-schema";
import { UploadFormSchema } from "@/app/upload/upload-form-schema";
import { ArrowLeft, Loader, ArrowRight } from "lucide-react";

import type { Dispatch, SetStateAction } from "react";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
"use client";

import useMeasure from "react-use-measure";
import StepInfoAndControls from "@/components/upload/info-and-controls";
import { Form } from "@/components/ui/form";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useUploadPack } from "@/hooks/upload-pack";
import { useUploadPack } from "@/app/upload/use-upload-pack";
import { container, item } from "@/anim";
import { useMemo, useState } from "react";
import { AnimatePresence, motion, MotionConfig } from "motion/react";

import {
uploadFormSchema,
type UploadFormSchema,
} from "@/components/upload-form-schema";
} from "@/app/upload/upload-form-schema";

import StepInfoAndControls from "@/app/upload/_components/info-and-controls";

import {
StepFive,
Expand All @@ -22,7 +23,7 @@ import {
StepSix,
StepThree,
StepTwo,
} from "@/components/upload/steps/mod";
} from "@/app/upload/_components/steps/mod";

const VARIANTS = {
initial: (direction: number) => ({ x: `${110 * direction}%`, opacity: 0 }),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Dropzone from "react-dropzone";
import { Input } from "@/components/ui/input";
import { UseFormReturn } from "react-hook-form";
import { UploadFormSchema } from "@/components/upload-form-schema";
import { UploadFormSchema } from "@/app/upload/upload-form-schema";

import {
FormField,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
FormMessage,
} from "@/components/ui/form";

import type { UploadFormSchema } from "@/components/upload-form-schema";
import type { UploadFormSchema } from "@/app/upload/upload-form-schema";
import type { UseFormReturn } from "react-hook-form";
import { Textarea } from "@/components/ui/textarea";

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Dropzone from "react-dropzone";
import { FormField, FormItem, FormControl } from "@/components/ui/form";
import { Input } from "@/components/ui/input";

import type { UploadFormSchema } from "@/components/upload-form-schema";
import type { UploadFormSchema } from "@/app/upload/upload-form-schema";
import type { Dispatch, SetStateAction } from "react";
import type { UseFormReturn } from "react-hook-form";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { Loader, Check } from "lucide-react";

import type { UseFormReturn } from "react-hook-form";
import type { UploadFormSchema } from "@/components/upload-form-schema";
import type { UploadFormSchema } from "@/app/upload/upload-form-schema";

export default function StepSix({
form,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Dropzone from "react-dropzone";
import { Input } from "@/components/ui/input";
import { UploadFormSchema } from "@/components/upload-form-schema";
import { UploadFormSchema } from "@/app/upload/upload-form-schema";

import {
FormField,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
AnimatePresence,
} from "framer-motion";

import type { UploadFormSchema } from "@/components/upload-form-schema";
import type { UploadFormSchema } from "@/app/upload/upload-form-schema";

export default function StepTwo({
form,
Expand Down
25 changes: 25 additions & 0 deletions src/app/upload/md5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import CryptoJS from "crypto-js";

export async function md5(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
try {
const buffer = event.target?.result;
if (!(buffer instanceof ArrayBuffer)) {
throw new Error("Failed to read file as ArrayBuffer.");
}

const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(buffer));
const md5Hash = CryptoJS.MD5(wordArray);
const base64Hash = md5Hash.toString(CryptoJS.enc.Base64);
resolve(base64Hash);
} catch (error) {
console.error("Error generating MD5 hash:", error);
reject(error);
}
};
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
}
2 changes: 1 addition & 1 deletion src/app/upload/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import UploadPage from "@/components/upload/multi-step";
import UploadPage from "@/app/upload/_components/multi-step";
import { STORAGE_LIMIT_50_GB_IN_BYTES } from "@/consts";
import { doesUserHaveStripeAccount, getUserUsedStorage } from "@/db/mod";
import { hasRequirementsDue } from "@/stripe";
Expand Down
File renamed without changes.
4 changes: 4 additions & 0 deletions src/aws/upload.ts → src/app/upload/upload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { md5 } from "@/app/upload/md5";

export type UploadToS3Data = {
zipFileSignedUrl: string;
zipFile: File;
Expand Down Expand Up @@ -37,11 +39,13 @@ export async function handleUploadToS3({
}

async function uploadFile(url: string, file: File) {
const hash = await md5(file);
const response = await fetch(url, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type,
"Content-MD5": hash,
},
});

Expand Down
25 changes: 21 additions & 4 deletions src/hooks/upload-pack.tsx → src/app/upload/use-upload-pack.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { UploadToS3Data, handleUploadToS3 } from "@/aws/upload";
import { UploadToS3Data, handleUploadToS3 } from "@/app/upload/upload";
import { createSamplePackName, getAudioDuration, isDev } from "@/utils";
import { useMutation } from "@tanstack/react-query";
import {
Expand All @@ -9,7 +9,8 @@ import {
updateUserUsedStorageAction,
} from "@/actions";

import type { UploadFormSchema } from "@/components/upload-form-schema";
import type { UploadFormSchema } from "@/app/upload/upload-form-schema";
import { md5 } from "@/app/upload/md5";

type PreSignedUrls = Awaited<ReturnType<typeof createPreSignedUrlAction>>;

Expand All @@ -23,8 +24,24 @@ export function useUploadPack({
data: preSignedUrls,
isPending: isCreatingPresignedUrls,
} = useMutation({
mutationFn: async () =>
await createPreSignedUrlAction(formValues.samples?.length),
mutationFn: async () => {
const [imageHash, zipHash, ...samplesHashes] = await Promise.all([
md5(formValues.zipFile),
md5(formValues.img),
...formValues.samples.map((sample) => md5(sample.file)),
]);

const data = {
img: { hash: imageHash, fileType: formValues.img.type },
zip: { hash: zipHash, fileType: formValues.zipFile.type },
samples: samplesHashes.map((hash, index) => ({
hash,
fileType: formValues.samples[index].file.type,
})),
};

return await createPreSignedUrlAction(data);
},
onSuccess: createSignedUrlsOnSuccess,
});

Expand Down
10 changes: 8 additions & 2 deletions src/aws/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,22 @@ export async function listBuckets() {

export async function createPresignedUrl({
bucketName,
type,
hash,
fileType,
}: {
bucketName: string;
fileType: "image" | "samples" | "zip";
type: "image" | "samples" | "zip";
hash: string;
fileType: string;
}) {
const randomPrefix = crypto.randomBytes(16).toString("hex");
const key = `uploads/${fileType}/${randomPrefix}`;
const key = `uploads/${type}/${randomPrefix}`;
const command = new PutObjectCommand({
Bucket: bucketName,
Key: key,
ContentMD5: hash,
ContentType: fileType,
});

const url = await getSignedUrl(s3, command, {
Expand Down

0 comments on commit 12b418a

Please sign in to comment.