diff --git a/app/api/upload/check/route.ts b/app/api/upload/check/route.ts index a629f8f..ca4332b 100644 --- a/app/api/upload/check/route.ts +++ b/app/api/upload/check/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { validateUploadSecret } from '@/lib/auth' import { getRemoteFolder } from '@/lib/github' import { classifyFileChanges } from '@/lib/diff-files' +import { getModelFolderPath } from '@/lib/model-paths' import { ensurePreparedStagingAssets } from '@/lib/upload-staging' export const runtime = 'nodejs' @@ -30,7 +31,7 @@ export async function POST(req: NextRequest) { try { const { folderName, filesToPush } = await ensurePreparedStagingAssets(stagingId) - const folderPath = `public/models/${folderName}` + const folderPath = getModelFolderPath(folderName) const { exists, files } = await getRemoteFolder(folderPath) if (exists) { diff --git a/app/api/upload/git/route.ts b/app/api/upload/git/route.ts index 763af11..5e01e30 100644 --- a/app/api/upload/git/route.ts +++ b/app/api/upload/git/route.ts @@ -3,6 +3,7 @@ import { validateUploadSecret } from '@/lib/auth' import { getRemoteFolder, pushAllToGitHub } from '@/lib/github' import { buildCommitMessage } from '@/lib/commit-message' import { classifyFileChanges } from '@/lib/diff-files' +import { getModelFolderPath } from '@/lib/model-paths' import { cleanupStagingUpload, ensurePreparedStagingAssets, readStagedManifest } from '@/lib/upload-staging' import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock' @@ -52,7 +53,7 @@ export async function POST(req: NextRequest) { } = await ensurePreparedStagingAssets(stagingId) // --- Detect existing files and classify changes --- - const folderPath = `public/models/${folderName}` + const folderPath = getModelFolderPath(folderName) let remoteFileMap: Map try { diff --git a/components/UploadZone.tsx b/components/UploadZone.tsx index fcff137..56d9f54 100644 --- a/components/UploadZone.tsx +++ b/components/UploadZone.tsx @@ -1,6 +1,7 @@ 'use client' import type { FolderEntry } from '@/lib/client-types' +import { revokeEntryUrls } from '@/lib/client-object-urls' import { useSecret } from '@/hooks/useSecret' import { useFolderEntries } from '@/hooks/useFolderEntries' import { useUploadOrchestrator } from '@/hooks/useUploadOrchestrator' @@ -60,12 +61,7 @@ export default function UploadZone() { }) const handleFolderSelected = (entry: FolderEntry) => { - entries.forEach((current) => { - const urls = new Set() - if (current.modelUrl) urls.add(current.modelUrl) - Object.values(current.assetUrls || {}).forEach((url) => urls.add(url)) - urls.forEach((url) => URL.revokeObjectURL(url)) - }) + entries.forEach(revokeEntryUrls) setGlobalError(null) setEntries([entry]) } diff --git a/hooks/useFolderEntries.ts b/hooks/useFolderEntries.ts index b2b63fd..bbf9505 100644 --- a/hooks/useFolderEntries.ts +++ b/hooks/useFolderEntries.ts @@ -2,13 +2,7 @@ import { useState, useCallback } from 'react' import type { FolderEntry } from '@/lib/client-types' - -function revokeEntryUrls(entry: FolderEntry) { - const urls = new Set() - if (entry.modelUrl) urls.add(entry.modelUrl) - Object.values(entry.assetUrls || {}).forEach((url) => urls.add(url)) - urls.forEach((url) => URL.revokeObjectURL(url)) -} +import { revokeEntryUrls } from '@/lib/client-object-urls' export function useFolderEntries() { const [entries, setEntries] = useState([]) diff --git a/lib/client-object-urls.ts b/lib/client-object-urls.ts new file mode 100644 index 0000000..befb81b --- /dev/null +++ b/lib/client-object-urls.ts @@ -0,0 +1,9 @@ +import type { FolderEntry } from '@/lib/client-types' + +export function revokeEntryUrls(entry: FolderEntry) { + const urls = new Set() + + if (entry.modelUrl) urls.add(entry.modelUrl) + Object.values(entry.assetUrls || {}).forEach((url) => urls.add(url)) + urls.forEach((url) => URL.revokeObjectURL(url)) +} diff --git a/lib/diff-files.ts b/lib/diff-files.ts index 2ac3c70..c254d54 100644 --- a/lib/diff-files.ts +++ b/lib/diff-files.ts @@ -3,12 +3,7 @@ // --------------------------------------------------------------------------- import { MODEL_EXTENSIONS } from './constants' -import type { FileChange } from './types' - -interface PushFile { - path: string - contentBase64: string -} +import type { FileChange, PushFile } from './types' export interface DiffResult { /** Map of lowercase filename → change status (for commit message) */ diff --git a/lib/github.ts b/lib/github.ts index 57eb081..3874002 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -1,7 +1,7 @@ import { createHash } from 'crypto' import { Octokit } from '@octokit/rest' import { LFS_EXTENSIONS } from './constants' -import type { RemoteFile } from './types' +import type { PushFile, RemoteFile } from './types' // --------------------------------------------------------------------------- // Octokit helpers @@ -237,7 +237,7 @@ export async function getRemoteFolder( // --------------------------------------------------------------------------- export async function pushAllToGitHub( - files: { path: string; contentBase64: string }[], + files: PushFile[], deletePaths: string[], commitMessage: string, ): Promise<{ commitUrl: string }> { @@ -247,7 +247,7 @@ export async function pushAllToGitHub( // --- Separate LFS files from regular files --- const lfsFiles: { path: string; contentBase64: string; oid: string; size: number }[] = [] - const regularFiles: { path: string; contentBase64: string }[] = [] + const regularFiles: PushFile[] = [] for (const f of files) { if (isLfsFile(f.path)) { diff --git a/lib/model-paths.ts b/lib/model-paths.ts new file mode 100644 index 0000000..49f2970 --- /dev/null +++ b/lib/model-paths.ts @@ -0,0 +1,7 @@ +export function getModelFolderPath(folderName: string) { + return `public/models/${folderName}` +} + +export function getModelAssetPath(folderName: string, filename: string) { + return `${getModelFolderPath(folderName)}/${filename}` +} diff --git a/lib/prepare-git-assets.ts b/lib/prepare-git-assets.ts index dc7484a..1a6f1b3 100644 --- a/lib/prepare-git-assets.ts +++ b/lib/prepare-git-assets.ts @@ -1,11 +1,7 @@ import { compressTextureBuffer } from '@/lib/texture-compression' import { classifyAssetCategory } from '@/lib/asset-classification' -import type { ParsedFile, PreparedAssetSummary } from '@/lib/types' - -interface PushFile { - path: string - contentBase64: string -} +import { getModelAssetPath } from '@/lib/model-paths' +import type { ParsedFile, PreparedAssetSummary, PushFile } from '@/lib/types' interface PrepareGitAssetsParams { folderName: string @@ -61,7 +57,7 @@ export async function prepareGitAssets({ } filesToPush.push({ - path: `public/models/${folderName}/${pf.filename}`, + path: getModelAssetPath(folderName, pf.filename), contentBase64: content.toString('base64'), }) } diff --git a/lib/types.ts b/lib/types.ts index 91ea5fb..85c268e 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -10,6 +10,11 @@ export interface ParsedFile { isModel: boolean } +export interface PushFile { + path: string + contentBase64: string +} + export type FileChange = 'new' | 'changed' | 'unchanged' export interface FileDiff { diff --git a/lib/upload-staging.ts b/lib/upload-staging.ts index 17913e0..68c8634 100644 --- a/lib/upload-staging.ts +++ b/lib/upload-staging.ts @@ -3,8 +3,9 @@ import { dirname, join } from 'path' import { mkdir, readdir, readFile, rm, writeFile } from 'fs/promises' import { existsSync } from 'fs' import { TMP_DIR } from '@/lib/constants' +import { getModelAssetPath } from '@/lib/model-paths' import { prepareGitAssets } from '@/lib/prepare-git-assets' -import type { ParsedFile, PreparedAssetSummary } from '@/lib/types' +import type { ParsedFile, PreparedAssetSummary, PushFile } from '@/lib/types' const STAGING_ROOT = join(TMP_DIR, 'staging') const STAGING_TTL_MS = 60 * 60 * 1000 @@ -30,11 +31,6 @@ interface StagingManifest { prepared?: StagedPreparedData } -interface PushFile { - path: string - contentBase64: string -} - interface PreparedStageAssetsResult { folderName: string filesToPush: PushFile[] @@ -148,7 +144,7 @@ async function buildPreparedPushFiles(stagingId: string, manifest: StagingManife manifest.originals.map(async (file) => { const buffer = await readFile(join(preparedDir, file.filename)) return { - path: `public/models/${manifest.folderName}/${file.filename}`, + path: getModelAssetPath(manifest.folderName, file.filename), contentBase64: buffer.toString('base64'), } }),