Fixed problem 2, needs more testing
This commit is contained in:
parent
0b94846b21
commit
e9f569d5df
3 changed files with 53 additions and 120 deletions
|
|
@ -1,32 +1,28 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
//src/app/upload/_actions.ts)
|
||||||
// src/app/upload/_actions.ts
|
|
||||||
|
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { createFileNode, getAllFolders, upsertFileNodeByHash } from "@/data-access/file-nodes";
|
import { createFileNode, getAllFolders, upsertFileNodeByHash } from "@/data-access/file-nodes";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive";
|
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) {
|
export async function checkDuplicateAction(hash: string) {
|
||||||
const existing = await prisma.fileNode.findUnique({
|
const existing = await prisma.fileNode.findFirst({
|
||||||
where: { hash },
|
where: { hash },
|
||||||
select: { name: true }
|
select: { name: true, parentId: true }
|
||||||
});
|
});
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* 1. CREATE VIRTUAL FOLDER
|
|
||||||
*/
|
|
||||||
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 internalId = crypto.randomUUID();
|
const internalId = crypto.randomUUID();
|
||||||
// Swapped createNode for createFileNode
|
|
||||||
const newNode = await createFileNode({
|
const newNode = await createFileNode({
|
||||||
id: internalId,
|
id: internalId,
|
||||||
oneDriveId: null,
|
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");
|
throw new Error(error.message || "Failed to create virtual folder");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* 2. UPLOAD FILE (Physical UUID Folder)
|
|
||||||
*/
|
|
||||||
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,25 +57,16 @@ export async function uploadFileAction(formData: FormData) {
|
||||||
if (!file) throw new Error("No file selected");
|
if (!file) throw new Error("No file selected");
|
||||||
|
|
||||||
const rootFolder = "WebCalibre";
|
const rootFolder = "WebCalibre";
|
||||||
const internalId = crypto.randomUUID(); // Used for both DB ID and OneDrive Folder Name
|
const internalId = crypto.randomUUID();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// A. Ensure root exists
|
|
||||||
await ensureOneDriveFolder(session.user.id, rootFolder);
|
await ensureOneDriveFolder(session.user.id, rootFolder);
|
||||||
|
|
||||||
// B. Create the physical UUID folder on OneDrive
|
|
||||||
const subFolderRes = await createOneDriveFolder(session.user.id, rootFolder, internalId);
|
const subFolderRes = await createOneDriveFolder(session.user.id, rootFolder, internalId);
|
||||||
const subFolderData = await subFolderRes.json();
|
const subFolderData = await subFolderRes.json();
|
||||||
|
|
||||||
// C. Upload the file binary into that specific folder
|
|
||||||
const uploadedFileData = await uploadToFolderId(session.user.id, file, subFolderData.id);
|
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,
|
id: internalId,
|
||||||
oneDriveId: uploadedFileData.id,
|
oneDriveId: uploadedFileData.id,
|
||||||
|
|
@ -94,7 +79,7 @@ await createFileNode({
|
||||||
ownerId: session.user.id,
|
ownerId: session.user.id,
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
metadata: {
|
metadata: {
|
||||||
...customMetadata, // User's custom keys from the form
|
...customMetadata,
|
||||||
type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN",
|
type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN",
|
||||||
mimeType: file.type
|
mimeType: file.type
|
||||||
}
|
}
|
||||||
|
|
@ -108,16 +93,12 @@ await createFileNode({
|
||||||
return { success: false, error: error.message };
|
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() {
|
export async function getFoldersAction() {
|
||||||
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 {
|
||||||
// This calls the helper in your data-access/file-nodes.ts
|
|
||||||
const folders = await getAllFolders();
|
const folders = await getAllFolders();
|
||||||
return folders;
|
return folders;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -125,15 +106,10 @@ export async function getFoldersAction() {
|
||||||
return [];
|
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) {
|
export async function executeBulkItemAction(formData: FormData) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
// 1. Security check
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
throw new Error("Unauthorized: You must be logged in to perform bulk actions.");
|
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 hash = formData.get("hash") as string;
|
||||||
const targetFolderIdRaw = formData.get("targetFolderId") as string | null;
|
const targetFolderIdRaw = formData.get("targetFolderId") as string | null;
|
||||||
|
|
||||||
// Standardize the target folder ID
|
|
||||||
const targetFolderId = (targetFolderIdRaw === "" || targetFolderIdRaw === "root")
|
const targetFolderId = (targetFolderIdRaw === "" || targetFolderIdRaw === "root")
|
||||||
? null
|
? null
|
||||||
: targetFolderIdRaw;
|
: targetFolderIdRaw;
|
||||||
|
|
@ -153,24 +128,17 @@ export async function executeBulkItemAction(formData: FormData) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rootFolder = "WebCalibre";
|
const rootFolder = "WebCalibre";
|
||||||
// We generate a unique internal ID to serve as the physical folder name on OneDrive
|
|
||||||
const internalId = crypto.randomUUID();
|
const internalId = crypto.randomUUID();
|
||||||
|
|
||||||
// 2. Physical Storage (OneDrive)
|
// A. Direct upload to OneDrive
|
||||||
// Ensure the root app folder exists first
|
|
||||||
await ensureOneDriveFolder(session.user.id, rootFolder);
|
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 subFolderRes = await createOneDriveFolder(session.user.id, rootFolder, internalId);
|
||||||
const subFolderData = await subFolderRes.json();
|
const subFolderData = await subFolderRes.json();
|
||||||
|
|
||||||
// Perform the actual binary upload
|
|
||||||
const uploadedFileData = await uploadToFolderId(session.user.id, file, subFolderData.id);
|
const uploadedFileData = await uploadToFolderId(session.user.id, file, subFolderData.id);
|
||||||
|
|
||||||
// 3. Database Logic (Data Access Layer)
|
// B. Write or update PostgreSQL records via the safe wrapper
|
||||||
// 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.
|
|
||||||
const result = await upsertFileNodeByHash({
|
const result = await upsertFileNodeByHash({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
|
|
@ -180,13 +148,12 @@ export async function executeBulkItemAction(formData: FormData) {
|
||||||
ownerId: session.user.id,
|
ownerId: session.user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Refresh UI
|
|
||||||
revalidatePath("/dashboard");
|
revalidatePath("/dashboard");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
id: result.id,
|
id: result.node.id,
|
||||||
mode: result.createdAt === result.updatedAt ? 'created' : 'restored'
|
mode: result.mode
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Bulk Item Execution Failure:", error);
|
console.error("Bulk Item Execution Failure:", error);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||||
|
|
||||||
// --- OUR UTILITIES ---
|
// --- OUR UTILITIES ---
|
||||||
import { calculateFileHash } from '@/lib/hashing-client';
|
import { calculateFileHash } from '@/lib/hashing-client';
|
||||||
import { checkDuplicateAction } from '../_actions';
|
import { checkDuplicateAction } from '@/app/upload/_actions';
|
||||||
|
|
||||||
interface UploadQueueItem {
|
interface UploadQueueItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,6 @@ import { getOneDriveFileBuffer } from "@/services/onedrive";
|
||||||
import { extractMetadata } from "@/lib/metadata-extractor";
|
import { extractMetadata } from "@/lib/metadata-extractor";
|
||||||
import { prisma } from "@/lib/prisma";
|
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() {
|
export async function getAllFileNodes() {
|
||||||
return await prisma.fileNode.findMany({
|
return await prisma.fileNode.findMany({
|
||||||
orderBy: {
|
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) {
|
export async function getFileNodeById(id: string) {
|
||||||
return await prisma.fileNode.findUnique({
|
return await prisma.fileNode.findUnique({
|
||||||
where: { id },
|
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) {
|
export async function updateFileNode(id: string, data: any) {
|
||||||
return await prisma.fileNode.update({
|
return await prisma.fileNode.update({
|
||||||
where: { id },
|
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) {
|
export async function deleteFileNode(id: string) {
|
||||||
return await prisma.fileNode.delete({
|
return await prisma.fileNode.delete({
|
||||||
where: { id },
|
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: {
|
export async function createFileNode(data: {
|
||||||
id?: string; // Optional: used for virtual folders/UUID storage
|
id?: string;
|
||||||
oneDriveId: string | null;
|
oneDriveId: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
hash?: string | null; // ✅ ADDED: For SHA-256 duplicate prevention
|
hash?: string | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
isFolder: boolean;
|
isFolder: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
parentId?: string | null; // Optional: for nested structures
|
parentId?: string | null;
|
||||||
size?: bigint;
|
size?: bigint;
|
||||||
metadata: any;
|
metadata: any;
|
||||||
}) {
|
}) {
|
||||||
return await prisma.fileNode.create({
|
return await prisma.fileNode.create({
|
||||||
data: {
|
data: {
|
||||||
...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: {
|
export async function upsertFileNode(oneDriveId: string, data: {
|
||||||
name: string;
|
name: string;
|
||||||
size: bigint;
|
size: bigint;
|
||||||
|
|
@ -92,7 +63,7 @@ export async function upsertFileNode(oneDriveId: string, data: {
|
||||||
path: string;
|
path: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
metadata: any;
|
metadata: any;
|
||||||
hash?: string | null; // ✅ ADDED: Keep hash in sync during upserts
|
hash?: string | null;
|
||||||
}) {
|
}) {
|
||||||
return await prisma.fileNode.upsert({
|
return await prisma.fileNode.upsert({
|
||||||
where: { oneDriveId },
|
where: { oneDriveId },
|
||||||
|
|
@ -101,7 +72,7 @@ export async function upsertFileNode(oneDriveId: string, data: {
|
||||||
size: data.size,
|
size: data.size,
|
||||||
isFolder: data.isFolder,
|
isFolder: data.isFolder,
|
||||||
path: data.path,
|
path: data.path,
|
||||||
hash: data.hash, // ✅ ADDED
|
hash: data.hash,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
|
|
@ -113,15 +84,11 @@ export async function upsertFileNode(oneDriveId: string, data: {
|
||||||
path: data.path,
|
path: data.path,
|
||||||
ownerId: data.ownerId,
|
ownerId: data.ownerId,
|
||||||
metadata: data.metadata,
|
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() {
|
export async function getAllFolders() {
|
||||||
return await prisma.fileNode.findMany({
|
return await prisma.fileNode.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -138,9 +105,7 @@ export async function getAllFolders() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UPSERT BY HASH: The core of the Disaster Recovery process.
|
* UPSERT BY HASH: FIXED FOR SYSTEM STABILITY
|
||||||
* If a hash exists, we update the OneDrive ID (Restoring the link).
|
|
||||||
* If not, we create a new entry.
|
|
||||||
*/
|
*/
|
||||||
export async function upsertFileNodeByHash(data: {
|
export async function upsertFileNodeByHash(data: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -149,30 +114,28 @@ export async function upsertFileNodeByHash(data: {
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
size: bigint;
|
size: bigint;
|
||||||
ownerId: string;
|
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({
|
const existing = await prisma.fileNode.findFirst({
|
||||||
where: { hash: data.hash }
|
where: { hash: data.hash }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// 🛡️ DISASTER RECOVERY MODE
|
// 🛡️ DISASTER RECOVERY MODE (RESTORE)
|
||||||
// The database knows about this file, but the OneDrive link is old.
|
const updatedNode = await prisma.fileNode.update({
|
||||||
// We update the existing record with the NEW cloud ID.
|
|
||||||
return await prisma.fileNode.update({
|
|
||||||
where: { id: existing.id },
|
where: { id: existing.id },
|
||||||
data: {
|
data: {
|
||||||
oneDriveId: data.oneDriveId,
|
oneDriveId: data.oneDriveId,
|
||||||
// Optional: Update parent if the user chose a new folder during restore
|
|
||||||
parentId: data.parentId || existing.parentId,
|
parentId: data.parentId || existing.parentId,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return { node: updatedNode, mode: 'restored' as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✨ NEW UPLOAD MODE
|
// ✨ NEW UPLOAD MODE
|
||||||
return await prisma.fileNode.create({
|
const fileExtension = data.name.split('.').pop()?.toUpperCase() || "UNKNOWN";
|
||||||
|
const newNode = await prisma.fileNode.create({
|
||||||
data: {
|
data: {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
oneDriveId: data.oneDriveId,
|
oneDriveId: data.oneDriveId,
|
||||||
|
|
@ -182,9 +145,12 @@ export async function upsertFileNodeByHash(data: {
|
||||||
isFolder: false,
|
isFolder: false,
|
||||||
ownerId: data.ownerId,
|
ownerId: data.ownerId,
|
||||||
parentId: data.parentId || null,
|
parentId: data.parentId || null,
|
||||||
path: data.name, // Simplified for now
|
path: `/WebCalibre/Bulk/${data.name}`,
|
||||||
metadata: {}, // Placeholder for extractMetadata logic
|
metadata: {
|
||||||
|
type: fileExtension,
|
||||||
|
mimeType: "application/octet-stream"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return { node: newNode, mode: 'created' as const };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue