fix: migration storage backend and also user avatar upload.

This commit is contained in:
charlesgauthereau
2026-01-18 11:45:36 +01:00
parent f29444a219
commit 3d695709d5
11 changed files with 251 additions and 132 deletions
@@ -2,7 +2,7 @@
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
import {UploadIcon} from "lucide-react";
import {toast} from "sonner";
import {uploadImageAction} from "@/features/upload/public/upload.action";
import {uploadUserImageAction} from "@/features/upload/public/upload.action";
import {useMutation} from "@tanstack/react-query";
import {updateImageUserAction} from "@/components/wrappers/dashboard/profile/actions/avatar.action";
import {useRouter} from "next/navigation";
@@ -21,24 +21,27 @@ export const AvatarWithUpload = (props: AvatarWithUploadProps) => {
mutationFn: async (file: File) => {
const formData = new FormData();
formData.set("file", file);
const uploadImage = await uploadImageAction(formData);
const data = uploadImage?.data?.data;
const result = await uploadUserImageAction(formData);
if (uploadImage?.serverError || !data) {
toast.error(uploadImage?.serverError);
return;
const inner = result?.data;
if (inner?.success) {
const updateUser = await updateImageUserAction(inner.value ?? "");
const dataUser = updateUser?.data;
if (updateUser?.serverError || !dataUser) {
toast.error(updateUser?.serverError);
return;
}
toast.success(inner.actionSuccess?.message);
router.refresh();
} else {
toast.error(inner?.actionError?.message);
}
const updateUser = await updateImageUserAction(data.url);
const dataUser = updateUser?.data?.data;
if (updateUser?.serverError || !dataUser) {
toast.error(updateUser?.serverError);
return;
}
toast.success("Successfully uploaded user image!");
router.refresh();
},
});
+49 -16
View File
@@ -6,6 +6,10 @@ DO $$
s RECORD;
channel_id UUID;
existing_local RECORD;
p RECORD;
d RECORD;
b RECORD;
BEGIN
-- Get the current settings (assume single row)
@@ -73,21 +77,50 @@ DO $$
SET default_storage_channel_id = channel_id
WHERE id = s.id;
-- For all backups, create backup_storage entries
FOR b IN SELECT * FROM backups LOOP
INSERT INTO backup_storage (
id, backup_id, storage_channel_id, status, path, size, checksum, created_at, updated_at
)
VALUES (
gen_random_uuid(),
b.id,
channel_id,
'success',
b.file,
b.file_size,
NULL,
NOW(),
NOW()
);
/*
* Loop order:
* project -> database -> backup
*/
FOR p IN
SELECT id, slug
FROM projects
LOOP
FOR d IN
SELECT id
FROM databases
WHERE project_id = p.id
LOOP
FOR b IN
SELECT *
FROM backups
WHERE database_id = d.id
LOOP
INSERT INTO backup_storage (
id,
backup_id,
storage_channel_id,
status,
path,
size,
checksum,
created_at,
updated_at
)
VALUES (
gen_random_uuid(),
b.id,
channel_id,
'success',
format('backups/%s/%s', p.slug, b.file),
b.file_size,
NULL,
NOW(),
NOW()
);
END LOOP;
END LOOP;
END LOOP;
END $$;
+4 -1
View File
@@ -49,7 +49,7 @@ export async function storeBackupFiles(
return [];
}
const path = `${database.project?.slug}/${fileName}`;
const path = `backups/${database.project?.slug}/${fileName}`;
const size = file.length;
const checksum = computeChecksum(file);
@@ -99,6 +99,9 @@ export async function storeBackupFiles(
})
);
console.debug("Storage backup results", results);
const backupStatus = results.some(r => r.success) ? "success" : "failed";
await db
+29 -18
View File
@@ -5,7 +5,8 @@ import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput}
import fs from "node:fs";
import {getServerUrl} from "@/utils/get-server-url";
const BASE_DIR = "/private/uploads/files/";
// const BASE_DIR = "/private/uploads/files/";
const BASE_DIR = "/private/uploads/";
export async function uploadLocal(
config: { baseDir?: string },
@@ -18,11 +19,12 @@ export async function uploadLocal(
await mkdir(dir, {recursive: true});
await writeFile(fullPath, input.data.file);
const baseUrl = getServerUrl();
return {
success: true,
provider: 'local',
url: path.join(fullPath),
url: `${baseUrl}/api/${input.data.path}`,
};
}
@@ -43,25 +45,34 @@ export async function getLocal(
provider: 'local',
});
}
const crypto = require("crypto");
const baseUrl = getServerUrl();
const expiresAt = Date.now() + 60 * 1000;
const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex");
if (input.data.signedUrl) {
const crypto = require("crypto");
const baseUrl = getServerUrl();
const params = new URLSearchParams({
path: input.data.path,
token,
expires: expiresAt.toString(),
});
const expiresAt = Date.now() + 60 * 1000;
const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex");
const params = new URLSearchParams({
path: input.data.path,
token,
expires: expiresAt.toString(),
});
return {
success: true,
provider: 'local',
file: file,
url: `${baseUrl}/api/files/?${params.toString()}`,
};
} else {
return {
success: true,
provider: 'local',
file: file,
};
}
return {
success: true,
provider: 'local',
file: file,
url: `${baseUrl}/api/files/?${params.toString()}`,
};
}
export async function deleteLocal(
@@ -79,7 +90,7 @@ export async function deleteLocal(
export async function pingLocal(
config: { baseDir?: string }
): Promise<StorageResult> {
): Promise<StorageResult> {
const base = config.baseDir || BASE_DIR;
const fullPath = path.join(process.cwd(), base, "ping.txt");
+1 -1
View File
@@ -20,7 +20,7 @@ async function getS3Client(config: S3Config) {
});
}
const BASE_DIR = "backups/";
const BASE_DIR = "/";
async function ensureBucket(config: S3Config) {
+103 -26
View File
@@ -7,51 +7,128 @@ import path from "path";
import {env} from "@/env.mjs";
import {checkMinioAlive, saveFileInBucket} from "@/utils/s3-file-management";
//@ts-ignore
import {UploadedObjectInfo} from "minio/src/internal/type";
import {getServerUrl} from "@/utils/get-server-url";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {Setting} from "@/db/schema/01_setting";
import * as drizzleDb from "@/db";
import {ServerActionResult} from "@/types/action-type";
import {dispatchStorage} from "@/features/storages/dispatch";
import {StorageInput} from "@/features/storages/types";
// const imageDir = "images/";
const imageDir = "images/";
export const uploadUserImageAction = userAction.schema(
z.instanceof(FormData)
).action(async ({parsedInput: formData}): Promise<ServerActionResult<string>> => {
try {
export const uploadImageAction = userAction
.schema(z.instanceof(FormData))
.action(async ({parsedInput: formData, ctx}) => {
const file = formData.get("file") as File;
const uuid = uuidv4();
const fileFormat = file.name.split(".").pop();
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const [settings] = await db
.select()
.from(drizzleDb.schemas.setting)
.where(eq(drizzleDb.schemas.setting.name, "system"))
.limit(1);
if (!settings) throw new Error("System settings not found.");
const settings = await db.query.setting.findFirst({
where: eq(drizzleDb.schemas.setting.name, "system"),
with: {
storageChannel: true
}
});
let fileName: string | null = null;
let result: void | UploadedObjectInfo;
if (settings.storage === "local") {
fileName = `${imageDir}${uuid}.${fileFormat}`;
result = await uploadLocal(fileName, buffer);
} else if (settings.storage === "s3") {
fileName = `${imageDir}${uuid}.${fileFormat}`;
result = await uploadS3Compatible(env.S3_BUCKET_NAME ?? "", fileName, buffer);
} else {
throw new Error(`Unsupported storage type: ${settings.storage}`);
if (!settings || !settings.storageChannel) {
return {
success: false,
actionError: {
message: "An error occurred with default settings.",
status: 500,
},
};
}
const url = `${getServerUrl()}/api/${fileName}`
return {data: {result, url}};
});
const path = `images/${uuid}.${fileFormat?.toLowerCase()}`;
const input: StorageInput = {
action: "upload",
data: {
path: path,
file: buffer
}
}
const result = await dispatchStorage(input, undefined, settings.storageChannel.id);
if (!result.success) {
return {
success: false,
actionError: {
message: "Failed to upload user avatar.",
status: 500,
cause: result.error
},
};
}
return {
success: true,
value: result.url,
actionSuccess: {
message: "Avatar successfully updated",
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to upload user avatar.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
},
};
}
});
//
// export const uploadImageAction = userAction
// .schema(z.instanceof(FormData))
// .action(async ({parsedInput: formData, ctx}) => {
// const file = formData.get("file") as File;
// const uuid = uuidv4();
// const fileFormat = file.name.split(".").pop();
// const arrayBuffer = await file.arrayBuffer();
// const buffer = Buffer.from(arrayBuffer);
//
//
// const settings = await db.query.setting.findFirst({
// where: eq(drizzleDb.schemas.setting.name, "system"),
// with: {
// storageChannel: true
// }
// });
//
//
// if (!settings) throw new Error("System settings not found.");
//
// let fileName: string | null = null;
// let result: void | UploadedObjectInfo;
//
// if (settings.storage === "local") {
// fileName = `${imageDir}${uuid}.${fileFormat}`;
// result = await uploadLocal(fileName, buffer);
// } else if (settings.storage === "s3") {
// fileName = `${imageDir}${uuid}.${fileFormat}`;
// result = await uploadS3Compatible(env.S3_BUCKET_NAME ?? "", fileName, buffer);
// } else {
// throw new Error(`Unsupported storage type: ${settings.storage}`);
// }
//
//
// const url = `${getServerUrl()}/api/${fileName}`
// return {data: {result, url}};
// });
//
async function uploadLocal(fileName: string, buffer: any) {
const localDir = "private/uploads/";
-1
View File
@@ -23,7 +23,6 @@ export const deleteBackupCronAction = action
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
try {
const backup = await db.query.backup.findFirst({
where: and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.backupId))
});