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 함수는 충돌이 발생할 수 있다고 보고되어 있습니다.

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

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

  • OSOR2 2021.01.10 17:29 신고  댓글주소  수정/삭제  댓글쓰기

    웹쪽을 공부하는 학생인데 파일명 숨기는 글은 여기가 최고로 정리가 잘 되있네요. 큰 도움이 되었습니다.

  • ReadyTxT 2021.04.10 13:17 신고  댓글주소  수정/삭제  댓글쓰기

    이거 파일 아이디를 구분하는 GET 인수 없이 download.php로 접근하면 php가 자기 자신을 octet-stream으로 덮어서 php 파일이 그대로 다운로드 되는 대참사가 일어나네요...ㅋㅋ 수정이 필요 해보입니다.

    • pentode 2021.07.04 12:03 신고  댓글주소  수정/삭제

      안녕하세요. 이 글은 보안 사항을 고려하여 파일 업로드와 다운로드를 작성할 때의 개념적인 설명을 한것입니다.

      글 끝에서 예기한 것처럼 실제로 사용 가능한 코드로 만들기 위해서는 데이터베이스, 파일관련등 다양한 에러처리를 해야만 합니다.

      그러한 코드들은 직접 작성해서 사용해야겠죠.

      마지막으로 download.php를 직접 호출하면 파일관련 에러가 발생할 것입니다. download.php 파일이 그냥 다운로드 되었다면 서버 마임타입 설정에 문제가 있는게 아닐까 생각됩니다.

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

  • Helpme 2021.08.29 16:52  댓글주소  수정/삭제  댓글쓰기

    안녕하세요, 해당 블로그 기반하여 다운로드 웹페이지를 만들었습니다. 잘 작동합니다만, 오피스 파일(워드/엑셀/파워포인트 등)의 경우, 파일을 복구하시겠습니까? 라는 메시지가 출력됩니다. 파일 복구 결과를 보니, 원래 파일이 깨지진 않은 것 같은데, 메시지가 사용자들을 불편하게 만들고 있습니다. 해결책을 알 수 있을까요? ㅜㅜ

    • pentode 2021.08.29 22:32 신고  댓글주소  수정/삭제

      안녕하세요. 이글의 소스로 오피스 파일 또는 한글 파일등을 테스트 해보아도 문제 없이 실행이 되었습니다.

      원본과 다운로드 파일의 파일 크기가 다르지 않은지 확인을 해보세요. 파일에 마우스 오른쪽 키를 눌러 속성을 보면 바이트 단위까지의 크기를 확인할 수 있습니다. 그리고 혹시 원본에 문제가 있었던것이 아닌지도 확인해보세요.

      잘 해결되길 바라겠습니다.

  • 코학 2021.09.19 03:30  댓글주소  수정/삭제  댓글쓰기

    혹시 업로드를 하니
    upload_process.php
    의 주소에서 계속 하얀색인데 어떻게 오류를 찾을수 있을까요?

    • pentode 2021.10.09 13:11 신고  댓글주소  수정/삭제

      php.ini 파일에 에러를 보여주는부분을

      error_reporting = E_ALL

      처럼 해 두면 모든 에러를 볼 수 있습니다.

      오류 찾기를 바라겠습니다.

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