fix: 웹에서 mp4/mp3 재생이 아니라 다운로드로 떨어지던 문제
- /api/files 라우트가 영상·오디오를 attachment + application/octet-stream으로 서빙해서 브라우저가 재생 대신 다운로드만 했던 것(2026-09-25 사용자 실측). - 영상·오디오 MIME 추가(video/mp4, video/webm, audio/mpeg 등) + inline 처리. - Range 요청(206 Partial) 지원 — 영상 시킹 시 처음부터 전체 재수신 안 함. - 회귀 확인: PDF inline 뷰어, PPTX 강제 다운로드 기존 동작 유지. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+30
-4
@@ -494,6 +494,10 @@ const IMAGE_TYPES: Record<string, string> = {
|
||||
// 보내면 브라우저가 latin1로 해석해 작업실 개요/readme.md의 한글이 전부 깨져 보였음.
|
||||
'.pdf': 'application/pdf', '.txt': 'text/plain; charset=utf-8', '.json': 'application/json; charset=utf-8',
|
||||
'.csv': 'text/csv; charset=utf-8', '.html': 'text/html; charset=utf-8', '.md': 'text/plain; charset=utf-8',
|
||||
// 영상·오디오 — octet-stream으로 보내면 브라우저가 재생을 포기하고 다운로드만 한다.
|
||||
'.mp4': 'video/mp4', '.m4v': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime',
|
||||
'.mkv': 'video/x-matroska', '.avi': 'video/x-msvideo',
|
||||
'.mp3': 'audio/mpeg', '.m4a': 'audio/mp4', '.wav': 'audio/wav', '.ogg': 'audio/ogg',
|
||||
};
|
||||
|
||||
// Code-editor sessions whose files live in the browser's local folder (File System
|
||||
@@ -2976,10 +2980,11 @@ app.get('/api/files/{*filePath}', (req: express.Request, res: express.Response)
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentType = IMAGE_TYPES[ext] || 'application/octet-stream';
|
||||
const filename = path.basename(filePath);
|
||||
// Force download for non-image files (pptx, pdf, xlsx, docx, zip, etc.)
|
||||
// PDF/txt/csv: inline (browser viewer); other binary files: attachment (force download)
|
||||
const inlineExts = ['.pdf', '.txt', '.csv', '.md'];
|
||||
const downloadExts = ['.pptx', '.xlsx', '.xls', '.docx', '.doc', '.zip', '.mp4', '.mp3'];
|
||||
// Force download for non-previewable files (pptx, xlsx, docx, zip, etc.)
|
||||
// PDF/txt/csv: inline (browser viewer); 영상·오디오(mp4/mp3/…)도 inline — attachment로
|
||||
// 밀면 <video>/<audio> 재생 대신 무조건 다운로드로 떨어진다(2026-09-25 사용자 실측).
|
||||
const inlineExts = ['.pdf', '.txt', '.csv', '.md', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.avi', '.mp3', '.m4a', '.wav', '.ogg'];
|
||||
const downloadExts = ['.pptx', '.xlsx', '.xls', '.docx', '.doc', '.zip'];
|
||||
if (inlineExts.includes(ext)) {
|
||||
const encodedFilename = encodeURIComponent(filename);
|
||||
res.setHeader('Content-Disposition', `inline; filename="${encodedFilename}"; filename*=UTF-8''${encodedFilename}`);
|
||||
@@ -2989,9 +2994,30 @@ app.get('/api/files/{*filePath}', (req: express.Request, res: express.Response)
|
||||
}
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Cache-Control', 'public, max-age=60');
|
||||
res.setHeader('Accept-Ranges', 'bytes');
|
||||
// Use createReadStream instead of sendFile for cross-platform reliability (Express 5)
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
// Range 요청(영상/오디오 시킹) — 206 부분 응답. 없으면 재생은 돼도 시킹할 때마다 처음부터 전체 재수신.
|
||||
const rangeReq = String((req as any).headers?.range || '');
|
||||
const rm = /^bytes=(\d*)-(\d*)$/.exec(rangeReq);
|
||||
if (rm && stat.size > 0) {
|
||||
let start = rm[1] === '' ? Math.max(0, stat.size - parseInt(rm[2], 10)) : parseInt(rm[1], 10);
|
||||
const end = (rm[1] !== '' && rm[2] !== '') ? Math.min(parseInt(rm[2], 10), stat.size - 1) : stat.size - 1;
|
||||
if (!Number.isFinite(start) || start > end || start >= stat.size) {
|
||||
res.status(416); res.setHeader('Content-Range', `bytes */${stat.size}`); return;
|
||||
}
|
||||
res.status(206);
|
||||
res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
|
||||
res.setHeader('Content-Length', end - start + 1);
|
||||
const rs = fs.createReadStream(filePath, { start, end });
|
||||
rs.on('error', (streamErr: any) => {
|
||||
console.error('[files] stream error:', streamErr.message);
|
||||
if (!res.headersSent) res.status(500).json({ error: 'Failed to stream file' });
|
||||
});
|
||||
rs.pipe(res);
|
||||
return;
|
||||
}
|
||||
res.setHeader('Content-Length', stat.size);
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on('error', (streamErr: any) => {
|
||||
|
||||
Reference in New Issue
Block a user