modification of FileNode to include hash-calculated hash for existing files

This commit is contained in:
stephen 2026-02-14 11:48:55 +11:00
parent 6a3470229e
commit 5dd5ec2792
12 changed files with 2399 additions and 135 deletions

369
backup/db-backup-26-02-14 Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1162
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
"backfill": "tsx scripts/backfill-hashes.ts",
"build": "next build",
"start": "next start",
"lint": "eslint",
@ -21,7 +22,7 @@
"@mui/material-nextjs": "^7.3.6",
"@mui/x-data-grid": "^8.24.0",
"@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0",
"@prisma/client": "7.4.0",
"epub": "^1.3.0",
"epub2": "^3.0.2",
"exif-reader": "^2.0.3",
@ -43,7 +44,8 @@
"dotenv-cli": "^11.0.0",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"prisma": "^6.19.2",
"prisma": "7.4.0",
"tsx": "^4.21.0",
"typescript": "^5"
}
}

View file

@ -1,13 +1,13 @@
// prisma.config.ts
import { config } from "dotenv";
config({ path: ".env.local" });
import { defineConfig } from "prisma/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
datasource: {
// This is now the ONLY place where the DB connection string is defined
// prisma.config.ts
url: process.env.DATABASE_URL!,
// This is where Prisma 7 looks for the connection string
url: env("DATABASE_URL"),
},
});

View file

@ -1,20 +1,58 @@
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "public";
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"azureAdUserId" TEXT NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"displayName" TEXT,
"role" "Role" NOT NULL DEFAULT 'USER',
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"azureAdUserId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FileNode" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"size" BIGINT,
"hash" TEXT,
"isFolder" BOOLEAN NOT NULL DEFAULT false,
"oneDriveId" TEXT,
"path" TEXT NOT NULL,
@ -29,11 +67,17 @@ CREATE TABLE "FileNode" (
CONSTRAINT "FileNode_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_azureAdUserId_key" ON "User"("azureAdUserId");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "FileNode_oneDriveId_key" ON "FileNode"("oneDriveId");
@ -47,8 +91,15 @@ CREATE INDEX "FileNode_orderIndex_idx" ON "FileNode"("orderIndex");
-- CreateIndex
CREATE UNIQUE INDEX "FileNode_ownerId_path_key" ON "FileNode"("ownerId", "path");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FileNode" ADD CONSTRAINT "FileNode_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FileNode" ADD CONSTRAINT "FileNode_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "FileNode"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "FileNode" ADD CONSTRAINT "FileNode_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "FileNode"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -1,52 +0,0 @@
/*
Warnings:
- You are about to drop the column `displayName` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "displayName",
ADD COLUMN "emailVerified" TIMESTAMP(3),
ADD COLUMN "image" TEXT,
ADD COLUMN "name" TEXT,
ALTER COLUMN "azureAdUserId" DROP NOT NULL;
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[hash]` on the table `FileNode` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "FileNode_hash_key" ON "FileNode"("hash");

View file

@ -60,6 +60,7 @@ model FileNode {
id String @id
name String
size BigInt? // Preserved your BigInt size column
hash String? @unique // <--- Added for duplicate detection (MD5 or SHA-256)
isFolder Boolean @default(false)
oneDriveId String? @unique
path String

View file

@ -0,0 +1,58 @@
import 'dotenv/config';
import { prisma } from '../src/lib/prisma';
import * as nodeCrypto from 'crypto';
function generateFileHash(buffer: Buffer): string {
return nodeCrypto.createHash('md5').update(buffer).digest('hex');
}
/**
* Replace this with your actual OneDrive download logic!
*/
async function getFromOneDrive(oneDriveId: string): Promise<Buffer> {
// For now, let's pretend we downloaded it to test the DB update
// DELETE THESE 2 LINES when you add your real OneDrive fetch code:
console.log(` ⬇️ Downloading ${oneDriveId}...`);
return Buffer.from(`mock-data-for-${oneDriveId}`);
}
async function backfill() {
console.log('🏁 Starting backfill with Prisma Adapter...');
try {
const files = await prisma.fileNode.findMany({
where: { isFolder: false, hash: null },
});
console.log(`📂 Found ${files.length} files to process.`);
for (const file of files) {
try {
process.stdout.write(`Processing: ${file.name}... `);
// 1. Get the file content
const buffer = await getFromOneDrive(file.oneDriveId!);
// 2. Generate the hash
const hash = generateFileHash(buffer);
// 3. Update the database
await prisma.fileNode.update({
where: { id: file.id },
data: { hash: hash }
});
console.log(`✅ Success! (Hash: ${hash.substring(0, 8)}...)`);
} catch (err) {
console.log(`❌ Failed: ${err instanceof Error ? err.message : err}`);
}
}
} catch (error) {
console.error('🚨 Script Error:', error);
} finally {
await prisma.$disconnect();
console.log('🏁 Finished.');
}
}
backfill();

8
src/lib/hashing.ts Normal file
View file

@ -0,0 +1,8 @@
import crypto from 'crypto';
/**
* Generates an MD5 hash from a file buffer.
*/
export function generateFileHash(buffer: Buffer): string {
return crypto.createHash('md5').update(buffer).digest('hex');
}

View file

@ -61,7 +61,7 @@ export async function extractMetadata(buffer: Buffer, filename: string): Promise
}
// --- 2. IMAGE EXTRACTION ---
if (['jpg', 'jpeg', 'png', 'webp'].includes(extension || '')) {
if (['jpg', 'jpeg', 'png', 'webp','heic'].includes(extension || '')) {
const image = sharp(buffer);
const metadata = await image.metadata();
let details = {};