웹 사이트를 운영하다보면 파일업로드 기능을 많이 사용하기도 하지만 직접 구현해야할때도 있다. 이럴땐 여기저기서 예제소스 가져다가 붙이곤 하는데 예외처리가 많이 안되다보니 사용하다 업로드에 문제가 생겼을 시 체크하기가 어렵다.  특히나 파일 업로드 권한이 특정 권한을 가진 유저가 아닌 모든 유저에게 부여된다면 역시나 믿을 수 없다. 

 

하루에도 수백번씩 이상한 방법을 총 동원해서 사이트의 여러군데를 공격하는 바퀴벌레 같은 녀석들이 많기때문에 기능을 만들어서 서비스 할때는 언제나 신중한 설계와 보안이 필요하다. 다만 이번에 소개할 파일업로드 예제소스는 보안을 크게 신경쓰지는 않았으니 소스는 참고하되 언제나 보안은 서버 환경에 맞게 처리해주는 센스가 필요하다. 

 

파일업로드 예제소스
<?php
if(!empty($_POST) || !empty($_FILES)){ 

   $response = array(); // 응답
   $uploadDir = dirname(__FILE__)."/upfile"; // 업로드 경로 (0777 권한필요)

   // 파일업로드 에러 상세내용 변수
   $errorKey = array(
      1=>'UPLOAD_ERR_INI_SIZE (업로드된 파일이 php.ini의 upload_max_filesize 지시어를 초과합니다.)',
      2=>'UPLOAD_ERR_FORM_SIZE (업로드된 파일이 HTML 형식에 지정된 MAX_FILE_SIZE 지시문을 초과합니다.)',
      3=>'UPLOAD_ERR_PARTIAL (업로드된 파일이 부분적으로만 업로드되었습니다.)',
      4=>'UPLOAD_ERR_NO_FILE (파일이 업로드되지 않았습니다.)',
      6=>'UPLOAD_ERR_NO_TMP_DIR (임시 폴더가 없습니다.)',
      7=>'UPLOAD_ERR_CANT_WRITE (파일을 디스크에 쓰지 못했습니다.)',
      8=>'UPLOAD_ERR_EXTENSION (PHP 확장 프로그램이 파일 업로드를 중지했습니다. PHP는 파일 업로드를 중지시킨 확장자를 확인하는 방법을 제공하지 않습니다. phpinfo()로 로드된 확장 목록을 검사하면 도움이 될 수 있습니다.)',
   );

   // 업로드 가능한 파일 확장자 
   $availableFileExt = array('jpg','jpeg','png','gif','zip');

   try{

      // 디렉토리를 날짜별로 저장 - 미사용시 주석처리 (업로드 디렉토리가 없을 시 디렉토리 생성)
      $uploadDir = $uploadDir.'/'.date('Ymd');
      if( !is_dir($uploadDir) && !mkdir($uploadDir, 0777, true)){ 
         throw new Exception("업로드 디렉토리 생성에 실패하였습니다.");
      }

      // 업로드 경로 다시한번 체크 
      if(!is_dir($uploadDir)) {
         throw new Exception("업로드 가능한 디렉토리가 존재하지 않습니다.");
      }

      if( !is_readable($uploadDir) || !is_writable($uploadDir) ){
         throw new Exception("업로드 불가능한 디렉토리 입니다. (디렉토리 권한 확인)");
      }

      if( !isset($_FILES) || !is_array($_FILES) || count($_FILES) < 1 ){ throw new Exception("업로드된 파일이 없습니다.");}

      $response['files'] = $response['info'] = array();   // 업로드 정보 변수

      // 업로드 데이터 가공
      foreach($_FILES as $k=>$v){
         if( is_array($v['name']) && count($v['name']) > 0){
            foreach($v['name'] as $sk=>$sv){
               $response['files'][] = array(
                  'name'=>empty($_FILES[$k]['name'][$sk]) ? '':$_FILES[$k]['name'][$sk],
                  'type'=>empty($_FILES[$k]['type'][$sk]) ? '':$_FILES[$k]['type'][$sk],
                  'tmp_name'=>empty($_FILES[$k]['tmp_name'][$sk]) ? '':$_FILES[$k]['tmp_name'][$sk],
                  'error'=>empty($_FILES[$k]['error'][$sk]) ? '':$_FILES[$k]['error'][$sk],
                  'size'=>empty($_FILES[$k]['size'][$sk]) ? '':$_FILES[$k]['size'][$sk],
                  'key'=>$k.'['.$sk.']',
               );                
            }
         }
         else{
            $response['files'][] = array(
               'name'=>empty($_FILES[$k]['name']) ? '':$_FILES[$k]['name'],
               'type'=>empty($_FILES[$k]['type']) ? '':$_FILES[$k]['type'],
               'tmp_name'=>empty($_FILES[$k]['tmp_name']) ? '':$_FILES[$k]['tmp_name'],
               'error'=>empty($_FILES[$k]['error']) ? '':$_FILES[$k]['error'],
               'size'=>empty($_FILES[$k]['size']) ? '':$_FILES[$k]['size'],
               'key'=>$k,
            );            
         }
      }

      // 실제 업로드 체크 및 처리
      if( count($response['files']) < 1){throw new Exception("업로드 가능한 파일이 없습니다.");}
      foreach($response['files'] as $k=>$v){

         // 실패처리
         if( $v['error'] > 0 ){
            // if( $v['error'] == 4){ continue; } // 빈 파일은 실패로 기록하지 않을 경우 
            $errorMsg = empty($errorKey[$v['error']]) ? '알수없음': $errorKey[$v['error']];
            $response['info']['fail'][] = array('[ERROR] file key `'.$v['key'].'` => '.$errorMsg);
            continue;
         }        

         // 파일명처리
         $ext = end(explode(".",basename($v["name"])));
         $filename = uniqid().'.'.$ext;         

         // 확장자 체크 
         if( in_array($ext, $availableFileExt) < 1){
            $errorMsg = $ext.'확장자는 업로드가 불가능합니다('.implode(",",$availableFileExt).' 확장자만 업로드 가능)';
            $response['info']['fail'][] = array('[ERROR] file key:`'.$v['key'].'`,file name: `'.$v['name'].'` => '.$errorMsg);
            continue;            
         }

         // 업로드 처리
         move_uploaded_file($v["tmp_name"], $uploadDir . "/".$filename);

         // 성공결과 저장
         $response['info']['success'][] = array('[SUCCESS] file key `'.$v['key'].'` => '.$filename);
      } 

      if( count($response['info']['success']) > 0){ $response['msg'].= '(성공:'.count($response['info']['success']).')';}
      else{
         // 성공한게 없다면 실패처리 
         throw new Exception("파일 업로드에 실패하였습니다.");
      }

      if( count($response['info']['fail']) > 0){ $response['msg'].= '(실패:'.count($response['info']['fail']).')';}

      // 성공처리
      $response['rst'] = 'success';
      $response['msg'] = '파일 업로드가 완료되었습니다.';

   }catch(Exception $e){
      $response['rst'] = 'fail';
      $response['msg'] = $e->getMessage();
      $response['code'] = $e->getCode();
      $response['line'] = $e->getLine();
   }

   // 결과출력
   if( $response['rst'] == 'success'){
      echo $response['msg'];
   }
   else{
      echo $response['msg']."(code:".$response['code'].", line:".$response['line'].")";
   }

   // 전체출력결과 시 
   // echo '<pre>'; print_r($response); echo '</pre>';
}
?>
<form id="form" action="<?php echo $_SERVER['REQUEST_URI']; ?>" method="POST" enctype="multipart/form-data" >
    
    <p>싱글파일: <input type="file" name="files" value=""></p>
    <p>멀티파일: <input type="file" name="filesMultiple[]" value="" multiple=""></p>
    <input type="submit" value="업로드">
</form>

 

위와 같이 예제소스를 실행하면 파일 첨부를 할 수 있으며 싱글,멀티파일 지원이 되기때문에 필요한 부분만 사용해도 무관하다. 참고로 멀티파일의 경우 input 에 `multiple` 을 붙이면 반영이 되는데 업로드 처리시 에는  $_FILES 의 형태가 싱글처리와는 다르니 주의가 필요하다. 물론 예제소스는 해당 처리가 되어있기 때문에 고려할 필요는 없다.