2010年12月4日 星期六

JSP 教學 - 多檔上傳

前一篇介紹了檔案上傳,但是只能一次上傳一個檔案

而且做法是簡單說就是邊讀傳流的資料邊判斷編寫入,過程其實不好控制,也複雜

更重要的是也很難實現多檔上傳,所以那個方法並非非常理想,但也是個選擇

而這篇介紹的多檔上傳就是一次把串流的資料讀完再去做分檔即寫入的動作

還記得上傳的本文內容吧,以下來看看如果是多檔上傳時他的本文內容是啥樣子


------WebKitFormBoundaryGq7bBR8Pb4HYPKfp
Content-Disposition: form-data; name="uploadFile"; filename="m.txt"
Content-Type: text/plain

內容一內容一內容一內容一
 ------WebKitFormBoundaryGq7bBR8Pb4HYPKfp
Content-Disposition: form-data; name="uploadFile"; filename="m1.txt"
Content-Type: text/plain

內容二內容二內容二內容二
------WebKitFormBoundaryGq7bBR8Pb4HYPKfp-- 


基本上還是如此,只是如果讀到的那串亂數後面有再多加兩個減號就是結束

如果沒有的話就代表以下還有資料,以下來介紹一下程式,HTML 的部份就不說了

按下上傳之後會先送到後台的 servlet

multiUpload.java
package fsc.regLogIdent.controller;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import fsc.regLogIdent.model.multiFileProcessor;
import java.util.ArrayList;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class mutilUpload extends HttpServlet
{
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        int s;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        if(request.getContentLength() < 10*1024*1024){  //檔案大小限制在 10MB
            ServletInputStream sin = request.getInputStream();
            byte[] bTmp = new byte[1024];  //一次讀 1KB
            while((s = sin.read(bTmp)) != -1){
                baos.write(bTmp, 0, s);  //寫入 ByteArrayOutputStream 
            }
            byte[] bContent = baos.toByteArray();  //轉成 byte 陣列
            multiFileProcessor multiFs = new multiFileProcessor(bContent);  //建構 multiFileProcessor 物件  multiFileProcessor 為處理寫入及擷取所需資訊的類別
            if(multiFs.saveToDisk()){  //呼叫 saveToDisk() 以將所有檔案儲存
                ArrayList files = multiFs.getTotalFiles();
                request.setAttribute("fileCount", files.size());
                request.setAttribute("files", files);
                request.getRequestDispatcher("/multiUploadisFinish.jsp").forward(request, response);
            }
        }else{
            out.println("Out of size !!");
        }
    }
}
接下來看看樂樂長的 multiFileProcessor.java
package fsc.regLogIdent.model;
import java.io.*;
import java.util.ArrayList;
public class multiFileProcessor
{
    private int currpos = 0, TmpCurrpos, start, end;  //currpos 為目前的指標, TmpCurrpos 為暫時指標
    private String boundary, disposition, ContenType, filename;
    private byte[] bContent;
    ArrayList<fsc.regLogIdent.model.File> list;
    public multiFileProcessor(byte[] bContent){
        this.bContent = bContent;    //本文內容
    }

    private String getBoundary(byte[] bCon){  //取得 boundary
        String boundaryTmp = "";
        for(;bCon[currpos] != 13;currpos++){  //用 currpos 做, 13 為回車鍵
            boundaryTmp = boundaryTmp + (char)bCon[currpos];
        }
        currpos+=2;  //跳過兩個位置
        return boundaryTmp;
    }
    public ArrayList getTotalFiles(){
        return this.list;
    }
    public boolean saveToDisk()  //將所有上傳檔案都寫入 C 槽
    {
        int time=0;
        list = new ArrayList<fsc.regLogIdent.model.File>();  //存放自製的 File 物件的集合
        java.io.File savefile;
        fsc.regLogIdent.model.File aFile;
        boundary = getBoundary(bContent);  //先取得這次上傳的 boundary
        TmpCurrpos = currpos;   //將目前的指標給到暫存指標
        while(getFileEnd(bContent))  //呼叫 getFileEnd 找出各檔案的結束位置
        {
            time++;
            disposition = getDisposition(bContent);  //先抓 disposition 
            filename = getFilename();
            ContenType = getContentType(bContent);  //在抓 ContenType
            //目前的暫存指標, 已經停留在呼叫 getContentType() 之後, 也就是檔案的起始位置了
            start = TmpCurrpos;   
            savefile = new java.io.File("C:\\" + filename);
            try{
                java.io.FileOutputStream fw = new java.io.FileOutputStream(savefile);
                fw.write(bContent, start, end-start-2);  //從 start 開始, 寫入長度 end-start-2
                fw.close();
            }catch (FileNotFoundException ex){
                System.out.println(ex.getMessage());
                return false;
            }
            catch(IOException ioe){
                System.out.println(ioe.getMessage());
                return false;
            }
            //目前 currpos 是停在呼叫完 getFileEnd 之後的位置, 順帶把暫存指標指到, 以便下次擷取資訊
            TmpCurrpos = currpos;
            TmpCurrpos+=2;   //跳過 \r\n  這很重要, 忘了會導致下一迴圈讀取 disposition 時, 發生錯誤
                             //因為讀到第二個就是\n, 導致回傳回來的是 \r , 空值就是了...
            aFile = new fsc.regLogIdent.model.File();  //配置自製的 File 實體
            aFile.setBoundary(boundary);   
            aFile.setFilename(filename);
            aFile.setContenttype(ContenType);
            String fileType = filename.substring(filename.lastIndexOf("."));
            aFile.setFiletype(fileType);  //設定副檔名
            list.add(aFile);  //加入 集合
        }
        return true;
    }
    private boolean getFileEnd(byte[] bCon)
    {
        int bpos = 0, i = 0;  //bpos 為指向 boundary byte 陣列的索引
        boolean find = false;
        byte[] boundaryByte = boundary.getBytes();  //將剛剛取得的 boundary 轉成byte陣列
        //因為 currpos 已經在 getBoundary() 使用過, 所以 currpos 指標已經離開一開始的 boundary
        while(!find && currpos<bCon.length){   
            if(bCon[currpos] == boundaryByte[bpos]){  //currpos 指標指的 byte 同 boundaryByte
                if(bpos == 39){   //那串亂數有40個,所以等於bpos 前進到索引值39時, 代表已經找到 boundary
                    find = true;
                    end = currpos-39;//end 表示資料讀取的結束位置,減39是因為總不能把boundary 讀入吧...
                }
                bpos++;  //將bpos 前進一個
            }else{
                bpos = 0; //記得歸零
            }
            currpos++;  //currpos 指標要一直走
        }
        return find;
    }
    private String getDisposition(byte[] bCons){  //取得 disposition, 目的是要抓 filename
        String dispositionTmp = "";
        //用暫存指標, 注意目前暫存指標的位置, 以第一次執行回圈時,暫存指標是指到currpos 讀取完 boundary 後的位置
        for(;bCons[TmpCurrpos] != 13;TmpCurrpos++){   
            dispositionTmp = dispositionTmp + (char)bCons[TmpCurrpos];  //轉成 char 並加入字串
        }
        TmpCurrpos+=2;  //跳過 \r\n
        return dispositionTmp;
    }
    private String getContentType(byte[] bCon){   
        String contentTypeTmp = "";
        for(;bCon[TmpCurrpos] != 13;TmpCurrpos++){
            contentTypeTmp = contentTypeTmp + (char)bCon[TmpCurrpos];
        }
        TmpCurrpos+=4;  //跳過 \r\n\n
        return contentTypeTmp;
    }
    private String getFilename()
    {
        int s, r;
        String filenameTmp = disposition;  //用剛剛擷取到的 disposition 找出 filename
        if((s = filenameTmp.indexOf("filename=")) != -1){
            filenameTmp = filenameTmp.substring(s+10, disposition.length()-1);
        }
        return filenameTmp;

    }
}

看完以上程式後

關鍵只有一個,就是細心的去處理那些本文內容,注意它的格式,起先在寫程式的時候也是抓不到原因

最後還是要慢慢把錯誤找出,分析內容格式,熟了之後再撰寫會比較快

當然這才會有很多支援上傳的套件供程式設計師使用,畢竟自己撰寫這些程式有點累XD

加上我的程式主要只是實現上傳的這個動作,其他部分就不多說了

不過還是希望能實作過上傳的過程會更好

如果有問題歡迎留言

下篇繼續介紹 檔案的下載

沒有留言:

張貼留言