Files
sam-kd/voice_ai/download_audio.php

191 lines
7.4 KiB
PHP
Raw Normal View History

<?php
// 출력 버퍼링 시작
ob_start();
error_reporting(0);
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
require_once($_SERVER['DOCUMENT_ROOT'] . "/lib/mydb.php");
// 권한 체크
if (!isset($user_id) || $level > 5) {
header('HTTP/1.0 403 Forbidden');
die('접근 권한이 없습니다.');
}
// 회의 ID 확인
$meeting_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$tenant_id = $user_id; // session.php에서 user_id를 tenant_id로 사용
if ($meeting_id <= 0) {
header('HTTP/1.0 400 Bad Request');
die('잘못된 요청입니다.');
}
try {
$pdo = db_connect();
// MVP 단계: 모든 사용자가 모든 데이터를 볼 수 있도록 tenant_id 조건 제거
$sql = "SELECT audio_file_path, title, created_at
FROM meeting_logs
WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$meeting_id]);
$meeting = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$meeting || empty($meeting['audio_file_path'])) {
header('HTTP/1.0 404 Not Found');
die('오디오 파일을 찾을 수 없습니다.');
}
// 파일 경로 구성
$file_path = $_SERVER['DOCUMENT_ROOT'] . $meeting['audio_file_path'];
// 경로 정규화 (슬래시 통일)
$file_path = str_replace('\\', '/', $file_path);
$file_path = preg_replace('#/+#', '/', $file_path);
// 파일 존재 확인
if (!file_exists($file_path)) {
// 디버깅 정보
error_log('다운로드 실패 - 파일 경로: ' . $file_path);
error_log('다운로드 실패 - DB 경로: ' . $meeting['audio_file_path']);
error_log('다운로드 실패 - DOCUMENT_ROOT: ' . $_SERVER['DOCUMENT_ROOT']);
error_log('다운로드 실패 - 파일 존재 여부: ' . (file_exists($file_path) ? 'YES' : 'NO'));
// 대체 경로 시도 (경로가 잘못된 경우)
$alt_path = $_SERVER['DOCUMENT_ROOT'] . str_replace('//', '/', $meeting['audio_file_path']);
if (file_exists($alt_path)) {
$file_path = $alt_path;
} else {
header('HTTP/1.0 404 Not Found');
header('Content-Type: text/plain; charset=utf-8');
die('오디오 파일이 서버에 존재하지 않습니다.\n경로: ' . $file_path);
}
}
// 파일 확장자 확인 및 MIME 타입 설정
// 파일 경로에서 직접 확장자 추출
$file_extension = '';
if (preg_match('/\.([a-z0-9]+)$/i', $file_path, $matches)) {
$file_extension = strtolower($matches[1]);
}
// 확장자가 없으면 경로에서 추출 시도
if (empty($file_extension)) {
$path_info = pathinfo($file_path);
$file_extension = isset($path_info['extension']) ? strtolower($path_info['extension']) : '';
}
// 확장자가 여전히 없으면 webm으로 기본 설정
if (empty($file_extension)) {
$file_extension = 'webm';
}
$mime_types = [
'webm' => 'audio/webm',
'wav' => 'audio/wav',
'mp3' => 'audio/mpeg',
'ogg' => 'audio/ogg',
'm4a' => 'audio/mp4'
];
$content_type = isset($mime_types[$file_extension])
? $mime_types[$file_extension]
: 'audio/webm'; // 기본값을 audio/webm으로 설정
// 다운로드 파일명 생성 (회의록 + 제목 + 날짜 + 확장자)
$title = $meeting['title'] ?: '회의녹음';
$date = date('Ymd_His', strtotime($meeting['created_at']));
// 파일명 안전하게 처리 (특수문자 제거, 공백을 언더스코어로)
$safe_title = preg_replace('/[^a-zA-Z0-9가-힣_\-]/u', '_', $title);
$safe_title = preg_replace('/\s+/', '_', $safe_title);
$safe_title = trim($safe_title, '_');
// 제목이 비어있거나 언더스코어만 있는 경우 기본값 사용
if (empty($safe_title) || preg_match('/^_+$/', $safe_title)) {
$safe_title = '회의녹음';
}
// 제목에 "회의록"이 이미 포함되어 있으면 제거 (중복 방지)
$safe_title = preg_replace('/^회의록[_\s]*/u', '', $safe_title);
$safe_title = preg_replace('/[_\s]*회의록$/u', '', $safe_title);
$safe_title = trim($safe_title, '_');
// 제목이 비어있으면 기본값 사용
if (empty($safe_title)) {
$safe_title = '회의녹음';
}
// 파일명 앞에 "회의록" 추가 (한 번만)
$download_filename = '회의록_' . $safe_title . '_' . $date . '.' . $file_extension;
// 출력 버퍼 비우기
ob_clean();
// 파일명이 확장자를 포함하는지 최종 확인
if (!preg_match('/\.' . preg_quote($file_extension, '/') . '$/i', $download_filename)) {
$download_filename .= '.' . $file_extension;
}
// 헤더 설정 (브라우저 호환성을 위해 파일명 처리)
header('Content-Type: ' . $content_type);
// 파일명 처리: "회의록"을 "Meeting"으로 변환하여 브라우저 호환성 확보
$ascii_filename = str_replace('회의록', 'Meeting', $download_filename);
// "Meeting"이 중복되지 않도록 처리 (제목에 이미 "Meeting"이 포함된 경우)
$ascii_filename = preg_replace('/Meeting[_\s]*Meeting/i', 'Meeting', $ascii_filename);
// 나머지 한글과 특수문자를 언더스코어로 변환
$ascii_filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $ascii_filename);
// 연속된 언더스코어를 하나로 통합
$ascii_filename = preg_replace('/_+/', '_', $ascii_filename);
// 확장자가 확실히 포함되도록 재확인
if (!preg_match('/\.' . preg_quote($file_extension, '/') . '$/i', $ascii_filename)) {
$ascii_filename .= '.' . $file_extension;
}
// 파일 크기 재확인
$file_size = filesize($file_path);
if ($file_size === false || $file_size == 0) {
header('HTTP/1.0 500 Internal Server Error');
header('Content-Type: text/plain; charset=utf-8');
die('파일을 읽을 수 없습니다. (크기: ' . $file_size . ')');
}
// Content-Disposition 헤더를 더 명확하게 설정 (RFC 5987 형식)
// 브라우저 호환성을 위해 두 가지 형식 모두 사용
$encoded_filename = rawurlencode($ascii_filename);
header('Content-Disposition: attachment; filename="' . $ascii_filename . '"; filename*=UTF-8\'\'' . $encoded_filename);
header('Content-Length: ' . $file_size);
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Expires: 0');
header('X-Content-Type-Options: nosniff');
// 디버깅용 (필요시 주석 해제)
// error_log('Download: ' . $download_filename . ' (extension: ' . $file_extension . ', path: ' . $file_path . ', size: ' . $file_size . ')');
// 파일 출력 (청크 단위로 읽어서 메모리 효율성 향상)
$handle = @fopen($file_path, 'rb');
if ($handle === false) {
header('HTTP/1.0 500 Internal Server Error');
header('Content-Type: text/plain; charset=utf-8');
die('파일을 열 수 없습니다.');
}
// 청크 단위로 출력 (8KB씩)
while (!feof($handle)) {
$chunk = fread($handle, 8192);
if ($chunk === false) {
break;
}
echo $chunk;
flush();
}
fclose($handle);
exit;
} catch (Exception $e) {
header('HTTP/1.0 500 Internal Server Error');
die('파일 다운로드 중 오류가 발생했습니다.');
}