# History
[[WebApi] 搗鼓一個資源管理器--文件下載](http://blog.csdn.net/qiujuer/article/details/41621781)
[[WebApi] 搗鼓一個資源管理器--多文件上傳](http://blog.csdn.net/qiujuer/article/details/41675299)
# In This
##### 序言
在上一章中我們講解了怎么實現文件的上傳;在文件上傳到服務器后似乎不好管理;每次要下載一個文件也似乎不知從何處下手。
在這一章中將會講解如何結合數據進行文件的上傳存儲,保證文件的存儲足夠安全以及足夠簡單;同時增加不重復保存同一個文件。
##### 簡要分析
1. 在我們想來,一個文件存儲到數據庫后如何才能保證其有一定的安全性呢?這個簡單,最簡單的做法就是去除掉后綴名。雖然和加密比起來相差甚遠;但是如果有人得到了想要知道其中的東西似乎還是需要花些心思才行。
2. 在滿足上面需求的情況下卻增加了額外的負擔,比如文件全部都沒有后綴了那么我們需要下載文件時又怎么辦?這時我們就需要記錄下文件的基本信息了。
3. 我們知道在一般的上傳文件控件上都是按照年月日進行文件夾分開存儲的;但是你是否想過同一個文件重復提交的問題呢?針對這樣的問題似乎在其他插件上都不行;不是不能做,而是沒有這樣做!我們這里既然要做那就要做好才行;所以我們需要具有這樣的功能,那么怎么唯一標識一個文件呢?最簡單的方法莫過于MD5.
4. 針對以上的問題;似乎我們都有思路了;所以現在我們應該知道數據庫不是用來存儲文件,而是用來存儲文件的存儲信息,用來標識管理文件。
# CodeTime
好了又到了我們的代碼模塊了。
##### 改動
先來看看這一次我們做了哪些文件的改動;當然是相對上一版本。

可以看出,共更改了**6**個文件;其中修改:**Web、Upload、Resouce、ResouceApi**;添加:**HashUtils、WebResouceContext**。
##### Model
###### Resource.cs
~~~
using System;
using System.ComponentModel.DataAnnotations;
namespace WebResource.Models
{
/// <summary>
/// 文件存儲信息表
/// </summary>
public class Resource
{
/// <summary>
/// Id 用于唯一標識
/// 存儲時記錄文件MD5
/// 在斷點續傳下存儲文件名稱+文件大小的MD5值
/// </summary>
[Key]
public string Id { get; set; }
/// <summary>
/// 文件名稱
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// 文件大小
/// </summary>
[Required]
public long Size { get; set; }
/// <summary>
/// 當前存儲位置
/// 斷點續傳下用于判斷是否傳輸完成
/// </summary>
[Required]
public long Cursor { get; set; }
/// <summary>
/// 文件類型
/// </summary>
[Required]
public string Type { get; set; }
/// <summary>
/// 存儲文件夾
/// </summary>
[Required]
public string Folder { get; set; }
/// <summary>
/// 點擊數
/// </summary>
[Required]
public int Clicks { get; set; }
/// <summary>
/// 存儲日期
/// </summary>
[Required]
public DateTime Published { get; set; }
}
}
~~~
這個是在上一個版本基礎上進行修改后的數據庫表結構;在我看來一個文件的存儲信息應該包含上面的信息;當然其中用于斷點續傳的Cursor屬性暫時是虛的,沒有投入實際使用;當然這個在后續版本將會詳細講解如何添加斷點續傳。針對文件的基本信息各位如有更好的方案還請提出來。
###### HashUtils.cs
~~~
using System;
using System.IO;
using System.Security.Cryptography;
namespace WebResource.Models
{
public class HashUtils
{
/// <summary>
/// 獲取文件的MD5-格式[大寫]
/// </summary>
/// <param name="fileStream">文件FileStream</param>
/// <returns>MD5Hash</returns>
public static string GetMD5Hash(FileStream fileStream)
{
try
{
MD5 md5Provider = new MD5CryptoServiceProvider();
byte[] buffer = md5Provider.ComputeHash(fileStream);
md5Provider.Clear();
return BitConverter.ToString(buffer).Replace("-", "");
}
catch { return null; }
}
/// <summary>
/// 獲取字符串的MD5-格式[大寫]
/// </summary>
/// <param name="str">字符串</param>
/// <returns>MD5Hash</returns>
public static string GetMD5Hash(string str)
{
try
{
MD5 md5Provider = new MD5CryptoServiceProvider();
byte[] buffer = md5Provider.ComputeHash(System.Text.Encoding.UTF8.GetBytes(str));
md5Provider.Clear();
return BitConverter.ToString(buffer).Replace("-", "");
}
catch { return null; }
}
/// <summary>
/// 計算Byte數組的MD5-格式[大寫]
/// </summary>
/// <param name="bytes">byte[]</param>
/// <returns>MD5Hash</returns>
public static string GetMD5Hash(byte[] bytes)
{
try
{
MD5 md5Provider = new MD5CryptoServiceProvider();
byte[] buffer = md5Provider.ComputeHash(bytes);
md5Provider.Clear();
return BitConverter.ToString(buffer).Replace("-", "");
}
catch { return null; }
}
}
}
~~~
顧名思義,這個類就是用來進行計算**MD5**值而準備的;當然實際使用中只會使用第3個方法;前面兩個只是一并寫出來說不定以后要用上。
###### WebResourceContext.cs
~~~
using System.Data.Entity;
namespace WebResource.Models
{
public class WebResourceContext : DbContext
{
public WebResourceContext() : base("name=WebResourceContext")
{
}
public System.Data.Entity.DbSet<WebResource.Models.Resource> Resources { get; set; }
}
}
~~~
這個類簡單明了;用過EF Db的應該一看就懂了;這個類是用來進行數據表查詢使用的類;當然使用的是EF。
其中指定了"name=WebResourceContext",這個就是用來對應配置文件中的數據庫連接字段的。下面就會講解。
##### Web.config
~~~
<?xml version="1.0" encoding="utf-8"?>
<configuration>
...
<connectionStrings>
<add name="WebResourceContext" connectionString="Data Source=(localdb)\v11.0; Initial Catalog=WebResourceContext-20141204104407; Integrated Security=True; MultipleActiveResultSets=True; AttachDbFilename=|DataDirectory|WebResourceContext-20141204104407.mdf"
providerName="System.Data.SqlClient" />
</connectionStrings>
...
</configuration>
~~~
在這個文件中,有很多的配置;但是其中簡單的來說,就是上面的那一條語句;在這條語句中現在指定的是本地的數據庫;你也可以指定一個遠程的數據連接。
##### Controllers
###### ResourceApiController.cs
~~~
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Description;
using WebResource.Models;
namespace WebResource.Controllers
{
[RoutePrefix("Res")]
public class ResourceApiController : ApiController
{
private WebResourceContext db = new WebResourceContext();
private static readonly long MEMORY_SIZE = 64 * 1024 * 1024;
private static readonly string ROOT_PATH = HttpContext.Current.Server.MapPath("~/App_Data/");
/// <summary>
/// Post File
/// </summary>
/// <param name="Id">Md5</param>
/// <returns>Resource</returns>
[HttpPost]
[Route("Upload/{Id?}")]
[ResponseType(typeof(Resource))]
public async Task<IHttpActionResult> Post(string Id = null)
{
List<Resource> resources = new List<Resource>();
// multipart/form-data
var provider = new MultipartMemoryStreamProvider();
await Request.Content.ReadAsMultipartAsync(provider);
foreach (var item in provider.Contents)
{
if (item.Headers.ContentDisposition.FileName != null)
{
//Strem
var ms = item.ReadAsStreamAsync().Result;
using (var br = new BinaryReader(ms))
{
if (ms.Length <= 0)
break;
var data = br.ReadBytes((int)ms.Length);
//Md5
string id = HashUtils.GetMD5Hash(data);
Resource temp = await db.Resources.FindAsync(id);
if (temp == null)
{
//Create
Resource resource = new Resource();
resource.Id = id;
resource.Size = ms.Length;
resource.Cursor = resource.Size;
resource.Published = DateTime.Now;
//Info
FileInfo info = new FileInfo(item.Headers.ContentDisposition.FileName.Replace("\"", ""));
resource.Type = info.Extension.Substring(1).ToLower();
resource.Name = info.Name.Substring(0, info.Name.LastIndexOf("."));
//Relative
resource.Folder = DateTime.Now.ToString("yyyyMM/dd/", DateTimeFormatInfo.InvariantInfo);
//Write
try
{
string dirPath = Path.Combine(ROOT_PATH, resource.Folder);
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
File.WriteAllBytes(Path.Combine(dirPath, resource.Id), data);
//Save To Datebase
db.Resources.Add(resource);
await db.SaveChangesAsync();
temp = await db.Resources.FindAsync(resource.Id);
}
catch { }
}
if (temp != null)
resources.Add(temp);
}
}
}
if (resources.Count == 0)
return BadRequest();
else if (resources.Count == 1)
return Ok(resources.FirstOrDefault());
else
return Ok(resources);
}
}
}
~~~
這個類才是本次講解的核心部分。
在這個類中,首先看第一個修改的地方,我把API地址重定向為:[RoutePrefix("Res")],這個是為了方便以后使用,輸入**Res**總是要比**Resource**要舒服些的。
另外我刪除了**Get()**方法,因為這個版本中主要講解上傳;另外下載的話也將集合數據進行操作;所以在下一個版本將會添加新的**Get()**。
來看看現在的API接口:

現在只有一個接口了。
說說流程:
1. 觸發上傳接口時;服務端會把上傳的文件全部存儲到內存中;而后遍歷上傳的每一個文件。
2. 計算每個文件的MD5,使用MD5去數據庫查詢是否有該記錄;如果有就直接返回數據庫中的信息,且不保存該文件。
3. 如果沒有,提取文件的基本信息,并添加到類Resource Model中,而后保存文件且保存該文件的信息到數據庫。
4. 循環完成返回保存結果。
##### View
###### Upload.cshtml
~~~
@{
ViewBag.Title = "Upload";
}
<h2>Upload</h2>
<div id="body">
<h1>多文件上傳模式</h1>
<section class="main-content clear-fix">
<form name="form1" method="post" enctype="multipart/form-data" action="/Res/Upload">
<fieldset>
<legend>File Upload Example</legend>
<div>
<label for="caption">File1</label>
<input name="file1" type="file" />
</div>
<div>
<label for="image1">File2</label>
<input name="file2" type="file" />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</fieldset>
</form>
</section>
</div>
~~~
這個文件中只改動了一個地方,就是?action="/Res/Upload"。
# RunTime
終于到了這里了,說實話寫一個這個挺累的。
運行:localhost:60586/Home/Upload

提交后:

數據庫

文件


可以看見文件是按照:年月/日/文件MD5 進行存儲的。
# END
與往常一樣。
##### 資源文件
[[WebApi] 搗鼓一個資源管理器--多文件上傳+數據庫輔助](http://download.csdn.net/detail/qiujuer/8223605)
##### 下一章
在下一章中將會講解;怎么在這一章的基礎上進行文件的訪問。
已發布:[[WebApi] 搗鼓一個資源管理器--數據庫輔助服務器文件訪問](http://blog.csdn.net/qiujuer/article/details/41744733)