From 0abd21533b3485f2e53a41cf5644eb7ef46e5b62 Mon Sep 17 00:00:00 2001 From: "Claude (owner)" Date: Tue, 9 Jun 2026 02:53:36 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=8F=99=EC=8B=9C=20=EC=98=81=EC=83=81?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EC=8B=9C=20=EA=B3=B5=EC=9C=A0=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=ED=8C=8C=EC=9D=BC=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=9C=EA=B1=B0=20(=EA=B3=A0=EC=9C=A0=20tmp=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensureThumbnail / upscaleOriginalTo60Fps / applyTrimToVideo 가 영상당 고정된 tmp 파일명(thumb.jpg.tmp.jpg, original.bump.tmp.mp4, edited..tmp.) 을 써서, 같은 영상에 대한 요청이 동시에 들어오면 ffmpeg 프로세스들이 서로의 tmp 를 덮어쓰고 rename 이 경합해 일부 요청이 헛되이 실패했다. 증상: 같은 썸네일을 동시 10회 요청하면 6건만 성공하고 4건이 404. 원인: 호출마다 같은 tmp 경로 공유. 해결: randomUUID 로 호출별 고유 tmp 경로. 검증: 수정 후 동일 동시요청 10/10 성공, tmp 잔여물 없음. Co-Authored-By: Claude Opus 4.7 --- src/editor.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/editor.ts b/src/editor.ts index 2e4572b..c8626ea 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,5 +1,6 @@ import { spawn, spawnSync } from 'node:child_process' import { promises as fs, existsSync } from 'node:fs' +import { randomUUID } from 'node:crypto' import path from 'node:path' import { getVideoInFolder, updateVideo, videoDiskDir, type VideoTrim } from './storeDb.js' import { binDir } from './paths.js' @@ -335,7 +336,8 @@ export async function upscaleOriginalTo60Fps( // 재인코딩 결과는 항상 mp4 로 통일 (소스가 webm/mkv 여도). const outName = 'original.mp4' const outPath = path.join(dir, outName) - const tmpPath = path.join(dir, 'original.bump.tmp.mp4') + // 동시 호출이 같은 tmp 파일을 덮어쓰지 않도록 호출마다 고유한 임시 경로를 쓴다. + const tmpPath = path.join(dir, `original.bump.${randomUUID()}.tmp.mp4`) // vfilter 조립: // - fps=60 : motion interpolation 대신 단순 프레임 복제 (속도 우선). @@ -422,7 +424,8 @@ export async function applyTrimToVideo( const ext = path.extname(meta.originalFile) || '.mp4' const outName = `edited${ext}` const outPath = path.join(dir, outName) - const tmpPath = outPath + '.tmp' + ext + // 동시 호출이 같은 tmp 파일을 덮어쓰지 않도록 호출마다 고유한 임시 경로를 쓴다. + const tmpPath = `${outPath}.${randomUUID()}.tmp${ext}` const startSec = Math.max(0, Number(trim.startSec) || 0) const endSec = trim.endSec == null ? null : Math.max(startSec, Number(trim.endSec)) @@ -522,7 +525,10 @@ export async function ensureThumbnail(folderId: number, videoId: string): Promis } catch { return null } - const tmpPath = thumbPath + '.tmp.jpg' + // 동시 호출이 같은 tmp 파일을 덮어쓰지 않도록 호출마다 고유한 임시 경로를 쓴다. + // (두 명이 같은 영상을 동시에 열면 thumb 가 아직 없을 때 generation 이 겹쳐 + // 서로의 tmp 를 덮어써 일부 요청이 헛되이 실패하던 레이스를 막는다.) + const tmpPath = `${thumbPath}.${randomUUID()}.tmp.jpg` const vf = "scale='min(480,iw)':-2" // -ss 1 (1초 지점). 영상이 1초보다 짧으면 프레임이 안 나와 실패하므로 0초로 재시도. let ok = await runFfmpeg(bin, [