Fixed problem 2, needs more testing
This commit is contained in:
parent
0b94846b21
commit
d4390c0305
3 changed files with 53 additions and 120 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue