78f4aa83e0
- Extract API helpers from UploadZone into lib/upload-api.ts (FormData builder, checkFolderDiffs, uploadDrive, uploadGit)
- Extract upload orchestration into hooks/useUploadOrchestrator.ts (UploadZone: 489 → 162 lines)
- Extract file diff classification into lib/diff-files.ts (from git route)
- Extract shared SVG icons into components/ui/icons.tsx (7 icons, 0 duplication)
- Extract shared modal wrapper into components/ui/Modal.tsx + ModalActions
- Extract DriveStatusLine sub-component from FolderCard
- Fix checkFolderDiffs silently swallowing auth/network errors (now throws)
- Fix type safety: remove as never casts, add isHttpError type guard, use discriminated union for validateFolder
- Fix nextcloud: cache getConfig, add max bound to findNextVersion, optimize mkdirRecursive (skip PROPFIND)
- Fix drive route: remove req.clone(), extend parseMultiUpload to return extra fields
- Fix commit message: model shown as unchanged with ↔️ on updates (not falsely marked as modified)
- Clean dead code: unused folderExists import, FileStatus/DriveStatus exports, ParsedFile.textureName, getConfig basePath
- Add security headers in next.config.ts (HSTS, X-Content-Type-Options, X-Frame-Options, etc.)
- Update README with new project structure
84 lines
3.0 KiB
TypeScript
84 lines
3.0 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// File diff classification — compares local files against a remote file map
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { MODEL_EXTENSIONS } from './constants'
|
|
import type { FileChange } from './types'
|
|
|
|
interface PushFile {
|
|
path: string
|
|
contentBase64: string
|
|
}
|
|
|
|
export interface DiffResult {
|
|
/** Map of lowercase filename → change status (for commit message) */
|
|
fileChanges: Map<string, FileChange>
|
|
/** 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 (compression makes size comparison unreliable),
|
|
* but marked as 'unchanged' in the commit message when the folder already
|
|
* exists (we can't know if the model really changed after Blender).
|
|
* - Textures: compared by size (not compressed, reliable).
|
|
* - Orphan remote files: classified as deletions.
|
|
*/
|
|
export function classifyFileChanges(
|
|
filesToPush: PushFile[],
|
|
remoteFileMap: Map<string, number>,
|
|
folderPath: string,
|
|
): DiffResult {
|
|
const fileChanges = new Map<string, FileChange>()
|
|
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 }
|
|
}
|