관리 메뉴

투덜이 개발자

[PHP] 파일 다운로드 함수 본문

Program Language/PHP

[PHP] 파일 다운로드 함수

엠투 2025. 3. 27. 12:53
반응형

PHP 파일 다운로드 함수

<?php
	
	function downloadFile($filePath, $fileName = null, $useStreaming = false, $bufferSize = 1048576) {
		// 파일이 존재하는지 확인
		if (!file_exists($filePath)) {
			// echo "<script>alert('파일을 찾을 수 없습니다.'); history.back();</script>";
			header('Content-Type: application/json');
			header("HTTP/1.0 404 Not Found");
			echo json_encode(["error" => "파일을 찾을 수 없습니다."]);
			exit;
		}
		
		// 파일 크기 미리 가져오기 (최적화)
		$fileSize = filesize($filePath);
		
		// 파일 이름 설정 (제공되지 않으면 파일 경로에서 추출)
		if ($fileName === null) {
			$fileName = basename($filePath);
		}
		
		$mimeType = 'application/octet-stream'; // MIME 타입 기본값 설정
		
		if (extension_loaded('fileinfo')) {
			$finfo = finfo_open(FILEINFO_MIME_TYPE);
			$detectedType = finfo_file($finfo, $filePath);
			finfo_close($finfo);
			
			$mimeType = $detectedType ?: $mimeType; // 감지된 타입이 유효하면 사용
		}
		
		// 한글 파일명 처리를 위한 URL 인코딩
		$encodedFileName = rawurlencode($fileName);
		
		// 출력 버퍼 제거 (예상치 못한 출력 방지)
		if (ob_get_level()) {
			ob_end_clean();
		}
		
		// 다운로드 헤더 설정
		header('Content-Description: File Transfer');
		header("Content-Type: $mimeType");
		header('Content-Disposition: attachment; filename="' . $fileName . '"; filename*=UTF-8\'\'' . $encodedFileName);
		header('Expires: 0');
		header('Cache-Control: no-cache, must-revalidate');
		header('Pragma: no-cache');
		header('Content-Length: ' . $fileSize);
		header('X-Content-Type-Options: nosniff'); // MIME 타입 스니핑 방지
		header('Accept-Ranges: bytes'); // 대용량 파일 다운로드 최적화
		
		if ($useStreaming) {
			// **스트리밍 다운로드 방식**
			$handle = fopen($filePath, 'rb');
			if ($handle === false) {
				// echo "<script>alert('파일을 열 수 없습니다.'); history.back();</script>";
				header('Content-Type: application/json');
				header("HTTP/1.0 500 Internal Server Error");
				echo json_encode(["error" => "파일을 열 수 없습니다."]);
				exit;
			}
			
			while (!feof($handle)) {
				echo fread($handle, $bufferSize);
				ob_flush();
				flush();
			}
			
			fclose($handle);
		} else {
			// **일반 다운로드 방식**
			readfile($filePath);
			flush(); // 대용량 다운로드 안정성 향상
		}
		
		exit;
	}
	
	// 사용 예시:
	// 일반 파일 다운로드
	// downloadFile('/path/to/your/file.mp4');
	
	// 스트리밍 다운로드
	// downloadFile('/path/to/your/large_video.mp4', 'my_large_video.mp4', true);
?>

 

모듈화 처리하여 아래와 같이 변경

<?php
	
	function downloadFile($filePath, $fileName = null, $useStreaming = false, $bufferSize = 1048576) {
		// 파일 경로 보안 검증
		$realPath = realpath($filePath);
		if ($realPath === false || !file_exists($realPath)) {
			sendErrorResponse("파일을 찾을 수 없습니다.", 404);
		}
		
		// 파일 읽기 권한 확인
		if (!is_readable($realPath)) {
			sendErrorResponse("파일에 접근할 수 없습니다.", 403);
		}
		
		// 파일 크기 확인
		$fileSize = filesize($realPath);
		if ($fileSize === false) {
			sendErrorResponse("파일 크기를 확인할 수 없습니다.", 500);
		}
		
		// 파일 이름 설정
		$fileName = $fileName ?? basename($realPath);
		$encodedFileName = rawurlencode($fileName);
		
		// MIME 타입 감지
		$mimeType = detectMimeType($realPath);
		
		// 출력 버퍼 정리
		clearOutputBuffers();
		
		// 다운로드 헤더 설정
		setDownloadHeaders($fileName, $encodedFileName, $mimeType, $fileSize);
		
		// 파일 전송
		if ($useStreaming) {
			// **스트리밍 다운로드 방식**
			streamFile($realPath, $bufferSize);
		} else {
			// **일반 다운로드 방식**
			readfile($realPath);
			@ob_flush();
			@flush();
		}
		
		exit;
	}
	
	/**
	 * 오류 응답 전송
	 */
	function sendErrorResponse($message, $statusCode = 500) {
		header('Content-Type: application/json');
		header("HTTP/1.0 {$statusCode}");
		echo json_encode(["error" => $message]);
		exit;
	}
	
	/**
	 * MIME 타입 감지
	 */
	function detectMimeType($filePath) {
		$mimeType = 'application/octet-stream'; // 기본값 설정
		
		if (extension_loaded('fileinfo')) {
			$finfo = finfo_open(FILEINFO_MIME_TYPE);
			$detectedType = finfo_file($finfo, $filePath);
			finfo_close($finfo);
			
			$mimeType = $detectedType ?: $mimeType; // 감지된 타입이 유효하면 사용
		}
		
		return $mimeType;
	}
	
	/**
	 * 출력 버퍼 정리
	 */
	function clearOutputBuffers() {
		while (ob_get_level()) {
			ob_end_clean();
		}
	}
	
	/**
	 * 다운로드 헤더 설정
	 */
	function setDownloadHeaders($fileName, $encodedFileName, $mimeType, $fileSize) {
		header('Content-Description: File Transfer');
		header("Content-Type: {$mimeType}");
		header('Content-Disposition: attachment; filename="' . $fileName . '"; filename*=UTF-8\'\'' . $encodedFileName);
		header('Expires: 0');
		header('Cache-Control: no-store, no-cache, must-revalidate');
		header('Pragma: no-cache');
		header('Content-Length: ' . $fileSize);
		header('X-Content-Type-Options: nosniff');
		header('Accept-Ranges: bytes');
	}
	
	/**
	 * 스트리밍 방식으로 파일 전송
	 */
	function streamFile($filePath, $bufferSize) {
		$handle = fopen($filePath, 'rb');
		if ($handle === false) {
			sendErrorResponse("파일을 열 수 없습니다.", 500);
		}
		
		while (!feof($handle) && ($buffer = fread($handle, $bufferSize)) !== false) {
			echo $buffer;
			ob_flush();
			flush();
		}
		
		fclose($handle);
	}

 

클래스 화

	class DownloadFile
	{
		public int $defaultBufferSize; // 기본 버퍼 크기 속성
		
		function __construct(int $defaultBufferSize = 1048576)
		{
			$this->defaultBufferSize = $defaultBufferSize;
		}
		
		public function downloadFile($filePath, $fileName = null, $useStreaming = false, $bufferSize = null)
		{
			// 파일 경로 보안 검증
			$realPath = realpath($filePath);
			if ($realPath === false || !file_exists($realPath)) {
				self::sendErrorResponse("파일을 찾을 수 없습니다.", 404);
			}
			
			// 파일 읽기 권한 확인
			if (!is_readable($realPath)) {
				self::sendErrorResponse("파일에 접근할 수 없습니다.", 403);
			}
			
			// 파일 크기 확인
			$fileSize = filesize($realPath);
			if ($fileSize === false) {
				self::sendErrorResponse("파일 크기를 확인할 수 없습니다.", 500);
			}
			
			// 파일 이름 설정
			$fileName = $fileName ?? basename($realPath);
			$encodedFileName = rawurlencode($fileName);
			
			// MIME 타입 감지
			$mimeType = self::detectMimeType($realPath);
			
			// 출력 버퍼 정리
			self::clearOutputBuffers();
			
			// 다운로드 헤더 설정
			self::setDownloadHeaders($fileName, $encodedFileName, $mimeType, $fileSize);
			
			// 파일 전송
			if ($useStreaming) {
				// **스트리밍 다운로드 방식**
				$bufferSizeToUse = $bufferSize ?? $this->defaultBufferSize; // 인자로 받은 버퍼 크기 우선 사용
				self::streamFile($realPath, $bufferSizeToUse);
				
			} else {
				// **일반 다운로드 방식**
				readfile($realPath);
				@ob_flush();
				@flush();
			}
			
			exit;
		}
		
		/**
		 * 오류 응답 전송
		 */
		private function sendErrorResponse($message, $statusCode = 500)
		{
			header('Content-Type: application/json');
			header("HTTP/1.0 {$statusCode}");
			echo json_encode(["error" => $message]);
			exit;
		}
		
		/**
		 * MIME 타입 감지
		 */
		private function detectMimeType($filePath)
		{
			$mimeType = 'application/octet-stream'; // 기본값 설정
			
			if (extension_loaded('fileinfo')) {
				$finfo = finfo_open(FILEINFO_MIME_TYPE);
				$detectedType = finfo_file($finfo, $filePath);
				finfo_close($finfo);
				
				$mimeType = $detectedType ?: $mimeType; // 감지된 타입이 유효하면 사용
			}
			
			return $mimeType;
		}
		
		/**
		 * 출력 버퍼 정리
		 */
		private function clearOutputBuffers()
		{
			while (ob_get_level()) {
				ob_end_clean();
			}
		}
		
		/**
		 * 다운로드 헤더 설정
		 */
		private function setDownloadHeaders($fileName, $encodedFileName, $mimeType, $fileSize)
		{
			header('Content-Description: File Transfer');
			header("Content-Type: {$mimeType}");
			header('Content-Disposition: attachment; filename="' . $fileName . '"; filename*=UTF-8\'\'' . $encodedFileName);
			header('Expires: 0');
			header('Cache-Control: no-store, no-cache, must-revalidate');
			header('Pragma: no-cache');
			header('Content-Length: ' . $fileSize);
			header('X-Content-Type-Options: nosniff');
			header('Accept-Ranges: bytes');
		}
		
		/**
		 * 스트리밍 방식으로 파일 전송
		 */
		private function streamFile($filePath, $bufferSize)
		{
			$handle = fopen($filePath, 'rb');
			if ($handle === false) {
				self::sendErrorResponse("파일을 열 수 없습니다.", 500);
			}
			
			while (!feof($handle) && ($buffer = fread($handle, $bufferSize)) !== false) {
				echo $buffer;
				ob_flush();
				flush();
			}
			
			fclose($handle);
		}
		
		
	}
반응형