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:
@@ -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 로 폴백.
|
||||
|
||||
Reference in New Issue
Block a user