摘要:上篇文章寫到一種上傳圖片的方法,其中提到那種方法的局限性,就是上傳的文件只能保存在本項目目錄下,在其他目錄中訪問不到該文件。這與瀏覽器的安全性機制有關,瀏覽器不允許用戶用任意的路徑訪問服務器上的資源,因為這可能造成服務器上其他位置的信息被泄露。瀏覽器只允許用戶用相對路徑直接訪問本項目路徑下的資源。那么,如果A項目要訪問B項目上傳的文件資源,這就產生問題了。所以這就需要另外一種方法來解決這個問題,那就是通過 流(Stream)的形式上傳和下載文件資源。這種方法因為不是通過路徑直接訪問文件,而是先把文件讀取的流中,然后將流中的數據寫入到新的文件中,還原需要上傳的文件,所以也就不存在上面的問題了。本片博客,着重介紹一下這種方式的實現。
一、准備工作
首先,還是做一下准備工作:
(1)創建一個解決方案(圖片上傳),一個mvc項目(Console);
(2)然后新建控制器(UploadImageController.cs);
如圖:
我這個demo是在一個code first實現案例上寫的,所以你看到這個解決方案還有其他幾個項目在里面,但是不用擔心,本案例只涉及mvc項目(Console),不與其他幾個項目產生依賴。
(3)引入layui相關的依賴,編寫前端代碼:
本案例中前台頁面使用的是layui,所以提前引入layui的依賴,然后寫好頁面的代碼(該代碼自layui網站上copy),如下:
html:
<link href="~/Content/layui/css/layui.css" rel="stylesheet" />
<script src="~/Scripts/jquery-3.3.1.min.js"></script>
<script src="~/Content/layui/layui.js"></script>
<script src="~/Content/layui/layui.all.js"></script>
<div class="layui-upload" style="margin-top:100px;">
<button type="button" class="layui-btn" id="test1">上傳圖片</button>
<div class="layui-upload-list">
<img class="layui-upload-img" id="demo1" style="width:100px;height:auto;">
<p id="demoText"></p>
</div>
</div>
js:
<script type="text/javascript"> layui.use('upload', function(){ var $ = layui.jquery, upload = layui.upload; //普通圖片上傳
var uploadInst = upload.render({ elem: '#test1', url: '@Url.Action("Upload", "UploadImage")' ,before: function(obj){ //預讀本地文件示例,不支持ie8
obj.preview(function(index, file, result){ $('#demo1').attr('src', result); //圖片鏈接(base64)
}); } ,done: function(res){ //如果上傳失敗
alert(JSON.stringify(res)); // return layer.msg("上傳成功");
//上傳成功
} ,error: function(){ //演示失敗狀態,並實現重傳
var demoText = $('#demoText'); demoText.html('<span style="color: #FF5722;">上傳失敗</span> <a class="layui-btn layui-btn-xs demo-reload">重試</a>'); demoText.find('.demo-reload').on('click', function(){ uploadInst.upload(); }); } }); }); </script>
以上代碼為layui的圖片上傳示例代碼,可到layui 文件上傳部分獲取。
上面的代碼中,只需把url處的鏈接換成后台的圖片上傳方法即可。
如圖所示:
就一個按鈕,上面和下面的內容都是母版頁里自帶的。
二、上傳功能實現
1.簡述流上傳文件的過程
在使用流上傳文件時,最好通過閱讀書籍,對相關的知識有一定的了解。使用流上傳文件與直接上傳文件相比,過程更復雜,這其實相當於把一個文件 由整拆為零,傳輸到對應位置后再 由零重建為整 的一個過程。
關於流的使用中,有幾個點需要了解:
(1)路徑:path,這是文件會被保存的地方,通常會使用 Path.Conbine(path1,path2). 將路徑和文件名組合為一個完整的路徑,如下:
string filePath = Path.Combine(@"D:\Asp.Net\C#code\C#基礎補習\Upload",fileName);
(2)緩存數組:buffer,這是一個字節類型的數組,輸入流中的數據會被依次存儲到緩存數組中,然后緩存數組把其中的數據寫到新的流(輸出流)中;
byte[] buffer;
(3)FileStream:文件流,這個類主要用於在二進制文件中 “讀” 和 “寫” 二進制數據。上圖中流讀取文件和寫入文件都是過這個類來實現的。下面給出幾條示例:
var inputStream = new FileStream(inputfile,FileMode.Open,FileAccess.Read,FileShare.Read);
上一句 創建一個文件流的對象,這個對象有幾個參數,用於控制這個流來進行什么樣的操作:
inputfile:這是一個文件路徑,表示把這個路徑指定的二進制文件讀入到流中。如:
var inputStream = new FileStream(@“D:\Asp.Net\C#code\C#基礎補習\Upload\1.jpg”,FileMode.Open,FileAccess.Read);
就是把這個1.jpg讀入到流中。
FileMode:指定系統打開選定的文件的方式,有以下幾個選項(枚舉值):
//
// 摘要: // 指定操作系統打開文件的方式。
[ComVisible(true)] public enum FileMode { //
// 摘要: // 指定操作系統應創建一個新的文件。 這要求 System.Security.Permissions.FileIOPermissionAccess.Write // 權限。 如果該文件已存在, System.IO.IOException 則會引發異常。
CreateNew = 1, //
// 摘要: // 指定操作系統應創建一個新的文件。 如果該文件已存在,則會覆蓋它。 這要求 System.Security.Permissions.FileIOPermissionAccess.Write // 權限。 FileMode.Create 等效於請求,如果該文件不存在,則使用 System.IO.FileMode.CreateNew; 否則為使用 System.IO.FileMode.Truncate。 // 如果該文件已存在但為隱藏的文件, System.UnauthorizedAccessException 則會引發異常。
Create = 2, //
// 摘要: // 指定操作系統應打開現有文件。 若要打開該文件的能力是依賴於指定的值 System.IO.FileAccess 枚舉。 一個 System.IO.FileNotFoundException // 如果文件不存在將引發異常。
Open = 3, //
// 摘要: // 指定操作系統應打開一個文件,是否它存在,則否則,應創建一個新的文件。 如果使用打開該文件 FileAccess.Read, ,System.Security.Permissions.FileIOPermissionAccess.Read // 權限是必需的。 如果文件訪問是 FileAccess.Write, ,System.Security.Permissions.FileIOPermissionAccess.Write // 權限是必需的。 如果使用打開該文件 FileAccess.ReadWrite, ,這兩個 System.Security.Permissions.FileIOPermissionAccess.Read // 和 System.Security.Permissions.FileIOPermissionAccess.Write 權限是必需的。
OpenOrCreate = 4, //
// 摘要: // 指定操作系統應打開現有文件。 當打開文件時,應被截斷,以便其大小為零字節。 這要求 System.Security.Permissions.FileIOPermissionAccess.Write // 權限。 嘗試從文件中讀取使用打開 FileMode.Truncate 導致 System.ArgumentException 異常。
Truncate = 5, //
// 摘要: // 如果它存在,並且查找到該文件的末尾,或者創建一個新文件,請打開該文件。 這要求 System.Security.Permissions.FileIOPermissionAccess.Append // 權限。 FileMode.Append 可以僅在結合使用 FileAccess.Write。 嘗試查找該文件將引發結束之前將其置於 System.IO.IOException // 異常,並且任何嘗試讀取失敗,將引發 System.NotSupportedException 異常。
Append = 6 }
常用的幾個項為:FileMode.Create /CreateNew/Open/OpenOrCreate,
其中Open表示這個流會打開這個文件,Create表示會在該路徑下創建一個這個命名的文件,
FileMode和FileAccess共同控制流對文件進行操作的方式。
FileAccess:控制對該文件進行讀或者寫的權限,比如,你要上傳一個文件,那么你首先要讀取這個文件里的數據,那這個就要設置為 讀 ,又比如,某個文件的數據已經讀到緩存區了,需要把它存到指定的位置,那么這個時候,就要把數據寫入一個新的文件,那么就要用寫。這個也有幾個選項(枚舉值):
// 摘要: // 對於讀、 寫或讀/寫訪問的文件中定義的常數。
[ComVisible(true)] [Flags] public enum FileAccess { //
// 摘要: // 對文件的讀取訪問權限。 可以從文件讀取數據。 將與結合起來 Write 為讀/寫訪問。
Read = 1, //
// 摘要: // 對文件的寫入訪問權限。 數據可以寫入該文件。 將與結合起來 Read 為讀/寫訪問。
Write = 2, //
// 摘要: // 讀取和寫入到文件的訪問。 可以寫入和從文件中讀取數據。
ReadWrite = 3 }
FileMode和FileAccess對應起來使用,一般Open和Read組合,Create和Write組合。
(4)偏移量 offset:流中的數據寫入(或讀出)到緩存數組中時,數據是按照類似排隊的順序,一個一個寫的,流中有一個指針一樣的東西,數據讀了幾個,這個指針就向前移動幾位,指針移動的多少就是偏移量,偏移量作為流的使用中的一個重要的參數,在文件分段上傳中作用明顯。
2.上傳功能的實現:
這里我直接給出代碼,代碼里有詳細的解釋,不再另作說明:
public string Upload() { ///獲取上傳的文件
var file = Request.Files[0]; //獲取上傳文件的文件名
string fileName = file.FileName; //上傳路徑
string filePath = Path.Combine(@"D:\Asp.Net\C#code\C#基礎補習\Upload",fileName); //定義緩存數組
byte[] buffer; //將文件數據塞到流里
var inputStream = file.InputStream; ///獲取讀取數據的長度
int readLength = Convert.ToInt32(inputStream.Length); ///給緩存數組指定大小
buffer = new byte[readLength]; //設置指針的位置為 最開始 的位置
inputStream.Seek(0,SeekOrigin.Begin); //從位置 0 開始讀取上傳的文件的數據,數據讀取到第一個參數buffer(緩存區)中
inputStream.Read(buffer,0,readLength); //創建輸出文件流,指定文件的輸出位置,模式為創建該新文件,讀寫權限為 寫
using (var outputStream = new FileStream(filePath,FileMode.Create,FileAccess.Write)) { //設置指針的位置為 最開始 的位置
outputStream.Seek(0,SeekOrigin.Begin); //從起始位置 將 第一個參數 buffer(緩存區)里的數據寫入到 filePath 指定的文件中
outputStream.Write(buffer,0,buffer.Length); }
//向前台返回上傳文件的文件名,表示上傳成功 return JsonConvert.SerializeObject(new { Name = fileName }); }
寫好該文件后,將前端js中的 url 處寫上指向該代碼的鏈接, 然后運行,查看結果:如圖所示:
然后,打開對應目錄的文件夾,查看文件是否已上傳:
3.另一種寫法,針對比較大的文件
上一種方法我們給定數組的大小是根據流的長度來確定的,因為這里是上傳的圖片,數據量不是很大,這樣做沒什么問題,但是上傳的文件比較大的話,文件可能不會很順利的上傳。
這里提供另外一種上傳方法,當然,還是用 流 上傳 ,但不是定義一個 剛剛好的數組 ,一次性上傳,而是定義一個固定大小的數組,每次取一定量的數據,然后把數據寫到新文件中,再清空數組,之后又用數組去取定量的數據,再寫入,在取數據,再寫入,像這樣循環往復,直至文件上傳完畢為止。下面是這種方法的代碼,同樣有比較詳細的注釋,不再另作說明:
/// <summary>
/// 文件上傳 /// </summary>
/// <returns></returns>
public string UploadFile() { var file = Request.Files[0]; int BUFFERSIZE = 4096; var name = string.Empty; // var uploadTime = DateTime.Now;
///文件上傳的最底層目錄路徑 格式為 \文件id\文件名
var fileId = Guid.NewGuid(); name = file.FileName; // var uploadPath = Path.Combine(fileId.ToString(), name);
///文件上傳后的位置
var outputPath = Path.Combine(@"D:\Asp.Net\C#code\C#基礎補習\Upload", name); ///將接收到的文件轉化為流
var inputStream = file.InputStream; ///流數據讀取到數組中的偏移量
long offset = 0; ///獲取或設置光標在當前流中的位置
inputStream.Position = offset; ///存儲流中數據的數組
// byte[] buffer = new byte[BUFFERSIZE];
///讀取流中的數據,讀到數組中
while (offset < inputStream.Length) { ///存儲流中數據的數組,該數組大小根據流中未讀取數據量大小調整,若未讀取數據量大於規定的數組最大大小,則數組大小設為該數組的最大容量
byte[] buffer = new byte[Math.Min(BUFFERSIZE,inputStream.Length - offset)]; int nRead = inputStream.Read(buffer,0,buffer.Length); if (nRead <0 ) { ///若讀取完畢,則跳出循環
break; } try { ///將讀取到數組中的數據寫入新的文件中,再保存到指定的位置
using (var outputStream = new FileStream(outputPath, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { outputStream.Seek(offset, SeekOrigin.Begin);//將流中的光標移到第一次讀取的數據之后
outputStream.Write(buffer, 0, buffer.Length); outputStream.Flush(); offset = outputStream.Length; } } catch (Exception exception) { throw exception; } } return JsonConvert.SerializeObject(new { Id = fileId,Name = name}); }
同樣,演示一下這種方法是否能成功:
先把url處改為 @Url.Action("UploadFile","UploadImage"),
,
效果如下:
三、下載文件
既然有文件上傳,按必然就少不了文件下載,下面給出一個文件下載的功能實現。
首先,在前端頁面添加一個 a標簽按鈕 和 一個圖片鏈接 按鈕,如下圖所示:
<div>
<a href="@Url.Action("DownloadFile","UploadImage")">下載圖片</a>
<img src="@Url.Action("DownloadFile","UploadImage")" alt="Alternate Text" style="width:100px;height:auto;"/>
</div>
其中DownloadFile是后台代碼,然后給出后台代碼,由於下載是上傳的逆過程,所以這里不再做出詳細解釋:
/// <summary>
/// 文件下載 ,該案例僅為一個文件下載的demo,其文件名和路徑等信息,此處直接給出固定值,實際應用中可根據需求靈活給定文件名和路徑 /// </summary>
/// <returns>返回文件</returns>
public ActionResult DownloadFile() { string fileName = "角樓.jpg"; byte[] buffer; string contentType = "application/octec-stream"; ///MemoryStream()內存流
using (var outputStream = new MemoryStream()) { try { long offset = 0; string inputFilePath = Path.Combine(@"D:\Asp.Net\C#code\C#基礎補習\Upload", fileName); using (var inputStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read)) { long readLength = inputStream.Length; buffer = new byte[readLength]; inputStream.Seek(offset,SeekOrigin.Begin); inputStream.Read(buffer,0,Convert.ToInt32(readLength)); } } catch (Exception exception) { throw exception; } outputStream.Write(buffer,0,buffer.Length); return File(outputStream.GetBuffer(),contentType,fileName); } }
下面給出演示圖片:
下載此圖:
文件默認下載到電腦上的 “下載” ,文件夾。
關於文件.net mvc下另一種圖片上傳的方法就介紹到這里,本篇只着重介紹文件上傳和下載的過程,實際應用中會有很多其他方面的點要涉及,這里不進行說明,如果時間允許,會再介紹。
本程序的源代碼需要的同學可給留言 郵箱 ,我會第一時間發給你。
本人系5個月.net程序員 ,菜雞一只,以上所述,如有重大謬誤,大牛請狠批!
我的聯系方式:eMail:3074596466@qq.com
祝大家小年快樂!
如有幫助,能不能點個推薦呢,哈哈哈!有點恬不知恥哈,不要介意!