diff --git a/src/app/dashboard/actions.ts b/src/app/dashboard/actions.ts index 9c47fdc..38eea6f 100644 --- a/src/app/dashboard/actions.ts +++ b/src/app/dashboard/actions.ts @@ -6,7 +6,7 @@ import { revalidatePath } from "next/cache"; 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() { 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) { const session = await auth(); @@ -35,7 +62,7 @@ export async function deleteFileAction(fileId: string) { if (!node) { revalidatePath("/dashboard"); - return { success: true, message: "Item already removed from database" }; + return { success: true }; } // @ts-ignore @@ -48,14 +75,17 @@ export async function deleteFileAction(fileId: string) { try { const accessToken = await getFreshAccessToken(session.user.id); - if (accessToken) { - const rootFolder = "WebCalibre"; - const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}/${node.id}`; - - const onedriveRes = await fetch(onedrivePath, { - method: "DELETE", - headers: { Authorization: `Bearer ${accessToken}` }, - }); + + // Only attempt cloud deletion if it's a file/storage with a oneDriveId. + // Virtual folders created in the DB have no oneDriveId and are skipped. + if (accessToken && node.oneDriveId) { + const onedriveRes = await fetch( + `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}`, + { + method: "DELETE", + headers: { Authorization: `Bearer ${accessToken}` }, + } + ); if (!onedriveRes.ok && onedriveRes.status !== 404) { 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) { 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 - * Uses FormData to handle the binary file upload and text fields. + * 5. UPDATE & REPLACE: Full update of metadata and OneDrive content */ export async function updateFileFullAction(formData: FormData) { const session = await auth(); @@ -110,19 +139,16 @@ export async function updateFileFullAction(formData: FormData) { const metadataStr = formData.get("metadata") as string; const newFile = formData.get("file") as File | null; - // Handle the "root" placeholder back to null for Prisma const parentId = parentIdRaw === "root" ? null : parentIdRaw; let metadata = JSON.parse(metadataStr); try { 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) - if (newFile && newFile.size > 0) { - const rootFolder = "WebCalibre"; - // PUT request to the specific file ID path overwrites content - const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}/${id}:/content`; + // Update physical file content only if a new file is uploaded and we have a target oneDriveId + if (newFile && newFile.size > 0 && node?.oneDriveId) { + const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content`; const uploadRes = await fetch(onedrivePath, { method: "PUT", @@ -133,17 +159,12 @@ export async function updateFileFullAction(formData: FormData) { body: Buffer.from(await newFile.arrayBuffer()), }); - if (!uploadRes.ok) { - const err = await uploadRes.json(); - throw new Error(`OneDrive error: ${err.error?.message}`); - } + if (!uploadRes.ok) throw new Error("OneDrive content update failed"); - // Update metadata with new file properties metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'; metadata.mimeType = newFile.type; } - // 2. Database Update await prisma.fileNode.update({ where: { id }, data: { @@ -151,7 +172,6 @@ export async function updateFileFullAction(formData: FormData) { description, parentId, metadata, - // If file changed, update size; otherwise keep existing size: newFile ? BigInt(newFile.size) : undefined, updatedAt: new Date(), } @@ -160,7 +180,7 @@ export async function updateFileFullAction(formData: FormData) { revalidatePath("/dashboard"); return { success: true }; } 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."); } } \ No newline at end of file diff --git a/src/app/upload/_actions.ts b/src/app/upload/_actions.ts index 096fbb0..ef11360 100644 --- a/src/app/upload/_actions.ts +++ b/src/app/upload/_actions.ts @@ -1,46 +1,29 @@ 'use server'; + import { auth } from "@/auth"; import { getFreshAccessToken } from "@/lib/auth-utils"; import { prisma } from "@/lib/prisma"; 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) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); try { - const accessToken = await getFreshAccessToken(session.user.id); - const rootFolder = "WebCalibre"; 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({ data: { id: internalId, - oneDriveId: onedriveData.id, // SAVED HERE + oneDriveId: null, // Virtual folders do not have a cloud ID name: name, isFolder: true, - path: `/${rootFolder}/${internalId}`, + path: `virtual:/${name}`, ownerId: session.user.id, parentId: parentId || null, metadata: { type: "FOLDER" } @@ -49,12 +32,19 @@ export async function createFolderAction(name: string, parentId?: string | null) revalidatePath("/upload"); revalidatePath("/dashboard"); + return { success: true, node: newNode }; } 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) { const session = await auth(); 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 parentIdRaw = formData.get("parentId") as string | null; const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; + const customMetadataRaw = formData.get("customMetadata") as string; const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; @@ -70,17 +61,29 @@ export async function uploadFileAction(formData: FormData) { const accessToken = await getFreshAccessToken(session.user.id); 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`, { method: "POST", - headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, - body: JSON.stringify({ name: internalId, folder: {} }) + headers: { + 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(); - // 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 sessionRes = await fetch(sessionUrl, { method: "POST", @@ -91,6 +94,7 @@ export async function uploadFileAction(formData: FormData) { const { uploadUrl } = await sessionRes.json(); const buffer = Buffer.from(await file.arrayBuffer()); + // 3. PUT the file binary const uploadRes = await fetch(uploadUrl, { method: "PUT", headers: { @@ -100,16 +104,16 @@ export async function uploadFileAction(formData: FormData) { body: buffer }); - // 3. GET THE FINAL FILE ID FROM MICROSOFT const uploadedFileData = await uploadRes.json(); const oneDriveId = uploadedFileData.id; - 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({ data: { id: internalId, - oneDriveId: oneDriveId, // SAVED HERE + oneDriveId: oneDriveId, name: file.name, description: description, size: BigInt(file.size),