FileSqlServerSync/Server/Common/Dir.cs
zerlei 1809dbebf5 chore: 完成了文件操作,现在可以对旧文件夹执行更新策略,并添加了单元测试
1. 写逻辑复杂的程序,单元测试必不可少,重构和修改更有信心
2. 单元测试每个测试函数应该是独立的,不相互依赖的,也应该是简单的,否则出现问题不好排查,它叫单元测试
3. 运行测试和debug的时候不要害怕😰😰😰😰😰🙃🙃🙃🙃🙃❤️❤️❤️❤️❤️,它帮你找到了错误,完全没有否定你的意思·····
2024-10-12 21:58:37 +08:00

671 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace Common;
/// <summary>
/// 文件夹结构,它包含文件和文件夹
/// </summary>
/// <param name="path">绝对路径</param>
/// <param name="children">子文件或文件夹</param>
public class Dir(string path, List<AFileOrDir>? children = null, NextOpType? nextOp = null)
: AFileOrDir(path, DirOrFile.Dir, nextOp)
{
public List<AFileOrDir> Children { get; set; } = children ?? [];
public override bool IsEqual(AFileOrDir other)
{
if (other is not Dir otherDir)
{
return false;
}
else
{
if (this.FormatedPath != otherDir.FormatedPath || this.NextOp != otherDir.NextOp)
{
return false;
}
if (this.Children.Count != otherDir.Children.Count)
{
return false;
}
this.Children.Sort(AFileOrDir.Compare);
otherDir.Children.Sort(AFileOrDir.Compare);
for (int i = 0; i < this.Children.Count; i++)
{
if (!this.Children[i].IsEqual(otherDir.Children[i]))
{
return false;
}
}
return true;
}
}
/// <summary>
/// clone, 但是更改根目录
/// </summary>
/// <param name="optype">操作步骤</param>
/// <param name="oldRootPath">旧根路径</param>
/// <param name="newRootPath">新根路径</param>
/// <param name="IsResetNextOpType">是否重置下步操作</param>
/// <returns></returns>
public Dir Clone(
NextOpType? optype,
string oldRootPath,
string newRootPath,
bool IsResetNextOpType = false
)
{
var ndir = this.Clone(optype, IsResetNextOpType);
ndir.ResetRootPath(oldRootPath, newRootPath);
return ndir;
}
/// <summary>
/// clone
/// </summary>
/// <param name="optype"></param>
/// <param name="IsResetNextOpType"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public Dir Clone(NextOpType? optype = null, bool IsResetNextOpType = false)
{
var ndir = new Dir(this.FormatedPath, [], IsResetNextOpType ? optype : this.NextOp);
var nchildren = this
.Children.AsEnumerable()
.Select(x =>
{
if (x is File file)
{
return new File(
file.FormatedPath,
file.MTime,
IsResetNextOpType ? optype : file.NextOp
) as AFileOrDir;
}
else if (x is Dir dir)
{
return dir.Clone(optype, IsResetNextOpType);
}
else
{
throw new Exception("cannot be here!");
}
})
.ToList();
ndir.Children = nchildren;
return ndir;
}
/// <summary>
/// 重设置根目录
/// </summary>
/// <param name="oldPath"></param>
/// <param name="newPath"></param>
public void ResetRootPath(string oldPath, string newPath)
{
this.FormatedPath = this.FormatedPath.Replace(oldPath, newPath);
this.Children.ForEach(e =>
{
if (e is File file)
{
file.FormatedPath = file.FormatedPath.Replace(oldPath, newPath);
}
else if (e is Dir dir)
{
dir.ResetRootPath(oldPath, newPath);
}
});
}
/// <summary>
/// 文件夹合并
/// </summary>
/// <param name="fileDirOp">具体操作步骤</param>
/// <param name="diffdir">将要更新的内容</param>
/// <param name="IsUpdateObject"> 是否更新Object对象</param>
/// <param name="IsUpdateDirFile">是否更新文件目录树</param>
/// <returns></returns>
public (bool, string) Combine(
FileDirOp? fileDirOp,
Dir diffdir,
bool IsUpdateObject = true,
bool IsUpdateDirFile = false
)
{
if (this.FormatedPath != diffdir.FormatedPath)
{
return (false, "their path is not same");
}
else
{
var ldir = this;
var rdir = diffdir;
foreach (var oc in diffdir.Children)
{
if (oc is File rfile)
{
if (rfile.NextOp != null)
{
if (oc.NextOp == NextOpType.Add)
{
if (IsUpdateObject)
{
ldir.AddChild(new File(rfile.FormatedPath, rfile.MTime));
}
if (IsUpdateDirFile)
{
fileDirOp?.FileCreate(rfile.FormatedPath, rfile.MTime);
}
}
else
{
var n = ldir
.Children.Where(x =>
x.FormatedPath == oc.FormatedPath && x.Type == DirOrFile.File
)
.FirstOrDefault();
if (n is not null)
{
if (oc.NextOp == NextOpType.Del)
{
if (IsUpdateObject)
{
ldir.Children.Remove(n);
}
if (IsUpdateDirFile)
{
fileDirOp?.FileDel(rfile.FormatedPath);
}
}
else if (oc.NextOp == NextOpType.Modify)
{
if (n is File lfile)
{
if (IsUpdateObject)
{
lfile.MTime = rfile.MTime;
}
if (IsUpdateDirFile)
{
fileDirOp?.FileModify(rfile.FormatedPath, rfile.MTime);
}
}
}
}
}
}
}
else if (oc is Dir rrdir)
{
//新增和删除意味着整个文件夹都被新增和删除
if (rrdir.NextOp == NextOpType.Add)
{
if (IsUpdateDirFile)
{
fileDirOp?.DirCreate(rrdir);
}
if (IsUpdateObject)
{
ldir.AddChild(rrdir.Clone(null, true));
}
}
else if (rrdir.NextOp == NextOpType.Del)
{
if (IsUpdateDirFile)
{
fileDirOp?.DirDel(rrdir,false);
}
if (IsUpdateObject)
{
ldir.Children.RemoveAt(
ldir.Children.FindIndex(x => x.FormatedPath == rrdir.FormatedPath)
);
}
}
//当子文件夹和文件不确定时
else
{
var n = ldir
.Children.Where(x =>
x.FormatedPath == rrdir.FormatedPath && x.Type == DirOrFile.Dir
)
.FirstOrDefault();
if (n is Dir lldir)
{
lldir.Combine(fileDirOp, rrdir, IsUpdateObject, IsUpdateDirFile);
}
}
}
}
}
return (true, "");
}
public (bool, string) Combine_Old(Dir other)
{
if (this.FormatedPath != other.FormatedPath)
{
return (false, "their path is not same");
}
else
{
var ldir = this;
var rdir = other;
foreach (var oc in other.Children)
{
if (oc is File rfile)
{
if (rfile.NextOp != null)
{
if (oc.NextOp == NextOpType.Add)
{
ldir.AddChild(new File(rfile.FormatedPath, rfile.MTime));
}
else
{
var n = ldir
.Children.Where(x =>
x.FormatedPath == oc.FormatedPath && x.Type == DirOrFile.File
)
.FirstOrDefault();
if (n is not null)
{
if (oc.NextOp == NextOpType.Del)
{
ldir.Children.Remove(n);
}
else if (oc.NextOp == NextOpType.Modify)
{
if (n is File lfile)
{
lfile.MTime = rfile.MTime;
}
}
}
}
}
}
else if (oc is Dir rrdir)
{
//新增和删除意味着整个文件夹都被新增和删除
if (rrdir.NextOp == NextOpType.Add)
{
ldir.AddChild(rrdir.Clone(null, true));
}
else if (rrdir.NextOp == NextOpType.Del)
{
ldir.Children.RemoveAt(
ldir.Children.FindIndex(x => x.FormatedPath == rrdir.FormatedPath)
);
}
//当子文件夹和文件不确定时
else
{
var n = ldir
.Children.Where(x =>
x.FormatedPath == rrdir.FormatedPath && x.Type == DirOrFile.Dir
)
.FirstOrDefault();
if (n is Dir lldir)
{
lldir.Combine_Old(rrdir);
}
}
}
}
}
return (true, "");
}
/// <summary>
/// 合并两个文件夹,other不会发生改变this将合并一个副本这不会改变文件结构
/// </summary>
/// <param name="other">它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生</param>
/// <returns></returns>
public (bool, string) CombineJustObject(Dir other)
{
return Combine(null, other, true, false);
}
/// <summary>
/// 合并两个文件夹,other不会发生改变this将不会改变而文件结构会改变
/// </summary>
/// <param name="other">它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生</param>
/// <returns></returns>
public (bool, string) CombineJustDirFile(FileDirOp fileDirOp, Dir diffDir)
{
return Combine(fileDirOp, diffDir, false, true);
}
/// <summary>
/// 添加子节点,根目录相同,才会被添加进去
/// </summary>
/// <param name="child"></param>
/// <returns></returns>/
protected (bool, string) AddChild(AFileOrDir child)
{
if (child.FormatedPath[..this.FormatedPath.Length] != this.FormatedPath)
{
return (false, "their rootpath are not same!");
}
var filtedChildren = this.Children.Where(x => x.Type == child.Type);
var mached = filtedChildren.Where(x => x.FormatedPath == child.FormatedPath);
if (mached.Any())
{
if (child is File)
{
return (false, "there are same path in the children");
}
else if (child is Dir dir)
{
var tdir = mached.FirstOrDefault();
if (tdir is Dir ndir)
{
foreach (var d in dir.Children)
{
ndir.AddChild(d);
}
}
}
}
else
{
this.Children.Add(child);
}
return (true, "");
}
/// <summary>
/// 从文件目录结构提起文件信息注意此目录文件树不包含文件内容仅有修改时间mtime
/// </summary>
/// <returns></returns>
public (bool, string) ExtractInfo()
{
if (this.Children.Count != 0)
{
return (false, "this dir is not empty.");
}
string[] files = Directory.GetFiles(this.FormatedPath);
string[] dirs = Directory.GetDirectories(this.FormatedPath);
foreach (var file in files)
{
this.Children.Add(new File(file, System.IO.File.GetLastWriteTime($"{file}")));
}
foreach (var dir in dirs)
{
var ndir = new Dir(dir);
ndir.ExtractInfo();
this.Children.Add(ndir);
}
return (true, "");
}
/// <summary>
/// 写入目录文件树,首先必须定义写入文件的策略,此目录结构不包含文件内容,但有一个
/// 文件的修改时间,是否修改文件的修改时间,需要定义文件的写入策略 WriteFileStrageFunc
/// </summary>
/// <returns></returns>
public (bool, string) WriteByThisInfo(FileDirOp fileDirOp)
{
static (bool, string) f(Dir dir, FileDirOp fileDirOp)
{
foreach (var child in dir.Children)
{
if (child.Type == DirOrFile.Dir)
{
if (child is Dir childDir)
{
var (IsSuccess, Message) = fileDirOp.DirCreate(childDir, false);
if (!IsSuccess)
{
return (false, Message);
}
f(childDir, fileDirOp);
}
}
else
{
if (child is File childFile)
{
var (IsSuccess, Message) = fileDirOp.FileCreate(
child.FormatedPath,
childFile.MTime
);
if (!IsSuccess)
{
return (false, Message);
}
}
else
{
return (false, "child is not File!");
}
}
}
return (true, "");
}
return f(this, fileDirOp);
}
/// <summary>
/// 比较两个目录文件树是否相同,不相同返回差异部分,左侧是右侧的下一个版本
/// </summary>
/// <param name="otherRootDir"></param>
/// <returns></returns>
public Dir Diff(Dir other)
{
var ldir = this;
var rdir = other;
Dir? cDir = new Dir(rdir.FormatedPath);
//分别对文件和文件夹分组
List<File> lFiles = [];
List<File> rFiles = [];
List<Dir> lDirs = [];
List<Dir> rDirs = [];
var lGroups = ldir.Children.GroupBy(x => x.Type);
var rGroups = rdir.Children.GroupBy(x => x.Type);
foreach (var g in lGroups)
{
if (g.Key == DirOrFile.Dir)
{
lDirs = g.AsEnumerable()
.Select(n =>
{
if (n is Dir dir)
{
return dir;
}
throw new Exception("cannot be here");
})
.ToList();
}
else
{
lFiles = g.AsEnumerable()
.Select(n =>
{
if (n is File file)
{
return file;
}
throw new Exception("cannot be here");
})
.ToList();
}
}
foreach (var g in rGroups)
{
if (g.Key == DirOrFile.Dir)
{
rDirs = g.AsEnumerable()
.Select(n =>
{
if (n is Dir dir)
{
return dir;
}
throw new Exception("cannot be here");
})
.ToList();
}
else
{
rFiles = g.AsEnumerable()
.Select(n =>
{
if (n is File file)
{
return file;
}
throw new Exception("cannot be here");
})
.ToList();
}
}
//排序,然后对比
int lIndex_f = 0;
int rIndex_f = 0;
int lIndex_d = 0;
int rIndex_d = 0;
lFiles.Sort(Compare);
rFiles.Sort(Compare);
lDirs.Sort(Compare);
rDirs.Sort(Compare);
//对比文件
while (true)
{
//当两个线性表都走到最后时,退出循环
if (lIndex_f == lFiles.Count && rIndex_f == rFiles.Count)
{
break;
}
//左侧先到底,右侧都是将要删除的
if (lIndex_f == lFiles.Count)
{
var er = rFiles[rIndex_f];
cDir.Children.Add(new File(er.FormatedPath, er.MTime, NextOpType.Del));
rIndex_f++;
continue;
}
//右侧先到底,左侧都是要添加的
if (rIndex_f == rFiles.Count)
{
var el = lFiles[lIndex_f];
cDir.Children.Add(
new File(
el.FormatedPath.Replace(ldir.FormatedPath, rdir.FormatedPath),
el.MTime,
NextOpType.Add
)
);
lIndex_f++;
continue;
}
var l = lFiles[lIndex_f];
var r = rFiles[rIndex_f];
//将根路径差异抹平
var lreativePath = l.FormatedPath.Replace(ldir.FormatedPath, "");
var rreativePath = r.FormatedPath.Replace(rdir.FormatedPath, "");
//两文件相同对比文件修改时间不同增加到diff内容
if (lreativePath == rreativePath)
{
lIndex_f++;
rIndex_f++;
if (l.MTime != r.MTime)
{
cDir.Children.Add(new File(r.FormatedPath, l.MTime, NextOpType.Modify));
}
}
else
{
//因为已经按照文件路径排过序了,当左侧文件名大于右侧,那么将根据右侧,添加一个删除diff
if (lreativePath.CompareTo(rreativePath) > 0)
{
rIndex_f++;
cDir.Children.Add(new File(r.FormatedPath, r.MTime, NextOpType.Del));
}
//相反根据左侧添加一个新增diff
else
{
lIndex_f++;
cDir.Children.Add(
new File(
l.FormatedPath.Replace(ldir.FormatedPath, rdir.FormatedPath),
l.MTime,
NextOpType.Add
)
);
}
}
}
//文件夹的比较和文件类似但是他会递归调用文件夹的diff函数直至文件停止
while (true)
{
if (lIndex_d == lDirs.Count && rIndex_d == rDirs.Count)
{
break;
}
if (lIndex_d == lDirs.Count)
{
var er = rDirs[rIndex_d];
cDir.Children.Add(er.Clone(NextOpType.Del, true));
rIndex_d++;
continue;
}
if (rIndex_d == rDirs.Count)
{
var el = lDirs[lIndex_d];
cDir.Children.Add(
el.Clone(NextOpType.Add, ldir.FormatedPath, rdir.FormatedPath, true)
);
lIndex_d++;
continue;
}
var l = lDirs[lIndex_d];
var r = rDirs[rIndex_d];
var lreativePath = l.FormatedPath.Replace(ldir.FormatedPath, "");
var rreativePath = r.FormatedPath.Replace(rdir.FormatedPath, "");
if (lreativePath == rreativePath)
{
lIndex_d++;
rIndex_d++;
var rDir = l.Diff(r);
//而等于0这表示此文件夹的内容没有变化
if (rDir.Children.Count != 0)
{
cDir.Children.Add(rDir);
}
}
else
{
//文件夹重命名将会触发整个文件夹的删除和新增操作,这里没有办法定位到操作是修改文件夹(?) 和git类似。
//潜在的问题是,修改文件夹名,此文件夹包含大量的文件,将触发大量操作。
if (lreativePath.CompareTo(rreativePath) > 0)
{
cDir.Children.Add(r.Clone(NextOpType.Del, true));
rIndex_d++;
}
else
{
cDir.Children.Add(
l.Clone(NextOpType.Add, ldir.FormatedPath, rdir.FormatedPath, true)
);
lIndex_d++;
}
}
}
return cDir;
}
}