// --------------------------------------------------------------------------- // File diff classification — compares local files against a remote file map // --------------------------------------------------------------------------- import { MODEL_EXTENSIONS } from './constants' import type { FileChange, PushFile } from './types' export interface DiffResult { /** Map of lowercase filename → change status (for commit message) */ fileChanges: Map /** Files that actually need to be pushed (new or changed) */ changedFilesToPush: PushFile[] /** Filenames that were on remote but not in the new upload */ deletedFileNames: string[] /** Full paths for deletion on remote */ deletePaths: string[] } /** * Classify each file as new, changed, or unchanged by comparing against * the remote file map. * * Rules: * - Models: always re-pushed, * but marked as 'unchanged' in the commit message when the folder already * exists (we keep the current behavior of always delivering the model file). * - Textures: compared by size (not compressed, reliable). * - Orphan remote files: classified as deletions. */ export function classifyFileChanges( filesToPush: PushFile[], remoteFileMap: Map, folderPath: string, ): DiffResult { const fileChanges = new Map() const changedFilesToPush: PushFile[] = [] for (const f of filesToPush) { const filename = f.path.split('/').pop() ?? '' const ext = filename.slice(filename.lastIndexOf('.')).toLowerCase() const isModel = MODEL_EXTENSIONS.has(ext) if (isModel) { // Model: always re-push since compression makes size comparison unreliable. // Mark as 'unchanged' for the commit message when the folder already exists. const remoteSize = remoteFileMap.get(filename.toLowerCase()) fileChanges.set(filename.toLowerCase(), remoteSize === undefined ? 'new' : 'unchanged') changedFilesToPush.push(f) } else { // Texture: compare by size const localSize = Buffer.from(f.contentBase64, 'base64').length const remoteSize = remoteFileMap.get(filename.toLowerCase()) if (remoteSize === undefined) { fileChanges.set(filename.toLowerCase(), 'new') changedFilesToPush.push(f) } else if (remoteSize !== localSize) { fileChanges.set(filename.toLowerCase(), 'changed') changedFilesToPush.push(f) } else { fileChanges.set(filename.toLowerCase(), 'unchanged') } } } // Files on remote not in the new upload → deleted (orphans) const newFileNames = new Set(filesToPush.map((f) => (f.path.split('/').pop() ?? '').toLowerCase())) const deletedFileNames: string[] = [] const deletePaths: string[] = [] for (const [name] of remoteFileMap) { if (!newFileNames.has(name)) { deletedFileNames.push(name) deletePaths.push(`${folderPath}/${name}`) } } return { fileChanges, changedFilesToPush, deletedFileNames, deletePaths } }