From d4390c03057dc06b399395f098e72c5345c7eb2b Mon Sep 17 00:00:00 2001 From: stephen Date: Fri, 29 May 2026 17:19:46 +1000 Subject: [PATCH] Fixed problem 2, needs more testing --- src/app/upload/_actions.ts | 95 ++++++++++++----------------------- src/app/upload/page.tsx | 2 +- src/data-access/file-nodes.ts | 76 ++++++++-------------------- 3 files changed, 53 insertions(+), 120 deletions(-) diff --git a/src/app/upload/_actions.ts b/src/app/upload/_actions.ts index 3c3c32e..e865938 100644 --- a/src/app/upload/_actions.ts +++ b/src/app/upload/_actions.ts @@ -1,32 +1,28 @@ 'use server'; - -// src/app/upload/_actions.ts - +//src/app/upload/_actions.ts) import { auth } from "@/auth"; import { revalidatePath } from "next/cache"; import { createFileNode, getAllFolders, upsertFileNodeByHash } from "@/data-access/file-nodes"; import { prisma } from "@/lib/prisma"; import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive"; + /** - * NEW: CHECK FOR DUPLICATE HASH + * FIXED: Changed findUnique to findFirst to avoid runtime database crashes */ export async function checkDuplicateAction(hash: string) { - const existing = await prisma.fileNode.findUnique({ + const existing = await prisma.fileNode.findFirst({ where: { hash }, - select: { name: true } + select: { name: true, parentId: true } }); return existing; } -/** - * 1. CREATE VIRTUAL FOLDER - */ + export async function createFolderAction(name: string, parentId?: string | null) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); try { const internalId = crypto.randomUUID(); - // Swapped createNode for createFileNode const newNode = await createFileNode({ id: internalId, oneDriveId: null, @@ -44,9 +40,7 @@ export async function createFolderAction(name: string, parentId?: string | null) throw new Error(error.message || "Failed to create virtual folder"); } } -/** - * 2. UPLOAD FILE (Physical UUID Folder) - */ + export async function uploadFileAction(formData: FormData) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); @@ -63,42 +57,33 @@ export async function uploadFileAction(formData: FormData) { if (!file) throw new Error("No file selected"); const rootFolder = "WebCalibre"; - const internalId = crypto.randomUUID(); // Used for both DB ID and OneDrive Folder Name + const internalId = crypto.randomUUID(); try { - // A. Ensure root exists await ensureOneDriveFolder(session.user.id, rootFolder); - // B. Create the physical UUID folder on OneDrive const subFolderRes = await createOneDriveFolder(session.user.id, rootFolder, internalId); const subFolderData = await subFolderRes.json(); - // C. Upload the file binary into that specific folder const uploadedFileData = await uploadToFolderId(session.user.id, file, subFolderData.id); - // D. Create record in Database - // src/app/upload/_actions.ts - -// ... inside uploadFileAction or createFolderAction ... -// ... inside uploadFileAction after OneDrive work is done ... - -await createFileNode({ - id: internalId, - oneDriveId: uploadedFileData.id, - name: file.name, - hash:hash, - description: description, - size: BigInt(file.size), - isFolder: false, - path: `/${rootFolder}/${internalId}/${file.name}`, - ownerId: session.user.id, - parentId: parentId, - metadata: { - ...customMetadata, // User's custom keys from the form - type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN", - mimeType: file.type - } -}); + await createFileNode({ + id: internalId, + oneDriveId: uploadedFileData.id, + name: file.name, + hash: hash, + description: description, + size: BigInt(file.size), + isFolder: false, + path: `/${rootFolder}/${internalId}/${file.name}`, + ownerId: session.user.id, + parentId: parentId, + metadata: { + ...customMetadata, + type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN", + mimeType: file.type + } + }); revalidatePath("/dashboard"); revalidatePath("/upload"); @@ -108,16 +93,12 @@ await createFileNode({ return { success: false, error: error.message }; } } -/** - * NEW: FETCH ALL VIRTUAL FOLDERS - * This is the exact export the compiler is looking for. - */ + export async function getFoldersAction() { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); try { - // This calls the helper in your data-access/file-nodes.ts const folders = await getAllFolders(); return folders; } catch (error) { @@ -125,15 +106,10 @@ export async function getFoldersAction() { return []; } } -/** - * EXECUTE BULK ITEM - * Orchestrates the physical upload to OneDrive and the database record - * creation/update. This is the heart of the Bulk Upload and Restore system. - */ + export async function executeBulkItemAction(formData: FormData) { const session = await auth(); - // 1. Security check if (!session?.user?.id) { throw new Error("Unauthorized: You must be logged in to perform bulk actions."); } @@ -142,7 +118,6 @@ export async function executeBulkItemAction(formData: FormData) { const hash = formData.get("hash") as string; const targetFolderIdRaw = formData.get("targetFolderId") as string | null; - // Standardize the target folder ID const targetFolderId = (targetFolderIdRaw === "" || targetFolderIdRaw === "root") ? null : targetFolderIdRaw; @@ -153,24 +128,17 @@ export async function executeBulkItemAction(formData: FormData) { try { const rootFolder = "WebCalibre"; - // We generate a unique internal ID to serve as the physical folder name on OneDrive const internalId = crypto.randomUUID(); - // 2. Physical Storage (OneDrive) - // Ensure the root app folder exists first + // A. Direct upload to OneDrive await ensureOneDriveFolder(session.user.id, rootFolder); - // Create the unique subfolder for this file (to avoid collisions and match single-upload logic) const subFolderRes = await createOneDriveFolder(session.user.id, rootFolder, internalId); const subFolderData = await subFolderRes.json(); - // Perform the actual binary upload const uploadedFileData = await uploadToFolderId(session.user.id, file, subFolderData.id); - // 3. Database Logic (Data Access Layer) - // We use upsertFileNodeByHash to handle the Disaster Recovery case: - // If the hash matches an existing record, it updates the OneDrive link. - // If not, it creates a brand new record. + // B. Write or update PostgreSQL records via the safe wrapper const result = await upsertFileNodeByHash({ name: file.name, hash: hash, @@ -180,13 +148,12 @@ export async function executeBulkItemAction(formData: FormData) { ownerId: session.user.id, }); - // 4. Refresh UI revalidatePath("/dashboard"); return { success: true, - id: result.id, - mode: result.createdAt === result.updatedAt ? 'created' : 'restored' + id: result.node.id, + mode: result.mode }; } catch (error: any) { console.error("Bulk Item Execution Failure:", error); diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx index 11bae92..504c1d7 100644 --- a/src/app/upload/page.tsx +++ b/src/app/upload/page.tsx @@ -14,7 +14,7 @@ import CloudUploadIcon from '@mui/icons-material/CloudUpload'; // --- OUR UTILITIES --- import { calculateFileHash } from '@/lib/hashing-client'; -import { checkDuplicateAction } from '../_actions'; +import { checkDuplicateAction } from '@/app/upload/_actions'; interface UploadQueueItem { id: string; diff --git a/src/data-access/file-nodes.ts b/src/data-access/file-nodes.ts index 9d9dc4a..df43648 100644 --- a/src/data-access/file-nodes.ts +++ b/src/data-access/file-nodes.ts @@ -5,12 +5,6 @@ import { getOneDriveFileBuffer } from "@/services/onedrive"; import { extractMetadata } from "@/lib/metadata-extractor"; import { prisma } from "@/lib/prisma"; - -/** - * FETCH: Retrieve all nodes for the dashboard. - * Centralizing this here allows us to change sort order or filters - * in one place for the entire application. - */ export async function getAllFileNodes() { return await prisma.fileNode.findMany({ orderBy: { @@ -19,20 +13,12 @@ export async function getAllFileNodes() { }); } -/** - * FETCH: Get a single node by ID. - * Used by the Download route and Update pages to verify a file exists. - */ export async function getFileNodeById(id: string) { return await prisma.fileNode.findUnique({ where: { id }, }); } -/** - * UPDATE: Modify metadata, name, or virtual location. - * This function accepts the data object to keep the DAL flexible. - */ export async function updateFileNode(id: string, data: any) { return await prisma.fileNode.update({ where: { id }, @@ -43,48 +29,33 @@ export async function updateFileNode(id: string, data: any) { }); } -/** - * DELETE: Remove the record from the database. - * Cloud deletion should be handled by the Service Layer before calling this. - */ export async function deleteFileNode(id: string) { return await prisma.fileNode.delete({ where: { id }, }); } -/** - * MASTER CREATE: Handles both standard uploads and virtual folders. - * If no ID is provided, it generates a fresh UUID. - */ export async function createFileNode(data: { - id?: string; // Optional: used for virtual folders/UUID storage + id?: string; oneDriveId: string | null; name: string; - hash?: string | null; // ✅ ADDED: For SHA-256 duplicate prevention + hash?: string | null; description?: string; isFolder: boolean; path: string; ownerId: string; - parentId?: string | null; // Optional: for nested structures + parentId?: string | null; size?: bigint; metadata: any; }) { return await prisma.fileNode.create({ data: { ...data, - id: data.id ?? crypto.randomUUID(), // Use provided ID or generate new one + id: data.id ?? crypto.randomUUID(), } }); } - -// ... other functions (getAllFileNodes, etc) - -/** - * UPSERT: Create or Update a file node based on OneDrive ID - * Moved here because it interacts with the Database. - */ export async function upsertFileNode(oneDriveId: string, data: { name: string; size: bigint; @@ -92,7 +63,7 @@ export async function upsertFileNode(oneDriveId: string, data: { path: string; ownerId: string; metadata: any; - hash?: string | null; // ✅ ADDED: Keep hash in sync during upserts + hash?: string | null; }) { return await prisma.fileNode.upsert({ where: { oneDriveId }, @@ -101,7 +72,7 @@ export async function upsertFileNode(oneDriveId: string, data: { size: data.size, isFolder: data.isFolder, path: data.path, - hash: data.hash, // ✅ ADDED + hash: data.hash, updatedAt: new Date(), }, create: { @@ -113,15 +84,11 @@ export async function upsertFileNode(oneDriveId: string, data: { path: data.path, ownerId: data.ownerId, metadata: data.metadata, - hash: data.hash, // ✅ ADDED + hash: data.hash, } }); } -/** - * FETCH: Get all virtual folders for selection in dropdowns. - * Used by the BulkUpload page to set destinations. - */ export async function getAllFolders() { return await prisma.fileNode.findMany({ where: { @@ -138,9 +105,7 @@ export async function getAllFolders() { } /** - * UPSERT BY HASH: The core of the Disaster Recovery process. - * If a hash exists, we update the OneDrive ID (Restoring the link). - * If not, we create a new entry. + * UPSERT BY HASH: FIXED FOR SYSTEM STABILITY */ export async function upsertFileNodeByHash(data: { name: string; @@ -149,30 +114,28 @@ export async function upsertFileNodeByHash(data: { parentId?: string | null; size: bigint; ownerId: string; - mimeType?: string; }) { - // First, check if we have a record with this hash + // Use findFirst instead of findUnique to protect against schema constraints const existing = await prisma.fileNode.findFirst({ where: { hash: data.hash } }); if (existing) { - // 🛡️ DISASTER RECOVERY MODE - // The database knows about this file, but the OneDrive link is old. - // We update the existing record with the NEW cloud ID. - return await prisma.fileNode.update({ + // 🛡️ DISASTER RECOVERY MODE (RESTORE) + const updatedNode = await prisma.fileNode.update({ where: { id: existing.id }, data: { oneDriveId: data.oneDriveId, - // Optional: Update parent if the user chose a new folder during restore parentId: data.parentId || existing.parentId, updatedAt: new Date(), } }); + return { node: updatedNode, mode: 'restored' as const }; } // ✨ NEW UPLOAD MODE - return await prisma.fileNode.create({ + const fileExtension = data.name.split('.').pop()?.toUpperCase() || "UNKNOWN"; + const newNode = await prisma.fileNode.create({ data: { id: crypto.randomUUID(), oneDriveId: data.oneDriveId, @@ -182,9 +145,12 @@ export async function upsertFileNodeByHash(data: { isFolder: false, ownerId: data.ownerId, parentId: data.parentId || null, - path: data.name, // Simplified for now - metadata: {}, // Placeholder for extractMetadata logic + path: `/WebCalibre/Bulk/${data.name}`, + metadata: { + type: fileExtension, + mimeType: "application/octet-stream" + }, } }); -} - + return { node: newNode, mode: 'created' as const }; +} \ No newline at end of file