인터넷정보

PHP 업로드진행바(Pregress Bar) 구현

알 수 없는 사용자 2007. 10. 13. 16:22

사용자 삽입 이미지


▨▨▨▨ PHP 업로드진행바(Pregress Bar) 구현. ▨▨▨▨

- 2006.04.29
- 버그신고 : http://www.ziwoo.net
- 30일간 테스트: 테스트 게시판 기간 종료.
- 간단한 테스트: http://www.ziwoo.net/zuprogress/index.html


▨▨ 왜 필요할까?

PHP 또는 ASP 등 enctype="multipart/form-data" 로 FORM을 전송하는 경우,
업로드 파일이 서버의 특정 공간에 임시파일로 저장되어지기 전까지는 PHP가 파일에 대한 정보를 알 수 없다고 합니다. 그래서 파일이 몇%나 업로드 되고 있는지 알지 못한 채, 사용자는 지루한 화면만 보게 되는데요.. 이런 단점을 극복하고자 몇가지 방법들이 있습니다만, ActivX를이용하는 경우, 사용자에게 ActivX를 허용하겠다라는 Agreement를 받아야만 하므로 불편함이 있습니다. 때문에 ActivX의 기능을 빌리지 않고 자체적인 기능으로만 서버의 임시저장소에 올라오고 있는 임시파일을 찾아서 그 크기를 지속적으로 리포트하는 방법이 꾸준히 시도되는것 같습니다.

▨▨ 업로드 프로그레스을 위한 요건.

업로드진행바를 구현려면
첫째 업로드할 파일의 크기를 알아야 합니다.
둘째 임시저장소에 있는 다수의 임시파일중에서 업로드중인 사용자의 임시파일을 알아야 합니다.
셋째 알아낸 파일크기를 지속적으로 사용자의 화면에 보내줘야 합니다.

첫째, 업로드 하기전 파일 크기를 알아내는데에 브라우저별로 차이가 있습니다.
MS IE 에서는 파일크기를 알아올 수 있지만,
파이어폭스에서는 보안상의 이유로 브라우저가 사용자의 로컬정보를 알 수 없게 하였습니다.
기타 브라우저의 호환에 관해서는 잘 모르겠습니다만,
파일크기를 알아올 수 있는 브라우저와 그렇지 못한 브라우저로 분리하여 후자의 경우에는
전송된 크기와 남은 파일 갯수만 보여주고 진행%는 표시하지 않는것으로 하였습니다.

둘째, 임시파일과 시용자별 매치시키는 문제는 예전에 WndProc님께서 PHPSCHOOL Tip&Tech 게시판에,
임시파일의 이름을 지정한 이름으로 생성하게 하도록 PHP소스를 컴파일하는 방법을 소개한 적이 있습니다.
임시파일과 시용자별 매치시키는 문제는 PHP소스를 컴파일하는 방법이 가장 정확하겠습니다만,
버전이 바뀌면 그때마다 다시 PERL소스를 해석하고 수정해야 하는 번거로움이 있을 뿐 아니라,
웹호스팅을 이용하는 프로그래머들에겐 적당하지 않은 방법입니다.
이 문제에 관해서 외국사이트에서 임시폴더의 최근 파일을 검사하는 방법을 보고 힌트를 얻어 진행하던 중,
후에 행복한고니님께서 업그레이드 된 http://blog.joshuaeichorn.com/archives/2005/05/01/ajax-file-upload-progress/ 를 알려주었습니다. 위 사이트에서 Class로 되어있는 다운로드가 있습니다만, 이전 버전의 소스를 보고 어렵고 복잡하게 느꼈던 터라 최근버전의 소스를 참고하지는 않았습니다.
사실 내공이 부족하여 클래스는 소스분석도 잘 안됩니다. (-_-;)
임시폴더의 최근 파일을 검사하는 방법은 비교적 잘 맞지만, 업로드가 빈번한 서버의 경우 다른 사용자의 임시파일 정보를 잘못 캐치할 수도 있습니다. 설사 그렇다고 한들, 파일이 업로드 되는것과는 무관하므로 무시.(-,.-)

셋째, 임시파일의 크기를 거꾸로 사용자의 브라우저에 지속적으로 보내주는 것은, 약간의 꽁수가 필요합니다.
IFRAME으로 계속 리로드 하는방법이 있으나 이 경우 틱~틱~ 거리는 화면갱신 소리가 거슬려서 선호되지 않는 방법입니다.

<script id="Dynamic" language="JavaScript"></script>
<script>
function Repeat(){
    getElementById('Dynamic').src = "tmp_reader.php";
}
setTimeout("Repeat()",1000);
</script>

따라서 Ajax에서 사용하는 위 방법으로 해보았으나, MS IE에서는 잘 되었지만,
파이어폭스에서 되지가 않았습니다.
파이어폭스는 한번 읽은 내용을 캐쉬에 담아두고 갱신하지 않는것 같았으며,
"tmp_reader.php?랜덤쓰레기변수" 로 처리하여 다른 파일인것 처럼 해도 통하지 않았습니다.

<img id="Dynamic" width="1" height="1" src="nofile">
<script>
function Repeat(){
    getElementById('Dynamic').src = "tmp_reader.php";
}
setTimeout("Repeat()",1000);
</script>

위 방법은 언젠가 PHPSCHOOL에 메일 수신확인을 위한 꽁수로 소개된 방법입니다.
자바스크립트를 갱신하는 대신 이미지 파일을 갱신하는것으로 바꾸고,
tmp_reader.php 파일의 내용에 파일을 캐쉬하지 말라는 해더인

header("Expires:Mon,26 Jul 1997 05:00:00 GMT");
header("Last-Modified:".gmdate("D,d M Y H:i:s")."GMT");
header("Cache-Control:no-cache,must-reval!idate");
header("Content-Type: image/gif");

위 코드를 추가함으로서 파이어폭스에서도 지속적인 갱신이 가능했습니다.

▨▨ 임시파일을 알아내고 그 파일을 브라우저에 알려주는 tmp_reader.php 파일

<?
session_start();
if(!$_SESSION["upfile"]["tmp_name"] || !file_exists($_SESSION["upfile"]["tmp_name"]) || time()-filemtime($_SESSION["upfile"]["tmp_name"])>10 || $_SESSION["upfile"]["size"] == @filesize($_SESSION["upfile"]["tmp_name"])){
    $tmp_files = @glob("/tmp"."/[p][h][p]*",GLOB_NOSORT);
    $_SESSION["upfile"]["tmp_name"] = $tmp_files[0];
    $_SESSION["upfile"]["size"] = @filesize($tmp_files[0]);
}else{
    $_SESSION["upfile"]["size"] = @filesize($_SESSION["upfile"]["tmp_name"]);
}
setCookie("ZUfileSize",$_SESSION['upfile']["size"],time()+10,"/");
header("Expires:Mon,26 Jul 1997 05:00:00 GMT");
header("Last-Modified:".gmdate("D,d M Y H:i:s")."GMT");
header("Cache-Control:no-cache,must-reval!idate");
header("Content-Type: image/gif");
?>

소스는 위가 전부입니다. 최근의 임시이름과 크기를 세션으로 저장하여 계속 모니터하는 방법입니다.
소스를 보면 대충 알겠지만,
첫째 세션정보가 없는경우 최초의 업로드입니다.
둘때 세션정보에 있는 파일이 실제로 존재하지 않은 경우는 이전에 업로드 경험이 있는 사용자의 새 업로드입니다.
셋째 실제 임시파일이 10초 동안 변경되지 않았다면, 그 파일의 업로드가 완료된 것으로 간주합니다.
넷째 바로 이전에 체크했던 파일크기(세션)와 실제 파일크기가 같다면 그 파일의 업로드가 완료된 것으로 간주합니다.
위 네가지 경우에는 "php~" 이름을 가진 다른 최근파일을 찾아서 임시이름과 크기를 세션으로 저장합니다.
그렇지 않은 경우에는 업로드중이므로 파일크기만 검사해서 세션과 쿠키정보로 구워 놓습니다.
세션[파일크기는] 서버 정보이므로 클라이언트는 갱신할 수 없기 때문에 쿠키를 이용합니다.

▨▨ 일정한 간격으로 tmp_reader.php 실행, 쿠키정보 캐치, 업로드진행화면 갱신, 을 담당하는 progress.js 파일

function progress(Fname){ <= onSubmit="return progress('F')" 으로 호출되는 함수입니다.
    document.getElementById('ZU_div').style.display = "inline";
    document.getElementById('ZU_div').style.left = document.body.clientWidth/2 - 220;
    document.getElementById('ZU_div').style.top = 150;
   
    for (var i=0; i<document.forms[Fname].elements.length; i++) {
        if(document.forms[Fname].elements[i]["type"]=="file" && document.forms[Fname].elements[i]["value"]){
            array_FileName[i] = ZU_getFileName(document.forms[Fname].elements[i]["value"]);
            array_FileSize[i] = ZU_getFileSize(document.forms[Fname].elements[i]["value"]);
        }
    }
    var Extra_array_FileName = array_FileName;
    var Extra_array_FileSize = array_FileSize;
    array_FileName = new Array();
    array_FileSize = new Array();
    var c = 0;
    for (var i=0; i<Extra_array_FileName.length; i++) {
        if(Extra_array_FileName[i]){
            array_FileName[c] = Extra_array_FileName[i];
            array_FileSize[c] = Extra_array_FileSize[i];
            c++;
        }
    }
    window.status = "전송중";
    var StartTime = new Date();
    MicroStartTime = StartTime.getTime()/1000;
    var TakeStartTime = new Date();
    MicroTakeStartTime = TakeStartTime.getTime()/1000;
    ZU_repeat();
}

progress.js 파일은 위에서도 약간 언급했지만 핵심부분만 설명합니다. 자세한 내용은 소스파일을 열어서 살펴보세요.
위 함수는 <form name="F" ~~~~ onSubmit=progress('F')> 에서 Submit이 되면 일차적으로 호출되어지는 함수입니다.
<div id="ZU_div">업로드화면<div>을 보이게 하고 위치를 잡아줍니다.
submit 된 폼에서 타입이 file인 Element들의 "파일명"과 "파일크기"를 찾아내 배열에 담습니다.

function ZU_getFileSize(path){
    var obj = new Image();
    obj.dynsrc = path;
    return obj.fileSize;
}

파일크기를 찾는 ZU_getFileSize()함수의 소스는 당 파일의 상단에 위와 같이 정의되어 있습니다.
new Image(); 로 찾지만 이미지 파일이 아닌것도 잘 찾아집니다.
하단부에서 파일크기 배열의 내용이 없으면 비호환 브라우저로 보고 차전책인 화면을 내보냅니다.
사용자가 파일선택을 순차적으로 하지 않고 듬성듬성 한 경우를 고려해 배열을 앞쪽으로 정리합니다.
최초 업로드 시작시간, 선택파일 업로드 시작시간을 변수에 담아두고 본격적인 반복작업을 할 ZU_repeat()함수에 시동을 걸어줍니다.

function ZU_repeat(){
    document.getElementById('zudynamic').src="tmp_reader.php"; // 이미지 SRC 갱신(파일크기 검사 요청)
    ZUrepeat = setTimeout("ZU_repeat()", RepeatInterval*1000); // 반복지정
    ZU_makeProgress(getCookie('ZUfileSize')); //업로드화면을 갱신하는 함수에게 쿠키값 전달
   
    // 새로고침 비정상적인 submit의 경우에도 화면 활성
    if(last_TmpFileSize<getCookie('ZUfileSize')){
        document.getElementById('ZU_div').style.display = "inline";
    }
}

◎ 업로드 진행상황을 갱신하는 함수

function ZU_makeProgress(CookieSize){
    if(array_sum(array_FileSize)){ //파일크기를 알 수 있는 브라우저

    ~~ 전송된 파일크기, 남은 갯수, 진행율(%), 남은시간 등 표시 ~~

    }else{ //파일크기를 알 수 없는 브라우저

    ~~ 전송된 파일크기, 남은 갯수 만 표시 ~~

    }
}

ZU_makeProgress() 함수의 내용은 특징이 없는 부분이므로 설명하지 않습니다.

window.onload = function(){
    // 업로드 이미지들이 화면에 즉시 뜨도록 프리로드
    var ZUimg1 = new Image();
    ZUimg1.src = "images/progress_01.gif";
    ~~~~
    var ZUimg12 = new Image();
    ZUimg1.src = "images/progress_12.gif";

   
    //<img id="Dynamic" width="1" height="1" src="nofile"> <= 이 코드를 HTML에 넣지 않아도 되도록하는 부분
   
    spyImg = document.createElement("img");
    spyImg.setAttribute("src","tmp_read.php");
    spyImg.setAttribute("id","zudynamic");
    spyImg.setAttribute("width","1");
    spyImg.setAttribute("height","1");
    document.body.appendChild(spyImg);

   
    //<div id="ZU_div">업로드화면디자인 부분</div> <= 이 코드를 HTML에 넣지 않아도 되도록하는 부분

    progressDiv = document.createElement("div");
    progressDiv.setAttribute("id","ZU_div");
    document.body.appendChild(progressDiv);

    document.getElementById('ZU_div').style.position = "absolute";
    document.getElementById('ZU_div').style.width = "437px";
    document.getElementById('ZU_div').style.height = "200px";
    document.getElementById('ZU_div').style.zIndex = "10";
    document.getElementById('ZU_div').style.display = "none";

    document.getElementById('ZU_div').innerHTML="\
    <table width='437' height='57' cellspacing='0' cellpadding='0' border='0' background='images/progress_01.gif'>\
    ~~~~
    </table>";
}

▨▨ html에서 적용하기
상단에 <js링크>딱 한줄만 넣도록 하기위해 부족한 실력으로 많이 고생했습니다.
아마도 DOM 형식인것 같은데..
부족한 실력으로 구현하느라고 해당 코드가 필요이상으로 길고 지저분할 수 있으며,
위에 소개된 소스와 최종소스가 약간 다른부분도 있습니다.  

<html>
<head>
<meta name="generator" content="editplus">
<meta http-equiv="content-type" content="text/html; charset=euc-kr">
<script language="JavaScript" src="/zuprogress/progress.js"></script> <= 라인 추가
</head>

<body>
<form name="F" enctype="multipart/form-data" method="post" action=" ">
<input type="file" name="up1" style="width:400"><br>
<input type="file" name="up2" style="width:400"><br>
<input type="file" name="up3" style="width:400"><br>
<input type="file" name="up4" style="width:400"><br>
<input type="file" name="up5" style="width:400"><br>
<input type="submit" value="업로드"><br>
</form>
</body>
</html>

▨▨ 개선할 부분

가장 최근의 임시파일크기 정보가 사용자 임시파일 크기 정보인지 100% 신뢰할 수 없습니다.
업로드 전 로컬의 파일크기를 알아내는 getFileSize()함수가 잘못된 파일의 크기를 보고할 수 있습니다.
셀러론인 집의 피씨에선 잘보이던 업로드 배경화면이 사무실의 팬4 피씨에서는 잘 안보인 경우가 있었습니다.

다 해놓고 보니, 실무용이라기 보다는 어디로 튈지 모르는 연구용인것 처럼 느껴집니다.
실력있는 PHPER들이 부족한 부분을 보충해서 실무에도 사용할 수 있도록 공유했으면 좋겠습니다.

반응형