Files
upload-gltf/lib/github.ts
T
Tom Boullay 78f4aa83e0 refactor: full codebase audit — extract modules, fix type safety, clean dead code
- 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
2026-04-14 17:19:10 +02:00

152 lines
4.1 KiB
TypeScript

import { Octokit } from '@octokit/rest'
import type { RemoteFile } from './types'
// ---------------------------------------------------------------------------
// Octokit helpers
// ---------------------------------------------------------------------------
function isHttpError(err: unknown): err is { status: number } {
return typeof err === 'object' && err !== null && 'status' in err && typeof (err as Record<string, unknown>).status === 'number'
}
function getOctokit(): Octokit {
const token = process.env.GITHUB_TOKEN
if (!token) throw new Error('GITHUB_TOKEN non configure')
return new Octokit({ auth: token })
}
function parseRepoUrl(): { owner: string; repo: string } {
const url = process.env.GIT_REPO_URL
if (!url) throw new Error('GIT_REPO_URL non configure')
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+)/)
const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+)/)
const shortMatch = url.match(/^([^/]+)\/([^/]+)$/)
const match = httpsMatch || sshMatch || shortMatch
if (!match) throw new Error(`Format GIT_REPO_URL invalide: "${url}"`)
return { owner: match[1], repo: match[2] }
}
// ---------------------------------------------------------------------------
// Read remote folder contents (with size per file)
// ---------------------------------------------------------------------------
export async function getRemoteFolder(
folderPath: string,
): Promise<{ exists: boolean; files: RemoteFile[] }> {
const octokit = getOctokit()
const { owner, repo } = parseRepoUrl()
const branch = process.env.GIT_BRANCH ?? 'main'
try {
const { data } = await octokit.repos.getContent({
owner,
repo,
path: folderPath,
ref: branch,
})
if (Array.isArray(data)) {
return {
exists: true,
files: data.map((f) => ({ name: f.name, size: f.size })),
}
}
return { exists: false, files: [] }
} catch (err: unknown) {
if (isHttpError(err) && err.status === 404) {
return { exists: false, files: [] }
}
throw err
}
}
// ---------------------------------------------------------------------------
// Push all files in a single commit (with optional deletions)
// ---------------------------------------------------------------------------
export async function pushAllToGitHub(
files: { path: string; contentBase64: string }[],
deletePaths: string[],
commitMessage: string,
): Promise<{ commitUrl: string }> {
const octokit = getOctokit()
const { owner, repo } = parseRepoUrl()
const branch = process.env.GIT_BRANCH ?? 'main'
// 1. Get latest commit on branch
const { data: ref } = await octokit.git.getRef({
owner,
repo,
ref: `heads/${branch}`,
})
const latestCommitSha = ref.object.sha
// 2. Get that commit's tree
const { data: commit } = await octokit.git.getCommit({
owner,
repo,
commit_sha: latestCommitSha,
})
// 3. Create all blobs in parallel
const blobResults = await Promise.all(
files.map((f) =>
octokit.git.createBlob({
owner,
repo,
content: f.contentBase64,
encoding: 'base64',
}),
),
)
// 4. Build tree entries: new/changed files + deletions
const newFilePaths = new Set(files.map((f) => f.path))
const deleteEntries = deletePaths
.filter((p) => !newFilePaths.has(p))
.map((p) => ({
path: p,
mode: '100644' as const,
type: 'blob' as const,
sha: null,
}))
const { data: newTree } = await octokit.git.createTree({
owner,
repo,
base_tree: commit.tree.sha,
tree: [
...files.map((f, i) => ({
path: f.path,
mode: '100644' as const,
type: 'blob' as const,
sha: blobResults[i].data.sha,
})),
...deleteEntries,
],
})
// 5. Create a single commit
const { data: newCommit } = await octokit.git.createCommit({
owner,
repo,
message: commitMessage,
tree: newTree.sha,
parents: [latestCommitSha],
})
// 6. Update branch ref
await octokit.git.updateRef({
owner,
repo,
ref: `heads/${branch}`,
sha: newCommit.sha,
})
return { commitUrl: newCommit.html_url }
}