我心中(zhong)的核心組件(jian)(jian)(可插(cha)拔(ba)的AOP)~分布式(shi)文件(jian)(jian)上傳組件(jian)(jian)~基于FastDFS
一些概念
在(zai)大叔框架里總(zong)覺得缺(que)點什么(me),在(zai)最(zui)近的(de)項目(mu)開發中,終(zhong)于知道缺(que)什么(me)了,分(fen)布式文(wen)件存儲組(zu)件,就是(shi)缺(que)它,呵呵,對于分(fen)布式文(wen)件存儲來說,業界比較公認的(de)是(shi)FastDFS組(zu)件,它自己(ji)本身(shen)就是(shi)集群機制,有自己(ji)的(de)路由選擇(ze)和文(wen)件存儲兩個部分(fen),我們(men)通過FastDFS的(de)客戶(hu)端進行上傳后,它會返(fan)回一個在(zai)FastDFS上存儲的(de)路徑,這當(dang)然(ran)是(shi)IO路徑,我們(men)只(zhi)要在(zai)服務(wu)(wu)器上開個Http服務(wu)(wu)器,就可以(yi)以(yi)Http的(de)方法訪問你的(de)文(wen)件了。
我的組件實現方式
前端(duan)上(shang)傳(chuan)控件(表(biao)單方(fang)(fang)式(shi)(shi),swf方(fang)(fang)式(shi)(shi),js方(fang)(fang)法均可(ke))將文件流傳(chuan)給我(wo)們的(de)FastDFS客戶端(duan),通(tong)過客戶端(duan)與(yu)服務端(duan)建立(li)Socket連接,將數據包發(fa)給FastDFS服務端(duan)并等待返(fan)回(hui),上(shang)傳(chuan)成功后返(fan)回(hui)路徑(jing),我(wo)們可(ke)以對路徑(jing)進行HTTP的(de)處理,并存(cun)入(ru)數據庫
fastDFS配合nginx服務器自動生成指定尺寸的圖像
原圖像地址: //www.fastdfs.com/demo/pictruename.jpg
指定尺(chi)寸的圖(tu)像(xiang)地址://www.fastdfs.com/demo/pictruename_100x100.jpg
技術實現
1 一個接口,定義三種上傳規格,普(pu)通(tong)文件(jian)(jian),圖像文件(jian)(jian)和視頻(pin)文件(jian)(jian)(一般需要對(dui)它進行截力)
public interface IFileUploader { /// <summary> /// 上傳視頻文件 /// </summary> /// <param name="param"></param> /// <returns></returns> VideoUploadResult UploadVideo(VideoUploadParameter param); /// <summary> /// 上傳普(pu)通文(wen)件 /// </summary> /// <param name="filePath"></param> /// <returns></returns> FileUploadResult UploadFile(FileUploadParameter param); /// <summary> /// 上(shang)傳圖片 /// </summary> /// <param name="param"></param> /// <returns></returns> /// <remarks>Update:cyr(Ben) 20150317</remarks> ImageUploadResult UploadImage(ImageUploadParameter param); }
2 一批方法參數,包(bao)括了文件,圖像和視頻等
/// <summary> /// 文件上傳參(can)數基類 /// </summary> public abstract class UploadParameterBase { public UploadParameterBase() { MaxSize = 1024 * 1024 * 8; } /// <summary> /// 前一次上傳(chuan)(chuan)時生成的服務器端文件名(ming),如(ru)果(guo)需(xu)要斷點續傳(chuan)(chuan),需(xu)傳(chuan)(chuan)入此文件名(ming) /// </summary> public string ServiceFileName { get; set; } /// <summary> /// 文件(jian)流 /// </summary> public Stream Stream { get; set; } /// <summary> /// 文件名 /// </summary> public string FileName { get; set; } /// <summary> /// 文件(jian)大小限制(zhi)(單位(wei)bit 默(mo)認1M) /// </summary> public int MaxSize { get; protected set; } /// <summary> /// 上傳文件類型限制 /// </summary> public string[] FilenameExtension { get; set; } }
/// <summary> /// 圖(tu)片上傳(chuan)參數對(dui)象(xiang) /// </summary> public class ImageUploadParameter : UploadParameterBase { /// <summary> /// 構(gou)造(zao)方法 /// </summary> /// <param name="stream"></param> /// <param name="fileName"></param> /// <param name="filenameExtension">默(mo)認支持(chi)常用圖片格式</param> /// <param name="maxSize"></param> public ImageUploadParameter(Stream stream, string fileName, string[] filenameExtension = null, int maxSize = 3) { base.Stream = stream; base.FileName = fileName; base.MaxSize = maxSize; base.FilenameExtension = filenameExtension ?? new string[] { ".jpeg", ".jpg", ".gif", ".png" }; ; } /// <summary> /// 構造方法 /// </summary> /// <param name="stream"></param> /// <param name="fileName"></param> /// <param name="maxSize">單位為M</param> public ImageUploadParameter(Stream stream, string fileName, int maxSize) : this(stream, fileName, null, maxSize) { } }
3 一批返回類型,包(bao)括對文件,圖像和視頻等方法的返回數據的定義
/// <summary> /// 上(shang)傳文件返(fan)回對象(xiang)基類(lei) /// </summary> public abstract class UploadResultBase { /// <summary> /// 返回文件地址 /// </summary> public string FilePath { get; set; } /// <summary> /// 錯誤消息列表 /// </summary> public string ErrorMessage { get; set; } /// <summary> /// 是否上傳成功 /// </summary> public bool IsValid { get { return string.IsNullOrWhiteSpace(ErrorMessage); } } }
/// <summary> /// 視頻(pin)上傳(chuan)返回對象(xiang) /// </summary> public class VideoUploadResult : UploadResultBase { /// <summary> /// 上(shang)傳(chuan)的視頻截圖地址 /// </summary> public List<string> ScreenshotPaths { get; set; } /// <summary> /// 上傳狀(zhuang)態 /// </summary> public UploadStatus UploadStatus { get; set; } public VideoUploadResult() { ScreenshotPaths = new List<string>(); } /// <summary> /// 把VideoPath和(he)ScreenshotPaths拼(pin)起(qi)來 以豎線(xian)(|)隔開(kai) /// </summary> /// <returns></returns> public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append(FilePath); foreach (var item in ScreenshotPaths) { sb.Append("|" + item); } return sb.ToString(); } }
4 一個使用FastDFS實現的(de)文(wen)件(jian)上傳實現類(lei)
/// <summary> /// 使(shi)用fastDFS完成(cheng)文件上傳 /// </summary> internal class FastDFSUploader : IFileUploader { /// <summary> /// 目(mu)錄名,需要提前在(zai)fastDFS上建(jian)立 /// </summary> public string DFSGroupName { get { return "tsingda"; } } /// <summary> /// FastDFS結點 /// </summary> public StorageNode Node { get; private set; } /// <summary> /// 服務器地址 /// </summary> public string Host { get; private set; } /// <summary> /// 失敗次數 /// </summary> protected int FaildCount { get; set; } public int MaxFaildCount { get; set; } public FastDFSUploader() { InitStorageNode(); MaxFaildCount = 3; } #region Private Methods private void InitStorageNode() { Node = FastDFSClient.GetStorageNode(DFSGroupName); Host = Node.EndPoint.Address.ToString(); } private List<string> CreateImagePath(string fileName) { List<string> pathList = new List<string>(); string snapshotPath = ""; //視頻截圖 List<string> localList = new VideoSnapshoter().GetVideoSnapshots(fileName, out snapshotPath); foreach (var item in localList) { string aImage = SmallFileUpload(item); pathList.Add(aImage); } //清除本(ben)地多余的(de)圖(tu)片(pian),有(you)的(de)視頻(pin)截取(qu)的(de)圖(tu)片(pian)多,有(you)的(de)視頻(pin)截取(qu)的(de)圖(tu)片(pian)少 string[] strArr = Directory.GetFiles(snapshotPath); try { foreach (var strpath in strArr) { File.Delete(strpath); } Directory.Delete(snapshotPath); } catch (Exception ex) { Logger.Core.LoggerFactory.Instance.Logger_Info("刪除圖片截圖異常" + ex.Message); } return pathList; } private string SmallFileUpload(string filePath) { if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException("filePath 參數不(bu)能(neng)為(wei)空(kong)"); if (!File.Exists(filePath)) throw new Exception("上傳的文(wen)件不存在(zai)"); byte[] content; using (FileStream streamUpload = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { using (BinaryReader reader = new BinaryReader(streamUpload)) { content = reader.ReadBytes((int)streamUpload.Length); } } string shortName = FastDFSClient.UploadFile(Node, content, "png"); return GetFormatUrl(shortName); } /// <summary> /// 文(wen)件分塊(kuai)上傳,適合大(da)文(wen)件 /// </summary> /// <param name="file"></param> /// <returns></returns> private string MultipartUpload(UploadParameterBase param) { Stream stream = param.Stream; if (stream == null) throw new ArgumentNullException("stream參數(shu)不能為空(kong)"); int size = 1024 * 1024; byte[] content = new byte[size]; Stream streamUpload = stream; // 第(di)一個數據包上傳或獲取已上傳的位置(zhi) string ext = param.FileName.Substring(param.FileName.LastIndexOf('.') + 1); streamUpload.Read(content, 0, size); string shortName = FastDFSClient.UploadAppenderFile(Node, content, ext); BeginUploadPart(stream, shortName); return CompleteUpload(stream, shortName); } /// <summary> /// 斷點續傳 /// </summary> /// <param name="stream"></param> /// <param name="serverShortName"></param> private void ContinueUploadPart(Stream stream, string serverShortName) { var serviceFile = FastDFSClient.GetFileInfo(Node, serverShortName); stream.Seek(serviceFile.FileSize, SeekOrigin.Begin); BeginUploadPart(stream, serverShortName); } /// <summary> /// 從指定位置開始上傳文(wen)件 /// </summary> /// <param name="stream"></param> /// <param name="beginOffset"></param> /// <param name="serverShortName"></param> private void BeginUploadPart(Stream stream, string serverShortName) { try { int size = 1024 * 1024; byte[] content = new byte[size]; while (stream.Position < stream.Length) { stream.Read(content, 0, size); var result = FastDFSClient.AppendFile(DFSGroupName, serverShortName, content); if (result.Length == 0) { FaildCount = 0; continue; } } } catch (Exception ex) { Logger.Core.LoggerFactory.Instance.Logger_Info("上傳文件中斷!" + ex.Message); if (NetCheck()) { //重試(shi) if (FaildCount < MaxFaildCount) { FaildCount++; InitStorageNode(); ContinueUploadPart(stream, serverShortName); } else { Logger.Core.LoggerFactory.Instance.Logger_Info("已達到失敗重試次數仍沒有(you)上傳成功"); ; throw ex; } } else { Logger.Core.LoggerFactory.Instance.Logger_Info("當前網(wang)絡不可用(yong)"); throw ex; } } } /// <summary> /// 網絡可用為True,否則為False /// </summary> /// <returns></returns> private bool NetCheck() { return NetworkInterface.GetIsNetworkAvailable(); } /// <summary> /// 拼接Url /// </summary> /// <param name="shortName"></param> /// <returns></returns> private string GetFormatUrl(string shortName) { return string.Format("//{0}/{1}/{2}", Host, DFSGroupName, shortName); } private string CompleteUpload(Stream stream, string shortName) { stream.Close(); return GetFormatUrl(shortName); } private string GetShortNameFromUrl(string url) { if (string.IsNullOrEmpty(url)) return string.Empty; Uri uri = new Uri(url); string urlFirstPart = string.Format("//{0}/{1}/", Host, DFSGroupName); if (!url.StartsWith(urlFirstPart)) return string.Empty; return url.Substring(urlFirstPart.Length); } #endregion #region IFileUploader 成員 /// <summary> /// 上傳視頻 /// </summary> /// <param name="param"></param> /// <returns></returns> public VideoUploadResult UploadVideo(VideoUploadParameter param) { VideoUploadResult result = new VideoUploadResult(); string fileName = MultipartUpload(param); if (param.IsScreenshot) { result.ScreenshotPaths = CreateImagePath(fileName); } result.FilePath = fileName; return result; } /// <summary> /// 上傳(chuan)普通文(wen)件 /// </summary> /// <param name="param"></param> /// <returns></returns> public FileUploadResult UploadFile(FileUploadParameter param) { var result = new FileUploadResult(); try { string fileName = MultipartUpload(param); result.FilePath = fileName; } catch (Exception ex) { result.ErrorMessage = ex.Message; } return result; } /// <summary> /// 上傳(chuan)圖片 /// </summary> /// <param name="param"></param> /// <param name="message"></param> /// <returns></returns> public ImageUploadResult UploadImage(ImageUploadParameter param) { byte[] content; string shortName = ""; string ext = System.IO.Path.GetExtension(param.FileName).ToLower(); if (param.FilenameExtension != null && param.FilenameExtension.Contains(ext)) { if (param.Stream.Length > param.MaxSize) { return new ImageUploadResult { ErrorMessage = "圖片大小(xiao)超(chao)過指(zhi)定大小(xiao)" + param.MaxSize / 1048576 + "M,請重新(xin)選擇(ze)", FilePath = shortName }; } else { using (BinaryReader reader = new BinaryReader(param.Stream)) { content = reader.ReadBytes((int)param.Stream.Length); } shortName = FastDFSClient.UploadFile(Node, content, ext.Contains('.') ? ext.Substring(1) : ext); } } else { return new ImageUploadResult { ErrorMessage = "文件類型(xing)不匹(pi)配(pei)", FilePath = shortName }; } return new ImageUploadResult { FilePath = CompleteUpload(param.Stream, shortName), }; } #endregion }
5 一個文(wen)件上傳(chuan)的生產者,經典(dian)的單例(li)模式(shi)的體現
/// <summary> /// 文件上傳生產者 /// </summary> public class FileUploaderFactory { /// <summary> /// 上傳實(shi)例(li) /// </summary> public readonly static IFileUploader Instance; private static object lockObj = new object(); static FileUploaderFactory() { if (Instance == null) { lock (lockObj) { Instance = new FastDFSUploader(); } } } }
6 前臺的(de)文件上傳(chuan)控件,你(ni)可以(yi)隨便選(xuan)擇了,它與前臺是(shi)解耦的(de),沒有什(shen)么關系,用哪種方法實現(xian)都可以(yi),呵呵
[HttpPost] public ActionResult Index(FormCollection form) { var file = Request.Files[0]; var result = Project.FileUpload.FileUploaderFactory.Instance.UploadFile(new Project.FileUpload.Parameters.FileUploadParameter { FileName = file.FileName, Stream = file.InputStream, }); ViewBag.Path = result.FilePath; return View(); }
最后(hou)我們看一(yi)下我的Project.FileUpload的完(wan)整結構
它隸屬于大叔的Project.Frameworks集合,我們在這里,對Project.FileUpload說一聲:Hello FileUpload,We wait for you for a long time...