fix: 레거시 ID-only URL 은 전역 유일할 때만 제공하고 정규 경로 충돌 제거

폴더별 중복 ID 허용 이후, ID 만으로 영상을 찾는 레거시/공유 fallback
(getVideoByIdGlobal) 이 임의의 폴더(작은 folder_id) 를 돌려줘서 엉뚱한
영상이 나올 수 있었다. 전역에서 정확히 1건일 때만 반환하고, 중복이면
null → 호출 측 404 로 거절한다. 영향: /file/video/:id,
/file/video/:id/thumb, /api/video/:id(meta).

또한 죽은 레거시 라우트 /api/video/:videoId/file 를 제거했다. 이 라우트는
앱이 더는 만들지 않으면서 정규 경로 라우트 /api/video/:topName/:videoId 와
2세그먼트에서 충돌해, ID 가 리터럴 "file" 인 영상의 정규 URL 을 가로챘다.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Claude (owner)
2026-06-06 16:33:54 +09:00
parent 042d9f7059
commit ff78b60a4b
2 changed files with 18 additions and 11 deletions

View File

@@ -100,8 +100,14 @@ function sendVideoFile(video: Video, req: Parameters<RequestHandler>[0], res: Pa
/** /**
* 영상 파일 스트리밍 (짧은 ID-only URL — 외부 공유 호환용 / 레거시). * 영상 파일 스트리밍 (짧은 ID-only URL — 외부 공유 호환용 / 레거시).
* - GET /file/video/:videoId * - GET /file/video/:videoId
* - GET /api/video/:videoId/file * ID 가 전역에서 정확히 1건일 때만 제공한다. 같은 ID 가 여러 폴더에 있으면
* getVideoByIdGlobal 이 null 을 돌려주므로 404 (어느 폴더인지 모호).
* 폴더가 확실한 경우엔 정규 경로 URL `/api/video/폴더/[폴더/]ID` 를 쓸 것.
* ?edited=0 이면 원본을 강제, 기본은 편집본 있으면 편집본. * ?edited=0 이면 원본을 강제, 기본은 편집본 있으면 편집본.
*
* 주의: 예전 `/api/video/:videoId/file` 레거시 라우트는 제거했다. 정규 경로
* 라우트 `/api/video/:topName/:videoId` 와 2세그먼트에서 충돌해서, ID 가
* 리터럴 "file" 인 영상의 정규 URL `/api/video/폴더/file` 을 가로챘기 때문.
*/ */
const streamVideoHandler: RequestHandler = (req, res, next) => { const streamVideoHandler: RequestHandler = (req, res, next) => {
try { try {
@@ -116,7 +122,6 @@ const streamVideoHandler: RequestHandler = (req, res, next) => {
} }
} }
publicRouter.get('/file/video/:videoId', streamVideoHandler) publicRouter.get('/file/video/:videoId', streamVideoHandler)
publicRouter.get('/api/video/:videoId/file', streamVideoHandler)
/** /**
* 영상 파일 스트리밍 (폴더 경로 + ID — 정규 공유 URL). * 영상 파일 스트리밍 (폴더 경로 + ID — 정규 공유 URL).
@@ -125,9 +130,8 @@ publicRouter.get('/api/video/:videoId/file', streamVideoHandler)
* *
* 재생 페이지(/video/...)와 동일한 경로 규칙. 마지막 세그먼트가 영상 ID 이고 * 재생 페이지(/video/...)와 동일한 경로 규칙. 마지막 세그먼트가 영상 ID 이고
* 앞쪽 세그먼트는 그 영상이 실제 속한 폴더 경로와 일치해야 한다. * 앞쪽 세그먼트는 그 영상이 실제 속한 폴더 경로와 일치해야 한다.
* * (예전 `/api/video/:videoId/file` 레거시 라우트와의 2세그먼트 충돌은 그 라우트를
* 라우트 등록 순서 주의: 위의 `/api/video/:videoId/file` (2세그·끝이 리터럴 "file") * 제거해서 해소했다 — 이제 ID 가 "file" 인 영상도 정규 URL 로 정확히 찾는다.)
* 보다 뒤에 둬서, 레거시 URL 이 이 경로 라우트에 가로채이지 않게 한다.
*/ */
const streamVideoByPathHandler: RequestHandler = (req, res, next) => { const streamVideoByPathHandler: RequestHandler = (req, res, next) => {
try { try {

View File

@@ -309,15 +309,18 @@ export function getVideoInFolder(folderId: number, id: string): Video | null {
} }
/** /**
* ID 만으로 전역에서 첫 번째 영상을 찾는다 (폴더 경로 없는 레거시/외부공유 fallback). * ID 만으로 전역에서 영상을 찾는다 (폴더 경로 없는 레거시/외부공유 fallback).
* 같은 ID 가 여러 폴더에 있으면 folder_id 가 작은(=먼저 만든) 쪽을 돌려준다. * ID 는 이제 폴더별로만 유일하므로, 같은 ID 가 여러 폴더에 있으면 어느 쪽인지
* 모호하다. 그런 경우엔 아무 것도 돌려주지 않아 호출 측이 404 로 처리하게 한다.
* 전역에서 정확히 1건일 때만 그 영상을 돌려준다.
*/ */
export function getVideoByIdGlobal(id: string): Video | null { export function getVideoByIdGlobal(id: string): Video | null {
if (!isSafeVideoId(id)) return null if (!isSafeVideoId(id)) return null
const r = getDb() const rows = getDb()
.prepare(`SELECT * FROM videos WHERE id = ? ORDER BY folder_id LIMIT 1`) .prepare(`SELECT * FROM videos WHERE id = ? ORDER BY folder_id LIMIT 2`)
.get(id) as VideoRow | undefined .all(id) as VideoRow[]
return r ? rowToVideo(r) : null if (rows.length !== 1) return null // 없음 또는 중복(모호) → 404
return rowToVideo(rows[0])
} }
export function listVideosInFolder(folderId: number): Video[] { export function listVideosInFolder(folderId: number): Video[] {