ASP.NET MVC - 單一檔案與多檔案上傳及下載管理

之前在 ASP.NET 寫過一篇「透過資料庫上傳下載檔案」,不過此方法有些缺點,例如,資料庫肥大,這些進入資料庫的檔案都是二進位儲存,所以就算是使用資料庫壓縮,會讓資料庫長的肥肥胖胖的。再來,如果你的架構比較大,是將Web Application與Database分開不同伺服器,那麼Web Application與Database之間的頻寬、Disk I/O成本也會大些。

我們來改來方式,將檔案上傳至Web Application特定目錄下,然後將此上傳檔案的資訊儲存至資料庫,這些資訊都是文字,所以對資料庫大小影響不大,就算是上萬十萬筆資訊,文字資料的壓縮率是非常不錯的。當我要下載檔案時,從資料庫取出檔案資訊,然後由Web Application的特定目錄讀出檔案,傳送給Browser進行下載。

以下以ASP.NET MVC實作,會有兩個部分,一是單一檔案上傳,二是多檔案上傳。

資料庫資料


我在 App_Data 裡新檔一個 Files.mdf,表格名稱 FileDown,Schema為

FileId, int, PK
FileName, nvarchar(50)
FileSize, nvarchar(50)
FileType, nvarchar(50)
FileVersion, nvarchar(50)
PostDate, datetime2(7)
UploadDate, datetime2(7)
rowguid, uniqueidentifier

在 Models 目錄下建立 FileModel.edmx 及 FileModel.Context.tt,FileModel.tt。(Entity Framework 4.1)

MVC 單一檔案上傳


新增 FileController.vb,把我們的架構先寫出來。

Imports System.IO

Public Class fileController
    Inherits System.Web.Mvc.Controller

    ' 顯示檔案列表
    Function Index() As ActionResult
        Return View()
    End Function

    ' 單一檔案上傳表單
    Function Upload() As ActionResult
        Return View()
    End Function

    ' 處理單一檔案上傳
    <HttpPost()>
    Function Upload(upfile As HttpPostedFileBase, formData As FormCollection) As ActionResult
        Return View()
    End Function

    ' 多檔案上傳表單
    Function MultiUpload() As ActionResult
        Return View()
    End Function

    ' 處理多檔案上傳
    <HttpPost()>
    Function MultiUpload(form As FormCollection) As ActionResult
        Return View()
    End Function

    ' 處理檔案下載
    Function Download(id As Integer) As ActionResult
        Return View()
    End Function

我們一個一個來處理。

    ' 顯示檔案列表
    Function Index() As ActionResult
        Dim db As New FilesEntities
        Return View(db.FileDown)
    End Function
Index.aspx 內容
<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage(Of IEnumerable (Of UpDownFileFromDBMvc.FileDown))" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    檔案列表
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<h2>檔案列表</h2>
<ul>
    <li><%: Html.ActionLink("單一檔案上傳", "Upload")%></li>
    <li><%: Html.ActionLink("多檔案上傳", "MultiUpload")%></li>
</ul>
<table>
    <tr>
        <th>
            FileId
        </th>
        <th>
            FileName
        </th>
        <th>
            FileSize
        </th>
        <th>
            FileType
        </th>
        <th>
            FileVersion
        </th>
        <th>
            PostDate
        </th>
        <th>
            UploadDate
        </th>
        <th>
            rowguid
        </th>
    </tr>

<% For Each item In Model %>
    <% Dim currentItem = item %>
    <tr>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.FileId) %>
        </td>
        <td>
            <%: Html.ActionLink(currentItem.FileName, "download", New With {.id = currentItem.FileId})%>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.FileSize) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.FileType) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.FileVersion) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.PostDate) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.UploadDate) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.rowguid) %>
        </td>
    </tr>
<% Next %>

</table>

</asp:Content>

要注意的有 <%: Html.ActionLink(currentItem.FileName, "download", New With {.id = currentItem.FileId})%> ,上傳後,我們希望可以直接點擊檔案名稱就可以下載檔案,所以我們把原本的DisplayFor()改寫為Html.ActionLink()。

        ''' <summary>
        ''' MVC 單一檔案上傳
        ''' </summary>
        ''' <param name="upfile">上傳檔案</param>
        ''' <param name="formData">其他表單資料</param>
        <HttpPost()>
        Function Upload(upfile As HttpPostedFileBase, formData As FormCollection) As ActionResult
            Using db As New FilesEntities
                ' 需有上傳檔案
                If upfile IsNot Nothing Then
                    ' 0 < 容量 < 4 MB
                    Dim MBSize As Integer = upfile.ContentLength / 1000 / 1000
                    If upfile.ContentLength > 0 AndAlso MBSize < 4 Then
                        Dim savePath As String = Path.Combine(Server.MapPath("~/Files/"), upfile.FileName)

                        ' 只能上傳 7z 壓縮的檔案
                        If Path.GetExtension(savePath) <> ".7z" Then
                            ModelState.AddModelError("upfile", "檔案必須由 7z 壓縮才能上傳!")
                            Return View()
                        End If

                        ' 日期需正確
                        If IsDate(formData("PostDate")) = False Then
                            ModelState.AddModelError("PostDate", "日期格式不正確!")
                            Return View()
                        End If

                        ' 版本不得空白
                        If formData("FileVersion") = "" Then
                            ModelState.AddModelError("FileVersion", "請輸入此檔案的版本!")
                            Return View()
                        End If

                        ' 檔案不存在,進行上傳儲存動作
                        If System.IO.File.Exists(savePath) = False Then

                            ' 進行資料庫確認
                            Dim FileDuplicate As FileDown = (From f In db.FileDown
                                                             Where (f.FileName = upfile.FileName)
                                                             Select f).FirstOrDefault()

                            ' 檔名未重複
                            If FileDuplicate Is Nothing Then
                                ' 儲存至Disk
                                upfile.SaveAs(savePath)

                                ' 設定檔案資訊
                                Dim file As New FileDown
                                file.FileName = upfile.FileName
                                file.FileSize = upfile.ContentLength
                                file.FileType = upfile.ContentType
                                file.PostDate = formData("PostDate")
                                file.FileVersion = formData("FileVersion")
                                file.UploadDate = Date.Now()
                                file.rowguid = Guid.NewGuid()

                                ' 將檔案資訊儲存至資料庫
                                db.FileDown.Add(file)
                                db.SaveChanges()
                            Else
                                ModelState.AddModelError("upfile", "資料庫檔案資訊未刪除!")
                                Return View()
                            End If
                        Else
                            ModelState.AddModelError("upfile", "檔案已存在!")
                            Return View()
                        End If
                    End If
                End If
            End Using

            Return RedirectToAction("Index")
        End Function

我們在專案目鍵下建立一個 Files 目錄,用來儲存上傳的檔案。這裡要注意傳入參數的前後順序,Upload.aspx 的表單順序,如果上傳檔案欄位第一個,那麼 upfile As HttpPostedFileBase 就要在第一個,反之亦然。

Upload.aspx
<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Upload One File
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<h2>Upload One File</h2>

<%Using Html.BeginForm("Upload", "file", FormMethod.Post, New With {.enctype = "multipart/form-data"})%>
    <%: Html.ValidationSummary(True)%>
    File:<input id="upfile" name="upfile" type="file" value="" />
          <%: Html.ValidationMessage("upfile")%>
    <br />
    PostDate:<%: Html.TextBox("PostDate")%>
          <%: Html.ValidationMessage("PostDate")%>
    <br />
    Version:<%: Html.TextBox("FileVersion")%>
          <%: Html.ValidationMessage("FileVersion")%>
    <br />
    <input type="submit" value="Upload" />
<%End Using%>

</asp:Content>

MVC 多檔案上傳

接下來我們來看多檔案上傳。

        ''' <summary>
        ''' MVC 多檔案同時上傳
        ''' </summary>
        ''' <param name="form">表單資料</param>
        <HttpPost()>
        Function MultiUpload(form As FormCollection) As ActionResult
            ' 資料檢查,請自行設計

            Dim Msg As String = String.Empty
            For i As Integer = 0 To Request.Files.Count - 1
                Msg += Request.Files(i).FileName & " 上傳成功!<br />"
                Request.Files(i).SaveAs(Server.MapPath("~/Files/") & Request.Files(i).FileName)
            Next

            ViewBag.Msg = MvcHtmlString.Create(Msg)

            Return View()
        End Function

這裡讓我偷懶一下,檔案的檢查與資訊新增至資料庫都和Upload差不多,讓你動點手。

<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Upload Multi-Files
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

<h2>Upload Multi-Files</h2>
<%: Html.ActionLink("Go back Index","Index") %>
<% Using (Html.BeginForm("MultiUpload", "File", FormMethod.Post, New With {.enctype = "multipart/form-data"}))%>
        <div class="uploadfiles">
            <p>
                <input type="file" name="files" />
            </p>
        </div>
        <p>
            <a href="#add" id="additem">Add Upload File</a>
            <br /><br />
            <input type="submit" value="Upload" />
        </p>
<% End Using%>

<%: ViewBag.Msg %>
<script type="text/javascript">
    $('#additem').live('click', function () {
        $('.uploadfiles').append($("<p><input type='file' name='files' /></p>"));
    });
</script>
</asp:Content>

重點在列下方那一段 jQeruy,讓我們重態新增要上傳資料的欄位。

MVC 檔案下載

最後的Download程式。
Function Download(id As Integer) As ActionResult
            Using db As New FilesEntities

                ' 取得檔案資訊
                Dim getfile = (From f In db.FileDown
                              Where f.FileId = id
                              Select f).FirstOrDefault()

                If getfile.FileName IsNot Nothing Then
                    Dim FilePath As String = Server.MapPath("~/Files/" & getfile.FileName)

                    ' 進行下載
                    If System.IO.File.Exists(FilePath) Then
                        Return File(FilePath, getfile.FileType, getfile.FileName)
                    End If
                Else
                    ' 回應錯誤
                    Return Content("<span style='color:red'>無法下載檔案!</span>")
                End If
            End Using

            Return RedirectToAction("Index")
        End Function

我們資料表設計裡有Guid,你也可以設計一個使用Guid來讓使用者下載的方法,這樣使用者就無法使用1,2,3,4這樣的好猜的數字來下載資料。

MVC 檔案刪除

又想偷懶了,程式給你寫。

  1. 傳入id從資料庫找到檔案名稱。
  2. 使用Path.Combine與Server.MapPath()組合出完整路徑。
  3. 使用File.Exists(路徑)判斷檔案是否存在。
  4. 存在,刪除檔案及資料庫資訊。( File.Delete(路徑) )
這樣一個可上傳、下載、刪除的簡易檔案管理程式就完成了。

參考資料

  1. ASP.NET MVC 檔案上傳下載是很方便的 
  2. ASP.NET MVC的檔案上傳與下載 
  3. ASP.NET MVC FileUpload 檔案上傳

5 則留言:

  1. 留個記錄,如果上傳的是圖片檔案,除一般副檔案…等檢查外,最好還加上 byte() 檢查。

    1.http://www.dotnetexpertguide.com/2011/05/validate-uploaded-image-content-in.html

    2.http://www.mikekunz.com/image_file_header.html

    3.http://www.blueshop.com.tw/board/FUM20041006161839LRJ/BRD20070801180408XHE/2.html

    回覆刪除
  2. 我使用chrome會抓不到檔案

    回覆刪除
  3. 感謝反應,我找個時間測試一下,再回答。(應該是中秋過後)

    回覆刪除
  4. ASP.NET MVC 開發心得分享 (22):關於 executionTimeout: http://blog.miniasp.com/post/2011/09/08/ASPNET-MVC-Developer-Note-Part-22-About-httpRuntime-executionTimeout.aspx

    MVC 預設不理會 timeout 設定,記錄一下。

    回覆刪除
  5. Dear 匿名者
    我在 Chrome 13/14, IE9, FF6,測試下載程式,一切都正常。

    回覆刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。