经常需要将本地的网站发布到服务器上,觉得太麻烦,有时经常需要整个目录拷贝,于是就诞生了这个目录同步的工具。该工具需要一个服务端配合,我这里就用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();
}
}
}
珂珂的个人博客 - 一个程序猿的个人网站