PHP 파일 업로드와 다운로드 만들기

프로그래밍/PHP 2018. 4. 21. 00:23

PHP 에서 간단히 파일 업로드와 다운로드를 구현해 봅니다. 파일 업로드 할때는 파일명 중복 방지와 파일명을 추측해서 다운로드 하는 것을 방지하기 위해 랜덤하게 파일명을 만들어서 서버에 저장합니다. 보안을 위해서는 파일 업로드 위치를 웹루트 밖에 하는것이 좋습니다.


파일 정보를 데이터베이스에 저장할 때도 파일을 구분할 파일 아이디를 예측 가능하지 않게 만들어서 다운로드시 사용합니다. 보안을 위해서는 다운로드 프로그램에 권한이 적용되면 더 좋습니다.


전체 예제 파일은 글 하단에 첨부하여 두었습니다.



1. 파일정보를 저장할 테이블 구조

- 데이터베이스는 캐릭터셋 utf8, collation은 utf8_general_ci 로 만들었습니다.


CREATE TABLE upload_file (

  file_id   VARCHAR(255) NOT NULL PRIMARY KEY,

  name_orig VARCHAR(255),

  name_save VARCHAR(255),

  reg_time  TIMESTAMP NOT NULL

);


- file_id : 각각의 파일을 유일하게 구별하는 아이디 값입니다.

- name_orig : 업로드 당시의 원래 파일 이름입니다. 다운로드시 이 이름으로 파일이 생성됩니다.

- name_save : 예측하기 힘들게 변경된 파일 이름입니다. 이 이름으로 서버에 파일이 저장됩니다.

- reg_time : 파일이 업로드된 시간 입니다.



2. 파일 업로드 폼(up/upload.php)


<script type="text/javascript">

function formSubmit(f) {

    // 업로드 할 수 있는 파일 확장자를 제한합니다.

var extArray = new Array('hwp','xls','doc','xlsx','docx','pdf','jpg','gif','png','txt','ppt','pptx');

var path = document.getElementById("upfile").value;

if(path == "") {

alert("파일을 선택해 주세요.");

return false;

}

var pos = path.indexOf(".");

if(pos < 0) {

alert("확장자가 없는파일 입니다.");

return false;

}

var ext = path.slice(path.indexOf(".") + 1).toLowerCase();

var checkExt = false;

for(var i = 0; i < extArray.length; i++) {

if(ext == extArray[i]) {

checkExt = true;

break;

}

}


if(checkExt == false) {

alert("업로드 할 수 없는 파일 확장자 입니다.");

    return false;

}

return true;

}

</script>


<form name="uploadForm" id="uploadForm" method="post" action="upload_process.php" 

      enctype="multipart/form-data" onsubmit="return formSubmit(this);">

    <div>

        <label for="upfile">첨부파일</label>

        <input type="file" name="upfile" id="upfile" />

    </div>

    <input type="submit" value="업로드" />

</form>


- 업로드 폼을 체크하는 formSubmit(); 자바스크립트에서 업로드 할 수 있는 파일의 확장자를 제한하고 있습니다.

- 파일 업로드를 위해서 method="post" 이고, enctype="multipart/form-data" 로 지정해야 합니다.

- 파일 선택 엘리먼트의 이름은 "upfile" 입니다.






3. 서버측 파일 업로드 처리(upload_process.php)


<?php

$db_conn = mysqli_connect("localhost", "testdbadm", "testdbadm", "testdb");


if(isset($_FILES['upfile']) && $_FILES['upfile']['name'] != "") {

    $file = $_FILES['upfile'];

    $upload_directory = 'data/';

    $ext_str = "hwp,xls,doc,xlsx,docx,pdf,jpg,gif,png,txt,ppt,pptx";

    $allowed_extensions = explode(',', $ext_str);

    

    $max_file_size = 5242880;

    $ext = substr($file['name'], strrpos($file['name'], '.') + 1);

    

    // 확장자 체크

    if(!in_array($ext, $allowed_extensions)) {

        echo "업로드할 수 없는 확장자 입니다.";

    }

    

    // 파일 크기 체크

    if($file['size'] >= $max_file_size) {

        echo "5MB 까지만 업로드 가능합니다.";

    }

    

    $path = md5(microtime()) . '.' . $ext;

    if(move_uploaded_file($file['tmp_name'], $upload_directory.$path)) {

        $query = "INSERT INTO upload_file (file_id, name_orig, name_save, reg_time) VALUES(?,?,?,now())";

        $file_id = md5(uniqid(rand(), true));

        $name_orig = $file['name'];

        $name_save = $path;

        

        $stmt = mysqli_prepare($db_conn, $query);

        $bind = mysqli_stmt_bind_param($stmt, "sss", $file_id, $name_orig, $name_save);

        $exec = mysqli_stmt_execute($stmt);

      

        mysqli_stmt_close($stmt);

        

        echo"<h3>파일 업로드 성공</h3>";

        echo '<a href="file_list.php">업로드 파일 목록</a>';

        

    }

} else {

    echo "<h3>파일이 업로드 되지 않았습니다.</h3>";

    echo '<a href="javascript:history.go(-1);">이전 페이지</a>';

}


mysqli_close($db_conn);

?>


- 데이터베이스는 mysqli 를 사용해서 prepared statement로 작업을 했습니다.SQL Injection을 대비하는 가장 좋은 방법 입니다.

- 서버측에서도 업로드된 파일의 확장자를 체크해야 합니다.

- 업로드 가능한 파일 용량도 체크합니다. 제한을 하지 않을 경우 대용량 파일을 업로드 하여 서버를 다운시키는 공격을 받을 수 있습니다.

- $file_iduniqid(rand(), true) 함수로 생성한 유일한 값을 md5() 로 해시해서 추측하기 힘들 값을 만들어 사용합니다.



4. 파일 목록 조회(file_list.php)


<table border="1">

<tr>

<th>파일 아이디</th>

<th>원래 파일명</th>

<th>저장된 파일명</th>

</tr>

<?php

$db_conn = mysqli_connect("localhost", "testdbadm", "testdbadm", "testdb");

$query = "SELECT file_id, name_orig, name_save FROM upload_file ORDER BY reg_time DESC";

$stmt = mysqli_prepare($db_conn, $query);

$exec = mysqli_stmt_execute($stmt);

$result = mysqli_stmt_get_result($stmt);

while($row = mysqli_fetch_assoc($result)) {

?>

<tr>

  <td><?= $row['file_id'] ?></td>

  <td><a href="download.php?file_id=<?= $row['file_id'] ?>" target="_blank"><?= $row['name_orig'] ?></a></td>

  <td><?= $row['name_save'] ?></td>

</tr>

<?php


mysqli_free_result($result); 

mysqli_stmt_close($stmt);

mysqli_close($db_conn);

?>

</table>


- 파일 다운로드를 위해 파일 목록을 조회합니다.

- download.php?file_id=<?= $row['file_id'] ?> 로 파일 다운로드 링크를 만듭니다. 파일 아이디 단순한 번호로 만들면 번호만 바꿔서 호출하여 다른 파일을 다운로드 받을 수 있게 됩니다. 필요하다면 실제 구현에서는 권한을 적용하여 자신에게 다운로드 권한이 없는 파일을 다운 받을 수 없도록 하여야 겠습니다.




5. 파일 다운로드(download.php)


<?php

$file_id = $_REQUEST['file_id'];


$db_conn = mysqli_connect("localhost", "testdbadm", "testdbadm", "testdb");

$query = "SELECT file_id, name_orig, name_save FROM upload_file WHERE file_id = ?";

$stmt = mysqli_prepare($db_conn, $query);


$bind = mysqli_stmt_bind_param($stmt, "s", $file_id);

$exec = mysqli_stmt_execute($stmt);


$result = mysqli_stmt_get_result($stmt);

$row = mysqli_fetch_assoc($result);


$name_orig = $row['name_orig'];

$name_save = $row['name_save'];


$fileDir = "data/";

$fullPath = $fileDir."/".$name_save;

$length = filesize($fullPath);


header("Content-Type: application/octet-stream");

header("Content-Length: $length");

header("Content-Disposition: attachment; filename=".iconv('utf-8','euc-kr',$name_orig));

header("Content-Transfer-Encoding: binary");


$fh = fopen($fullPath, "r");

fpassthru($fh);


mysqli_free_result($result);

mysqli_stmt_close($stmt);

mysqli_close($db_conn);


exit;

?>


- GET 방식으로 받은 file_id를 이용하여 파일정보를 조회합니다.

- Content-Disposition 헤더를 사용하여 다운로드될 파일 이름을 원래 파일명으로 지정합니다. 데이터베이스에 UTF-8로 데이터가 저장되어 있으므로 euc-kr로 변환해야 한글 파일명이 깨지지 않습니다.



이것으로 간단히 PHP 에서 파일을 업로드하고 다운로드 하는 방법을 알아보았습니다. 실제 구현에서는 데이터베이스 연결정보를 추출해서 include 파일로 만들고, 업로드 위치를 웹루트 밖으로 바꾸고, 업로드/다운로드에 권한을 적용하고, 데이터베이스 에러처리, 파일 관련 에러처리 등이 추가되어야 겠습니다.


※ 전체소스

upload.zip


댓글을 달아 주세요

  • 김동휘 2020.01.24 11:45  댓글주소  수정/삭제  댓글쓰기

    혹시 라즈베리파이에서 이걸 구축하려고 하는데 mysql이 설치되어있어야하나요?
    또 기본적인 세팅이 필요한가요?

    • pentode 2020.02.08 19:47 신고  댓글주소  수정/삭제

      안녕하세요. 이예제는 PHP와 MySQL이 사용되었고 Windows 10에서 실행한 결과 입니다. 라즈베리파이는 사용해보지 못해서 어떻게 될지 알 수 가 없네요.

  • 김침침 2020.04.08 18:09  댓글주소  수정/삭제  댓글쓰기

    좋은 글 잘 읽었습니다.

    다만 오탈자를 하나 발견해서 댓글 남기고 갑니다.
    서버사이드에 확장자명 변수 지정시
    $ext = substr($file['name'], strrpos($file['name'], '.') + 1);

    strrpos -> strpos 로 수정해야합니다.

    감사합니다.

  • 김건 (peter) 2020.06.01 15:00  댓글주소  수정/삭제  댓글쓰기

    혹시 이 스크립트 실행 시, ubuntu server에서 하시는 분은,
    php 스크립트 파일 안에, tmp 폴더를 만드시고, 아래의 명령어로 권한 주셔야 에러 없이 실행 될거에요~
    sudo chmd a+rwxt /tmp /php파일경로/tmp

    전 이렇게 해서 permission denied 관련 에러 잡았네요 ㅎ

    • pentode 2020.06.06 23:45 신고  댓글주소  수정/삭제

      네. 리눅스 서버의 경우 파일을 업로드하면 시스템 템프 폴더인 /temp 폴더에 임시 저장되고, 업로드가 완료되면 지정된 폴더로 복사가 됩니다.

      호스팅 업체의 경우 보안의 이유로 /temp 폴더의 접근을 막아두는 곳이 있는데, 풀어주지 않는다면 자신이 권한이 있는 폴더에 /temp 기능을 가지는 폴더를 만들고, PHP 설정에서 temp 폴더른 지정하여 해결하는 방법도 있겠습니다.

      방문해 주셔서 감사합니다.^^

  • 2020.06.11 14:17  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

    • 2020.06.11 22:15  댓글주소  수정/삭제

      비밀댓글입니다

    • 라이네리TV 2020.06.13 10:59 신고  댓글주소  수정/삭제

      이 댓글 달았던 사람입니다. 제가 모르고 비회원 비밀댓글 걸어버려서 다시 여쭤보는데요... 혹시 작성해주신 답글 내용 다시 한번 적어주실수 있으신가요....?

    • pentode 2020.06.26 22:24 신고  댓글주소  수정/삭제

      가져다 쓰셔도 됩니다. 하지만 이 코드로 발생하는 어떤 문제도 제가 책임지지는 않습니다.

      글 끝에도 있듯이 실제로 사용하려면 많은 부분이 추가되어야하고, 최적화도 되어야 할 것입니다.

      이러한 개념을 사용한다 정도로 생각하시고 잘 만들어 보시면 좋겠습니다.

      하나 말씀드리고 싶은것은 md5 함수는 충돌이 발생할 수 있다고 보고되어 있습니다.

      다른 값을 적용해서 같은 해시값이 나올수 있다는 것입니다. 이런 충돌 확율이 사용하려는 프로젝트에서 중요하게 작용하지 않는지 반드시 고려하셔야할 것입니다.

      하시는일 잘 되시길 바라겠습니다.^^