Fixed problem 2, needs more testing

This commit is contained in:
stephen 2026-05-29 17:19:46 +10:00
parent 0b94846b21
commit d4390c0305
3 changed files with 53 additions and 120 deletions

View file

@ -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,30 +57,21 @@ 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({
await createFileNode({
id: internalId,
oneDriveId: uploadedFileData.id,
name: file.name,
hash:hash,
hash: hash,
description: description,
size: BigInt(file.size),
isFolder: false,
@ -94,11 +79,11 @@ await createFileNode({
ownerId: session.user.id,
parentId: parentId,
metadata: {
...customMetadata, // User's custom keys from the form
...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);

View file

@ -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;

View file

@ -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 };
}