From 71b567ad0494aad87610b1fe02fe13187c59b34b Mon Sep 17 00:00:00 2001 From: stephen Date: Sun, 1 Feb 2026 21:37:02 +1100 Subject: [PATCH] not full extracting metadata --- docs/notes_tmp.html | 2873 ------------------------------ src/app/dashboard/actions.ts | 127 +- src/data-access/file-nodes.ts | 20 - src/lib/metadata-extractor.ts | 76 +- src/services/metadata-service.ts | 23 + src/services/onedrive.ts | 57 +- 6 files changed, 109 insertions(+), 3067 deletions(-) delete mode 100644 docs/notes_tmp.html create mode 100644 src/services/metadata-service.ts diff --git a/docs/notes_tmp.html b/docs/notes_tmp.html deleted file mode 100644 index bd3dd6f..0000000 --- a/docs/notes_tmp.html +++ /dev/null @@ -1,2873 +0,0 @@ - - - -notes.md - - - - - - - - - - - - - -

1. Reference

-

1.1. Next.js Authentication Made Easy with Microsoft Entra ID

-

Next.js Authentication Made Easy with Microsoft Entra ID

-

1.2. Next.js 16 Middleware DEPRECATED - Authentication In Proxy Or Data Access Layer?

-

Next.js 16 Middleware DEPRECATED - Authentication In Proxy Or Data Access Layer?

- -

Overview of user consent and how to manage it in Microsoft Entra | Microsoft

-

1.4. Add Tailwind CSS to an Existing Next js Project

-

Add Tailwind CSS to an Existing Next js Project

-

1.5. How to style Material UI component with Tailwindcss in React project

-

How to style Material UI component with Tailwindcss in React project

-

1.6. How to Use Prisma 7 in Next.js

-

How to Use Prisma 7 in Next.js

-

2. Project Structure

-

The following is the desire structure

-
web-calibre/ -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ app/ # All Routes (Pages & APIs) -β”‚ β”‚ β”œβ”€β”€ api/ # API ROUTES -β”‚ β”‚ β”‚ β”œβ”€β”€ auth/ # NextAuth routes [...nextauth] -β”‚ β”‚ β”‚ └── upload/ # Custom API for OneDrive sessions -β”‚ β”‚ β”œβ”€β”€ dashboard/ # Protected user area -β”‚ β”‚ β”œβ”€β”€ layout.tsx # Global Layout -β”‚ β”‚ └── page.tsx # Landing Page -β”‚ β”œβ”€β”€ components/ # MUI Components (FileTable, Navbar) -β”‚ β”œβ”€β”€ lib/ # Config (Prisma, Theme, Auth) -β”‚ β”‚ β”œβ”€β”€ prisma.ts -β”‚ β”‚ └── theme.ts <-- MUI Theme Defined Here -β”‚ └── middleware.ts <-- Protected Routes Logic -β”œβ”€β”€ prisma/ # DB Schema -└── .env # Secrets -
-

3. App Registrations

-

WebCalibre2 -Application (client) ID = 'b549931a-f491-436b-b7bc-d37d8ca3c17e'

-

Object ID ='0b8256a2-e2ac-472b-896a-4b5845bc32fe'

-

Directory (tenant) ID = '1c06ce7c-7884-4796-8652-d4c32d75a5d0'

-

Expires = 1/3/2028

-

Value='O3p8Q~oMph-0kSLwkvzEJzJdx_iHGOjJjrr5Pa1A'

-

Secret ID='6a242be1-c711-4cc3-a132-03c1f57993ed'

-

WebCalibre2 | Certificates & secrets

-

Since you are using NextAuth.js, the URL must follow a very specific pattern.

-

For local development: http://localhost:3000/api/auth/callback/azure-ad

-

For production: https://your-domain.com/api/auth/callback/azure-ad

-

Note: Replace azure-ad with whatever ID you give the provider in your code. By default, in NextAuth, it is usually azure-ad.

-

WebCalibre3

- - - - - - - - - - - - - - - - - - - - - -
Display nameWebCalibre3
Application (client) IDa95ef644-cb9d-465c-8dae-f822a78a8ac3
Object ID1e1f2bda-661c-4ad0-9aae-82313c603076
Directory (tenant) ID.1c06ce7c-7884-4796-8652-d4c32d75a5d0
-

Client credentials

- - - - - - - - - - - - - - - - - -
DescriptionExpiresValueSecret ID
WebCalibre31/5/2028VZ48Q~EOsgZcgBI25cc0zys.h9zM7hN9I7DhZdnWd647cf28-80ed-45fb-a89e-e2bc323a40e9
-

3.1. Permissions Needed

-

Permissions Needed

-

4. Steps

-

4.1. create nextjs app

-

The following command creates the nextjs folder structure

-

Yes, Next.js has a specific syntax to create a project in your current directory instead of creating a new subfolder.

-

To do this, navigate to your empty project folder in the terminal and run:

-
-npx create-next-app@latest . -
-

Important Details:

-
    -
  1. -

    The Dot (.): This tells the installer to use the current directory as the project root.

    -
  2. -
  3. -

    Naming Conflict: If the folder name contains capital letters (e.g., WebCalibre), the command will fail with an error. Next.js requires the root folder name to be all lowercase and URL-friendly. You must rename your folder to web-calibre before running the command. -This caused me rename to all lower case.

    -
  4. -
-

3.Empty Folder: The directory must be empty (except for hidden files like .git). If you already have files in there, the installer will stop to prevent overwriting your work.

-

What happens next? -Even though you are using the current directory, the interactive prompt will still ask you:

-

"What is your project named?" You can just hit Enter to accept the current folder name.

-

Configuration: It will then ask you about TypeScript, ESLint, Tailwind CSS, the src/ directory, and the App Router.

-

Based on our previous discussions, I recommend selecting "Yes" for TypeScript, ESLint, Tailwind, the src/ directory, and the App Router to match the professional structure we planned for WebCalibre.

-

4.2. Create .env file

-

In Next.js, the distinction between .env and .env.local is primarily about security and overriding defaults. -While both store key-value pairs, they are handled differently by Git and the Next.js runtime.

-
    -
  1. Security & Version Control (Git)The most important reason is that .env.local is automatically ignored by Git in standard Next.js projects.
  2. -
- -
    -
  1. Priority & Overriding -Next.js uses a "hierarchy" of loading.1 If you define the same variable in both files, .env.local wins. -This allows you to set a generic placeholder in .env for the team, but use your specific local developer settings on your own machine. -For example:
  2. -
- -

The Next.js Loading Order -Next.js looks for variables in this specific order (from highest priority to lowest):

-
    -
  1. process.env (System environment variables)
  2. -
  3. .env.development.local (Only during npm run dev)
  4. -
  5. .env.local (Available in all environments except test)
  6. -
  7. .env.development
  8. -
  9. .env (The final fallback)
  10. -
-
DATABASE_URL="postgresql://stephen:Web2025$$@192.168.1.210:5432/webcalibre2" - -# Credentials for your Microsoft App (NextAuth) -AZURE_AD_CLIENT_ID="549931a-f491-436b-b7bc-d37d8ca3c17e" -AZURE_AD_CLIENT_SECRET="6a242be1-c711-4cc3-a132-03c1f57993ed" -AZURE_AD_TENANT_ID="1c06ce7c-7884-4796-8652-d4c32d75a5d0" -
-

Summary Comparison Table

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Feature.env.env.local
PurposeShared defaults/constantsPrivate secrets & local overrides
Committed to Git?YesNo (Added to .gitignore)
Sensitive Data?NEVERYES (API Keys, Secrets)
ScopeAll developers/environmentsJust your machine
-

**Best Practice Tip: ** Since .env.local isn't shared on GitHub, it’s a good idea to create a file named .env.example in your project. This file should contain the names of the keys (e.g., AZURE_AD_CLIENT_SECRET=) but leave the values blank, so other developers know which variables they need to create on their own machines.

-

4.3. Configure Git

-

Nextjs initializes git by default. But is doesn't add some configs that I do

-

git config --global push.followTags true

-

git remote add origin "/Users/stephenlohning/Library/CloudStorage/OneDrive-Personal/Documents/10_GIT_Repositories/124_WebCalibre2.git"

-

git remote -v -origin /Users/stephenlohning/Library/CloudStorage/OneDrive-Personal/Documents/10_GIT_Repositories/124_WebCalibre2.git (fetch) -origin /Users/stephenlohning/Library/CloudStorage/OneDrive-Personal/Documents/10_GIT_Repositories/124_WebCalibre2.git (push)

-

4.4. Test nextjs

-
npm run dev - -> 124_webcalibre2@0.1.0 dev -> next dev - -β–² Next.js 16.1.1 (Turbopack) -- Local: http://localhost:3000 -- Network: http://192.168.1.100:3000 -- Environments: .env - -βœ“ Starting... -βœ“ Ready in 648ms -
-

If it working proceed

-

4.5. Check Authentication

-

I ran through the Video:-

-

Next.js Authentication Made Easy with Microsoft Entra ID

-

4.6. Install NextAuth.js v5

-

npm install next-auth@beta

-

Setup Environment -The only environment variable that is mandatory is the AUTH_SECRET. This is a random value used by the library to encrypt tokens and email verification hashes. (See Deployment to learn more). You can generate one via the official Auth.js CLI running:

-

npx auth secret

-

created .env.local

-
AUTH_SECRET="7cz3Z4kUI2kB3mdAPo58iioUDLSRJ92X8+boEixvO8k=" # Added by `npx auth`. Read more: https://cli.authjs.dev -
-

5. .env files

-

5.1. .env.local

-
# values generated by Gemini -# Generated for security -AUTH_SECRET="7cz3Z4kUI2kB3mdAPo58iioUDLSRJ92X8+boEixvO8k=" - -# Use 'common' for multi-tenant (Work + Personal) support -AUTH_MICROSOFT_ENTRA_ID_TENANT_ID="common" - -# The Application (client) ID -AUTH_MICROSOFT_ENTRA_ID_ID="b549931a-f491-436b-b7bc-d37d8ca3c17e" - -# The Client Secret VALUE (ensure no leading '6' or spaces) -AUTH_MICROSOFT_ENTRA_ID_SECRET="O3p8Q~oMph-0kSLwkvzEJzJdx_iHGOjJjrr5Pa1A" - -# Required to tell Auth.js to trust your localhost/proxy URL -AUTH_TRUST_HOST=true -
-

5.2. .env

-
DATABASE_URL="postgresql://stephen:Web2025$$@192.168.1.210:5432/webcalibre2" - -# Credentials for your Microsoft App (NextAuth) -AZURE_AD_CLIENT_ID="549931a-f491-436b-b7bc-d37d8ca3c17e" -AZURE_AD_CLIENT_SECRET="6a242be1-c711-4cc3-a132-03c1f57993ed" -AZURE_AD_TENANT_ID="1c06ce7c-7884-4796-8652-d4c32d75a5d0" -
-

6. Prisma Install

-

To get Prisma fully installed and ready to talk to your PostgreSQL database, you need to install the CLI (for migrations) and the Client (for your code to query the database).

-

Run these three commands in your terminal:

-
    -
  1. Install the Prisma packages -Bash
  2. -
-

6.1. The CLI is for development tasks like migrations

-

npm install prisma --save-dev

-

##The Client is used by your Next.js code to talk to the DB

-
npm install @prisma/client -
-
    -
  1. Install the Dotenv helper -Since you are keeping your DATABASE_URL in .env.local (and your password has special characters), you need this to bridge the gap between Next.js and the Prisma CLI:
  2. -
-
-npm install dotenv-cli --save-dev -
-
    -
  1. Initialize and Generate -Once the packages are installed, you need to initialize the Prisma folder (if you haven't already) and generate the types for your FileNode model:
  2. -
-

Bash

-

6.2. Create the prisma folder

-
npx prisma init -
-

7. Run this whenever you change your schema.prisma file

-

npx dotenv -e .env.local -- npx prisma generate -4. Apply your Schema to PostgreSQL -Finally, run this to actually create the tables in your webcalibre2 database:

-

npx dotenv -e .env.local -- npx prisma migrate dev --name init_database

-

7.1. Pro-Tip: Update your package.json

-

To avoid typing that long dotenv command every time, open your package.json and add this to the "scripts" section:

-

JSON

-

"scripts": { -"db:migrate": "dotenv -e .env.local -- prisma migrate dev", -"db:studio": "dotenv -e .env.local -- prisma studio" -} -Now, you can just run npm run db:migrate whenever you update your schema!

-

8. Where we are up to 8/1/2026

-

We can authenticate with MS Azure, upload a file and store it on my personal OneDrive. -I can see a few problems,

-
    -
  1. -

    if the file gets stored with its original filename, if I store the same file again it tells that it fail, but in reality it did not fail the it got renamed with 1.pdf added this is not added data base.

    -
  2. -
  3. -

    We may be better to create a folder and put the file in the folder, to me the logical name would id of the file, but that is automatically create and assigned when the data is stored as a UUID.

    -
  4. -
  5. -

    We would have to create the UUID ourselves, so that we can create a folder with that specific UUID and store the file in the folder.

    -
  6. -
  7. -

    The Upload file Page needs to be able to create folder as well select a parent , add a description of either the file

    -
  8. -
-

9. Testing Creating And Uploading Folders and Files

-

The functionality works may not the best user interface

-

Projects ID 0371fdfe-a2d1-4f25-b229-804f299bc064 -Project-1 ID c8eebe0e-6e90-4cfb-8e44-c05f7064f3b1 parentID 0371fdfe-a2d1-4f25-b229-804f299bc064 -AS-NZS3000-2018.pdf parentID c8eebe0e-6e90-4cfb-8e44-c05f7064f3b1

-

Now we will go now and add metadata

-

Now we have the metadata along with some great filters on the data board page

-

Now we are going to implement an an update of a file or folder stored. 12/1/2026

-

For download there 2 function we need to implement

-
    -
  1. -

    down load the file to ~/user/Downloads, this should work for any file type including pdf

    -
  2. -
  3. -

    if the file is a pdf open the file in another tab in the browser as most browser support reading a pdf

    -
  4. -
-

For download there 2 function we need to implement

-
    -
  1. -

    down load the file to ~/user/Downloads, this should work for any file type including pdf

    -
  2. -
  3. -

    if the file is a pdf open the file in another tab in the browser as most browser support reading a pdf

    -
  4. -
-

So we need to implement 2 new actions, downLoad file and open pdf to read. Does this make sense ?

-

10. πŸ“š WebCalibre Development Notes 17/1/2026

-

10.1. πŸš€ Project Overview

-

A Next.js (App Router) dashboard for managing a OneDrive-synced file library. -Uses Prisma + PostgreSQL for metadata storage and MUI X v8 (DataGrid) for the frontend.

-
-

10.2. πŸ›  Critical Technical Learnings

-

10.2.1. MUI X v8 DataGrid Toolbar Fix

- - - -

10.2.3. Server-Side Extraction

- -
-

10.3. πŸ“‘ Essential Commands

- - - - - - - - - - - - - - - - - - - - - - - - - -
CommandPurpose
npx tsc --noEmitRun this frequently. Validates types across the entire project. Finds errors your IDE might miss.
npx prisma generateUpdates the Prisma Client types after schema changes.
npx prisma db pushPushes schema changes to the PostgreSQL database.
npm run devStarts the development server.
-
-

10.4. πŸ“… Future Roadmap

-

10.4.1. Phase 1: Metadata Utility (In Progress)

- -

10.4.2. Phase 2: UI Enhancements

- -
-

10.5. πŸ’‘ Reminders

- -

1.AUIAfM4GHIR4lkeGUtTDLXWl0BqTSbWR9GtDt7zTfYyjwX6kAElCAA.BQABAwIAAAADAOz_BQD0_yyFlWKgjcwr_ed5f58Tdi53aj4fqbWRmjTI7vE5M94UQZT5SZLBxnTN7Psa7w-KJCMfY3AAzGgJzfOXOHGXSySvCzC6IlHwXL7uXLZYf84jdhgNEP2F1E1nz9ecmsG4tfjvMzwNPsh9nlY48Cz_qh_KDLu2odNesttqW7Om3MxsXLakskvMXynX07lJT6gbXT9OMoFk_F3M4Dz9DoYJD6CACZPdKaYqMeesfPt-B6Yj7tq1q9L4RUc6Fbrsew9chMqg1vurMlggcX1aBU7LF6JP50B3IkOB4qXrGDJtxM61gF64EXRay5UYVtWMh09et7SX4FcLZlHTyxpsvjSxaKxfpY9DFY2y0dhp0Ce5dZXoK7pplQwcxsv-dCeZmk4A3dqvAdxM6p0XNrowlOawpAlWqHoIC1ZmESDpyxueP0rlpfD9h3wkWcVAG9xOS_d-W9Z3y1G9PgBycOysvQTufueLs4PCjjId9-5AI4UaecQyUtpRCJnOxp23_e5ROLduihcDMrV87D8_ZN6nl49jFqr7kQZHbLanuVht18vSUyk31c9OtkFmC4p48_1xoIe5sYlPjoUd9Misgef7EziwdzapRyr649kb4Em_XMmJf_Y5zVDpT6NXRqlQAw3AhP53Px12hQsGsFkd0kzDBuwUSrKlda5E2lom4Wg5uXwb4zLD5DA6xwKfxBrAYICZT8QQYG5fTHjSP_2t9gs_K9-1-7UqKTb2VWGYf25QVnhgBXL1KbzosmFBOKjr31XjYo8IbgMjuEDGP-YfmyYOvmRg2Lx-VNlu8gTPcdSsdIivFSvEvHdUmIeReHMIdKjr0ZgVYwTtzG7QBs-8UXRvNo6IWecZnHEjx0QSLztyNXicgeOmT4gKDeJ0YT6rkWkNg_B8fYpx3j11ceRKpgQzm2E7UalGgBsbAHA1a4jnM7NrDv4TY_A_LRTabHPVrHpQWZ2HOJi5UlTQLv8xbiiJf94HVlxwncJd6YPy7ARjhvIA5nE5IiZJ8ZSUFwg4waCFibV-NDqiglo7YBCfx3DOrLsNjuufYQnjog_IDi6ac-DG7ulKS01nmAq7WfvshiW_mXLmwtPVusAcmLcfD-40B3kh12Cv-_EXSzfaVPP7M1Obi13TyoQDeVl-pMJuEC68xHzqA78SmZR8WluDRAJ5kOLBggvPbuHRFpkqtvFG8sGNzOqiBkq30qUErfk2Ao-SnHAh3fa3CAM2aYf3LdzXHh2_qGoVnrKdMfnUI14H6vrTxxBpDUinYejr8rLzbjH6tjDYWvjen5-BrDzOjazI7_ctS8zdLugG093kbWlYDW26FgYnKtdPCi-7F-emm8HGD82V_5e7Jc1qVs1G_D5eu0ohb_B1ky_AmxgCE8oL9Bdx1ehHnijNCarubf_MWMcloAv0XNxvrU4PoXdb7PgVBpYsKRT7tdnOGpokQj2n2xQSHvQVypd8xkHYloczdlmi9zvytM_Da8jnLdE0Td3mlnI-jlovrbzykD4YhS_ZcwIc_8c_WiHDeC3yAeTTx0EhuNh89HFSsmfod2BBaRgvf5SRpNwUjxd0PROvrGD_Hl0emtsC9YWdfULQBk3SKqJMV0x7Zv_Erj8JZoMzXbe8jv5bM4AmYqOPqznkXtAWAkedqLVYug0od9AgMIn-FFPpH4AvmHhhliMevIw74yEXCj28dxlHBZvyyqpbEwp8vr_kTjarjyp3s1p6QsYEoQ82vx-nQPiVFwWeGDsJcSmUD4FbtVIVCGY3p2cCtZHyRRWAK2LGgO32v3DhK7FSLKV-3f0LAafqdpZyGnqoF7pwn_RcpZz8l9tGctFl8Xd50Y1naLz8F08SlbJ197xdjzHLQ8r4jsVn5m9BkGbR0Kv3olXGRmtr5dkqGI-68_FCjkkqpwfOHg

-

1.AUIAfM4GHIR4lkeGUtTDLXWl0BqTSbWR9GtDt7zTfYyjwX6kAElCAA.BQABAwIAAAADAOz_BQD0_yyFlWKgjcwr_ed5f58Tdi53aj4fqbWRmjTI7vE5M94UQZT5SZLBxnTN7Psa7w-KJCMfY3AAzGgJzfOXOHGXSySvCzC6IlHwXL7uXLZYf84jdhgNEP2F1E1nz9ecmsG4tfjvMzwNPsh9nlY48Cz_qh_KDLu2odNesttqW7Om3MxsXLakskvMXynX07lJT6gbXT9OMoFk_F3M4Dz9DoYJD6CACZPdKaYqMeesfPt-B6Yj7tq1q9L4RUc6Fbrsew9chMqg1vurMlggcX1aBU7LF6JP50B3IkOB4qXrGDJtxM61gF64EXRay5UYVtWMh09et7SX4FcLZlHTyxpsvjSxaKxfpY9DFY2y0dhp0Ce5dZXoK7pplQwcxsv-dCeZmk4A3dqvAdxM6p0XNrowlOawpAlWqHoIC1ZmESDpyxueP0rlpfD9h3wkWcVAG9xOS_d-W9Z3y1G9PgBycOysvQTufueLs4PCjjId9-5AI4UaecQyUtpRCJnOxp23_e5ROLduihcDMrV87D8_ZN6nl49jFqr7kQZHbLanuVht18vSUyk31c9OtkFmC4p48_1xoIe5sYlPjoUd9Misgef7EziwdzapRyr649kb4Em_XMmJf_Y5zVDpT6NXRqlQAw3AhP53Px12hQsGsFkd0kzDBuwUSrKlda5E2lom4Wg5uXwb4zLD5DA6xwKfxBrAYICZT8QQYG5fTHjSP_2t9gs_K9-1-7UqKTb2VWGYf25QVnhgBXL1KbzosmFBOKjr31XjYo8IbgMjuEDGP-YfmyYOvmRg2Lx-VNlu8gTPcdSsdIivFSvEvHdUmIeReHMIdKjr0ZgVYwTtzG7QBs-8UXRvNo6IWecZnHEjx0QSLztyNXicgeOmT4gKDeJ0YT6rkWkNg_B8fYpx3j11ceRKpgQzm2E7UalGgBsbAHA1a4jnM7NrDv4TY_A_LRTabHPVrHpQWZ2HOJi5UlTQLv8xbiiJf94HVlxwncJd6YPy7ARjhvIA5nE5IiZJ8ZSUFwg4waCFibV-NDqiglo7YBCfx3DOrLsNjuufYQnjog_IDi6ac-DG7ulKS01nmAq7WfvshiW_mXLmwtPVusAcmLcfD-40B3kh12Cv-_EXSzfaVPP7M1Obi13TyoQDeVl-pMJuEC68xHzqA78SmZR8WluDRAJ5kOLBggvPbuHRFpkqtvFG8sGNzOqiBkq30qUErfk2Ao-SnHAh3fa3CAM2aYf3LdzXHh2_qGoVnrKdMfnUI14H6vrTxxBpDUinYejr8rLzbjH6tjDYWvjen5-BrDzOjazI7_ctS8zdLugG093kbWlYDW26FgYnKtdPCi-7F-emm8HGD82V_5e7Jc1qVs1G_D5eu0ohb_B1ky_AmxgCE8oL9Bdx1ehHnijNCarubf_MWMcloAv0XNxvrU4PoXdb7PgVBpYsKRT7tdnOGpokQj2n2xQSHvQVypd8xkHYloczdlmi9zvytM_Da8jnLdE0Td3mlnI-jlovrbzykD4YhS_ZcwIc_8c_WiHDeC3yAeTTx0EhuNh89HFSsmfod2BBaRgvf5SRpNwUjxd0PROvrGD_Hl0emtsC9YWdfULQBk3SKqJMV0x7Zv_Erj8JZoMzXbe8jv5bM4AmYqOPqznkXtAWAkedqLVYug0od9AgMIn-FFPpH4AvmHhhliMevIw74yEXCj28dxlHBZvyyqpbEwp8vr_kTjarjyp3s1p6QsYEoQ82vx-nQPiVFwWeGDsJcSmUD4FbtVIVCGY3p2cCtZHyRRWAK2LGgO32v3DhK7FSLKV-3f0LAafqdpZyGnqoF7pwn_RcpZz8l9tGctFl8Xd50Y1naLz8F08SlbJ197xdjzHLQ8r4jsVn5m9BkGbR0Kv3olXGRmtr5dkqGI-68_FCjkkqpwfOHg

-

1.AUIAfM4GHIR4lkeGUtTDLXWl0BqTSbWR9GtDt7zTfYyjwX6kAElCAA.BQABAwIAAAADAOz_BQD0_yyFlWKgjcwr_ed5f58Tdi53aj4fqbWRmjTI7vE5M94UQZT5SZLBxnTN7Psa7w-KJCMfY3AAzGgJzfOXOHGXSySvCzC6IlHwXL7uXLZYf84jdhgNEP2F1E1nz9ecmsG4tfjvMzwNPsh9nlY48Cz_qh_KDLu2odNesttqW7Om3MxsXLakskvMXynX07lJT6gbXT9OMoFk_F3M4Dz9DoYJD6CACZPdKaYqMeesfPt-B6Yj7tq1q9L4RUc6Fbrsew9chMqg1vurMlggcX1aBU7LF6JP50B3IkOB4qXrGDJtxM61gF64EXRay5UYVtWMh09et7SX4FcLZlHTyxpsvjSxaKxfpY9DFY2y0dhp0Ce5dZXoK7pplQwcxsv-dCeZmk4A3dqvAdxM6p0XNrowlOawpAlWqHoIC1ZmESDpyxueP0rlpfD9h3wkWcVAG9xOS_d-W9Z3y1G9PgBycOysvQTufueLs4PCjjId9-5AI4UaecQyUtpRCJnOxp23_e5ROLduihcDMrV87D8_ZN6nl49jFqr7kQZHbLanuVht18vSUyk31c9OtkFmC4p48_1xoIe5sYlPjoUd9Misgef7EziwdzapRyr649kb4Em_XMmJf_Y5zVDpT6NXRqlQAw3AhP53Px12hQsGsFkd0kzDBuwUSrKlda5E2lom4Wg5uXwb4zLD5DA6xwKfxBrAYICZT8QQYG5fTHjSP_2t9gs_K9-1-7UqKTb2VWGYf25QVnhgBXL1KbzosmFBOKjr31XjYo8IbgMjuEDGP-YfmyYOvmRg2Lx-VNlu8gTPcdSsdIivFSvEvHdUmIeReHMIdKjr0ZgVYwTtzG7QBs-8UXRvNo6IWecZnHEjx0QSLztyNXicgeOmT4gKDeJ0YT6rkWkNg_B8fYpx3j11ceRKpgQzm2E7UalGgBsbAHA1a4jnM7NrDv4TY_A_LRTabHPVrHpQWZ2HOJi5UlTQLv8xbiiJf94HVlxwncJd6YPy7ARjhvIA5nE5IiZJ8ZSUFwg4waCFibV-NDqiglo7YBCfx3DOrLsNjuufYQnjog_IDi6ac-DG7ulKS01nmAq7WfvshiW_mXLmwtPVusAcmLcfD-40B3kh12Cv-_EXSzfaVPP7M1Obi13TyoQDeVl-pMJuEC68xHzqA78SmZR8WluDRAJ5kOLBggvPbuHRFpkqtvFG8sGNzOqiBkq30qUErfk2Ao-SnHAh3fa3CAM2aYf3LdzXHh2_qGoVnrKdMfnUI14H6vrTxxBpDUinYejr8rLzbjH6tjDYWvjen5-BrDzOjazI7_ctS8zdLugG093kbWlYDW26FgYnKtdPCi-7F-emm8HGD82V_5e7Jc1qVs1G_D5eu0ohb_B1ky_AmxgCE8oL9Bdx1ehHnijNCarubf_MWMcloAv0XNxvrU4PoXdb7PgVBpYsKRT7tdnOGpokQj2n2xQSHvQVypd8xkHYloczdlmi9zvytM_Da8jnLdE0Td3mlnI-jlovrbzykD4YhS_ZcwIc_8c_WiHDeC3yAeTTx0EhuNh89HFSsmfod2BBaRgvf5SRpNwUjxd0PROvrGD_Hl0emtsC9YWdfULQBk3SKqJMV0x7Zv_Erj8JZoMzXbe8jv5bM4AmYqOPqznkXtAWAkedqLVYug0od9AgMIn-FFPpH4AvmHhhliMevIw74yEXCj28dxlHBZvyyqpbEwp8vr_kTjarjyp3s1p6QsYEoQ82vx-nQPiVFwWeGDsJcSmUD4FbtVIVCGY3p2cCtZHyRRWAK2LGgO32v3DhK7FSLKV-3f0LAafqdpZyGnqoF7pwn_RcpZz8l9tGctFl8Xd50Y1naLz8F08SlbJ197xdjzHLQ8r4jsVn5m9BkGbR0Kv3olXGRmtr5dkqGI-68_FCjkkqpwfOHg

-

-
export async function getMetadataPreviewAction(fileId: string) { - const session = await auth(); - if (!session?.user?.id) throw new Error("Unauthorized"); - - console.log(`\n--- πŸ” Magic Fill Started for File ID: ${fileId} ---`); - - try { - const node = await getFileNodeById(fileId); - if (!node || !node.oneDriveId) throw new Error("File not found"); - - // 1. Get the token from our utility - const token = await getFreshAccessToken(session.user.id); - - // LOG: Just check the length to be sure it's not empty - console.log(`πŸ”‘ Token retrieved (Length: ${token.length})`); - - console.log(`πŸ“‘ Fetching binary for: ${node.name}...`); - - // 2. Fetch the content from Microsoft Graph - const response = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': '*/*' - }, - } - ); - - if (!response.ok) { - // If it fails here, we'll see the real reason from Microsoft - const errorText = await response.text(); - console.error("❌ Microsoft Graph Error Response:", errorText); - throw new Error(`OneDrive Download Failed: ${response.status}`); - } - - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - console.log(`πŸ“¦ Success! Downloaded ${buffer.length} bytes.`); - - // 3. Extract Metadata - const extractedData = await extractMetadata(buffer, node.name); - - console.log("βœ… RAW DATA EXTRACTED:"); - console.dir(extractedData, { depth: null, colors: true }); - - return { success: true, data: extractedData }; - - } catch (error: any) { - console.error("❌ Magic Fill Error:", error.message); - return { success: false, error: error.message }; - } -} -
-

Dump of metadata for an image

-
npm run dev - -> 124_webcalibre2@0.1.0 dev -> next dev - -β–² Next.js 16.1.1 (Turbopack) -- Local: http://localhost:3000 -- Network: http://192.168.1.100:3000 -- Environments: .env.local, .env -- Experiments (use with caution): - Β· serverActions - -βœ“ Starting... -βœ“ Ready in 380ms -βœ… Prisma 7 connected to PostgreSQL successfully - GET /dashboard 200 in 643ms (compile: 139ms, proxy.ts: 115ms, render: 388ms) -βœ… Prisma 7 connected to PostgreSQL successfully - GET /update/d3ccd68a-0493-487d-9273-5fa8ff6a6f9a 200 in 329ms (compile: 274ms, proxy.ts: 19ms, render: 37ms) - ---- πŸ” Magic Fill Started for File ID: d3ccd68a-0493-487d-9273-5fa8ff6a6f9a --- -πŸ”„ Access token expired. Refreshing for user: cc6f11ff-549d-40fa-883f-7886ee6487cf -βœ… Prisma 7 connected to PostgreSQL successfully -πŸ”‘ Token retrieved (Length: 1484) -πŸ“‘ Fetching binary for: IMG_2360.jpeg... -πŸ“¦ Success! Downloaded 1233596 bytes. -πŸ“Έ FULL RAW EXIF DATA: { - "bigEndian": true, - "Image": { - "Make": "Apple", - "Model": "iPhone 14", - "Orientation": 1, - "XResolution": 72, - "YResolution": 72, - "ResolutionUnit": 2, - "Software": "18.6.2", - "DateTime": "2025-10-03T08:02:39.000Z", - "HostComputer": "iPhone 14", - "YCbCrPositioning": 1, - "ExifTag": 228, - "GPSTag": 2586 - }, - "Thumbnail": { - "Compression": 6, - "XResolution": 72, - "YResolution": 72, - "ResolutionUnit": 2, - "JPEGInterchangeFormat": 2990, - "JPEGInterchangeFormatLength": 3322 - }, - "Photo": { - "ExposureTime": 0.025, - "FNumber": 1.5, - "ExposureProgram": 2, - "ISOSpeedRatings": 500, - "ExifVersion": { - "type": "Buffer", - "data": [ - 48, - 50, - 51, - 50 - ] - }, - "DateTimeOriginal": "2025-10-03T08:02:39.000Z", - "DateTimeDigitized": "2025-10-03T08:02:39.000Z", - "OffsetTime": "+02:00", - "OffsetTimeOriginal": "+02:00", - "OffsetTimeDigitized": "+02:00", - "ComponentsConfiguration": { - "type": "Buffer", - "data": [ - 1, - 2, - 3, - 0 - ] - }, - "ShutterSpeedValue": 5.327015336217066, - "ApertureValue": 1.1699250021066825, - "BrightnessValue": -0.4198113650227582, - "ExposureBiasValue": 0, - "MeteringMode": 5, - "Flash": 16, - "FocalLength": 5.7, - "SubjectArea": [ - 2006, - 1506, - 2213, - 1327 - ], - "MakerNote": { - "type": "Buffer", - "data": [ - 65, - 112, - 112, - 108, - 101, - 32, - 105, - 79, - 83, - 0, - 0, - 1, - 77, - 77, - 0, - 49, - 0, - 1, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 15, - 0, - 2, - 0, - 7, - 0, - 0, - 2, - 0, - 0, - 0, - 2, - 96, - 0, - 3, - 0, - 7, - 0, - 0, - 0, - 104, - 0, - 0, - 4, - 96, - 0, - 4, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 5, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 149, - 0, - 6, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 152, - 0, - 7, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 8, - 0, - 10, - 0, - 0, - 0, - 3, - 0, - 0, - 4, - 200, - 0, - 12, - 0, - 10, - 0, - 0, - 0, - 2, - 0, - 0, - 4, - 224, - 0, - 13, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 39, - 0, - 14, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 16, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 20, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 12, - 0, - 22, - 0, - 7, - 0, - 0, - 0, - 72, - 0, - 0, - 4, - 240, - 0, - 23, - 0, - 16, - 0, - 0, - 0, - 1, - 0, - 0, - 5, - 56, - 0, - 25, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 32, - 2, - 0, - 26, - 0, - 2, - 0, - 0, - 0, - 6, - 0, - 0, - 5, - 64, - 0, - 31, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 32, - 0, - 2, - 0, - 0, - 0, - 37, - 0, - 0, - 5, - 70, - 0, - 33, - 0, - 10, - 0, - 0, - 0, - 1, - 0, - 0, - 5, - 107, - 0, - 35, - 0, - 9, - 0, - 0, - 0, - 2, - 0, - 0, - 5, - 115, - 0, - 37, - 0, - 16, - 0, - 0, - 0, - 1, - 0, - 0, - 5, - 123, - 0, - 38, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 3, - 0, - 39, - 0, - 10, - 0, - 0, - 0, - 1, - 0, - 0, - 5, - 131, - 0, - 43, - 0, - 2, - 0, - 0, - 0, - 37, - 0, - 0, - 5, - 139, - 0, - 45, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 30, - 150, - 0, - 46, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 47, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 48, - 0, - 48, - 0, - 10, - 0, - 0, - 0, - 1, - 0, - 0, - 5, - 176, - 0, - 54, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 42, - 0, - 55, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 4, - 0, - 58, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 59, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 60, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 4, - 0, - 63, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 64, - 0, - 7, - 0, - 0, - 0, - 74, - 0, - 0, - 5, - 184, - 0, - 65, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 67, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 68, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 69, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 70, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 74, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 2, - 0, - 77, - 0, - 10, - 0, - 0, - 0, - 1, - 0, - 0, - 6, - 2, - 0, - 78, - 0, - 7, - 0, - 0, - 0, - 121, - 0, - 0, - 6, - 10, - 0, - 79, - 0, - 7, - 0, - 0, - 0, - 43, - 0, - 0, - 6, - 131, - 0, - 82, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 11, - 0, - 83, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 1, - 0, - 85, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 88, - 0, - 9, - 0, - 0, - 0, - 1, - 0, - 0, - 7, - 3, - 0, - 0, - 0, - 0, - 239, - 1, - 205, - 1, - 169, - 1, - 135, - 1, - 103, - 1, - 73, - 1, - 45, - 1, - 19, - 1, - 251, - 0, - 228, - 0, - 207, - 0, - 188, - 0, - 172, - 0, - 158, - 0, - 146, - 0, - 136, - 0, - 54, - 2, - 17, - 2, - 232, - 1, - 193, - 1, - 155, - 1, - 118, - 1, - 85, - 1, - 55, - 1, - 26, - 1, - 0, - 1, - 231, - 0, - 209, - 0, - 189, - 0, - 173, - 0, - 160, - 0, - 147, - 0, - 125, - 2, - 89, - 2, - 47, - 2, - 2, - 2, - 216, - 1, - 175, - 1, - 137, - 1, - 99, - 1, - 64, - 1, - 33, - 1, - 4, - 1, - 233, - 0, - 210, - 0, - 190, - 0, - 173, - 0, - 159, - 0, - 8, - 3, - 217, - 2, - 170, - 2, - 120, - 2, - 65, - 2, - 13, - 2, - 218, - 1, - 171, - 1, - 125, - 1, - 83, - 1, - 44, - 1, - 10, - 1, - 235, - 0, - 210, - 0, - 188, - 0, - 170, - 0, - 252, - 2, - 242, - 2, - 210, - 2, - 170, - 2, - 131, - 2, - 81, - 2, - 30, - 2, - 230, - 1, - 175, - 1, - 124, - 1, - 77, - 1, - 36, - 1, - 255, - 0, - 225, - 0, - 199, - 0, - 178, - 0, - 69, - 1, - 93, - 1, - 121, - 1, - 168, - 1, - 208, - 1, - 206, - 1, - 193, - 1, - 168, - 1, - 135, - 1, - 107, - 1, - 69, - 1, - 32, - 1, - 251, - 0, - 220, - 0, - 196, - 0, - 176, - 0, - 183, - 0, - 185, - 0, - 182, - 0, - 183, - 0, - 181, - 0, - 182, - 0, - 175, - 0, - 179, - 0, - 192, - 0, - 183, - 0, - 178, - 0, - 169, - 0, - 158, - 0, - 143, - 0, - 139, - 0, - 131, - 0, - 173, - 0, - 179, - 0, - 173, - 0, - 185, - 0, - 187, - 0, - 231, - 0, - 145, - 1, - 164, - 1, - 147, - 1, - 107, - 1, - 38, - 1, - 157, - 0, - 139, - 0, - 166, - 0, - 169, - 0, - 152, - 0, - 36, - 0, - 55, - 0, - 64, - 0, - 58, - 0, - 57, - 0, - 55, - 0, - 56, - 0, - 50, - 0, - 45, - 0, - 42, - 0, - 37, - 0, - 27, - 0, - 25, - 0, - 23, - 0, - 30, - 0, - 43, - 0, - 10, - 0, - 11, - 0, - 12, - 0, - 14, - 0, - 19, - 0, - 23, - 0, - 25, - 0, - 24, - 0, - 23, - 0, - 21, - 0, - 20, - 0, - 18, - 0, - 17, - 0, - 16, - 0, - 16, - 0, - 16, - 0, - 6, - 0, - 7, - 0, - 7, - 0, - 9, - 0, - 11, - 0, - 9, - 0, - 12, - 0, - 12, - 0, - 15, - 0, - 13, - 0, - 13, - 0, - 12, - 0, - 11, - 0, - 10, - 0, - 9, - 0, - 10, - 0, - 4, - 0, - 5, - 0, - 6, - 0, - 6, - 0, - 7, - 0, - 9, - 0, - 9, - 0, - 9, - 0, - 9, - 0, - 9, - 0, - 10, - 0, - 9, - 0, - 9, - 0, - 6, - 0, - 7, - 0, - 7, - 0, - 4, - 0, - 4, - 0, - 5, - 0, - 4, - 0, - 5, - 0, - 6, - 0, - 6, - 0, - 6, - 0, - 7, - 0, - 8, - 0, - 8, - 0, - 8, - 0, - 6, - 0, - 5, - 0, - 5, - 0, - 5, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 6, - 0, - 6, - 0, - 4, - 0, - 5, - 0, - 4, - 0, - 4, - 0, - 5, - 0, - 4, - 0, - 4, - 0, - 5, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 5, - 0, - 5, - 0, - 4, - 0, - 5, - 0, - 3, - 0, - 5, - 0, - 6, - 0, - 7, - 0, - 6, - 0, - 5, - 0, - 4, - 0, - 5, - 0, - 5, - 0, - 4, - 0, - 4, - 0, - 4, - 0, - 5, - 0, - 3, - 0, - 3, - 0, - 4, - 0, - 4, - 0, - 5, - 0, - 98, - 112, - 108, - 105, - 115, - 116, - 48, - 48, - 212, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 85, - 102, - 108, - 97, - 103, - 115, - 85, - 118, - 97, - 108, - 117, - 101, - 89, - 116, - 105, - 109, - 101, - 115, - 99, - 97, - 108, - 101, - 85, - 101, - 112, - 111, - 99, - 104, - 16, - 1, - 19, - 0, - 1, - 62, - 142, - 239, - 162, - 85, - 109, - 18, - 59, - 154, - 202, - 0, - 16, - 0, - 8, - 17, - 23, - 29, - 39, - 45, - 47, - 56, - 61, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 9, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 63, - 255, - 255, - 84, - 169, - 0, - 0, - 168, - 221, - 0, - 0, - 54, - 115, - 0, - 9, - 142, - 158, - 255, - 255, - 244, - 153, - 0, - 2, - 69, - 230, - 0, - 0, - 0, - 91, - 0, - 0, - 0, - 128, - 0, - 0, - 0, - 81, - 0, - 0, - 1, - 0, - 98, - 112, - 108, - 105, - 115, - 116, - 48, - 48, - 95, - 16, - 28, - 65, - 88, - 118, - 67, - 67, - 48, - 55, - 76, - 49, - 117, - 79, - 73, - 113, - 48, - 73, - 98, - 101, - 51, - 76, - 121, - 112, - 112, - 98, - 98, - 86, - 43, - 108, - 113, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 39, - 0, - 0, - 0, - 0, - 0, - 80, - 32, - 4, - 113, - 55, - 53, - 48, - 110, - 0, - 55, - 67, - 65, - 54, - 53, - 50, - 48, - 68, - 45, - 67, - 50, - 49, - 55, - 45, - 52, - 56, - 68, - 49, - 45, - 56, - 48, - 49, - 67, - 45, - 50, - 69, - 49, - 57, - 54, - 69, - 66, - 66, - 68, - 52, - 65, - 50, - 0, - 0, - 0, - 249, - 65, - 0, - 1, - 39, - 154, - 0, - 0, - 1, - 55, - 16, - 0, - 0, - 27, - 0, - 0, - 0, - 0, - 0, - 0, - 20, - 142, - 0, - 2, - 96, - 195, - 0, - 0, - 19, - 224, - 51, - 67, - 53, - 51, - 52, - 69, - 66, - 69, - 45, - 52, - 65, - 53, - 51, - 45, - 52, - 55, - 69, - 70, - 45, - 57, - 49, - 55, - 65, - 45, - 66, - 55, - 56, - 53, - 57, - 55, - 51, - 55, - 50, - 53, - 57, - 52, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 98, - 112, - 108, - 105, - 115, - 116, - 48, - 48, - 212, - 1, - 2, - 3, - 4, - 5, - 6, - 6, - 7, - 81, - 51, - 81, - 49, - 81, - 50, - 81, - 48, - 16, - 0, - 34, - 0, - 0, - 0, - 0, - 16, - 1, - 8, - 17, - 19, - 21, - 23, - 25, - 27, - 32, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 34, - 0, - 2, - 38, - 42, - 0, - 0, - 19, - 59, - 98, - 112, - 108, - 105, - 115, - 116, - 48, - 48, - 210, - 1, - 2, - 3, - 4, - 81, - 49, - 81, - 50, - 16, - 3, - 162, - 5, - 10, - 210, - 6, - 7, - 8, - 9, - 83, - 50, - 46, - 49, - 83, - 50, - 46, - 50, - 35, - 64, - 157, - 225, - 32, - 96, - 0, - 0, - 0, - 35, - 64, - 124, - 160, - 0, - 0, - 0, - 0, - 0, - 210, - 6, - 7, - 11, - 12, - 35, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 35, - 64, - 72, - 0, - 0, - 0, - 0, - 0, - 0, - 8, - 13, - 15, - 17, - 19, - 22, - 27, - 31, - 35, - 44, - 53, - 58, - 67, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 13, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 76, - 98, - 112, - 108, - 105, - 115, - 116, - 48, - 48, - 16, - 0, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 10 - ] - }, - "SubSecTimeOriginal": "714", - "SubSecTimeDigitized": "714", - "FlashpixVersion": { - "type": "Buffer", - "data": [ - 48, - 49, - 48, - 48 - ] - }, - "ColorSpace": 65535, - "PixelXDimension": 4032, - "PixelYDimension": 3024, - "SensingMethod": 2, - "SceneType": { - "type": "Buffer", - "data": [ - 1 - ] - }, - "ExposureMode": 0, - "WhiteBalance": 0, - "FocalLengthIn35mmFilm": 26, - "SceneCaptureType": 0, - "LensSpecification": [ - 1.5399999618512084, - 5.699999809263318, - 1.5, - 2.4 - ], - "LensMake": "Apple", - "LensModel": "iPhone 14 back dual wide camera 5.7mm f/1.5", - "CompositeImage": 2 - }, - "GPSInfo": { - "GPSLatitudeRef": "N", - "GPSLatitude": [ - 42, - 42, - 22.91 - ], - "GPSLongitudeRef": "W", - "GPSLongitude": [ - 7, - 1, - 50.76 - ], - "GPSAltitudeRef": 0, - "GPSAltitude": 1265.3340248962656, - "GPSTimeStamp": [ - 6, - 2, - 39 - ], - "GPSSpeedRef": "K", - "GPSSpeed": 0, - "GPSImgDirectionRef": "T", - "GPSImgDirection": 147.2551498127341, - "GPSDestBearingRef": "T", - "GPSDestBearing": 147.2551498127341, - "GPSDateStamp": "2025:10:03", - "GPSHPositioningError": 4.748651528267963 - } -} -βœ… RAW DATA EXTRACTED: -{ - type: 'IMAGE (JPEG)', - dimensions: '4032x3024', - title: 'IMG_2360.jpeg' -} - POST /update/d3ccd68a-0493-487d-9273-5fa8ff6a6f9a 200 in 4.0s (compile: 9ms, proxy.ts: 19ms, render: 4.0s) -
-

Where the code should exist

-

Two questions

-
    -
  1. should the -// 2. Fetch the content from Microsoft Graph -const response = await fetch( -https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content, -{ -headers: { -'Authorization': Bearer ${token}, -'Accept': '/' -}, -} -); -Should this be in the services/onedrive.ts to be compliant with the Data Access Layer (DAL) that nextjs recomments?
  2. -
  3. where should this getMetadataFromJson(buffer: Buffer)Β  get placed , I think it should be placed in lib/metadata-extractor and exported from there, what do you think?
  4. -
-

Those are excellent architectural questions. You're moving from "making it work" to "making it scale," which is exactly where you want to be.

-

1. Where to put the fetch call?

-

Yes, it absolutely belongs in services/onedrive.ts.

-

Next.js (and general clean architecture) recommends a Data Access Layer (DAL) to decouple your business logic from the specific implementation of your data source.

-

Why this is better:

- -

Recommended structure for services/onedrive.ts:

-
export async function getOneDriveFileBuffer(oneDriveId: string, token: string): Promise<Buffer> { - const response = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${oneDriveId}/content`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': '*/*' - }, - } - ); - - if (!response.ok) { - throw new Error(`OneDrive download failed: ${response.statusText}`); - } - - const arrayBuffer = await response.arrayBuffer(); - return Buffer.from(arrayBuffer); -} - -
-
-

2. Where to put getMetadataFromJson?

-

lib/metadata-extractor.ts is the perfect spot.

-

In the Next.js ecosystem, the lib/ directory is the standard convention for "Internal Libraries" or "Utilities"β€”code that is purely functional, doesn't depend on React, and performs a specific technical task (like parsing a buffer).

-

Why this is the right move:

- -

Structure for lib/metadata-extractor.ts:

-
import exifReader from 'exif-reader'; - -// Define an interface for your metadata to keep your Json field typed -export interface FileMetadata { - deviceModel?: string; - capturedAt?: Date; - lat?: number; - lng?: number; - [key: string]: any; // Allows for other fields -} - -export function extractExifMetadata(buffer: Buffer): FileMetadata { - // logic goes here... -} - -
-
-

πŸ—οΈ How it looks all together

-

When you put these in their proper places, your API Route or Server Action becomes very readable "glue" code:

-
    -
  1. Get Token (Auth Service)
  2. -
  3. Download Buffer (services/onedrive.ts)
  4. -
  5. Extract Data (lib/metadata-extractor.ts)
  6. -
  7. Save to DB (Prisma / DAL)
  8. -
-

Would you like me to help you refine the internal logic of the extractExifMetadata function to ensure it handles the specific "Rational" numbers (fractions) that EXIF often uses for exposure and GPS?

- - - diff --git a/src/app/dashboard/actions.ts b/src/app/dashboard/actions.ts index 4658908..bcb5129 100644 --- a/src/app/dashboard/actions.ts +++ b/src/app/dashboard/actions.ts @@ -16,12 +16,14 @@ import { uploadToOneDrive } from "@/services/onedrive"; -import { getEnrichedMetadataFromCloud } from "@/data-access/file-nodes"; +//import { getEnrichedMetadataFromCloud } from "@/data-access/file-nodes"; import { getFreshAccessToken } from "@/lib/auth-utils"; import { getOneDriveFileBuffer } from "@/services/onedrive"; // Import your working service import { extractMetadata } from "@/lib/metadata-extractor"; + + /** * 1. FETCH: Get all file nodes * Now simply calls the DAL. Error handling is left to the caller (the UI). @@ -120,134 +122,35 @@ export async function updateFileNodeAction(id: string, formData: FormData) { } } -// export async function getMetadataPreviewAction(fileId: string) { -// const session = await auth(); -// if (!session?.user?.id) throw new Error("Unauthorized"); -// try { -// console.log(`πŸ” Starting enhancement for file: ${fileId}`); - -// // This calls the DAL -> which calls the Service -> which calls OneDrive -// const data = await getEnrichedMetadataFromCloud(fileId); - -// // This log will show you exactly what we found in your terminal! -// console.log("βœ… Extracted Metadata Result:", data); - -// return { success: true, data }; -// } catch (error: any) { -// console.error("❌ Enhancement Action Error:", error.message); -// return { success: false, error: error.message }; -// } -// } -/** - * 5. ENHANCE (Magic Fill): Extracts deep metadata from the actual file binary - */ -// export async function getMetadataPreviewAction(fileId: string) { -// const session = await auth(); -// if (!session?.user?.id) throw new Error("Unauthorized"); -// console.log(`\n--- πŸ” Magic Fill Started for File ID: ${fileId} ---`); -// try { -// // 1. Get the record from DB to get the oneDriveId and Name -// const node = await getFileNodeById(fileId); -// if (!node || !node.oneDriveId) { -// throw new Error("File not found or not synced with OneDrive"); -// } - -// // 2. Get fresh token -// const token = await getFreshAccessToken(session.user.id); - -// // 3. Fetch the actual binary content from Microsoft Graph -// console.log(`πŸ“‘ Fetching binary from Microsoft Graph...`); -// let response; -// try { -// response = await fetch( -// `https://graph.microsoftonline.com/v1.0/me/drive/items/${node.oneDriveId}/content`, -// { -// headers: { Authorization: `Bearer ${token}` }, -// cache: 'no-store' // Ensure we aren't hitting a stale server cache -// } -// ); -// } catch (err: any) { -// console.error("❌ THE ACTUAL NETWORK ERROR:"); -// console.error("Message:", err.message); -// console.error("Cause/Stack:", err.cause || err.stack); // This is the gold mine -// throw new Error(`Server-side fetch failed: ${err.message}`); -// } - -// if (!response.ok) { -// throw new Error(`Failed to fetch file content: ${response.statusText}`); -// } - -// const arrayBuffer = await response.arrayBuffer(); -// const buffer = Buffer.from(arrayBuffer); -// console.log(`πŸ“¦ Downloaded ${buffer.length} bytes.`); - -// // 4. Run the Metadata Utility -// const extractedData = await extractMetadata(buffer, node.name); - -// // --- 🏁 THE TERMINAL LOG YOU REQUESTED --- -// console.log("βœ… RAW DATA EXTRACTED FROM FILE:"); -// console.dir(extractedData, { depth: null, colors: true }); -// console.log(`--- 🏁 Magic Fill Finished ---\n`); - -// return { success: true, data: extractedData }; -// } catch (error: any) { -// console.error("❌ Magic Fill Error:", error.message); -// return { success: false, error: error.message }; -// } -// } export async function getMetadataPreviewAction(fileId: string) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); - console.log(`\n--- πŸ” Magic Fill Started for File ID: ${fileId} ---`); - try { const node = await getFileNodeById(fileId); - if (!node || !node.oneDriveId) throw new Error("File not found"); + if (!node || !node.oneDriveId) throw new Error("No OneDrive ID found"); - // 1. Get the token from our utility - const token = await getFreshAccessToken(session.user.id); + console.log(`πŸ“‘ Attempting fetch via Service for: ${node.name}`); - // LOG: Just check the length to be sure it's not empty - console.log(`πŸ”‘ Token retrieved (Length: ${token.length})`); + const token = await getFreshAccessToken(session.user.id); + + if (!token) throw new Error("Could not retrieve access token"); + // Call your existing service + const buffer = await getOneDriveFileBuffer(node.oneDriveId, token); + + console.log(`πŸ“¦ Buffer received: ${buffer.length} bytes`); - console.log(`πŸ“‘ Fetching binary for: ${node.name}...`); - - // 2. Fetch the content from Microsoft Graph - const response = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': '*/*' - }, - } - ); - - if (!response.ok) { - // If it fails here, we'll see the real reason from Microsoft - const errorText = await response.text(); - console.error("❌ Microsoft Graph Error Response:", errorText); - throw new Error(`OneDrive Download Failed: ${response.status}`); - } - - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - console.log(`πŸ“¦ Success! Downloaded ${buffer.length} bytes.`); - - // 3. Extract Metadata const extractedData = await extractMetadata(buffer, node.name); - console.log("βœ… RAW DATA EXTRACTED:"); - console.dir(extractedData, { depth: null, colors: true }); - + console.log("βœ… Extracted:", extractedData); return { success: true, data: extractedData }; } catch (error: any) { - console.error("❌ Magic Fill Error:", error.message); + console.error("❌ Service Fetch Error:", error.message); + // If this still says ENOTFOUND, the code is fine, but the terminal is blocked. return { success: false, error: error.message }; } } \ No newline at end of file diff --git a/src/data-access/file-nodes.ts b/src/data-access/file-nodes.ts index ce61177..3c4620b 100644 --- a/src/data-access/file-nodes.ts +++ b/src/data-access/file-nodes.ts @@ -109,23 +109,3 @@ export async function upsertFileNode(oneDriveId: string, data: any) { -/** - * Logic to coordinate getting a file from the cloud and extracting its data. - * This is the "Brain" function for your metadata enrichment. - */ -export async function getEnrichedMetadataFromCloud(fileId: string) { - // 1. Get the record from our DB so we know the filename (needed for extension logic) - const node = await prisma.fileNode.findUnique({ - where: { id: fileId } - }); - - if (!node) throw new Error("File not found in database."); - - // 2. Fetch the bytes using the service we just created - const buffer = await getOneDriveFileBuffer(fileId); - - // 3. Extract internal metadata (Title, Author, or GPS coordinates) - const deepMetadata = await extractMetadata(buffer, node.name); - - return deepMetadata; -} \ No newline at end of file diff --git a/src/lib/metadata-extractor.ts b/src/lib/metadata-extractor.ts index a4f7cd7..05657de 100644 --- a/src/lib/metadata-extractor.ts +++ b/src/lib/metadata-extractor.ts @@ -11,19 +11,44 @@ export interface ExtractedMetadata { pageCount?: number; latitude?: number; longitude?: number; + deviceModel?: string | null; + exposureTime?: string | null; + fNumber?: number | null; + iso?: number | null; type: string; } +/** + * Helper: Converts [Degrees, Minutes, Seconds] to Decimal + */ +function parseGps(values: number[] | undefined, ref: string | undefined): number | null { + if (!values || values.length < 3) return null; + const decimal = values[0] + values[1] / 60 + values[2] / 3600; + const multiplier = (ref === 'S' || ref === 'W') ? -1 : 1; + return Number((decimal * multiplier).toFixed(6)); +} + +/** + * Helper: Converts Rational fractions (1/4000) or Objects to readable values + */ +function parseRational(value: any, asString = false): string | number | null { + if (!value) return null; + if (typeof value === 'number') return value; + if (value.numerator && value.denominator) { + if (asString) return `${value.numerator}/${value.denominator}`; + return value.numerator / value.denominator; + } + return null; +} + export async function extractMetadata(buffer: Buffer, filename: string): Promise { const extension = filename.split('.').pop()?.toLowerCase(); try { // --- 1. PDF EXTRACTION --- - if (extension === 'pdf') { - // Use any to bypass the missing 'default' property error in ESM + if (extension === 'pdf') { const parsePdf = (pdf as any).default || pdf; const data = await parsePdf(buffer); - return { type: 'PDF', title: data.info?.Title || filename, @@ -33,44 +58,26 @@ export async function extractMetadata(buffer: Buffer, filename: string): Promise }; } - // --- 2. EPUB EXTRACTION --- - if (extension === 'epub') { - // Logic for EPub usually requires file path or custom stream handler - // Keeping placeholder for your existing EPub logic - return { type: 'EPUB', title: filename }; - } - - // --- 3. IMAGE EXTRACTION (Enhanced with GPS) --- + // --- 2. IMAGE EXTRACTION (Enhanced) --- if (['jpg', 'jpeg', 'png', 'webp'].includes(extension || '')) { const image = sharp(buffer); const metadata = await image.metadata(); - let gps: { latitude?: number; longitude?: number } = {}; + let exifData: Partial = {}; if (metadata.exif) { try { - // Cast to any to bypass strict Exif type checking for nested GPS properties const exif = exifReader(metadata.exif) as any; - - // Debugging log to see the raw structure in your terminal - console.log("πŸ“Έ FULL RAW EXIF DATA:", JSON.stringify(exif, null, 2)); - - if (exif.gps && exif.gps.GPSLatitude && exif.gps.GPSLongitude) { - // EXIF stores GPS as [Degrees, Minutes, Seconds] - // We convert to Decimal Degrees for Google Maps - const lat = exif.gps.GPSLatitude; - const lon = exif.gps.GPSLongitude; - - let latitude = lat[0] + lat[1] / 60 + lat[2] / 3600; - let longitude = lon[0] + lon[1] / 60 + lon[2] / 3600; - - // Adjust for South or West hemisphere - if (exif.gps.GPSLatitudeRef === 'S') latitude *= -1; - if (exif.gps.GPSLongitudeRef === 'W') longitude *= -1; - - gps.latitude = latitude; - gps.longitude = longitude; - } + console.log(" exif "); + console.log(exif); + exifData = { + deviceModel: exif.image?.Model || null, + exposureTime: parseRational(exif.photo?.ExposureTime, true) as string, + fNumber: parseRational(exif.photo?.FNumber) as number, + iso: exif.photo?.ISOSpeedRatings || null, + latitude: parseGps(exif.gps?.GPSLatitude, exif.gps?.GPSLatitudeRef) || undefined, + longitude: parseGps(exif.gps?.GPSLongitude, exif.gps?.GPSLongitudeRef) || undefined, + }; } catch (exifError) { console.warn("Could not parse EXIF data for:", filename, exifError); } @@ -80,10 +87,11 @@ export async function extractMetadata(buffer: Buffer, filename: string): Promise type: `IMAGE (${metadata.format?.toUpperCase()})`, dimensions: metadata.width && metadata.height ? `${metadata.width}x${metadata.height}` : undefined, title: filename, - ...gps + ...exifData }; } + // Default Fallback return { type: 'FILE', title: filename }; } catch (error) { console.error(`Extraction failed for ${filename}:`, error); diff --git a/src/services/metadata-service.ts b/src/services/metadata-service.ts new file mode 100644 index 0000000..c25b22b --- /dev/null +++ b/src/services/metadata-service.ts @@ -0,0 +1,23 @@ +// src/services/metadata-service.ts +import { getOneDriveFileBuffer } from "./onedrive"; +// 1. Change the import name +import { extractMetadata } from "@/lib/metadata-extractor"; +import { getFileNodeById, updateFileNode } from "@/data-access/file-nodes"; + +export async function enrichFileMetadata(fileId: string, token: string) { + const node = await getFileNodeById(fileId); + if (!node || !node.oneDriveId) throw new Error("Node not found"); + + const buffer = await getOneDriveFileBuffer(node.oneDriveId, token); + + // 2. Use the standard extractor (it handles PDF, Image, etc. automatically) + const deepMetadata = await extractMetadata(buffer, node.name); + + const updatedMetadata = { + ...(node.metadata as object), + ...deepMetadata, // This now contains type, dimensions, gps, deviceModel, etc. + magicFilled: true + }; + + return await updateFileNode(fileId, { metadata: updatedMetadata }); +} \ No newline at end of file diff --git a/src/services/onedrive.ts b/src/services/onedrive.ts index 994e374..7c6205f 100644 --- a/src/services/onedrive.ts +++ b/src/services/onedrive.ts @@ -171,35 +171,7 @@ export async function uploadToFolderId(userId: string, file: File, folderId: str if (!uploadRes.ok) throw new Error("Upload failed"); return await uploadRes.json(); } -/** - * Fetches the raw binary content (the actual file bytes) from OneDrive. - */ -export async function getOneDriveFileBuffer(fileId: string): Promise { - // Use your existing helper that manages the Microsoft Graph access token - const token = await getAccessToken(); - const response = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${fileId}/content`, - { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - }, - // Ensure we get fresh data and don't cache large file buffers - cache: 'no-store', - } - ); - - if (!response.ok) { - const errorText = await response.text(); - console.error("OneDrive Download Error:", errorText); - throw new Error(`Failed to download file content: ${response.statusText}`); - } - - // Convert the browser-style response into a Node.js Buffer - const arrayBuffer = await response.arrayBuffer(); - return Buffer.from(arrayBuffer); -} /** @@ -220,4 +192,33 @@ async function getAccessToken(): Promise { } return token; +} + +/** + * Fetches raw file content from OneDrive. + * Parameterized token allows this to be used in different contexts (User actions, Webhooks, etc.) + */ +export async function getOneDriveFileBuffer(oneDriveId: string, token: string): Promise { + const response = await fetch( + `https://graph.microsoft.com/v1.0/me/drive/items/${oneDriveId}/content`, + { + method: 'GET', // Explicit is better + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': '*/*' + }, + // CRITICAL: Next.js tends to cache fetch calls. + // We do NOT want to cache large binary buffers in memory/disk. + cache: 'no-store', + } + ); + + if (!response.ok) { + const errorBody = await response.text().catch(() => "No error body"); + console.error(`OneDrive Download Error (${response.status}):`, errorBody); + throw new Error(`OneDrive download failed: ${response.statusText}`); + } + + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); } \ No newline at end of file