寫檔案上傳下載參考了下列文章, 特別是下載部分, 一般的 PHP 書籍探討下載的較少, 我主要是參考 "php 檔案下載" 這篇, 採用 http 標頭來傳送檔案.
# Download a file by jQuery.Ajax
# 透過Javascript觸發檔案下載
# php 檔案下載
# PHP 如何取得 現在檔案的目錄 與 上層目錄
# Download File Using jQuery
# jQuery File Download Plugin for Ajax
# ASP.Net : Download file using jQuery
# jquery.fileDownload.js & jQuery UI Dialog
這陣子系統規劃已做了大幅度改變, 來不及紀錄, 因此只好就現況說明, 首先必須在系統安裝檔 install.php 增加一個 sys_files 來記錄已上傳的檔案資料 :
//建立 sys_files 資料表
$data_array["id"]="smallint(6) NOT NULL AUTO_INCREMENT PRIMARY KEY";
$data_array["file_name"]="varchar(255)"; //檔案名稱
$data_array["file_note"]="varchar(255)"; //檔案說明
$data_array["file_type"]="varchar(255)"; //檔案類型
$data_array["file_size"]="varchar(255)"; //檔案大小
$data_array["uploader"]="varchar(255)"; //上傳者
$data_array["upload_time"]="datetime"; //上傳時間
$data_array["download_count"]="int(10) unsigned"; //下載次數
$data_array["public"]="char(1)"; //公開 "Y"/"N"
$result=create_table("sys_files",$data_array);
if ($result) {$msg .= "建立資料表 sys_files ... 完成!<br>";}
其中 file_name, file_type, file_size 用來儲存上傳函式的傳回值, public 用來讓上傳者選擇是否讓所上傳的檔案可被其他人下載. download_count 用來統計下載次數, 每次被下載時, 此欄之值就加 1, 因此它須賦予初值 0.
然後在系統功能檔 sys.php 裡增添一個 files 的 case :
case "files" : {
?>
<?php
break;
}
我們的檔案列表用 datagrid 來顯示, 就放在上面的 ?> 與 <?php 中間 :
<div class="tab" title="檔案">
<!--檔案 sys_files 列表-->
<table id="sys_files" title="檔案列表" style="width:auto" data-options="tools:'#files_tools',toolbar:'#files_toolbar'"></table>
<div id="files_tools">
<a href="#" id="upload_file" class="icon-add" title="上傳檔案"></a>
<a href="#" id="remove_file" class="icon-remove" title="刪除檔案"></a>
<a href="#" id="reload_files" class="icon-reload" title="重新載入"></a>
<a href="#" id="download_file" class="icon-save" title="下載另存"></a>
</div>
<div id="files_toolbar" style="text-align:right;padding:2px;">
<select id="files_search_field" class="easyui-combobox" data-options="panelHeight:120">
<option value="file_name">檔案名稱</option>
<option value="file_note">檔案說明</option>
<option value="file_type">檔案類型</option>
<option value="uploader">上傳者</option>
<option value="upload_time">上傳時間</option>
</select>
<input id="files_search_what" class="easyui-textbox">
</div>
上面的 HTML 主要就是在頁籤裡頭放一個 id="sys_files" 的 datagrid, 它具有一個工具 (tools) 以及一個工具按鈕列 (toolbars), 前者用來放置控制檔案操作的小圖示 (上傳, 刪除, 下載, 更新); 後者用來放置搜尋框.
當然這個 datagrid 需要 JS 來初始化 :
//檔案 sys_files
$('#sys_files').datagrid({
columns:[[
{field:'id',title:'id',sortable:true},
{field:'file_name',title:'檔案名稱',sortable:true},
{field:'file_note',title:'檔案說明',sortable:true},
{field:'file_type',title:'檔案類型',sortable:true},
{field:'file_size',title:'檔案大小',align:'right',sortable:true},
{field:'uploader',title:'上傳者',sortable:true},
{field:'upload_time',title:'上傳時間',sortable:true},
{field:'download_count',title:'下載次數',align:'right',sortable:true}
]],
url:"sys.php",
queryParams:{op:"list_files"},
fitColumns:true,
singleSelect:true,
pagination:true,
pageSize:10,
rownumbers:true,
onDblClickRow:function(index,row){
var url="sys.php?op=download_file&id=" + row.id +
"&file_name=" + row.file_name;
window.open(url);
}
});
$("#files_search_what").textbox({
icons:[{
iconCls:"icon-search",
handler:function(e){
$('#sys_files').datagrid("load",{
op:"list_files",
search_field:$("#files_search_field").combobox("getValue"),
search_what:$("#files_search_what").textbox("getValue")
});
}
}]
});
這裡很重要的一點是, url 與 queryParams 這兩個參數指定了 datagrid 的資料來源, 所有架站系統的功能我都整合在 sys.php 這個檔案裡 (雖然專家不建議這麼做, 好像難維護, 但我討厭一大堆零零散散的小檔案), 然後用 op 參數與 switch-case 指令來選擇要執行的功能. 所以要在 sys.php 裡增添一個 case=list_files 的模組來輸出 datagrid 所需的 json :
case "list_files" : {
$page=isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 1;
$rows=isset($_REQUEST['rows']) ? intval($_REQUEST['rows']) : 10;
$sort=isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'upload_time';
$order=isset($_REQUEST['order']) ? $_REQUEST['order'] : 'desc';
if (isset($_REQUEST['search_field'])) { //有 search
if ($_SESSION["user_level"]==9) { //管理員 (搜尋全部)
$where="WHERE ".$_REQUEST['search_field']." LIKE '%".
$_REQUEST['search_what']."%'";
}
else { //非管理員 (搜尋公開的與自己上傳的)
$where="WHERE (public='Y' OR uploader='".$_SESSION["user_name"].
"') AND ".$_REQUEST['search_field']." LIKE '%".
$_REQUEST['search_what']."%'";
}
}
else { //無 search (顯示全部)
if ($_SESSION["user_level"]==9) {$where="";} //管理員
else { //非管理員 (只能看得到公開的與自己上傳的)
$where="WHERE public='Y' OR uploader='".$_SESSION["user_name"]."'";
}
}
$start=($page-1) * $rows; //本頁第一個列索引 (0 起始)
$SQL="SELECT COUNT(*) FROM `sys_files`";
$RS=run_sql($SQL);
$total=$RS[0][0]; //紀錄總筆數
$SQL="SELECT * FROM sys_files ".$where." ORDER BY ".
$sort." ".$order." LIMIT ".$start.",".$rows;
$RS=run_sql($SQL);
$files=Array();
if (is_array($RS)) {
for ($i=0; $i<count($RS); $i++) {
$files[$i]=Array("id" => $RS[$i]["id"],
"file_name" => $RS[$i]["file_name"],
"file_note" => $RS[$i]["file_note"],
"file_type" => $RS[$i]["file_type"],
"file_size" => $RS[$i]["file_size"],
"uploader" => $RS[$i]["uploader"],
"upload_time" => $RS[$i]["upload_time"],
"download_count" => $RS[$i]["download_count"]
);
}
}
$arr=array("total" => $total, "rows" => $files);
echo json_encode($arr);
break;
}
上面預設排序是依上傳時間 upload_time 倒序排列, 亦即最新上傳的會列在最上面. 管理員可以看到所有上傳的檔案列表, 而一般使用者只能看到自己上傳的檔案以及別人公開的檔案, 公開與否由 public 這欄位來決定, 上傳時若未指明, 預設為公開.
其次, 我們替 datagrid 指定了 Double Click 事件, 當用戶雙擊任何一列時就會觸發此事件, 並傳回其索引 index 與列物件 row, 透過 row.id 與 row.file_name 即可取得要下載的檔案的 id 與名稱, 組成 url 後開啟一個新視窗來進行下載.
接著是處理檔案上傳按鈕圖示 (id="upload_file") 被按下時的情況, 這時要顯示一個 dialog 交談框, 讓使用者上傳檔案, 所以在上面的 HTML 後面添加 :
<!--上傳檔案表單對話框-->
<div id="upload_file_dialog" class="easyui-dialog" title="上傳應用程式" style="width:420px;height:225px;" data-options="closed:'true',buttons:'#upload_file_buttons'">
<form id="upload_file_form" style="padding:15px" action="sys.php?op=upload_file" method="post" enctype="multipart/form-data" target="upload_target">
<div style="margin:10px">
<label style="width:60px;display:inline-block;">檔案 : </label>
<input name="upload" class="easyui-filebox" data-options="buttonText:'選擇檔案',prompt:'',required:true" style="width:280px">
</div>
<div style="margin:10px">
<label style="width:60px;display:inline-block;">說明 : </label>
<input name="file_note" class="easyui-textbox" style="width:280px" required="true">
</div>
<div style="margin:10px">
<label style="width:60px;display:inline-block;">公開 : </label>
<select name="public" class="easyui-combobox" panelHeight="50" style="width:80px">
<option value="Y">Y</option>
<option value="N">N</option>
</select>
<img src="images/uploading.gif" id="uploading" style="visibility:hidden;margin-left:20px;">
</div>
<div>
<iframe id="upload_target" name="upload_target" src="#" style="width:0;height:0;border:0px solid #ffffff;">
</iframe>
</div>
</form>
</div>
<div id="upload_file_buttons" style="padding-right:15px;">
<a href="#" id="clear_file" class="easyui-linkbutton" iconCls="icon-clear" style="width:90px">重設</a>
<a href="#" id="upload_file_go" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">上傳</a>
</div>
</div>
其事件處理的 JS 如下 :
$("#clear_file").bind("click",function(){
$("#upload_file_form")[0].reset();
});
$("#upload_file_go").bind("click",function(){
$("#uploading").css("visibility","visible");
$("#upload_file_form").form("submit");
});
$("#upload_file").bind("click",function(){
$("#upload_file_dialog").dialog("open").dialog("setTitle","上傳檔案");
$("#upload_file_form").form("clear");
});
這個 dialog 對話框預設不可以顯示, 因此在 data-options 中要將 closed 屬性設為 true, 必須等按下右上角的 upload_file 按鈕時才呼叫 open 方法將其開啟. 這 dialog 內放了一個表單, 注意, 其編碼型態 enctype 須設為 multipart/form-data 以便傳送檔案. 而後端程式執行結果的輸出目的 target 設為 upload_target, 這是 dialog 對話框中一個看不到 (大小為 0) 的 iframe 的 id, 這 iframe 是作為一個前後端的中介站, 用來執行上傳完畢時要顯示的訊息框程式. 因為檔案上傳後, 前端網頁程式並不知道後端何時會完成收檔, 因此也無法適時跳出一個訊息框來顯示上傳完成之訊息, 因此我們偷偷在前端網頁設置一個看不到的 iframe 當窗口, 當後端完成檔案接收後, 就輸出一個顯示傳檔已完成的 JS 程式丟回這個 iframe 中執行來執行, 讓使用者誤以為我們前端網頁能預測傳檔何時能完成呢, 真是巧妙啊.
顯示上傳結果的 JS 如下之 upload_callback() 所示, 它首先會將顯示上傳中的 gif 動畫圖檔還原為隱藏狀態, 然後關閉上傳對話框, 重新載入 datagrid, 最後顯示後端傳回來的上傳結果 (參數 status 與 msg) :
<!--Top Level Script -->
function upload_callback(status,msg) { //必須在最上層, 不可放在 $ 內
$("#uploading").css("visibility","hidden");
$("#upload_file_dialog").dialog("close");
$("#sys_files").datagrid("reload",{op:"list_files"});
if (status=="success") {var msg=msg + '檔案上傳成功! <br>';}
else {var msg=msg + '檔案上傳失敗, 請重新上傳! <br>';}
$.messager.alert("上傳結果", msg);
}
在此 dialog 中用來指定要上傳之檔案的元件是 id=upload 的 easyui-filebox 元件, 按下其選擇檔案按鈕會跳出作業系統的檔案選擇對話框, 當選定檔案後, 其文字欄位會顯示檔案名稱, 但路徑一律顯示 C:\fakepath. 按下上傳鈕會向後端提交表單, 這時對話框中會冒出一個 uploading 的動畫圖形, 顯示還在上傳中, 這是因為我們在對話框中偷偷放了一個預設看不見 (visibility:none) 的 uploading.gif 圖檔, 這是我在網路上蒐來的, 因為 EasyUI 本身沒有提供, 我把它放在根目錄的 images 下. 當檔案接收完成, 必須再度將此圖檔隱藏起來.
所以在 sys.php 中須增添一個 case=upload_file 來處理上傳檔案之接收 :
case "upload_file" : {
$status="success"; //預設上傳成功
$msg=""; //執行結果字串
$upload_dir="./files/"; //設定上傳目錄
$file_note=$_REQUEST["file_note"]; //取得檔案說明
$public=empty($_REQUEST["public"])?"Y":$_REQUEST["public"]; //取得公開與否 (預設 Y)
$result=upload_file("upload", $upload_dir);
if ($result !== FALSE) {
$msg .= $result["name"]." (".$result["size"]." bytes).<br>";
$data_array["file_name"]=$result["name"];
$data_array["file_note"]=$file_note;
$data_array["file_type"]=$result["type"];
$data_array["file_size"]=$result["size"];
$data_array["uploader"]=$_SESSION["user_name"];
$data_array["upload_time"]=date("Y-m-d H:i:s");
$data_array["download_count"]=0;
$data_array["public"]=$public;
insert("sys_files", $data_array);
} //end of if
else { //上傳失敗
$msg .= $file_name." 上傳失敗 (".$result["file_error"].")!<br>";
$status="failure";
} //end of else
$para='"'.$status.'","'.$msg.'"'; //回傳回呼函式之參數
?>
<!-- 輸出 script 到 $op=files 中的 iframe 並執行其中的回呼函式 -->
<script language="javascript" type="text/javascript">
window.top.window.upload_callback(<?php echo $para; ?>);
</script>
<?php
break;
}
上面這個 JS 是就是檔案接收程式最後的輸出, 目的地就是前端網頁表單的 target 所指的 iframe 元素, 它會呼叫我們放在前端網頁視窗最上層的函式 upload_callback(), 其實就是把參數傳進去顯示上傳結果而已.
這個檔案接收程式使用了一個自建的函式 upload_file(), 我把它寫在 /lib/ 下的 file.php 中, 因此必須載入此函式庫才能使用 :
/*-----------------------------------------------------------------------------
upload_file($uploader,$path="./")
功能 :
此函數將上傳之單一檔案, 由暫存區移至指定路徑下.
參數 :
$uploader : 上傳元件名稱, 即 <input type="file" name="uploader"> 中之 name 值
$path : 檔案儲存路徑, 例如 "./images"
傳回值 :
成功傳回陣列, 失敗傳回 FALSE. 陣列有三個元素 :
$result["name"]=檔案名稱
$result["type"]=檔案類型
$result["size"]=檔案大小
範例 :
<input type="file" name="uploader">
$result=upload_file("uploader", "./images");
echo "檔案 ".$result["name"]." 上傳成功 (".$result["size"]." bytes)";
-----------------------------------------------------------------------------*/
function upload_file($uploader,$path="./") {
if ($_FILES[$uploader]["error"]==0) { //上傳成功
$tmp_name=$_FILES[$uploader]["tmp_name"];
$new_name=$path.$_FILES[$uploader]["name"];
if (move_uploaded_file($tmp_name, $new_name)) { //移動成功
$result["name"]=$_FILES[$uploader]["name"];
$result["type"]=$_FILES[$uploader]["type"];
$result["size"]=$_FILES[$uploader]["size"];
} //end of if
else {$result=FALSE;} //移動失敗
} //end of if
else {$result=FALSE;} //上傳失敗
return $result;
}
此函式只要傳入兩個參數, 第一參數 uploader 就是前端上傳元件 input 的 name, 這裡就是 "upload", 第二參數是上傳目錄, 這裡要傳入 "./files/". 如果上傳成功, 它會傳回一個關聯式陣列, 其元素分別是 name (檔案名稱), type (檔案類型), 與 size (檔案大小). 若上傳失敗就傳回 false.
2015-01-26 補充 :
今天回頭測試下載功能, 發現中文檔名的檔案無法下載, Google 了一下, 發現是 PHP 對於中文檔名支援問題, 試了 iconv 與 mb_convert_encoding 兩種函式將 big5 轉成 utf-8 來處理都不得法, 上傳 OK 但下載 NG (因為 PHP 的 basename 不支援中文) :
$file_name=$_FILES[$uploader]["name"];
$new_name=$path.mb_convert_encoding($file_name,"big5","utf-8");
我參考葉建榮的 "PHP 與 MySQL 網站規劃管理應用" 這本書的作法, 乾脆限制只能上傳英數檔名之檔案, 然後用檔案說明欄描述之. 當然這樣做有些不完美, 或許可以參考下面這個網站的建議處理中文檔名上傳問題 :
# php 中文檔名上傳問題
他的做法是上傳到伺服器後改以英數字檔名儲存 (例如日期時間+tmp_name), 但是把中文檔名記錄在資料庫裡, 檔案列表顯示的是資料庫所記錄的原始中文檔名, 但下載時卻連結到相對應的英數字檔名. 這個做法不錯, 但是必須於資料表中增加一個欄位來記錄真實檔名, 以便與原始中文檔名對應.
沒有留言 :
張貼留言