PHP에서 대용량 파일을 다운로드할 때 메모리 부족, 타임아웃, 파일 손상 등의 문제가 발생할 수 있습니다. 다음은 주요 원인과 해결책들입니다.
문제: 대용량 파일을 한 번에 메모리로 로드하여 메모리 부족 오류
해결책:
function downloadLargeFile($filePath) {
if (!file_exists($filePath)) {
http_response_code(404);
exit('File not found');
}
$fileName = basename($filePath);
$fileSize = filesize($filePath);
header('Content-Type: application/octet-stream');
header("Content-Disposition: attachment; filename=\"$fileName\"");
header("Content-Length: $fileSize");
$handle = fopen($filePath, 'rb');
while (!feof($handle)) {
echo fread($handle, 8192); // 8KB 청크로 읽기
flush();
}
fclose($handle);
exit();
}
문제: PHP 기본 설정으로 인한 메모리 및 시간 제한
해결책:
// 다운로드 시작 전 설정 변경
function optimizeForDownload() {
ini_set('memory_limit', '256M');
ini_set('max_execution_time', 0); // 무제한
ini_set('output_buffering', 'Off');
// 출력 버퍼링 해제
while (ob_get_level()) {
ob_end_clean();
}
}
function downloadFile($filePath) {
optimizeForDownload();
// 파일 다운로드 로직
$handle = fopen($filePath, 'rb');
while (!feof($handle)) {
echo fread($handle, 4096);
flush();
}
fclose($handle);
}
문제: 네트워크 중단 시 처음부터 다시 다운로드해야 함
해결책:
function downloadWithRange($filePath) {
$fileSize = filesize($filePath);
$start = 0;
$end = $fileSize - 1;
if (isset($_SERVER['HTTP_RANGE'])) {
if (preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches)) {
$start = intval($matches[1]);
if (!empty($matches[2])) {
$end = intval($matches[2]);
}
}
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$fileSize");
} else {
header('HTTP/1.1 200 OK');
}
header('Accept-Ranges: bytes');
header('Content-Length: ' . ($end - $start + 1));
header('Content-Type: application/octet-stream');
$handle = fopen($filePath, 'rb');
fseek($handle, $start);
$remaining = $end - $start + 1;
while ($remaining > 0 && !feof($handle)) {
$chunkSize = min(8192, $remaining);
echo fread($handle, $chunkSize);
$remaining -= $chunkSize;
flush();
}
fclose($handle);
}
문제: 대용량 파일 다운로드 진행 상황을 알 수 없음
해결책:
function downloadWithProgress($filePath, $sessionId) {
$fileSize = filesize($filePath);
$downloaded = 0;
header('Content-Length: ' . $fileSize);
header('Content-Type: application/octet-stream');
$handle = fopen($filePath, 'rb');
while (!feof($handle)) {
$chunk = fread($handle, 8192);
echo $chunk;
$downloaded += strlen($chunk);
$progress = round(($downloaded / $fileSize) * 100, 2);
// 세션에 진행률 저장
file_put_contents("/tmp/download_progress_$sessionId", $progress);
flush();
}
fclose($handle);
// 완료 후 진행률 파일 삭제
unlink("/tmp/download_progress_$sessionId");
}
// 진행률 확인 API
if (isset($_GET['progress'])) {
$sessionId = $_GET['session_id'];
$progress = file_get_contents("/tmp/download_progress_$sessionId");
echo json_encode(['progress' => $progress ?: 0]);
exit();
}
문제: 경로 조작 공격으로 인한 보안 취약점
해결책:
function secureDownload($fileName) {
$uploadDir = '/var/www/uploads/';
// 파일명 검증
if (strpos($fileName, '..') !== false) {
http_response_code(400);
exit('Invalid file name');
}
$filePath = realpath($uploadDir . basename($fileName));
// 업로드 디렉토리 내부인지 확인
if (strpos($filePath, realpath($uploadDir)) !== 0) {
http_response_code(403);
exit('Access denied');
}
if (!file_exists($filePath)) {
http_response_code(404);
exit('File not found');
}
downloadLargeFile($filePath);
}
문제: 원본 파일이 다운로드 중 변경되어 파일 손상
해결책:
function downloadViaTempFile($originalPath) {
$tempFile = tempnam(sys_get_temp_dir(), 'download_');
// 원본 파일을 임시 파일로 복사
if (!copy($originalPath, $tempFile)) {
http_response_code(500);
exit('Cannot prepare file for download');
}
$fileName = basename($originalPath);
header("Content-Disposition: attachment; filename=\"$fileName\"");
$handle = fopen($tempFile, 'rb');
while (!feof($handle)) {
echo fread($handle, 8192);
flush();
}
fclose($handle);
// 임시 파일 삭제
unlink($tempFile);
}
문제: 무제한 다운로드로 인한 서버 부하
해결책:
function downloadWithLimit($filePath, $userId) {
$logFile = '/tmp/download_log.txt';
$currentTime = time();
// 다운로드 로그 확인
$logs = file_exists($logFile) ? file($logFile, FILE_IGNORE_NEW_LINES) : [];
$recentDownloads = 0;
foreach ($logs as $log) {
list($logTime, $logUserId) = explode('|', $log);
if ($logUserId == $userId && ($currentTime - $logTime) < 3600) { // 1시간 내
$recentDownloads++;
}
}
// 시간당 5회 제한
if ($recentDownloads >= 5) {
http_response_code(429);
exit('Too many downloads. Please wait.');
}
// 로그 기록
file_put_contents($logFile, "$currentTime|$userId\n", FILE_APPEND);
// 파일 다운로드 실행
downloadLargeFile($filePath);
}
문제: 여러 파일을 압축하여 다운로드할 때 메모리 부족
해결책:
function streamZipDownload($files) {
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="files.zip"');
$zip = new ZipStream\ZipStream('files.zip');
foreach ($files as $filePath) {
if (file_exists($filePath)) {
$fileName = basename($filePath);
$zip->addFileFromPath($fileName, $filePath);
}
}
$zip->finish();
}
// 또는 간단한 방법
function createTempZip($files) {
$tempZip = tempnam(sys_get_temp_dir(), 'download_') . '.zip';
$zip = new ZipArchive();
if ($zip->open($tempZip, ZipArchive::CREATE) === TRUE) {
foreach ($files as $filePath) {
$zip->addFile($filePath, basename($filePath));
}
$zip->close();
downloadLargeFile($tempZip);
unlink($tempZip);
}
}