PHP 대용량 파일 다운로드 오류 문제 해결하기

프리터코더·2025년 5월 30일
0

php 문제 해결

목록 보기
49/79

PHP에서 대용량 파일을 다운로드할 때 메모리 부족, 타임아웃, 파일 손상 등의 문제가 발생할 수 있습니다. 다음은 주요 원인과 해결책들입니다.

1. 청크 단위 파일 읽기

문제: 대용량 파일을 한 번에 메모리로 로드하여 메모리 부족 오류

해결책:

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();
}

2. PHP 설정 최적화

문제: 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);
}

3. Range 헤더 지원 (부분 다운로드)

문제: 네트워크 중단 시 처음부터 다시 다운로드해야 함

해결책:

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);
}

4. 다운로드 진행률 추적

문제: 대용량 파일 다운로드 진행 상황을 알 수 없음

해결책:

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();
}

5. 안전한 파일 경로 처리

문제: 경로 조작 공격으로 인한 보안 취약점

해결책:

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);
}

6. 임시 파일을 통한 안전한 다운로드

문제: 원본 파일이 다운로드 중 변경되어 파일 손상

해결책:

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);
}

7. 다운로드 로그 및 제한

문제: 무제한 다운로드로 인한 서버 부하

해결책:

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);
}

8. 스트리밍 ZIP 다운로드

문제: 여러 파일을 압축하여 다운로드할 때 메모리 부족

해결책:

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);
    }
}
profile
일용직 개발자. freetercoder@gmail.com

0개의 댓글