Working again

This commit is contained in:
stephen 2026-01-14 18:29:52 +11:00
parent 86cdffdd1c
commit fe9eb915e1
2 changed files with 87 additions and 63 deletions

View file

@ -6,7 +6,7 @@ import { revalidatePath } from "next/cache";
import { getFreshAccessToken } from "@/lib/auth-utils"; import { getFreshAccessToken } from "@/lib/auth-utils";
/** /**
* 1. FETCH: Get all file nodes * 1. FETCH: Get all file nodes for the Dashboard
*/ */
export async function getFileNodes() { export async function getFileNodes() {
try { try {
@ -23,7 +23,34 @@ export async function getFileNodes() {
} }
/** /**
* 2. DELETE: Remove from OneDrive and Database * 2. DOWNLOAD: Generates the authenticated OneDrive URL
*/
export async function getDownloadUrlAction(id: string) {
const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized");
const file = await prisma.fileNode.findUnique({ where: { id } });
if (!file || !file.oneDriveId) throw new Error("File not found or missing cloud ID");
const accessToken = await getFreshAccessToken(session.user.id);
const res = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/items/${file.oneDriveId}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
if (!res.ok) throw new Error("Failed to contact OneDrive");
const data = await res.json();
const downloadUrl = data["@microsoft.graph.downloadUrl"];
if (!downloadUrl) throw new Error("OneDrive did not provide a download link");
return { downloadUrl };
}
/**
* 3. DELETE: Remove from OneDrive (via ID) and Database
* Folders are virtual (DB only), so cloud deletion is skipped if oneDriveId is null.
*/ */
export async function deleteFileAction(fileId: string) { export async function deleteFileAction(fileId: string) {
const session = await auth(); const session = await auth();
@ -35,7 +62,7 @@ export async function deleteFileAction(fileId: string) {
if (!node) { if (!node) {
revalidatePath("/dashboard"); revalidatePath("/dashboard");
return { success: true, message: "Item already removed from database" }; return { success: true };
} }
// @ts-ignore // @ts-ignore
@ -48,14 +75,17 @@ export async function deleteFileAction(fileId: string) {
try { try {
const accessToken = await getFreshAccessToken(session.user.id); const accessToken = await getFreshAccessToken(session.user.id);
if (accessToken) {
const rootFolder = "WebCalibre"; // Only attempt cloud deletion if it's a file/storage with a oneDriveId.
const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}/${node.id}`; // Virtual folders created in the DB have no oneDriveId and are skipped.
if (accessToken && node.oneDriveId) {
const onedriveRes = await fetch(onedrivePath, { const onedriveRes = await fetch(
method: "DELETE", `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}`,
headers: { Authorization: `Bearer ${accessToken}` }, {
}); method: "DELETE",
headers: { Authorization: `Bearer ${accessToken}` },
}
);
if (!onedriveRes.ok && onedriveRes.status !== 404) { if (!onedriveRes.ok && onedriveRes.status !== 404) {
console.warn("OneDrive Deletion Warning: Cloud record might still exist."); console.warn("OneDrive Deletion Warning: Cloud record might still exist.");
@ -76,7 +106,7 @@ export async function deleteFileAction(fileId: string) {
} }
/** /**
* 3. MOVE: Assign file to folder or folder to another folder * 4. MOVE: Assign file to folder or folder to another folder (Virtual Move)
*/ */
export async function moveNodeAction(nodeId: string, newParentId: string | null) { export async function moveNodeAction(nodeId: string, newParentId: string | null) {
const session = await auth(); const session = await auth();
@ -96,8 +126,7 @@ export async function moveNodeAction(nodeId: string, newParentId: string | null)
} }
/** /**
* 4. UPDATE & REPLACE: Full update of metadata and OneDrive content * 5. UPDATE & REPLACE: Full update of metadata and OneDrive content
* Uses FormData to handle the binary file upload and text fields.
*/ */
export async function updateFileFullAction(formData: FormData) { export async function updateFileFullAction(formData: FormData) {
const session = await auth(); const session = await auth();
@ -110,19 +139,16 @@ export async function updateFileFullAction(formData: FormData) {
const metadataStr = formData.get("metadata") as string; const metadataStr = formData.get("metadata") as string;
const newFile = formData.get("file") as File | null; const newFile = formData.get("file") as File | null;
// Handle the "root" placeholder back to null for Prisma
const parentId = parentIdRaw === "root" ? null : parentIdRaw; const parentId = parentIdRaw === "root" ? null : parentIdRaw;
let metadata = JSON.parse(metadataStr); let metadata = JSON.parse(metadataStr);
try { try {
const accessToken = await getFreshAccessToken(session.user.id); const accessToken = await getFreshAccessToken(session.user.id);
if (!accessToken) throw new Error("Access token expired."); const node = await prisma.fileNode.findUnique({ where: { id } });
// 1. OneDrive Overwrite (if file is provided) // Update physical file content only if a new file is uploaded and we have a target oneDriveId
if (newFile && newFile.size > 0) { if (newFile && newFile.size > 0 && node?.oneDriveId) {
const rootFolder = "WebCalibre"; const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content`;
// PUT request to the specific file ID path overwrites content
const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}/${id}:/content`;
const uploadRes = await fetch(onedrivePath, { const uploadRes = await fetch(onedrivePath, {
method: "PUT", method: "PUT",
@ -133,17 +159,12 @@ export async function updateFileFullAction(formData: FormData) {
body: Buffer.from(await newFile.arrayBuffer()), body: Buffer.from(await newFile.arrayBuffer()),
}); });
if (!uploadRes.ok) { if (!uploadRes.ok) throw new Error("OneDrive content update failed");
const err = await uploadRes.json();
throw new Error(`OneDrive error: ${err.error?.message}`);
}
// Update metadata with new file properties
metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'; metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN';
metadata.mimeType = newFile.type; metadata.mimeType = newFile.type;
} }
// 2. Database Update
await prisma.fileNode.update({ await prisma.fileNode.update({
where: { id }, where: { id },
data: { data: {
@ -151,7 +172,6 @@ export async function updateFileFullAction(formData: FormData) {
description, description,
parentId, parentId,
metadata, metadata,
// If file changed, update size; otherwise keep existing
size: newFile ? BigInt(newFile.size) : undefined, size: newFile ? BigInt(newFile.size) : undefined,
updatedAt: new Date(), updatedAt: new Date(),
} }
@ -160,7 +180,7 @@ export async function updateFileFullAction(formData: FormData) {
revalidatePath("/dashboard"); revalidatePath("/dashboard");
return { success: true }; return { success: true };
} catch (error: any) { } catch (error: any) {
console.error("Full Update Action Failure:", error); console.error("Full Update Failure:", error);
throw new Error(error.message || "Failed to update record."); throw new Error(error.message || "Failed to update record.");
} }
} }

View file

@ -1,46 +1,29 @@
'use server'; 'use server';
import { auth } from "@/auth"; import { auth } from "@/auth";
import { getFreshAccessToken } from "@/lib/auth-utils"; import { getFreshAccessToken } from "@/lib/auth-utils";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
/**
* 1. CREATE FOLDER: Virtual Only
* Logic: User-created organizational folders exist ONLY in the database.
* No call to OneDrive is made here.
*/
export async function createFolderAction(name: string, parentId?: string | null) { export async function createFolderAction(name: string, parentId?: string | null) {
const session = await auth(); const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized"); if (!session?.user?.id) throw new Error("Unauthorized");
try { try {
const accessToken = await getFreshAccessToken(session.user.id);
const rootFolder = "WebCalibre";
const internalId = crypto.randomUUID(); const internalId = crypto.randomUUID();
const onedriveRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: internalId,
folder: {},
"@microsoft.graph.conflictBehavior": "rename"
})
});
if (!onedriveRes.ok) {
const errorData = await onedriveRes.json();
throw new Error(errorData.error?.message || "OneDrive folder creation failed");
}
// CAPTURE Microsoft's ID
const onedriveData = await onedriveRes.json();
const newNode = await prisma.fileNode.create({ const newNode = await prisma.fileNode.create({
data: { data: {
id: internalId, id: internalId,
oneDriveId: onedriveData.id, // SAVED HERE oneDriveId: null, // Virtual folders do not have a cloud ID
name: name, name: name,
isFolder: true, isFolder: true,
path: `/${rootFolder}/${internalId}`, path: `virtual:/${name}`,
ownerId: session.user.id, ownerId: session.user.id,
parentId: parentId || null, parentId: parentId || null,
metadata: { type: "FOLDER" } metadata: { type: "FOLDER" }
@ -49,12 +32,19 @@ export async function createFolderAction(name: string, parentId?: string | null)
revalidatePath("/upload"); revalidatePath("/upload");
revalidatePath("/dashboard"); revalidatePath("/dashboard");
return { success: true, node: newNode }; return { success: true, node: newNode };
} catch (error: any) { } catch (error: any) {
throw new Error(error.message || "Failed to create folder"); console.error("Folder creation error:", error);
throw new Error(error.message || "Failed to create virtual folder");
} }
} }
/**
* 2. UPLOAD FILE: Physical Container
* Logic: Creates a physical folder (UUID) on OneDrive to hold the file.
* This ensures every file has a unique storage space in the cloud.
*/
export async function uploadFileAction(formData: FormData) { export async function uploadFileAction(formData: FormData) {
const session = await auth(); const session = await auth();
if (!session?.user?.id) throw new Error("Unauthorized"); if (!session?.user?.id) throw new Error("Unauthorized");
@ -63,6 +53,7 @@ export async function uploadFileAction(formData: FormData) {
const description = formData.get("description") as string || ""; const description = formData.get("description") as string || "";
const parentIdRaw = formData.get("parentId") as string | null; const parentIdRaw = formData.get("parentId") as string | null;
const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw;
const customMetadataRaw = formData.get("customMetadata") as string; const customMetadataRaw = formData.get("customMetadata") as string;
const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {};
@ -70,17 +61,29 @@ export async function uploadFileAction(formData: FormData) {
const accessToken = await getFreshAccessToken(session.user.id); const accessToken = await getFreshAccessToken(session.user.id);
const rootFolder = "WebCalibre"; const rootFolder = "WebCalibre";
const internalId = crypto.randomUUID(); const internalId = crypto.randomUUID(); // This UUID will be the OneDrive folder name
// 1. Create Storage Container // 1. Create the Physical Storage Folder on OneDrive
const createSubFolderRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, { const createSubFolderRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, {
method: "POST", method: "POST",
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, headers: {
body: JSON.stringify({ name: internalId, folder: {} }) Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: internalId,
folder: {},
"@microsoft.graph.conflictBehavior": "fail"
})
}); });
if (!createSubFolderRes.ok) {
const errorData = await createSubFolderRes.json();
throw new Error(errorData.error?.message || "Storage directory creation failed");
}
const subFolderData = await createSubFolderRes.json(); const subFolderData = await createSubFolderRes.json();
// 2. Upload File // 2. Create Upload Session inside the new Physical Folder
const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${subFolderData.id}:/${encodeURIComponent(file.name)}:/createUploadSession`; const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${subFolderData.id}:/${encodeURIComponent(file.name)}:/createUploadSession`;
const sessionRes = await fetch(sessionUrl, { const sessionRes = await fetch(sessionUrl, {
method: "POST", method: "POST",
@ -91,6 +94,7 @@ export async function uploadFileAction(formData: FormData) {
const { uploadUrl } = await sessionRes.json(); const { uploadUrl } = await sessionRes.json();
const buffer = Buffer.from(await file.arrayBuffer()); const buffer = Buffer.from(await file.arrayBuffer());
// 3. PUT the file binary
const uploadRes = await fetch(uploadUrl, { const uploadRes = await fetch(uploadUrl, {
method: "PUT", method: "PUT",
headers: { headers: {
@ -100,16 +104,16 @@ export async function uploadFileAction(formData: FormData) {
body: buffer body: buffer
}); });
// 3. GET THE FINAL FILE ID FROM MICROSOFT
const uploadedFileData = await uploadRes.json(); const uploadedFileData = await uploadRes.json();
const oneDriveId = uploadedFileData.id; const oneDriveId = uploadedFileData.id;
const extension = file.name.split('.').pop()?.toUpperCase() || "UNKNOWN"; const extension = file.name.split('.').pop()?.toUpperCase() || "UNKNOWN";
// 4. Create record in Database
// Link it to the VIRTUAL folder via parentId
await prisma.fileNode.create({ await prisma.fileNode.create({
data: { data: {
id: internalId, id: internalId,
oneDriveId: oneDriveId, // SAVED HERE oneDriveId: oneDriveId,
name: file.name, name: file.name,
description: description, description: description,
size: BigInt(file.size), size: BigInt(file.size),