modification of FileNode to include hash-calculated hash for existing files
This commit is contained in:
parent
6a3470229e
commit
5dd5ec2792
12 changed files with 2399 additions and 135 deletions
369
backup/db-backup-26-02-14
Normal file
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
1162
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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");
|
||||
|
|
@ -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
|
||||
|
|
|
|||
58
scripts/backfill-hashes.ts
Normal file
58
scripts/backfill-hashes.ts
Normal 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
8
src/lib/hashing.ts
Normal 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');
|
||||
}
|
||||
|
|
@ -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 = {};
|
||||
|
|
|
|||
Loading…
Reference in a new issue