经常需要将本地的网站发布到服务器上,觉得太麻烦,有时经常需要整个目录拷贝,于是就诞生了这个目录同步的工具。该工具需要一个服务端配合,我这里就用asp.net 的 ashx处理,有能力的可以改成TCP 传输。原理就是对比服务器文件的SHA1,如果相同就不用上传了,所以能节省不少时间。考虑到方便,我将配置文件写成了xml 这样每次选择对应配置就行了,配置还加入了一个忽略文件列表。我自己的网站也是这样上传的。
服务端代码,主要处理列出所有文件数据、上传、删除。
using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Web; using System.Xml; using System.Xml.Serialization; namespace WebUpdate { /// /// DirUpdate 的摘要说明 /// public class DirSync : IHttpHandler { private HttpRequest Request; private HttpResponse Response; public void ProcessRequest(HttpContext context) { Request = context.Request; Response = context.Response; context.Response.ContentType = "text/plain"; try { if (String.IsNullOrEmpty(Request["action"])) { if (!String.IsNullOrEmpty(Request["dir"])) { string appDir = context.Server.UrlDecode(Request["dir"]); if (!Directory.Exists(appDir)) { throw new Exception("远程目录不存在"); } List list = new List(); LoadFile(list, appDir, appDir); XmlSerializer sz = new XmlSerializer(list.GetType()); sz.Serialize(Response.OutputStream, list); } else { Response.Write("缺少dir参数"); } } if (Request["action"] == "upload") { string dir = Path.GetDirectoryName(context.Server.UrlDecode(Request["remotePath"])); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } Request.Files[0].SaveAs(context.Server.UrlDecode(Request["remotePath"])); Response.Write("server:upload ok----" + context.Server.UrlDecode(Request["remotePath"])); } if (Request["action"] == "delete") { File.Delete(context.Server.UrlDecode(Request["remotePath"])); Response.Write("server:delete file ok-----" + context.Server.UrlDecode(Request["remotePath"])); } } catch(Exception ex) { Utility.WriteLog(ex.Message+Environment.NewLine+ex.StackTrace); } } public class FileMessage { public long Size { get; set; } public string Sha1 { get; set; } public string FileName { get; set; } } private void LoadFile(List list, string p, string baseDir) { foreach (string file in Directory.GetFiles(p)) { if (file.Contains("update.config.xml")) { continue; } list.Add(GetFileInfo(file, baseDir)); } foreach (string folder in Directory.GetDirectories(p)) { LoadFile(list, folder, baseDir); } } /// /// 获取文件信息 /// private FileMessage GetFileInfo(String filePath, string baseDir) { FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); long lenth = fs.Length; System.Security.Cryptography.HashAlgorithm algorithm = System.Security.Cryptography.SHA1.Create(); String result = BitConverter.ToString(algorithm.ComputeHash(fs)).Replace("-", ""); fs.Close(); return new FileMessage { Size = lenth, Sha1 = result, FileName = filePath.Replace(baseDir, "").TrimStart('\\').TrimStart('/') }; } public bool IsReusable { get { return false; } } } }
下面的xml 的配置代码
http://update.fyj.me/DirSync.ashx D:\Git\Blogs-fyj\Blogs.UI.Main\ D:\VPS\Web\www.kecq.com\ App_Start Areas aspnet_client Controllers Filters Models obj Properties Blogs.UI.Main.csproj Blogs.UI.Main.csproj.user Global.asax.cs packages.config Web.config Web.Debug.config Web.Release.config
syncUrl是上面的处理url,localFolder是本地目录,remoteFolder是远程目录。
具体的处理逻辑,有点复杂
using System; using System.Collections.Generic; using System.Windows.Forms; using System.IO; using System.Collections; using System.Net; using System.Threading; using System.Diagnostics; using System.Xml.Serialization; using System.Xml; using System.Text.RegularExpressions; namespace DirSync { public partial class SyncForm : Form { public SyncForm() { InitializeComponent(); } public delegate void Action(); public void SafeCall(Control ctrl, Action callback) { if (ctrl.InvokeRequired) ctrl.Invoke(callback); else callback(); } void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { MessageBox.Show("同步程序发生错误,暂时无法运行" + e.Exception.Message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Error); this.btnSync.Text = "开始同步"; } public class FileMessage { public long Size { get; set; } public string Sha1 { get; set; } private string _fileName; public string FileName { get { return (_fileName == null ? null : _fileName.TrimStart('\\').TrimStart('/')); } set { _fileName = value; } } } public class UploadMessage { public long Size { get; set; } public string Sha1 { get; set; } public string LocalPath { get; set; } public string UploadUrl { get; set; } public string RemoteFileName { get; set; } public bool IsDelete { get; set; } } private FileMessage GetFileMessage(List list, string fileName) { foreach (FileMessage fm in list) { if (fm.FileName.Equals(fileName, StringComparison.CurrentCultureIgnoreCase)) { return fm; } } return null; } private void Form1_Load(object sender, EventArgs e) { Control.CheckForIllegalCrossThreadCalls = false; XmlDocument doc = new XmlDocument(); doc.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sync.config.xml")); List list = new List(); foreach (XmlNode node in doc.SelectNodes("/sync/app")) { list.Add(node.Attributes["name"].InnerText); } this.comboBox1.DataSource = list; } private void LoadFile(List list, string p, string baseDir) { foreach (string file in Directory.GetFiles(p)) { if (file.Contains("sync.config.xml")) { continue; } list.Add(GetFileInfo(file, baseDir)); } foreach (string folder in Directory.GetDirectories(p)) { LoadFile(list, folder, baseDir); } } /// /// 获取文件信息 /// private FileMessage GetFileInfo(String filePath, string baseDir) { FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); long lenth = fs.Length; System.Security.Cryptography.HashAlgorithm algorithm = System.Security.Cryptography.SHA1.Create(); String result = BitConverter.ToString(algorithm.ComputeHash(fs)).Replace("-", ""); fs.Close(); return new FileMessage { Size = lenth, Sha1 = result, FileName = filePath.Replace(baseDir, "") }; } #region 主要逻辑 //是否在忽略列表里 private bool IsIngore(List ignoreList, string localfile) { foreach (string s in ignoreList) { Match m=Regex.Match(s,"\\*(\\..+)"); if(m.Success) { if(localfile.EndsWith(m.Groups[1].Value,StringComparison.CurrentCultureIgnoreCase)) { return true; } } if (localfile.TrimStart('\\').ToLower().StartsWith(s.TrimStart('\\').ToLower())) { return true; } } return false; } private List GetSyncList(string updateUrl) { WebClient c = new WebClient(); XmlSerializer sz = new XmlSerializer(typeof(List)); //服务器文件信息 List serverList = (List)sz.Deserialize(c.OpenRead(updateUrl)); //本地文件信息 List localList = new List(); LoadFile(localList, txtLocal.Text.Trim(), txtLocal.Text.Trim()); //需要同步的文件列表 List syncList = new List(); //忽略列表 List ignoreList = new List(); XmlDocument doc = new XmlDocument(); doc.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sync.config.xml")); XmlNode appnode = doc.SelectSingleNode("/sync/app[@name='" + this.comboBox1.Text + "']"); foreach (XmlNode node in appnode.SelectNodes("ignore")) { ignoreList.Add(node.InnerText.TrimEnd('\\')); } foreach (FileMessage fm in localList) { if (IsIngore(ignoreList, fm.FileName)) { continue; } FileMessage srcFm = GetFileMessage(serverList, fm.FileName); //如果服务器上不存在该文件 则需同步 if (srcFm == null) { IDictionary dic = new Dictionary(); dic.Add("time", DateTime.Now.ToString("yyyyMMddHHmmss")); dic.Add("ak", System.Configuration.ConfigurationManager.AppSettings["appKey"]); dic.Add("remotePath", System.Web.HttpUtility.UrlEncode(Path.Combine(txtRemote.Text.Trim(), fm.FileName))); dic.Add("action", "upload"); string par = ""; string sign = HttpHelper.Sign(dic, System.Configuration.ConfigurationManager.AppSettings["secretKey"], out par, "sign"); string url = txtURL.Text + "?" + par + "&sign=" + sign; syncList.Add(new UploadMessage { UploadUrl = url, LocalPath = Path.Combine(txtLocal.Text.Trim(), fm.FileName), RemoteFileName = Path.Combine(txtRemote.Text.Trim(), fm.FileName), Size = fm.Size }); } else { //如果不等于服务器上的sha1 则需要同步 if (!srcFm.Sha1.Equals(fm.Sha1, StringComparison.CurrentCultureIgnoreCase)) { IDictionary dic = new Dictionary(); dic.Add("time", DateTime.Now.ToString("yyyyMMddHHmmss")); dic.Add("ak", System.Configuration.ConfigurationManager.AppSettings["appKey"]); dic.Add("remotePath", Path.Combine(txtRemote.Text.Trim(), fm.FileName)); dic.Add("action", "upload"); string par = ""; string sign = HttpHelper.Sign(dic, System.Configuration.ConfigurationManager.AppSettings["secretKey"], out par, "sign"); string url = txtURL.Text + "?" + par + "&sign=" + sign; syncList.Add(new UploadMessage { UploadUrl = url, LocalPath = Path.Combine(txtLocal.Text.Trim(), fm.FileName), RemoteFileName = Path.Combine(txtRemote.Text.Trim(), fm.FileName), Size = fm.Size }); } } } foreach (FileMessage fm in serverList) { if (IsIngore(ignoreList, fm.FileName)) { continue; } FileMessage destFm = GetFileMessage(localList, fm.FileName); //如果客户端不存在该文件 则需删除服务器上的 if (destFm == null) { IDictionary dic = new Dictionary(); dic.Add("time", DateTime.Now.ToString("yyyyMMddHHmmss")); dic.Add("ak", System.Configuration.ConfigurationManager.AppSettings["appKey"]); dic.Add("remotePath", Path.Combine(txtRemote.Text.Trim(), fm.FileName)); dic.Add("action", "delete"); string par = ""; string sign = HttpHelper.Sign(dic, System.Configuration.ConfigurationManager.AppSettings["secretKey"], out par, "sign"); string url = txtURL.Text + "?" + par + "&sign=" + sign; syncList.Add(new UploadMessage { UploadUrl = url, RemoteFileName = Path.Combine(txtRemote.Text.Trim(), fm.FileName), IsDelete = true }); } } return syncList; } private void Start() { try { this.txtStatus.Text = "正在获取同步信息..."; IDictionary dic = new Dictionary(); dic.Add("time", DateTime.Now.ToString("yyyyMMddHHmmss")); dic.Add("ak", System.Configuration.ConfigurationManager.AppSettings["appKey"]); dic.Add("dir", txtRemote.Text.Trim()); string par = ""; string sign = HttpHelper.Sign(dic, System.Configuration.ConfigurationManager.AppSettings["secretKey"], out par, "sign"); string updateUrl = txtURL.Text + "?" + par + "&sign=" + sign; //获取需要同步的文件 List syncList = GetSyncList(updateUrl); if (syncList.Count == 0) { MessageBox.Show("已经同步好了。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); this.btnSync.Text = "开始同步"; this.txtStatus.Text = "就绪"; return; } //计算总同步大小 long total = 0; foreach (UploadMessage d in syncList) { total += d.Size; } this.SafeCall(this.progressBar1, () => { this.progressBar1.Maximum = (int)total; }); foreach (UploadMessage mess in syncList) { try { if (mess.IsDelete) { if (checkBox1.Checked) { this.txtStatus.Text = "正在删除服务器文件" + mess.RemoteFileName; WebClient wc = new WebClient(); string d = wc.DownloadString(mess.UploadUrl); this.SafeCall(this.listBox1, () => { this.listBox1.Items.Add(d); }); continue; } } else { string localFile = mess.LocalPath; this.txtStatus.Text = "正在上传" + localFile; string url = mess.UploadUrl; //WebClient wb = new WebClient(); //byte[] b = wb.UploadFile(url, localFile); //string result = System.Text.Encoding.UTF8.GetString(b); //this.SafeCall(this.listBox1, () => //{ // this.listBox1.Items.Add(result); //}); HttpUploadHelper up = new HttpUploadHelper(); up.UploadStenEvent += up_UploadStenEvent; up.CompleteEvent += up_CompleteEvent; up.Upload_Request(url, localFile, mess.RemoteFileName); } } catch(Exception ex) { this.SafeCall(this.listBox1, () => { this.listBox1.Items.Add(ex.Message); }); this.SafeCall(this.txtErrorCount, () => { this.txtErrorCount.Text = (Convert.ToInt32(txtErrorCount.Text)+1).ToString(); }); } } this.txtStatus.Text = "同步完成"; this.btnSync.Text = "开始同步"; } catch (Exception ex) { this.txtStatus.Text = "同步出错:" + ex.Message; this.btnSync.Text = "开始同步"; } } #endregion void up_CompleteEvent(string result) { this.SafeCall(this.listBox1, () => { this.listBox1.Items.Add(result); }); } void up_UploadStenEvent(UploadEvent e) { this.SafeCall(this.progressBar1, () => { this.progressBar1.Value = this.progressBar1.Value += e.CurrentSize; if (this.progressBar1.Value == this.progressBar1.Maximum) { this.statusStrip1.Text = "同步完成"; this.btnSync.Text = "开始同步"; } }); this.SafeCall(this.txtTotal, () => { this.txtTotal.Text = this.progressBar1.Value + "/" + this.progressBar1.Maximum; }); } private void btnSyc_Click(object sender, EventArgs e) { Thread th = null; this.txtErrorCount.Text = "0"; this.progressBar1.Value = 0; this.listBox1.Items.Clear(); if (this.btnSync.Text == "开始同步") { if (txtURL.Text.Trim() == "") { MessageBox.Show("请输入远程URL", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (txtLocal.Text.Trim() == "") { MessageBox.Show("请输入本地目录", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (txtRemote.Text.Trim() == "") { MessageBox.Show("请输入远程目录", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } this.progressBar1.Value = 0; this.btnSync.Text = "取消"; th = new Thread(new ThreadStart(Start)); th.Start(); } else { if (this.btnSync.Text == "取消") { th.Abort(); this.btnSync.Text = "开始同步"; } } } protected override void WndProc(ref Message SystemMessage) { switch (SystemMessage.Msg) { case 0x0112: if (((int)SystemMessage.WParam) == 61536) { // this.Owner.Close(); Application.ExitThread(); } else { base.WndProc(ref SystemMessage); } break; default: base.WndProc(ref SystemMessage); break; } } private void comboBox1_SelectedIndexChanged(object sender, EventArgs e) { XmlDocument doc = new XmlDocument(); doc.Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sync.config.xml")); XmlNode appnode = doc.SelectSingleNode("/sync/app[@name='" + this.comboBox1.Text + "']"); this.txtURL.Text = appnode.SelectSingleNode("syncUrl").InnerText; this.txtLocal.Text = appnode.SelectSingleNode("localFolder").InnerText; this.txtRemote.Text = appnode.SelectSingleNode("remoteFolder").InnerText; } private void btnClear_Click(object sender, EventArgs e) { this.listBox1.Items.Clear(); } } }