feat(stream): canonical folder-path video file URL

영상 파일 스트림 정규 주소를 재생 페이지와 동일한 폴더 경로 규칙으로 변경:
  /api/video/:topName[/:subName]/:videoId
경로의 폴더 부분이 영상 위치와 일치할 때만 200, 불일치 시 404.
재생 페이지(/video/...)와 ID 기반 레거시 스트림(/file/video/:id,
/api/video/:id/file)은 그대로 유지(외부 공유 호환).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Claude (owner)
2026-06-02 01:07:20 +09:00
parent 34c040c15d
commit 7105349a45
5 changed files with 56 additions and 16 deletions

View File

@@ -83,10 +83,23 @@ const videoViewHandler: RequestHandler = (req, res, next) => {
publicRouter.get('/video/:topName/:videoId', videoViewHandler)
publicRouter.get('/video/:topName/:subName/:videoId', videoViewHandler)
/** 결정된 비디오의 파일을 응답으로 보낸다 (?edited=0 이면 원본 강제). */
function sendVideoFile(video: Video, req: Parameters<RequestHandler>[0], res: Parameters<RequestHandler>[1]): void {
const editedParam = typeof req.query.edited === 'string' ? req.query.edited : ''
const wantOriginal = editedParam === '0' || editedParam === 'false'
const fileName =
!wantOriginal && video.editedFile ? video.editedFile : video.originalFile
if (!fileName || fileName.includes('%(ext)s')) {
res.status(404).end()
return
}
res.sendFile(videoFileFsPath(video.id, fileName))
}
/**
* 영상 파일 스트리밍 (짧은 ID-only URL — 외부 공유 호환용).
* 영상 파일 스트리밍 (짧은 ID-only URL — 외부 공유 호환용 / 레거시).
* - GET /file/video/:videoId
* - GET /api/video/:videoId/file (legacy alias)
* - GET /api/video/:videoId/file
* ?edited=0 이면 원본을 강제, 기본은 편집본 있으면 편집본.
*/
const streamVideoHandler: RequestHandler = (req, res, next) => {
@@ -96,16 +109,7 @@ const streamVideoHandler: RequestHandler = (req, res, next) => {
res.status(404).end()
return
}
const editedParam = typeof req.query.edited === 'string' ? req.query.edited : ''
const wantOriginal = editedParam === '0' || editedParam === 'false'
const fileName =
!wantOriginal && video.editedFile ? video.editedFile : video.originalFile
if (!fileName || fileName.includes('%(ext)s')) {
res.status(404).end()
return
}
const fsPath = videoFileFsPath(video.id, fileName)
res.sendFile(fsPath)
sendVideoFile(video, req, res)
} catch (err) {
next(err)
}
@@ -113,6 +117,37 @@ const streamVideoHandler: RequestHandler = (req, res, next) => {
publicRouter.get('/file/video/:videoId', streamVideoHandler)
publicRouter.get('/api/video/:videoId/file', streamVideoHandler)
/**
* 영상 파일 스트리밍 (폴더 경로 + ID — 정규 공유 URL).
* - GET /api/video/:topName/:videoId
* - GET /api/video/:topName/:subName/:videoId
*
* 재생 페이지(/video/...)와 동일한 경로 규칙. 마지막 세그먼트가 영상 ID 이고
* 앞쪽 세그먼트는 그 영상이 실제 속한 폴더 경로와 일치해야 한다.
*
* 라우트 등록 순서 주의: 위의 `/api/video/:videoId/file` (2세그·끝이 리터럴 "file")
* 보다 뒤에 둬서, 레거시 URL 이 이 경로 라우트에 가로채이지 않게 한다.
*/
const streamVideoByPathHandler: RequestHandler = (req, res, next) => {
try {
const { folderSegments, videoId } = collectVideoSegments(req.params)
const video = getVideo(videoId)
if (!video) {
res.status(404).end()
return
}
if (!segmentsEqual(folderPathNames(video.folderId), folderSegments)) {
res.status(404).end()
return
}
sendVideoFile(video, req, res)
} catch (err) {
next(err)
}
}
publicRouter.get('/api/video/:topName/:videoId', streamVideoByPathHandler)
publicRouter.get('/api/video/:topName/:subName/:videoId', streamVideoByPathHandler)
/**
* 영상 썸네일 (카드용). 없으면 ffmpeg 로 즉석 생성 후 thumb.jpg 로 캐시.
* 생성 불가(ffmpeg 없음/다운로드 미완 등)면 404 → 카드에서 ▶ placeholder 로 폴백.