ResourceMgr是一个可扩展可定制的上传工具类,它提供上传进度和状态指示。配合可视化的组件,有类似于快车或迅雷下载软件的效果。
它的基类的完整实现如下:由于我是在应用程序整体框架中抠出来的代码,所以有些可能是不属于.net自带的类库中的方法。
using System; using System.IO; using System.ServiceModel; using Aosu.Com.Utils; using Bolan.com.FinancialEditor.Interfaces; using Bolan.com.FinancialEditor.Utils; using System.Diagnostics; using System.Threading; using System.Configuration; namespace Bolan.com.FinancialEditor.ClassLibs { /// <summary> /// 资源服务器管理对象,默认实现是WCF, 子类实现是HTTP带进度条 /// </summary> public class ResourceMgr { private string _gateway = ""; // 应用程序网关 private string _appName = "FinancialEditor"; // 应用程序名,表示资源在资源服务器上被集中管理的根路径。 private string _category = ""; // 资源分类标识 #region 记录线程状态(wang) /// <summary> /// 计时器,用于监控上传进度 /// </summary> protected Stopwatch stopWatch = new Stopwatch(); /// <summary> /// 上传线程 /// </summary> protected Thread upThread; private string localFilePath; /// <summary> /// 唯一标识,和它对应的文章的OBJECTID保持一致,或者为空 /// </summary> public string Id { get; set; } /// <summary> /// 本地文件完全路径名 /// </summary> public string LocalFilePath { get { return localFilePath; } set { localFilePath = value; FileInfo fi = new FileInfo(localFilePath); Total = fi.Length; FileName = fi.Name; FileCreateTime = fi.CreationTime; FileEditTime = fi.LastWriteTime; } } /// <summary> /// 不包括文件路径的文件名 /// </summary> public string FileName { get; set; } /// <summary> /// 本地文件的创建时间 /// </summary> public DateTime FileCreateTime { get; set; } /// <summary> /// 本地文件最后编辑的时间 /// </summary> public DateTime FileEditTime { get; set; } /// <summary> /// 上传开始时间 /// </summary> public DateTime StartTime { get; set; } /// <summary> /// 上传完成时间 /// </summary> public DateTime FinishedTime { get; set; } /// <summary> /// 总字节数 /// </summary> public long Total { get; set; } private long finished; /// <summary> /// 已上传完成的字节数 /// </summary> public long Finished { get { return finished; } set { if (finished != value) { finished = value; if (ProgressChanged != null) { ProgressChanged(this); } } } } /// <summary> /// 上传速率,字节/秒 /// </summary> public double Speed { get; set; } /// <summary> /// 上传已用去的时间 /// </summary> public TimeSpan Elapsed { get; set; } /// <summary> /// 上传剩余时间 /// </summary> public TimeSpan Remaining { get; set; } private UploadStatus status; /// <summary> /// 指示处理状态的标志位 /// </summary> public UploadStatus Status { get { if ((status < UploadStatus.完成中 && upThread != null && !upThread.IsAlive)) { upThread.Abort(); status = UploadStatus.等待中; } if ((status == UploadStatus.连接中 && Elapsed.TotalSeconds > 30 && upThread != null)) //长时间连接不上 { upThread.Abort(); status = UploadStatus.等待中; } return status; } set { if (status != value) { status = value; if (StatusChanged != null) StatusChanged(this); } } } /// <summary> /// 错误信息 /// </summary> public string ErrorMessage { get; set; } public int FinishedPercent { get { return Total == 0 ? 0 : (int)(Finished * 100 / Total); } } public override string ToString() { return String.Format("{0}/{1}KB", Finished / 1024, Total / 1024); } /// <summary> /// 最后上传到的绝对URL地址 /// </summary> public string ResultUrl { get; set; } public TransferAction ProgressChanged; public TransferAction StatusChanged; #endregion /// <summary> /// 应用程序网关 /// </summary> public string Gateway { get { return _gateway; } set { _gateway = value; } } /// <summary> /// 应用程序名,表示资源在资源服务器上被集中管理的根路径 /// </summary> public string AppName { get { return _appName; } set { _appName = value; } } /// <summary> /// 资源分类标识 /// </summary> public string Category { get { return _category; } set { _category = value; } } /// <summary> /// 附加信息 /// </summary> public object Tag { get; set; } /// <summary> /// 附加属性 /// </summary> public string AddinProp { get; set; } protected FileTransferMessage transferMsg; protected FileTransferMessage TransferMsg { get { if (transferMsg == null) CreateUploadMsg(); return transferMsg; } } /// <summary> /// 上传文件到资源服务器(同步方式) /// </summary> /// <param name="path">待上传资源的本地路径</param> /// <returns>返回资源的相对路径</returns> public string Upload(string path) { stopWatch.Restart(); Elapsed = stopWatch.Elapsed; ErrorMessage = ""; Status = UploadStatus.连接中; StartTime = DateTime.Now; LocalFilePath = path; CreateUploadMsg(); try { DoUpload(); Status = UploadStatus.上传完成; FinishedTime = DateTime.Now; Elapsed = stopWatch.Elapsed; Finished = Total; return ResultUrl; } catch (Exception ex) { FinishedTime = DateTime.Now; Elapsed = stopWatch.Elapsed; ErrorMessage = ex.Message; Status = UploadStatus.异常; return ""; } finally { stopWatch.Stop(); } } protected virtual void DoUpload() { ITransfer proxy = SvcChannel.CreateClient<ITransfer>(Gateway, "iUpload"); proxy.TransferFile(TransferMsg); // 返回资源的相对路径。 } public void UploadAsync() { Stop(); upThread = new Thread(new ThreadStart(delegate() { Upload(LocalFilePath); })); upThread.Start(); } protected static string UploadRootPath = ConfigurationManager.AppSettings["UploadFolder"]; protected virtual void CreateUploadMsg() { Id = CommOp.NewId(); // 没有指定上传根位置时返回错误 if (string.IsNullOrEmpty(_appName)) { throw new Exception("未指定上传资源所属的应用名,请与系统管理员联系!"); } // 未指定资源的分类 if (string.IsNullOrEmpty(_category)) { throw new Exception("未指定上传资源所属的分类,请与系统管理员联系!"); } // 得到文件信息对象 FileInfo fileObject = new FileInfo(LocalFilePath); // 计算资源种子位置 string strSeedPath = ""; //王家新改,直接new guid避免上传相同文件无效的问题。 string strSeedKey = CommOp.NewId(); strSeedPath = string.Format("{0}\\{1}\\{2}", _appName, _category, strSeedKey.Substring(0, 2)); // 创建文件传输消息对象。 transferMsg = new FileTransferMessage(); transferMsg.FileName = strSeedKey.Substring(2) + (((".".Equals(fileObject.Extension) || string.IsNullOrEmpty(fileObject.Extension))) ? "" : fileObject.Extension); // 文件名 transferMsg.FileData = new FileStream(LocalFilePath, FileMode.Open, FileAccess.Read); // 文件的数据流 transferMsg.UploadFolder = strSeedPath; ResultUrl = new Uri(new Uri(Gateway), "upload\\" + transferMsg.UploadFolder.Replace('\\', '/') + transferMsg.FileName).ToString(); } /// <summary> /// 停止上传动作 /// </summary> public void Stop() { Status = UploadStatus.等待中; if (upThread != null && upThread.IsAlive) { upThread.Abort(); } } } public enum UploadStatus { 等待中 = 0, 连接中, 上传中, 完成中, 上传完成, 处理完毕, 异常, } /// <summary> /// 资源管理引发的动作 /// </summary> /// <param name="mgr"></param> public delegate void TransferAction(ResourceMgr mgr); }
另外,由于WCF实现的上传没有办法实现进度条,所以我在服务器端提供了常规的asp.net的上传方式:
与此对应的ResourceMgr需要扩展它的实现,因此产生了ResourceMgr2:
public class ResourceMgr2 : ResourceMgr { // <summary> /// 将本地文件上传到指定的服务器(HttpWebRequest方法) /// </summary> protected override void DoUpload() { string address = String.Format("{0}?app={1}&cat={2}&{3}&{4}", Gateway, AppName, Category, LoginMgr.Instance.VerifyQuery, AddinProp); //时间戳 string strBoundary = "----------" + DateTime.Now.Ticks.ToString("x"); byte[] boundaryBytes = Encoding.ASCII.GetBytes("\r\n--" + strBoundary + "\r\n"); string strPostHeader = String.Format(@"--{0} Content-Disposition: form-data; name=""file"" filename=""{1}"" Content-Type:application/octet-stream ", strBoundary, new FileInfo(LocalFilePath).Name); byte[] postHeaderBytes = Encoding.UTF8.GetBytes(strPostHeader); // 根据uri创建HttpWebRequest对象 HttpWebRequest httpReq = (HttpWebRequest)WebRequest.Create(new Uri(address)); httpReq.Method = "POST"; //对发送的数据不使用缓存 httpReq.AllowWriteStreamBuffering = false; //设置获得响应的超时时间(3600秒) httpReq.Timeout = 3600000; httpReq.ContentType = "multipart/form-data; boundary=" + strBoundary; long fileLength = TransferMsg.FileData.Length; httpReq.ContentLength = TransferMsg.FileData.Length + postHeaderBytes.Length + boundaryBytes.Length; ; //每次上传64k int bufferLength = 65536; byte[] buffer = new byte[bufferLength]; //已上传的字节数 Finished = 0; Stream postStream = httpReq.GetRequestStream(); int size; //发送请求头部消息 postStream.Write(postHeaderBytes, 0, postHeaderBytes.Length); TransferMsg.FileData.Seek(0, SeekOrigin.Begin); while ((size = TransferMsg.FileData.Read(buffer, 0, bufferLength)) > 0 && Status != UploadStatus.等待中) { postStream.Write(buffer, 0, size); Finished += size; Elapsed = stopWatch.Elapsed; Speed = Finished / Elapsed.TotalMilliseconds * 1000; //KB Remaining = new TimeSpan(0, 0, (int)((Total - Finished) / Speed)); Speed /= 1024; if (Status == UploadStatus.连接中) { Status = UploadStatus.上传中; } } if (Status == UploadStatus.等待中) { return; } Status = UploadStatus.完成中; //添加尾部的时间戳 postStream.Write(boundaryBytes, 0, boundaryBytes.Length); postStream.Close(); //获取服务器端的响应 WebResponse webRespon = httpReq.GetResponse(); //读取服务器端返回的消息 ResultUrl = WebHelper.GetWebResponseText(webRespon); TransferMsg.FileData.Close(); } protected override void CreateUploadMsg() { transferMsg = new Interfaces.FileTransferMessage(); transferMsg.FileData = new FileStream(LocalFilePath, FileMode.Open, FileAccess.Read); // 文件的数据流 } }
对应的服务器端的实现:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.IO; using System.Configuration; using Bolan.com.FinancialEditor.WebBaseClass; using Bolan.com.FinancialEditor.Utils; namespace Bolan.com.FinancialEditor { /// <summary> /// HttpHandler上传文件 /// 需要传入的参数: app=应用名&cat=分类名 /// 当上传研报时补充:as=是否挂起(0/1)&cid=栏目id & id=文章ID(当文章ID不为空时,处理逻辑有所不同) /// 附加的身份验证信息(uid=用户ID&pwd=加密后的密码&mcode=机器码&sid=会话ID) /// </summary> public class FileUpload : BaseHandler { /// <summary> /// 上传文件保存到服务器的绝对路径 /// </summary> public string FilePath; /// <summary> /// 上传文件的客户端文件名 /// </summary> public string LocalFileName; /// <summary> /// 服务端返回的文件的完整URL /// </summary> public string RemoteFileUrl; /// <summary> /// 唯一标识,用来在存数据库时决定OBJECTID /// </summary> public string ID; string appName; //应用名 string category; //分类名 //服务器上传文件的根目录 static string UPLOAD_ROOT_FOLDER = ConfigurationManager.AppSettings["UploadFolder"]; //服务器上传文件的Web虚拟目录 static string VIRTUAL_UPLOAD_ROOT = ConfigurationManager.AppSettings["VirtualUploadRoot"]; protected override string GetOutput() { HttpPostedFile hpf = Request.Files[0]; appName = Request.QueryString["app"]; category = Request.QueryString["cat"]; LocalFileName = hpf.FileName; string path = CreatePath(); FilePath = Path.Combine(UPLOAD_ROOT_FOLDER, path); string uploadFolder = new FileInfo(FilePath).DirectoryName; // 创建保存文件夹 if (!Directory.Exists(uploadFolder)) { Directory.CreateDirectory(uploadFolder); } hpf.SaveAs(FilePath); RemoteFileUrl = new Uri(Request.Url, VIRTUAL_UPLOAD_ROOT + '/' + path.Replace('\\', '/')).ToString(); if (!String.IsNullOrEmpty(category)) { UploadAction.DoAction(category, this); } return RemoteFileUrl; } /// <summary> /// 生成服务端保存文件的路径 /// </summary> /// <returns></returns> string CreatePath() { if (string.IsNullOrEmpty(appName)) { throw new Exception("未指定上传资源所属的应用名,请与系统管理员联系!"); } if (string.IsNullOrEmpty(category)) { throw new Exception("未指定上传资源所属的分类,请与系统管理员联系!"); } FileInfo fi = new FileInfo(LocalFileName); string rid = CommOp.NewId(); //路径分四部分{分类管理路径}/{种子访问效率控制路径}/{资源保存名字部分}{扩展名} string path = string.Format("{0}\\{1}\\{2}\\{3}{4}", appName, category, rid.Substring(0, 2), rid.Substring(2), (((".".Equals(fi.Extension) || string.IsNullOrEmpty(fi.Extension))) ? "" : fi.Extension)); return path; } } }
至于客户端的应用,我给出一个最简单的带进度条的上传对话框:
public partial class FormUploadProgress : FormGeneral { ResourceMgr resourceMgr; public FormUploadProgress(ResourceMgr mgr) { InitializeComponent(); resourceMgr = mgr; mgr.ProgressChanged = (mgr1) => { if (InvokeRequired) Invoke(new MethodInvoker(delegate { progressBarControl1.Position = mgr1.FinishedPercent; })); else progressBarControl1.Position = mgr1.FinishedPercent; }; mgr.StatusChanged = (mgr1) => { if (InvokeRequired) { Invoke(new MethodInvoker(EndCall)); } else EndCall(); }; } void EndCall() { if (resourceMgr.Status == UploadStatus.上传完成) DialogResult = DialogResult.OK; else if (resourceMgr.Status == UploadStatus.异常) { btnRetry.Enabled = true; lblUploadTips.Text = resourceMgr.ErrorMessage; } } protected override void SetFormAttributes() { LoadAccessOnStart = false; ShowSplashOnStart = false; } protected override void FormInit() { resourceMgr.Status = UploadStatus.等待中; lblUploadTips.Text = "正在上传 " + resourceMgr.LocalFilePath; resourceMgr.UploadAsync(); } private void btnCancel_Click(object sender, EventArgs e) { resourceMgr.Stop(); } private void btnRetry_Click(object sender, EventArgs e) { FormInit(); } }
运行效果如下:
当然,你可以把若干个ResourceMgr放在一个IList集合里,作为数据源给网络控件,这样就可以显示多个文件同时上传的进度。
另外,它的服务端实现也可扩展,主要扩展点是
服务端的:
if (!String.IsNullOrEmpty(category))
{
UploadAction.DoAction(category, this);
}
这里调用了UploadAction.DoAction()方法,用于执行成功上传完成以后,服务端要完成的善后工作,可能包括留痕、写库或其他业务操作。
UploadAction是一个抽象的基类,它定义如下:
/// <summary> /// 定义文件上传后需要执行的业务的基类 /// </summary> public abstract class UploadAction { /// <summary> /// 执行上传后的行为 /// </summary> /// <param name="handler"></param> public abstract void DoAction(FileUpload handler); //生成各上传后业务对象的抽象工厂,为简单,直接写死。今后可以改用配置文件 public static UploadActionFactory Factory { get { return new UploadActionFactory(); } } public static void DoAction(string cat, FileUpload handler) { var action = Factory.CreateAction(cat); if (action != null) action.DoAction(handler); } }
它的子类可以实现具体的DoAction方法,用于实现具体的业务操作。然后由主调程序通过抽象工厂来瘊定调用哪个UploadAction.
抽象工厂的定义如下:
/// <summary> /// 工厂类:生成上传后要执行的业务对象 /// </summary> public class UploadActionFactory { /// <summary> /// 生成上传操作后要执行的业务 /// </summary> /// <param name="act">动作标识符</param> /// <returns>上传后的业务对象</returns> public UploadAction CreateAction(string act) { switch (act) { case "Files": return new ReportAction(); default: return null; } } }
当然,如果只是单纯的上传文件,而没有更多要求,以上的服务端扩展可以无视。