Upload to s3 for public image working.

This commit is contained in:
charles-gauthereau
2024-11-23 16:49:16 +01:00
parent e3fe6d1c80
commit 1871389a5e
6 changed files with 191 additions and 46 deletions
@@ -9,6 +9,11 @@ import {ButtonWithLoading} from "@/components/wrappers/Button/ButtonWithLoading/
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {checkConnexionToS3} from "@/features/upload/upload.action"; import {checkConnexionToS3} from "@/features/upload/upload.action";
import {toast} from "sonner"; import {toast} from "sonner";
import {updateUserAction} from "@/components/wrappers/Dashboard/Profile/UserForm/user-form.action";
import {useRouter} from "next/navigation";
import {
updateStorageSettingsAction
} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/StorageS3Form/s3-form.action";
export type SettingsStorageTabProps = { export type SettingsStorageTabProps = {
@@ -16,19 +21,37 @@ export type SettingsStorageTabProps = {
} }
export const SettingsStorageTab = (props: SettingsStorageTabProps) => { export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
const router = useRouter()
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
const result = await checkConnexionToS3() const result = await checkConnexionToS3()
if(result.error){ if(result.error){
toast.error("An error occured during the connexion !") toast.error("An error occured during the connexion !")
}else{
toast.success("Connexion succeed!")
} }
toast.success("Connexion succeed!")
} }
}) })
const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local"); const [isSwitched, setIsSwitched] = useState<boolean>(props.settings.storage !== "local");
const updateMutation = useMutation({
mutationFn: () => updateStorageSettingsAction({name: "system", data: {storage: isSwitched ? "s3": "local"}}),
onSuccess: () => {
toast.success(`Settings updated successfully.`);
router.refresh()
},
onError: () => {
toast.error(`An error occurred while updating settings information.`);
},
});
const HandleSwitchStorage = async () => {
setIsSwitched(!isSwitched);
await updateMutation.mutateAsync()
}
return ( return (
<div className="flex flex-col h-full py-4"> <div className="flex flex-col h-full py-4">
@@ -47,8 +70,8 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
<Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label> <Label htmlFor="storage-mode">Storage Mode (Local/s3 compatible)</Label>
<Switch <Switch
checked={isSwitched} checked={isSwitched}
onCheckedChange={() => { onCheckedChange={async () => {
setIsSwitched(!isSwitched); await HandleSwitchStorage()
}} }}
id="storage-mode"/> id="storage-mode"/>
</div> </div>
@@ -14,6 +14,11 @@ import {
S3FormSchema, S3FormSchema,
S3FormType S3FormType
} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/StorageS3Form/s3-form.schema"; } from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/StorageS3Form/s3-form.schema";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
import {
updateS3SettingsAction
} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/StorageS3Form/s3-form.action";
export type S3FormProps = { export type S3FormProps = {
defaultValues?: S3FormType; defaultValues?: S3FormType;
@@ -24,9 +29,21 @@ export const StorageS3Form = (props: S3FormProps) => {
schema: S3FormSchema, schema: S3FormSchema,
defaultValues: props.defaultValues, defaultValues: props.defaultValues,
}); });
const router = useRouter();
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async (values: S3FormType) => { mutationFn: async (values: S3FormType) => {
console.log(values)
const updateS3Settings = await updateS3SettingsAction({name: "system", data: values})
const data = updateS3Settings?.data?.data
if (updateS3Settings?.serverError || !data) {
console.log(updateS3Settings?.serverError);
toast.error(updateS3Settings?.serverError);
return;
}
toast.success(`Success updating storage informations`);
router.refresh()
} }
}) })
@@ -0,0 +1,51 @@
"use server"
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {prisma} from "@/prisma";
import {
S3FormSchema,
StorageSwitchSchema
} from "@/components/wrappers/Dashboard/Settings/SettingsStorageTab/StorageS3Form/s3-form.schema";
export const updateS3SettingsAction = userAction
.schema(
z.object({
name: z.string(),
data: S3FormSchema,
}
)
)
.action(async ({parsedInput, ctx}) => {
const updatedSettings = await prisma.settings.update({
where: {
name: parsedInput.name,
},
data: parsedInput.data,
})
return {
data: updatedSettings,
}
})
export const updateStorageSettingsAction = userAction
.schema(
z.object({
name: z.string(),
data: StorageSwitchSchema,
}
)
)
.action(async ({parsedInput, ctx}) => {
const updatedSettings = await prisma.settings.update({
where: {
name: parsedInput.name,
},
data: parsedInput.data,
})
return {
data: updatedSettings,
}
})
@@ -8,3 +8,10 @@ export const S3FormSchema = z.object({
}); });
export type S3FormType = z.infer<typeof S3FormSchema>; export type S3FormType = z.infer<typeof S3FormSchema>;
export const StorageSwitchSchema = z.object({
storage: z.string(),
})
export type StorageType= z.infer<typeof StorageSwitchSchema>;
+39 -26
View File
@@ -2,10 +2,13 @@
import {userAction} from "@/safe-actions"; import {userAction} from "@/safe-actions";
import {z} from "zod"; import {z} from "zod";
import {v4 as uuidv4} from 'uuid'; import {v4 as uuidv4} from 'uuid';
import { writeFile, access, mkdir } from "fs/promises"; import {mkdir, writeFile} from "fs/promises";
import path from "path"; import path from "path";
import {env} from "@/env.mjs"; import {env} from "@/env.mjs";
import {checkMinioAlive} from "@/utils/s3-file-management"; import {checkMinioAlive, createPublicBucket, saveFileInBucket} from "@/utils/s3-file-management";
import {prisma} from "@/prisma";
import {UploadedObjectInfo} from "minio/src/internal/type";
import {Settings} from "@prisma/client";
export const uploadImageAction = userAction export const uploadImageAction = userAction
@@ -14,17 +17,28 @@ export const uploadImageAction = userAction
const file = formData.get("file") as File const file = formData.get("file") as File
const uuid = uuidv4() const uuid = uuidv4()
const fileFormat = file.name.split(".").slice(-1)[0] const fileFormat = file.name.split(".").slice(-1)[0]
const fileName = uuid + "." + fileFormat const fileName = uuid + "." + fileFormat
const arrayBuffer = await file.arrayBuffer() const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer) const buffer = Buffer.from(arrayBuffer)
const settings = await prisma.settings.findUnique({
where:{
name: "system"
}
})
// await uploadS3Compatible() let result: void | UploadedObjectInfo;
const bucketName = 'public-image-bucket';
const result = await uploadLocal(fileName,buffer) if(settings.storage === "local") {
const url = getUrl(fileName) result = await uploadLocal(fileName,buffer)
}else if (settings.storage === "s3"){
result = await uploadS3Compatible(bucketName,fileName, buffer)
}
const url = getUrl(fileName, settings, bucketName)
console.log(url)
return { return {
data: {result: result, url: url}, data: {result: result, url: url},
} }
@@ -32,35 +46,29 @@ export const uploadImageAction = userAction
}); });
function getUrl(fileName:string):string {
let url: string = ""; function getUrl(fileName:string, settings: Settings, bucketName: string):string {
if (env.NODE_ENV === "production") { if (env.NODE_ENV === "production") {
// url = `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}` // url = `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}`
return url return "url"
} else { } else {
url = `http://localhost:8887/uploads/${fileName}` if(settings.storage === "s3"){
return url return `http://localhost:${env.S3_PORT}/${bucketName}/${fileName}`
}else if(settings.storage === "local"){
return `http://localhost:8887/uploads/${fileName}`
}
} }
} }
async function uploadLocal(fileName: string, buffer: any) {
async function uploadLocal(fileName: string, buffer: any): Promise<{ data: { result: void; url: string } }> {
const localDir = "public/uploads/" const localDir = "public/uploads/"
try { try {
await mkdir(path.join(process.cwd(), localDir), { recursive: true }); await mkdir(path.join(process.cwd(), localDir), { recursive: true });
const result = await writeFile( return await writeFile(
path.join(process.cwd(), localDir + fileName), path.join(process.cwd(), localDir + fileName),
buffer buffer
); )
let url: string = "";
if (env.NODE_ENV === "production") {
// url = `https://${env.S3_ENDPOINT}/${bucketName}/${fileName}`
} else {
url = `http://localhost:8887/uploads/${fileName}`
}
return {
data: {result: result, url: url},
}
} catch (error) { } catch (error) {
console.log("Error occured ", error); console.log("Error occured ", error);
throw new Error('An error occured while importing image'); throw new Error('An error occured while importing image');
@@ -68,8 +76,13 @@ async function uploadLocal(fileName: string, buffer: any): Promise<{ data: { res
} }
} }
async function uploadS3Compatible() { async function uploadS3Compatible(bucketName: string, fileName: string, buffer: any){
await checkMinioAlive() await createPublicBucket({bucketName});
return await saveFileInBucket({
bucketName,
fileName,
file: buffer,
})
} }
export async function checkConnexionToS3(){ export async function checkConnexionToS3(){
+50 -16
View File
@@ -3,11 +3,11 @@ import {env} from "@/env.mjs";
import internal from "node:stream"; import internal from "node:stream";
import {prisma} from "@/prisma"; import {prisma} from "@/prisma";
const settings = await prisma.settings.findUnique({ // const settings = await prisma.settings.findUnique({
where:{ // where:{
name: "system" // name: "system"
} // }
}) // })
// Create a new Minio client with the S3 endpoint, access key, and secret key // Create a new Minio client with the S3 endpoint, access key, and secret key
@@ -23,21 +23,47 @@ const settings = await prisma.settings.findUnique({
// secretKey: env.S3_SECRET_KEY ?? "", // secretKey: env.S3_SECRET_KEY ?? "",
// useSSL: env.S3_USE_SSL === 'true' // useSSL: env.S3_USE_SSL === 'true'
// }) // })
export const s3Client = env.NODE_ENV === "production" ? // export const s3Client = env.NODE_ENV === "production" ?
new Minio.Client({ // new Minio.Client({
endPoint: settings.s3EndPointUrl ?? "", // endPoint: settings.s3EndPointUrl ?? "",
accessKey: settings.s3AccessKeyId ?? "", // accessKey: settings.s3AccessKeyId ?? "",
secretKey: settings.s3SecretAccessKey ?? "", // secretKey: settings.s3SecretAccessKey ?? "",
}) : new Minio.Client({ // }) : new Minio.Client({
endPoint: settings.s3EndPointUrl ?? "", // endPoint: settings.s3EndPointUrl ?? "",
port: Number(env.S3_PORT ?? 0), // port: Number(env.S3_PORT ?? 0),
accessKey: settings.s3AccessKeyId ?? "", // accessKey: settings.s3AccessKeyId ?? "",
secretKey: settings.s3SecretAccessKey ?? "", // secretKey: settings.s3SecretAccessKey ?? "",
useSSL: env.S3_USE_SSL === 'true' // useSSL: env.S3_USE_SSL === 'true'
// })
async function gets3Client() {
const settings = await prisma.settings.findUnique({
where: {
name: "system"
}
}) })
const s3Client = env.NODE_ENV === "production" ?
new Minio.Client({
endPoint: settings.s3EndPointUrl ?? "",
accessKey: settings.s3AccessKeyId ?? "",
secretKey: settings.s3SecretAccessKey ?? "",
}) : new Minio.Client({
endPoint: settings.s3EndPointUrl ?? "",
port: Number(env.S3_PORT ?? 0),
accessKey: settings.s3AccessKeyId ?? "",
secretKey: settings.s3SecretAccessKey ?? "",
useSSL: env.S3_USE_SSL === 'true'
})
return s3Client
}
export async function checkMinioAlive() { export async function checkMinioAlive() {
try { try {
console.log("Check MinioAlive");
const s3Client = await gets3Client()
// Try to list buckets to check connectivity // Try to list buckets to check connectivity
const buckets = await s3Client.listBuckets(); const buckets = await s3Client.listBuckets();
console.log('MinIO is up and running. Buckets:', buckets); console.log('MinIO is up and running. Buckets:', buckets);
@@ -50,6 +76,8 @@ export async function checkMinioAlive() {
} }
export async function createBucketIfNotExists(bucketName: string) { export async function createBucketIfNotExists(bucketName: string) {
const s3Client = await gets3Client()
const bucketExists = await s3Client.bucketExists(bucketName) const bucketExists = await s3Client.bucketExists(bucketName)
if (!bucketExists) { if (!bucketExists) {
console.log(`Creating bucket ${bucketName}`); console.log(`Creating bucket ${bucketName}`);
@@ -88,6 +116,7 @@ export async function saveFileInBucket({
if (fileExists) { if (fileExists) {
throw new Error('File already exists') throw new Error('File already exists')
} }
const s3Client = await gets3Client()
// Upload image to S3 bucket // Upload image to S3 bucket
const result = await s3Client.putObject(bucketName, fileName, file) const result = await s3Client.putObject(bucketName, fileName, file)
@@ -101,6 +130,8 @@ export async function saveFileInBucket({
* @returns true if file exists, false if not * @returns true if file exists, false if not
*/ */
export async function checkFileExistsInBucket({bucketName, fileName}: { bucketName: string; fileName: string }) { export async function checkFileExistsInBucket({bucketName, fileName}: { bucketName: string; fileName: string }) {
const s3Client = await gets3Client()
try { try {
await s3Client.statObject(bucketName, fileName) await s3Client.statObject(bucketName, fileName)
} catch (error) { } catch (error) {
@@ -125,6 +156,7 @@ export async function createPresignedUrlToUpload({
}) { }) {
// Create bucket if it doesn't exist // Create bucket if it doesn't exist
await createBucketIfNotExists(bucketName) await createBucketIfNotExists(bucketName)
const s3Client = await gets3Client()
return await s3Client.presignedPutObject(bucketName, fileName, expiry) return await s3Client.presignedPutObject(bucketName, fileName, expiry)
} }
@@ -132,6 +164,8 @@ export async function createPresignedUrlToUpload({
// Function to create a bucket and make it public // Function to create a bucket and make it public
export async function createPublicBucket({bucketName}: { bucketName: string }) { export async function createPublicBucket({bucketName}: { bucketName: string }) {
const s3Client = await gets3Client()
try { try {
// Check if the bucket already exists // Check if the bucket already exists
const exists = await s3Client.bucketExists(bucketName); const exists = await s3Client.bucketExists(bucketName);