2010年11月28日 星期日

JSP 教學 - 檔案上傳

檔案上傳廣泛運用在網路硬碟上,提供使用者一個空間儲存資料,接下來介紹一下檔案上傳的一些關鍵點

首先是 HTML 的部份,通常都會用一個 form 來執行,而讓使用者選擇檔案的標籤則是 input type="file",看看底下的HTML片段吧
<form action="/regLogIdent/upload.do"method="post"
                                        enctype="multipart/form-data" >
            <input type="file" name="uploadFile" />
            <input type="submit" value="submit" />
</form>
以上的 form 標籤中有一個要點即是 enctype="multipart/form-data",這是上傳文件時須加載的屬性,所以 form 送出請求時的封包格式會有些差異
以下來看看上傳檔案時封包所顯示的格式,以下為程式片斷,觀察封包的標頭及值


//透過 HttpServletRequest 的 getHeaderNames() 取得所有標頭,注意型態為 Enumeration
Enumeration e = request.getHeaderNames(); 

while(e.hasMoreElements()){
    String headername = (String)e.nextElement(); //取得標頭
      out.println(headername + " : " + 
        request.getHeader(headername) + "<br />");//用 getHeader(String headerName) 取得該標頭值
}




以上列印出所有的標頭值,對於上傳來說最重要的就是 content-type 這個標頭了,看一下他的值,大概類似以下
multipart/form-data; boundary=----WebKitFormBoundary6hLYi5w6K4BzWHMz


這當中最重要的就是 boundary 之後的那串亂數值了,這串描述值在不同瀏覽器下會有不同結果,以 IE 為例其值為 boundary=---------------------------7da2ac2c1b0516


如果以 firefox 或是 chrome 的話就如同上述,至於不同的結果對於上傳檔案來說並沒有差別,該字串長度大小都一樣,重要的是要以那串亂數值進行內容的區隔


以下來看看上傳檔案時本文內容是如何表現的,以下為上傳一個 test.txt 的範例


- - - - - - W e b K i t F o r m B o u n d a r y e 0 7 x A Y z x y A W O u 6 e T 
C o n t e n t - D i s p o s i t i o n : f o r m - d a t a ; n a m e = " u p l o a d F i l e " ; f i l e n a m e = " t e s t . t x t " C o n t e n t - T y p e : t e x t / p l a i n 


There is Content !!
 - - - - - - W e b K i t F o r m B o u n d a r y e 0 7 x A Y z x y A W O u 6 e T - -


以上為上傳一個文字檔的內容結果,可以看到它會以那串亂數碼來區隔檔案內容 There is Content !! 即為內容了


除了內容之外還包含了一些資訊


C o n t e n t - D i s p o s i t i o n : f o r m - d a t a ; 這是對內容的描述
n a m e = " u p l o a d F i l e " ;  這是你上傳檔案的標籤的 name 屬性
f i l e n a m e = " t e s t . t x t "         這是你上傳的檔名
C o n t e n t - T y p e : t e x t / p l a i n         這是你上傳檔案的檔案類型


看完了這些條件後,就可以從中擷取你要的資訊,包括 檔案名稱、boundary...等


接下來就可以寫一個 Servlet 來執行以上的資訊擷取以及將檔案儲存的動作了


看一下 Upload.java 吧


package fsc.regLogIdent.controller;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Uploadextends HttpServlet
{
    public void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException
    {
        response.setContentType("text/html;charset=UTF-8");
        try {
            PrintWriter out = response.getWriter();
            String content= "";
            //可以建立一個 File 模組,儲存檔案的相關資訊
            fsc.regLogIdent.model.File newFile = new fsc.regLogIdent.model.File();
            getBoundary(request.getHeader("content-type"), newFile);  //取得 boundary
            //取得 client 端的輸入串流要用此串流讀取本文的內容,其回傳為 ServletInputStream 
            ServletInputStream in = request.getInputStream();   
            
            getFileName(in, newFile);     //取得檔案名稱
            getContentType(in, newFile);     //取得檔案內容,下載時要用到
            saveToDisk(newFile, in);  //儲存上傳檔案到主機
        }catch(IOException ioe){
            System.out.println(ioe);
        }
    }
    private void getBoundary(String contentType, fsc.regLogIdent.model.File file)    //取得封包標頭裡的 Content-Type 的 boundary 值
    {
        String boundary = contentType.substring(contentType.indexOf("=")+1);
        file.setBoundary(boundary);  //儲存 boundary
    }
    private void getFileName(ServletInputStream sin, fsc.regLogIdent.model.File file) throws IOException
    {
        String head = "";
        int n = 0, s = -1, r = -1;
        while((n = sin.read()) != -1) //一個一個讀入
        {
            char c = (char)n;  //將讀入的 byte 轉成 char
            head = head + c;    //將字元加入字串中
            if((s = head.indexOf("filename=")) != -1){  //判斷目前字串能否讀取到 "filename="
                if((r = head.indexOf("\r", s)) != -1){  //如果讀取到則從該位置繼續判斷下一個空白字元
                    head = head.substring(s+10, r-1); //filename= 長度為9再加上雙引號為10  
                    //目前 head 就是檔案名稱了 
                    break;
                }
            }
        }
        file.setFilename(head);    //儲存
    }
    private void getContentType(ServletInputStream sin, fsc.regLogIdent.model.File file) throws IOException  //此處理方法同處理 filename 一樣
    {
        String head = "";
        int n = 0, s = -1, r = -1;
        while((n = sin.read()) != -1)
        {
            char c = (char)n;
            head = head + c;
            if((s = head.indexOf("Content-Type: ")) != -1 ){
                if((r = head.indexOf("\r", s)) != -1){
                    head = head.substring(s+14, r);
                    break;
                }
            }
        }
        file.setFiletype(head);
    }
    private void saveToDisk(fsc.regLogIdent.model.File file, ServletInputStream sin) throws IOException
    {
        int s = 0;
        boolean first = true;
        //存到 C 槽,getFilename() 取得檔案名稱
        File savefile = new File("C:\\" + file.getFilename()); 
        //用 FileOutputStream 寫入檔案
        java.io.FileOutputStream fw = new java.io.FileOutputStream(savefile);
        byte[] bTmp = new byte[10];  //如果大小用 3,則下面迴圈就不用寫入 看下面說明
        ByteArrayOutputStream baosTmp = new ByteArrayOutputStream();
        while((s = sin.read(bTmp)) != -1)
        {
            if(first){  //如果是第一次進入回圈時
                for(int i=0;i<bTmp.length;i++){
                    if(i < 3){
                        continue;
                    }else{
                        fw.write(bTmp[i]);
                        //將剩下的寫入檔案, 如果上面配置陣列大小為3,這邊就不用寫入了
                    }
                }
                first = false;
                continue;
            }
            baosTmp.write(bTmp, 0, s);  //寫入 ByteArrayOutputStream 
            bTmp = new byte[1024];   //重新配置緩衝陣列大小
        }
        byte[] yes = baosTmp.toByteArray();  //轉成 byte 陣列
        int count = yes.length-1;   
        fw.write(yes, 0, count-45);  //扣掉後面那串亂數以及 /r/n 並寫入檔案
        fw.close(); 
        //記得要關閉 否則...寫黨成功後, 如果馬上到 C 槽刪除該檔, 會告訴你有人正在使用它
        //沒錯  如果你沒關掉串流的話
    }
}
我們可以透過 request.getInputStream() 取的客戶端的 input 串流,在上面的程式中

分別把取得的串流當作參數以便取得檔案名稱以及檔案類型,呼叫順序是先呼叫 getFileName 再呼叫 ContentType

注意一點的是,在 getFileName method 中,已經開始讀取串流資料了,讀到 filename="text.txt" 結束


所以接下來呼叫 getContentType,開始讀取資料時,已經是從剛剛結束的地方開始讀取,所以前面的資料已經讀取不到了喔

即便你再重新用 request 呼叫 getInputStream(),結果還是一樣的。

這是很重要的地方,一定要多加注意,可以的話可以將讀取的資料印出來看看,多多體會吧

在來繼續探討一下剛剛的擷取檔名以及檔案類型的部份

f i l e n a m e = " t e s t . t x t " C o n t e n t - T y p e : t e x t / p l a i n 

它的原始碼應該是像這樣
f i l e n a m e = " t e s t . t x t "\rC o n t e n t - T y p e : t e x t / p l a i n\r\n\n


而後才是接著檔案的內容所以在將檔案儲存至硬碟時,需要特別注意一下

所以在儲存時 第一次回圈執行才會忽略前面的 3 個 bytes 以免寫入檔案錯誤

當然也可以在抓取完 ContentType 值之後,順便把那三個 bytes 讀掉也可以

最後在儲存檔案到硬碟時,也是同上所述
在檔案內容結束時會有那串亂碼在加上兩個 -- (減號),在檔案內容結束後也是有 \r\n 的 !! 注意!

最後在說明一點就是重新配置緩衝區陣列的問題

以上述程式重新配置為 1024 KB 如果假設一開始我們就配置 1024

而且 while 迴圈內也沒有重新配置緩衝區大小

那會造成如果最後一次讀取迴圈如果讀不到 1024 KB 時,會讓緩衝區剩下一些上一次的讀取資料

如果直接把它寫入檔案,就會發生問題了,所以可以考慮兩個方法

一個就是 while((s = sin.read(bTmp)) != -1) 明確取出讀入的大小 s


再用 write(byte[] b, int off, int len)  寫入固定所需的資料

另一個就是如上直接重新配置,而以上我的程式是有呈現

最後在說明 擷取 filename 時對於不同瀏覽器的影響,以上的程式在IE執行會有問題


原因在於 filename= 之後接的不是純粹檔名,而是整個路徑,這和 firefox 即 chrome 不同


當然這是可以解決的只要取得封包標頭中紀錄使用者瀏覽器的類型在做額外處理即可

--------------------------------

最後說到 JSP 檔案處理上傳,也有很多的套件有支援上傳處理,以簡化開發過程

而以上是其中一個方法,還有其它方法以及檔案下載,之後再慢慢介紹

1 則留言: