From 91eaa5d186c3886f70f6e1677826bb23f249db8a Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Tue, 14 Apr 2026 14:39:19 +0200 Subject: [PATCH] fix: replace SHA comparison with file size for LFS compatibility + add NoChangesModal Git LFS stores pointer files whose SHA differs from the actual blob SHA, causing false-positive diffs on every upload. Switching to file size comparison resolves this for LFS-enabled repos. Also replaces the inline error message with a dedicated NoChangesModal when no differences are detected, offering cancel (reset) or modify (close modal) actions. --- app/api/upload/git/route.ts | 16 +++---- components/UploadZone.tsx | 49 +++++++++++----------- components/upload/NoChangesModal.tsx | 63 ++++++++++++++++++++++++++++ lib/github.ts | 2 +- lib/types.ts | 2 +- 5 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 components/upload/NoChangesModal.tsx diff --git a/app/api/upload/git/route.ts b/app/api/upload/git/route.ts index 221d2ff..c563080 100644 --- a/app/api/upload/git/route.ts +++ b/app/api/upload/git/route.ts @@ -5,7 +5,7 @@ import { existsSync } from 'fs' import { validateUploadSecret } from '@/lib/auth' import { parseMultiUpload } from '@/lib/parse-upload' import { compressWithBlender } from '@/lib/blender' -import { computeGitBlobSha, getRemoteFolder, pushAllToGitHub } from '@/lib/github' +import { getRemoteFolder, pushAllToGitHub } from '@/lib/github' import { buildCommitMessage } from '@/lib/commit-message' import { TMP_DIR } from '@/lib/constants' import type { FileChange } from '@/lib/types' @@ -84,13 +84,13 @@ export async function POST(req: NextRequest) { }) } - // --- Detect existing files and compare SHA to classify changes --- + // --- Detect existing files and compare size to classify changes (LFS-compatible) --- const folderPath = `public/models/${destination}/${folderName}` - let remoteFileMap: Map + let remoteFileMap: Map try { const remote = await getRemoteFolder(folderPath) - remoteFileMap = new Map(remote.files.map((f) => [f.name.toLowerCase(), f.sha])) + remoteFileMap = new Map(remote.files.map((f) => [f.name.toLowerCase(), f.size])) } catch { remoteFileMap = new Map() } @@ -103,13 +103,13 @@ export async function POST(req: NextRequest) { for (const f of filesToPush) { const filename = f.path.split('/').pop() ?? '' - const localSha = computeGitBlobSha(Buffer.from(f.contentBase64, 'base64')) - const remoteSha = remoteFileMap.get(filename.toLowerCase()) + const localSize = Buffer.from(f.contentBase64, 'base64').length + const remoteSize = remoteFileMap.get(filename.toLowerCase()) - if (!remoteSha) { + if (remoteSize === undefined) { fileChanges.set(filename.toLowerCase(), 'new') changedFilesToPush.push(f) - } else if (remoteSha !== localSha) { + } else if (remoteSize !== localSize) { fileChanges.set(filename.toLowerCase(), 'changed') changedFilesToPush.push(f) } else { diff --git a/components/UploadZone.tsx b/components/UploadZone.tsx index 3796fb7..a023967 100644 --- a/components/UploadZone.tsx +++ b/components/UploadZone.tsx @@ -13,22 +13,7 @@ import FolderDropzone from './upload/FolderDropzone' import FolderCard from './upload/FolderCard' import ActionButtons from './upload/ActionButtons' import OverwriteConfirmModal from './upload/OverwriteConfirmModal' - -// --------------------------------------------------------------------------- -// Client-side SHA computation (same as `git hash-object`) -// --------------------------------------------------------------------------- - -async function computeGitBlobSha(file: File): Promise { - const buffer = await file.arrayBuffer() - const content = new Uint8Array(buffer) - const header = new TextEncoder().encode(`blob ${content.length}\0`) - const store = new Uint8Array(header.length + content.length) - store.set(header) - store.set(content, header.length) - const hashBuffer = await crypto.subtle.digest('SHA-1', store) - const hashArray = Array.from(new Uint8Array(hashBuffer)) - return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') -} +import NoChangesModal from './upload/NoChangesModal' // --------------------------------------------------------------------------- // API helpers @@ -56,18 +41,19 @@ async function checkFolderDiffs( return { exists: false, diffs: [] } } - const remoteFiles: { name: string; sha: string }[] = data.files || [] - const remoteMap = new Map(remoteFiles.map((f) => [f.name.toLowerCase(), f.sha])) + const remoteFiles: { name: string; size: number }[] = data.files || [] + const remoteMap = new Map(remoteFiles.map((f) => [f.name.toLowerCase(), f.size])) - const localFiles: { name: string; sha: string }[] = [] + // Build local file list with sizes + const localFiles: { name: string; size: number }[] = [] localFiles.push({ name: folder.modelFile.name, - sha: await computeGitBlobSha(folder.modelFile), + size: folder.modelFile.size, }) for (const tex of folder.textures) { localFiles.push({ name: tex.name, - sha: await computeGitBlobSha(tex.file), + size: tex.file.size, }) } @@ -77,10 +63,10 @@ async function checkFolderDiffs( for (const local of localFiles) { const key = local.name.toLowerCase() localNames.add(key) - const remoteSha = remoteMap.get(key) - if (!remoteSha) { + const remoteSize = remoteMap.get(key) + if (remoteSize === undefined) { diffs.push({ name: local.name, status: 'new' }) - } else if (remoteSha !== local.sha) { + } else if (remoteSize !== local.size) { diffs.push({ name: local.name, status: 'changed' }) } } @@ -177,6 +163,7 @@ export default function UploadZone() { folderName: string diffs: FileDiff[] } | null>(null) + const [noChangesFolder, setNoChangesFolder] = useState(null) const abortRef = useRef(null) // -- Handlers -- @@ -207,7 +194,7 @@ export default function UploadZone() { const check = await checkFolderDiffs(folder, destination, secret, abortRef.current?.signal) if (check.exists) { if (check.diffs.length === 0) { - setGlobalError('Aucun fichier modifie — le dossier distant est identique.') + setNoChangesFolder(folder.folderName) return } setOverwriteConfirm({ folderName: folder.folderName, diffs: check.diffs }) @@ -330,6 +317,18 @@ export default function UploadZone() { onConfirm={proceedUpload} /> )} + + {noChangesFolder && ( + { + setNoChangesFolder(null) + handleReset() + }} + onModify={() => setNoChangesFolder(null)} + /> + )} ) } diff --git a/components/upload/NoChangesModal.tsx b/components/upload/NoChangesModal.tsx new file mode 100644 index 0000000..6cf5f90 --- /dev/null +++ b/components/upload/NoChangesModal.tsx @@ -0,0 +1,63 @@ +interface NoChangesModalProps { + destination: string + folderName: string + onCancel: () => void + onModify: () => void +} + +export default function NoChangesModal({ + destination, + folderName, + onCancel, + onModify, +}: NoChangesModalProps) { + return ( +
+
+
+
+ + + +
+
+

+ Aucun changement detecte +

+

+ Le dossier {destination}/{folderName} est identique au contenu distant. +

+
+
+ +
+ + +
+
+
+ ) +} diff --git a/lib/github.ts b/lib/github.ts index 39ecc36..9f7f101 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -55,7 +55,7 @@ export async function getRemoteFolder( if (Array.isArray(data)) { return { exists: true, - files: data.map((f) => ({ name: f.name, sha: f.sha })), + files: data.map((f) => ({ name: f.name, size: f.size })), } } diff --git a/lib/types.ts b/lib/types.ts index 6cba74a..70a7c9c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -18,7 +18,7 @@ export interface FileDiff { export interface RemoteFile { name: string - sha: string + size: number } export type UploadResponse =