FileSqlServerSync/Server/Common/Dir.cs
zhaoyouya 46315ba760 chore: 添加sqlite 数据库,remote server 增加一些接口
添加了一些数据模型,添加了一些简单的单元测试
2024-10-12 21:57:45 +08:00

543 lines
18 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>
/// 合并两个文件夹,other不会发生改变this将合并一个副本
/// </summary>
/// <param name="other">它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生</param>
/// <returns></returns>
public (bool, string) Combine(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(rrdir);
}
}
}
}
}
return (true, "");
}
/// <summary>
/// 添加子节点,根目录相同,才会被添加进去
/// </summary>
/// <param name="child"></param>
/// <returns></returns>/
protected (bool, string) AddChild(AFileOrDir child)
{
if (child.FormatedPath.Substring(0, 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()
{
static (bool, string) f(Dir dir)
{
foreach (var child in dir.Children)
{
if (child.Type == DirOrFile.Dir)
{
var (IsSuccess, Message) = WriteDirStrageFunc(child.FormatedPath);
if (!IsSuccess)
{
return (false, Message);
}
if (child is Dir childDir)
{
f(childDir);
}
}
else
{
if (child is File childFile)
{
var (IsSuccess, Message) = WriteFileStrageFunc(childFile);
if (!IsSuccess)
{
return (false, Message);
}
}
else
{
return (false, "child is not File!");
}
}
}
return (true, "");
}
return f(this);
}
#pragma warning disable CA2211 // Non-constant fields should not be visible
public static Func<File, (bool, string)> WriteFileStrageFunc = (File file) =>
{
return (false, "you must implement this function!");
};
#pragma warning restore CA2211 // Non-constant fields should not be visible
#pragma warning disable CA2211 // Non-constant fields should not be visible
public static Func<string, (bool, string)> WriteDirStrageFunc = (string path) =>
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
return (true, "");
};
#pragma warning restore CA2211 // Non-constant fields should not be visible
/// <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;
}
}