diff --git a/Server/.config/dotnet-tools.json b/Server/.config/dotnet-tools.json
new file mode 100644
index 0000000..bccd0eb
--- /dev/null
+++ b/Server/.config/dotnet-tools.json
@@ -0,0 +1,18 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "csharpier": {
+ "version": "0.28.2",
+ "commands": [
+ "dotnet-csharpier"
+ ]
+ },
+ "dotnet-ef": {
+ "version": "8.0.6",
+ "commands": [
+ "dotnet-ef"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Server/.vscode/launch.json b/Server/.vscode/launch.json
new file mode 100644
index 0000000..645e93b
--- /dev/null
+++ b/Server/.vscode/launch.json
@@ -0,0 +1,18 @@
+{
+ "configurations": [
+
+
+ {
+ "name": "RemoteServer",
+ "type": "dotnet",
+ "request": "launch",
+ "projectPath": "${workspaceFolder}\\RemoteServer\\RemoteServer.csproj",
+ "launchConfigurationId": "",
+ "serverReadyAction": {
+ "action": "openExternally",
+ // "pattern": "\\bNow listening on:\\s+https?://\\S+",
+ "uriFormat": "http://localhost:6888"
+ }
+ },
+ ]
+}
\ No newline at end of file
diff --git a/Server/Common/Common.csproj b/Server/Common/Common.csproj
new file mode 100644
index 0000000..fa71b7a
--- /dev/null
+++ b/Server/Common/Common.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/Server/Common/Dir.cs b/Server/Common/Dir.cs
new file mode 100644
index 0000000..1d17719
--- /dev/null
+++ b/Server/Common/Dir.cs
@@ -0,0 +1,542 @@
+namespace Common;
+
+///
+/// 文件夹结构,它包含文件和文件夹
+///
+/// 绝对路径
+/// 子文件或文件夹
+public class Dir(string path, List? children = null, NextOpType? nextOp = null)
+ : AFileOrDir(path, DirOrFile.Dir, nextOp)
+{
+ public List 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;
+ }
+ }
+
+ ///
+ /// clone, 但是更改根目录
+ ///
+ /// 操作步骤
+ /// 旧根路径
+ /// 新根路径
+ /// 是否重置下步操作
+ ///
+ public Dir Clone(
+ NextOpType? optype,
+ string oldRootPath,
+ string newRootPath,
+ bool IsResetNextOpType = false
+ )
+ {
+ var ndir = this.Clone(optype, IsResetNextOpType);
+ ndir.ResetRootPath(oldRootPath, newRootPath);
+ return ndir;
+ }
+
+ ///
+ /// clone
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// 重设置根目录
+ ///
+ ///
+ ///
+ 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);
+ }
+ });
+ }
+
+ ///
+ /// 合并两个文件夹,other不会发生改变,this将合并一个副本
+ ///
+ /// 它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生
+ ///
+ 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, "");
+ }
+
+ ///
+ /// 添加子节点,根目录相同,才会被添加进去
+ ///
+ ///
+ /// /
+ 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, "");
+ }
+
+ ///
+ /// 从文件目录结构提起文件信息,注意,此目录文件树不包含文件内容,仅有修改时间mtime
+ ///
+ ///
+ 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, "");
+ }
+
+ ///
+ /// 写入目录文件树,首先必须定义写入文件的策略,此目录结构不包含文件内容,但有一个
+ /// 文件的修改时间,是否修改文件的修改时间,需要定义文件的写入策略 WriteFileStrageFunc
+ ///
+ ///
+ 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 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 WriteDirStrageFunc = (string path) =>
+ {
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+ }
+ return (true, "");
+ };
+#pragma warning restore CA2211 // Non-constant fields should not be visible
+ ///
+ /// 比较两个目录文件树是否相同,不相同返回差异部分,左侧是右侧的下一个版本
+ ///
+ ///
+ ///
+ public Dir Diff(Dir other)
+ {
+ var ldir = this;
+ var rdir = other;
+ Dir? cDir = new Dir(rdir.FormatedPath);
+ //分别对文件和文件夹分组
+ List lFiles = [];
+ List rFiles = [];
+ List lDirs = [];
+ List 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;
+ }
+}
diff --git a/Server/Common/FileDirBase.cs b/Server/Common/FileDirBase.cs
new file mode 100644
index 0000000..bd83726
--- /dev/null
+++ b/Server/Common/FileDirBase.cs
@@ -0,0 +1,79 @@
+namespace Common;
+
+public enum DirOrFile
+{
+ Dir = 0,
+ File = 1,
+}
+
+public enum NextOpType
+{
+ Add = 0,
+ Modify = 1,
+ Del = 2
+}
+
+public abstract class AFileOrDir(
+ string path,
+ DirOrFile type = DirOrFile.File,
+ NextOpType? nextOp = null
+)
+{
+ public DirOrFile Type { get; set; } = type;
+ public NextOpType? NextOp { get; set; } = nextOp;
+
+ // private string Path = path;
+ ///
+ /// 全部为绝对路径... 占用资源会大一点,但是完全OK
+ ///
+ ///
+ private string Path = path;
+
+ ///
+ /// 相当于Path 包装,天杀的windows在路径字符串中使用两种分隔符,“/” 和“\”,这导致,即使两个字符串不相同,也可能是同一个路径。现在把它们统一起来
+ ///
+ public string FormatedPath
+ {
+ get { return Path.Replace("\\", "/"); }
+ set { Path = value; }
+ }
+ public abstract bool IsEqual(AFileOrDir other);
+
+ public static int Compare(AFileOrDir l, AFileOrDir r)
+ {
+ var pv = l.FormatedPath.CompareTo(r.FormatedPath);
+ if (pv == 0)
+ {
+ pv = l.Type.CompareTo(r.Type);
+ }
+ return pv;
+ }
+}
+
+///
+/// 文件
+///
+/// 绝对路径
+/// 文件的修改时间/
+public class File(string path, DateTime mtime, NextOpType? nextOp = null)
+ : AFileOrDir(path, DirOrFile.File, nextOp)
+{
+ public DateTime MTime { get; set; } = mtime;
+
+ public override bool IsEqual(AFileOrDir other)
+ {
+ if (other is not File otherFile)
+ {
+ return false;
+ }
+ else
+ {
+ var r =
+ this.MTime == otherFile.MTime
+ && this.FormatedPath == otherFile.FormatedPath
+ && this.NextOp == otherFile.NextOp;
+
+ return r;
+ }
+ }
+}
diff --git a/Server/FileSqlServerSync.sln b/Server/FileSqlServerSync.sln
index a09585e..4d57f6b 100644
--- a/Server/FileSqlServerSync.sln
+++ b/Server/FileSqlServerSync.sln
@@ -5,7 +5,11 @@ VisualStudioVersion = 17.10.35013.160
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteServer", "RemoteServer\RemoteServer.csproj", "{42EA16D0-00F2-444F-85B2-23E0C990261C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalServer", "LocalServer\LocalServer.csproj", "{AA0177A1-7CA3-44EA-9BCB-7E0731CE232B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalServer", "LocalServer\LocalServer.csproj", "{AA0177A1-7CA3-44EA-9BCB-7E0731CE232B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerTest", "ServerTest\ServerTest.csproj", "{0D507943-43A3-4227-903F-E123A5CAF7F4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{3EED9D63-BC7B-455F-BA15-95BB52311ED8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -21,6 +25,14 @@ Global
{AA0177A1-7CA3-44EA-9BCB-7E0731CE232B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA0177A1-7CA3-44EA-9BCB-7E0731CE232B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA0177A1-7CA3-44EA-9BCB-7E0731CE232B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0D507943-43A3-4227-903F-E123A5CAF7F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0D507943-43A3-4227-903F-E123A5CAF7F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0D507943-43A3-4227-903F-E123A5CAF7F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0D507943-43A3-4227-903F-E123A5CAF7F4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3EED9D63-BC7B-455F-BA15-95BB52311ED8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3EED9D63-BC7B-455F-BA15-95BB52311ED8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3EED9D63-BC7B-455F-BA15-95BB52311ED8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3EED9D63-BC7B-455F-BA15-95BB52311ED8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Server/RemoteServer/Controllers/SyncFilesController.cs b/Server/RemoteServer/Controllers/SyncFilesController.cs
new file mode 100644
index 0000000..0a68cef
--- /dev/null
+++ b/Server/RemoteServer/Controllers/SyncFilesController.cs
@@ -0,0 +1,128 @@
+using Microsoft.AspNetCore.Mvc;
+using RemoteServer.Models;
+namespace RemoteServer.Controllers;
+
+public class SyncFilesController(SqliteDbContext db) : ControllerBase
+{
+ private readonly SqliteDbContext _db = db;
+
+ [HttpGet("/GetSyncFilesLogs")]
+ public IActionResult GetSyncFilesLogs(
+ string? ClientName,
+ int? Status,
+ DateTime? SyncTimeStart,
+ DateTime? SyncTimeEnd,
+ int page,
+ int rows
+ )
+ {
+ var item =
+ from i in _db.SyncLogHeads
+ where
+ (
+ string.IsNullOrEmpty(ClientName)
+ || (i.ClientName != null && i.ClientName.Contains(ClientName))
+ )
+ && (Status == null || i.Status == Status)
+ && (SyncTimeStart == null || i.SyncTime >= SyncTimeStart)
+ && (SyncTimeEnd == null || i.SyncTime <= SyncTimeEnd)
+ orderby i.Id descending
+ select new
+ {
+ Head = i,
+ Files = (from j in _db.SyncLogFiles where j.HeadId == i.Id select j).ToList()
+ };
+
+ return Ok(item.Skip((page - 1) * rows).Take(rows).ToList());
+ }
+
+ public class InputFileInfo
+ {
+ public required string RelativePath { get; set; }
+ public DateTime MTime { get; set; }
+ }
+
+ public class OutputFileInfo : InputFileInfo
+ {
+ ///
+ /// 0 新增 1 修改 2 删除
+ ///
+ public int ServerOpType { get; set; }
+ }
+
+ public class ServerOpFileInfo : OutputFileInfo
+ {
+ public required string ServerRootDirPath { get; set; }
+ public required string ClientRootDirPath { get; set; }
+ }
+
+ public class InputFiles
+ {
+ public required string ServerRootDirPath { get; set; }
+
+ ///
+ /// 0 special 1 exclude
+ ///
+ public int Type { get; set; }
+ public List? Files { get; set; }
+ }
+
+ public class ServerOpFiles
+ {
+ public required string ServerRootDirPath { get; set; }
+ public string? ClientRootDirPath { get; set; }
+ public List? Files { get; set; }
+ }
+
+ [HttpPost("/GetFilesInfoByDir")]
+ public IActionResult GetFilesInfoByDir([FromBody] InputFiles inputFiles)
+ {
+ return Ok(new { IsSuccess = true });
+ }
+
+ [HttpPost("/InitASync")]
+ public IActionResult InitASync([FromBody] SyncLogHead head)
+ {
+ try
+ {
+ var CurrentSyncTaskCount = (
+ from i in _db.SyncLogHeads
+ where i.Status == 0
+ select i
+ ).Count();
+ if (CurrentSyncTaskCount > 0)
+ {
+ throw new Exception("存在未完成的任务,请等待完成!");
+ }
+ head.Id = Guid.NewGuid();
+ head.SyncTime = DateTime.Now;
+ head.Status = 0;
+ _db.SyncLogHeads.Add(head);
+ _db.SaveChanges();
+ return Ok(new { IsSuccess = true, head.Id });
+ }
+ catch (Exception e)
+ {
+ return Ok(new { IsSuccess = false, e.Message });
+ }
+ }
+
+ [HttpGet("/CloseASync")]
+ public IActionResult CloseASync(Guid Id, string Message, int Status)
+ {
+ try
+ {
+ var current =
+ (from i in _db.SyncLogHeads where i.Id == Id select i).FirstOrDefault()
+ ?? throw new Exception("任务不存在!");
+ current.Status = Status;
+ current.Message = Message;
+ _db.SaveChanges();
+ return Ok(new { IsSuccess = true });
+ }
+ catch (Exception e)
+ {
+ return Ok(new { IsSuccess = false, e.Message });
+ }
+ }
+}
diff --git a/Server/RemoteServer/Controllers/WeatherForecastController.cs b/Server/RemoteServer/Controllers/WeatherForecastController.cs
deleted file mode 100644
index b016946..0000000
--- a/Server/RemoteServer/Controllers/WeatherForecastController.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-
-namespace FileSqlServerSync.Controllers
-{
- [ApiController]
- [Route("[controller]")]
- public class WeatherForecastController : ControllerBase
- {
- private static readonly string[] Summaries = new[]
- {
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
- };
-
- private readonly ILogger _logger;
-
- public WeatherForecastController(ILogger logger)
- {
- _logger = logger;
- }
-
- [HttpGet(Name = "GetWeatherForecast")]
- public IEnumerable Get()
- {
- return Enumerable.Range(1, 5).Select(index => new WeatherForecast
- {
- Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
- TemperatureC = Random.Shared.Next(-20, 55),
- Summary = Summaries[Random.Shared.Next(Summaries.Length)]
- })
- .ToArray();
- }
- }
-}
diff --git a/Server/RemoteServer/Migrations/20240708101413_InitialCreate.Designer.cs b/Server/RemoteServer/Migrations/20240708101413_InitialCreate.Designer.cs
new file mode 100644
index 0000000..74cf683
--- /dev/null
+++ b/Server/RemoteServer/Migrations/20240708101413_InitialCreate.Designer.cs
@@ -0,0 +1,117 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using RemoteServer.Models;
+
+#nullable disable
+
+namespace RemoteServer.Migrations
+{
+ [DbContext(typeof(SqliteDbContext))]
+ [Migration("20240708101413_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
+
+ modelBuilder.Entity("RemoteServer.SyncGitCommit", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CommitId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CommitMessage")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CommitTime")
+ .HasColumnType("TEXT");
+
+ b.Property("CommitUserName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("HeadId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("SyncGitCommits");
+ });
+
+ modelBuilder.Entity("RemoteServer.SyncLogFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("ClientRootPath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("HeadId")
+ .HasColumnType("TEXT");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("ServerRootPath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("SyncLogFiles");
+ });
+
+ modelBuilder.Entity("RemoteServer.SyncLogHead", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("ClientID")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("ClientName")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Message")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncTime")
+ .HasColumnType("TEXT");
+
+ b.Property("VersionsFromTag")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("SyncLogHeads");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Server/RemoteServer/Migrations/20240708101413_InitialCreate.cs b/Server/RemoteServer/Migrations/20240708101413_InitialCreate.cs
new file mode 100644
index 0000000..c9f48ef
--- /dev/null
+++ b/Server/RemoteServer/Migrations/20240708101413_InitialCreate.cs
@@ -0,0 +1,76 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace RemoteServer.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "SyncGitCommits",
+ columns: table => new
+ {
+ Id = table.Column(type: "TEXT", nullable: false),
+ HeadId = table.Column(type: "TEXT", nullable: false),
+ CommitId = table.Column(type: "TEXT", nullable: false),
+ CommitUserName = table.Column(type: "TEXT", nullable: false),
+ CommitTime = table.Column(type: "TEXT", nullable: false),
+ CommitMessage = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SyncGitCommits", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "SyncLogFiles",
+ columns: table => new
+ {
+ Id = table.Column(type: "TEXT", nullable: false),
+ HeadId = table.Column(type: "TEXT", nullable: false),
+ ClientRootPath = table.Column(type: "TEXT", maxLength: 500, nullable: false),
+ ServerRootPath = table.Column(type: "TEXT", maxLength: 500, nullable: false),
+ RelativePath = table.Column(type: "TEXT", maxLength: 500, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SyncLogFiles", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "SyncLogHeads",
+ columns: table => new
+ {
+ Id = table.Column(type: "TEXT", nullable: false),
+ VersionsFromTag = table.Column(type: "TEXT", maxLength: 50, nullable: false),
+ SyncTime = table.Column(type: "TEXT", nullable: false),
+ ClientID = table.Column(type: "TEXT", maxLength: 200, nullable: false),
+ ClientName = table.Column(type: "TEXT", maxLength: 50, nullable: true),
+ Status = table.Column(type: "INTEGER", nullable: false),
+ Message = table.Column(type: "TEXT", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_SyncLogHeads", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "SyncGitCommits");
+
+ migrationBuilder.DropTable(
+ name: "SyncLogFiles");
+
+ migrationBuilder.DropTable(
+ name: "SyncLogHeads");
+ }
+ }
+}
diff --git a/Server/RemoteServer/Migrations/SqliteDbContextModelSnapshot.cs b/Server/RemoteServer/Migrations/SqliteDbContextModelSnapshot.cs
new file mode 100644
index 0000000..5d00a16
--- /dev/null
+++ b/Server/RemoteServer/Migrations/SqliteDbContextModelSnapshot.cs
@@ -0,0 +1,114 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using RemoteServer.Models;
+
+#nullable disable
+
+namespace RemoteServer.Migrations
+{
+ [DbContext(typeof(SqliteDbContext))]
+ partial class SqliteDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
+
+ modelBuilder.Entity("RemoteServer.SyncGitCommit", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CommitId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CommitMessage")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("CommitTime")
+ .HasColumnType("TEXT");
+
+ b.Property("CommitUserName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("HeadId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("SyncGitCommits");
+ });
+
+ modelBuilder.Entity("RemoteServer.SyncLogFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("ClientRootPath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("HeadId")
+ .HasColumnType("TEXT");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("ServerRootPath")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("SyncLogFiles");
+ });
+
+ modelBuilder.Entity("RemoteServer.SyncLogHead", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("ClientID")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("ClientName")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Message")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncTime")
+ .HasColumnType("TEXT");
+
+ b.Property("VersionsFromTag")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("SyncLogHeads");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Server/RemoteServer/Models/SqliteDbContext.cs b/Server/RemoteServer/Models/SqliteDbContext.cs
new file mode 100644
index 0000000..c1c0150
--- /dev/null
+++ b/Server/RemoteServer/Models/SqliteDbContext.cs
@@ -0,0 +1,22 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace RemoteServer.Models;
+
+public class SqliteDbContext : DbContext
+{
+ protected readonly IConfiguration Configuration;
+
+ public SqliteDbContext(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.UseSqlite(Configuration.GetConnectionString("DbPath"));
+ }
+
+ public DbSet SyncLogHeads { get; set; }
+ public DbSet SyncLogFiles { get; set; }
+ public DbSet SyncGitCommits { get; set; }
+}
diff --git a/Server/RemoteServer/Models/SyncFilesLog.cs b/Server/RemoteServer/Models/SyncFilesLog.cs
new file mode 100644
index 0000000..a8b9564
--- /dev/null
+++ b/Server/RemoteServer/Models/SyncFilesLog.cs
@@ -0,0 +1,95 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace RemoteServer;
+
+public class SyncLogHead
+{
+ [Key]
+ public Guid Id { get; set; }
+
+ ///
+ /// git versions
+ ///
+ [MaxLength(50)]
+ public required string VersionsFromTag { get; set; }
+
+ ///
+ /// 同步时间
+ ///
+ public DateTime SyncTime { get; set; }
+
+ ///
+ /// 客户端ID
+ ///
+ [MaxLength(200)]
+ public required string ClientID { get; set; }
+
+ ///
+ /// 客户端名称
+ ///
+ [MaxLength(50)]
+ public string? ClientName { get; set; }
+
+ ///
+ /// 状态 0 正在进行,1 已完成,2 失败有错误
+ ///
+ public int Status { get; set; }
+
+ ///
+ /// 同步消息
+ ///
+ public string? Message { get; set; }
+}
+
+public class SyncLogFile
+{
+ [Key]
+ public Guid Id { get; set; }
+
+ ///
+ /// 头部Id
+ ///
+ public Guid HeadId { get; set; }
+
+ ///
+ /// 客户端文件全目录
+ ///
+ [MaxLength(500)]
+ public required string ClientRootPath { get; set; }
+
+ ///
+ /// 服务器文件全目录
+ ///
+ [MaxLength(500)]
+ public required string ServerRootPath { get; set; }
+
+ ///
+ /// 相对路径
+ ///
+ [MaxLength(500)]
+ public required string RelativePath { get; set; }
+}
+
+public class SyncGitCommit
+{
+ [Key]
+ public Guid Id { get; set; }
+ public Guid HeadId { get; set; }
+ ///
+ /// git commit id
+ ///
+ public required string CommitId { get; set; }
+ ///
+ /// git commit 用户名
+ ///
+ public required string CommitUserName { get; set; }
+ ///
+ /// git commit 时间
+ ///
+ public DateTime CommitTime { get; set; }
+ ///
+ /// git 提交内容
+ ///
+ public required string CommitMessage { get; set; }
+}
diff --git a/Server/RemoteServer/Program.cs b/Server/RemoteServer/Program.cs
index e0269d7..e74c114 100644
--- a/Server/RemoteServer/Program.cs
+++ b/Server/RemoteServer/Program.cs
@@ -1,8 +1,15 @@
+using Microsoft.EntityFrameworkCore;
+using RemoteServer.Models;
+
+using RemoteServer;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
+builder.Services.AddDbContext(opions=>{
+ opions.UseSqlite(builder.Configuration.GetConnectionString("DbPath"));
+});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
@@ -15,9 +22,8 @@ if (app.Environment.IsDevelopment())
app.UseSwagger();
app.UseSwaggerUI();
}
-
-app.UseAuthorization();
-
+app.Urls.Clear();
+app.Urls.Add("http://0.0.0.0:6888");
app.MapControllers();
app.Run();
diff --git a/Server/RemoteServer/Properties/launchSettings.json b/Server/RemoteServer/Properties/launchSettings.json
index 5b4e296..cbff4a7 100644
--- a/Server/RemoteServer/Properties/launchSettings.json
+++ b/Server/RemoteServer/Properties/launchSettings.json
@@ -14,7 +14,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
- "applicationUrl": "http://localhost:5165",
+ "applicationUrl": "http://localhost:6888",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/Server/RemoteServer/RemoteServer.csproj b/Server/RemoteServer/RemoteServer.csproj
index 9daa180..07f04b6 100644
--- a/Server/RemoteServer/RemoteServer.csproj
+++ b/Server/RemoteServer/RemoteServer.csproj
@@ -7,7 +7,14 @@
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
-
+
+
+
diff --git a/Server/RemoteServer/RemoteServer.csproj.user b/Server/RemoteServer/RemoteServer.csproj.user
new file mode 100644
index 0000000..94e6971
--- /dev/null
+++ b/Server/RemoteServer/RemoteServer.csproj.user
@@ -0,0 +1,9 @@
+
+
+
+ ProjectDebugger
+
+
+ http
+
+
\ No newline at end of file
diff --git a/Server/RemoteServer/SyncFilesLog.db b/Server/RemoteServer/SyncFilesLog.db
new file mode 100644
index 0000000..86296ef
Binary files /dev/null and b/Server/RemoteServer/SyncFilesLog.db differ
diff --git a/Server/RemoteServer/WeatherForecast.cs b/Server/RemoteServer/WeatherForecast.cs
deleted file mode 100644
index 3ab6ac9..0000000
--- a/Server/RemoteServer/WeatherForecast.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace FileSqlServerSync
-{
- public class WeatherForecast
- {
- public DateOnly Date { get; set; }
-
- public int TemperatureC { get; set; }
-
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-
- public string? Summary { get; set; }
- }
-}
diff --git a/Server/RemoteServer/appsettings.json b/Server/RemoteServer/appsettings.json
index 10f68b8..dd1402f 100644
--- a/Server/RemoteServer/appsettings.json
+++ b/Server/RemoteServer/appsettings.json
@@ -1,4 +1,7 @@
{
+ "ConnectionStrings": {
+ "DbPath": "Data Source=SyncFilesLog.db"
+ },
"Logging": {
"LogLevel": {
"Default": "Information",
@@ -6,4 +9,4 @@
}
},
"AllowedHosts": "*"
-}
+}
\ No newline at end of file
diff --git a/Server/ServerTest/DirFileOpTest.cs b/Server/ServerTest/DirFileOpTest.cs
new file mode 100644
index 0000000..d199598
--- /dev/null
+++ b/Server/ServerTest/DirFileOpTest.cs
@@ -0,0 +1,71 @@
+namespace ServerTest;
+using Common;
+using Newtonsoft.Json;
+using System.Text.Json.Nodes;
+using Xunit;
+///
+/// xUnit将会对每个测试方法创建一个测试上下文,IClassFixture可以用来创建类中共享测试上下文,
+///
+/// XUnit 的测试方法不是按照顺序执行,所以注意对象状态
+///
+public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture
+{
+ private readonly FilesSeed filesSeed = filesSeed;
+
+
+ ///
+ /// 测试文件目录写入和提取
+ ///
+ [Fact]
+ public void FileDirWriteExtract()
+ {
+ var (IsSuccess, Message) = filesSeed.NewDir.WriteByThisInfo();
+ Assert.True(IsSuccess);
+ Dir nd = new(filesSeed.NewDir.FormatedPath);
+ nd.ExtractInfo();
+ Assert.True(nd.IsEqual(filesSeed.NewDir));
+ }
+
+ ///
+ /// 测试文件差异比较
+ ///
+ [Fact]
+ public void FileDirDiff()
+ {
+ var cDDir = filesSeed.NewDir.Diff(filesSeed.OldDir);
+
+ // Console.WriteLine("################################");
+ // Console.WriteLine(cDDir.Children.Count);
+ //Assert.True(IsSuccess);
+
+ var str = JsonConvert.SerializeObject(cDDir);
+ Assert.True(cDDir.Children.Count !=0);
+ Assert.True(filesSeed.DiffDir.IsEqual(cDDir));
+
+ }
+
+ ///
+ /// 测试文件合并
+ ///
+ [Fact]
+ public void DirsCombine()
+ {
+ var OldDirClone = filesSeed.OldDir.Clone();
+ var (IsSuccess, Message) = OldDirClone.Combine(filesSeed.DiffDir);
+ Assert.True(IsSuccess);
+ //Assert.False(filesSeed.NewDir.IsEqual(filesSeed.OldDir));
+ OldDirClone.ResetRootPath("OldDir","NewDir");
+ // Console.WriteLine(filesSeed.OldDir.Path);
+ Assert.True(OldDirClone.IsEqual(filesSeed.NewDir));
+
+ }
+
+ ///
+ /// 测试同步是否成功
+ ///
+ [Fact]
+ public void FinalSyncFileDir()
+ {
+ Assert.True(true);
+ }
+}
diff --git a/Server/ServerTest/FilesSeed.cs b/Server/ServerTest/FilesSeed.cs
new file mode 100644
index 0000000..9d0afd9
--- /dev/null
+++ b/Server/ServerTest/FilesSeed.cs
@@ -0,0 +1,206 @@
+using System.Text;
+using Common;
+
+namespace ServerTest;
+
+public class FilesSeed : IDisposable
+{
+ public FilesSeed()
+ {
+ Dir.WriteFileStrageFunc = (Common.File file) =>
+ {
+ //创建或者不创建直接打开文件
+ using (FileStream fs = System.IO.File.OpenWrite(file.FormatedPath))
+ {
+ byte[] info = Encoding.UTF8.GetBytes($"this is {file.FormatedPath},Now{DateTime.Now}");
+ fs.Write(info, 0, info.Length);
+ }
+ Console.WriteLine($"WriteFileStrageFunc {file.FormatedPath}");
+ System.IO.File.SetLastWriteTime(file.FormatedPath, file.MTime);
+ return (true, "");
+ };
+ Console.WriteLine("FilesSeed Construct");
+ // string TestPath = Path.Combine(Directory.GetCurrentDirectory(), "../../..");
+ DateTime NewTime = DateTime.Now.AddSeconds(-99);
+ DateTime OldTime = NewTime.AddSeconds(-20);
+ NewDir = new Dir(
+ TestPath + "/NewDir",
+ [
+ new Dir($"{TestPath}/NewDir/0"),
+ new Dir(
+ $"{TestPath}/NewDir/1",
+ [new Common.File($"{TestPath}/NewDir/1/1.txt", NewTime)]
+ ),
+ new Dir(
+ $"{TestPath}/NewDir/2",
+ [
+ new Common.File($"{TestPath}/NewDir/2/2.txt", NewTime),
+ new Dir(
+ $"{TestPath}/NewDir/2/2_1",
+ [
+ new Common.File($"{TestPath}/NewDir/2/2_1/1.txt", NewTime),
+ new Common.File($"{TestPath}/NewDir/2/2_1/2.txt", NewTime),
+ ]
+ ),
+ new Dir(
+ $"{TestPath}/NewDir/2/2_2",
+ [
+ new Common.File($"{TestPath}/NewDir/2/2_2/1.txt", NewTime),
+ new Common.File($"{TestPath}/NewDir/2/2_2/2.txt", NewTime),
+ new Dir(
+ $"{TestPath}/NewDir/2/2_2/2_3",
+ [
+ new Common.File(
+ $"{TestPath}/NewDir/2/2_2/2_3/1.txt",
+ NewTime
+ ),
+ ]
+ ),
+ ]
+ )
+ ]
+ ),
+ ]
+ );
+ DiffDir = new Dir(
+ $"{TestPath}/OldDir",
+ [
+ new Dir(
+ $"{TestPath}/OldDir/1",
+ [new Common.File($"{TestPath}/OldDir/1/2_D.txt", NewTime, NextOpType.Del),]
+ ),
+ new Dir(
+ $"{TestPath}/OldDir/2",
+ [
+ // 将要添加
+ new Common.File($"{TestPath}/OldDir/2/2.txt", NewTime, NextOpType.Add),
+ new Dir(
+ $"{TestPath}/OldDir/2/2_1",
+ [
+ new Common.File(
+ $"{TestPath}/OldDir/2/2_1/2.txt",
+ NewTime,
+ NextOpType.Modify
+ ),
+ ]
+ ),
+ new Dir(
+ $"{TestPath}/OldDir/2/2_2_M",
+ [
+ new Common.File(
+ $"{TestPath}/OldDir/2/2_2/1.txt",
+ OldTime,
+ NextOpType.Del
+ ),
+ new Common.File(
+ $"{TestPath}/OldDir/2/2_2/2.txt",
+ OldTime,
+ NextOpType.Del
+ ),
+ new Dir(
+ $"{TestPath}/OldDir/2/2_2/2_3",
+ [
+ new Common.File(
+ $"{TestPath}/OldDir/2/2_2/2_3/1.txt",
+ OldTime,
+ NextOpType.Del
+ ),
+ ],
+ NextOpType.Del
+ ),
+ ],
+ NextOpType.Del
+ ),
+ new Dir(
+ $"{TestPath}/OldDir/2/2_2",
+ [
+ new Common.File(
+ $"{TestPath}/OldDir/2/2_2/1.txt",
+ NewTime,
+ NextOpType.Add
+ ),
+ new Common.File(
+ $"{TestPath}/OldDir/2/2_2/2.txt",
+ NewTime,
+ NextOpType.Add
+ ),
+ new Dir(
+ $"{TestPath}/OldDir/2/2_2/2_3",
+ [
+ new Common.File(
+ $"{TestPath}/OldDir/2/2_2/2_3/1.txt",
+ NewTime,
+ NextOpType.Add
+ ),
+ ],
+ NextOpType.Add
+ ),
+ ],
+ NextOpType.Add
+ )
+ ]
+ ),
+ ]
+ );
+ OldDir = new Dir(
+ $"{TestPath}/OldDir",
+ [
+ new Dir($"{TestPath}/OldDir/0"),
+ new Dir(
+ $"{TestPath}/OldDir/1",
+ [
+ //不做修改
+ new Common.File($"{TestPath}/OldDir/1/1.txt", NewTime),
+ //将要删除
+ new Common.File($"{TestPath}/OldDir/1/2_D.txt", NewTime),
+ ]
+ ),
+ new Dir(
+ $"{TestPath}/OldDir/2",
+ [
+ new Dir(
+ $"{TestPath}/OldDir/2/2_1",
+ [
+ new Common.File($"{TestPath}/OldDir/2/2_1/1.txt", NewTime),
+ new Common.File($"{TestPath}/OldDir/2/2_1/2.txt", OldTime),
+ ]
+ ),
+ new Dir(
+ $"{TestPath}/OldDir/2/2_2_M",
+ [
+ new Common.File($"{TestPath}/OldDir/2/2_2/1.txt", OldTime),
+ new Common.File($"{TestPath}/OldDir/2/2_2/2.txt", OldTime),
+ new Dir(
+ $"{TestPath}/OldDir/2/2_2/2_3",
+ [
+ new Common.File(
+ $"{TestPath}/OldDir/2/2_2/2_3/1.txt",
+ OldTime
+ ),
+ ]
+ ),
+ ]
+ )
+ ]
+ ),
+ ]
+ );
+ }
+
+ private readonly string TestPath = Path.Combine(Directory.GetCurrentDirectory(), "../../..");
+ public Dir NewDir;
+ public Dir OldDir;
+
+ public Dir DiffDir;
+
+ public void Dispose()
+ {
+ Directory.Delete(NewDir.FormatedPath, true);
+ Console.WriteLine("FilesSeed Dispose");
+ GC.SuppressFinalize(this);
+ }
+ // ~FilesSeed()
+ // {
+ // Console.WriteLine("FilesSeed ~");
+ // }
+}
diff --git a/Server/ServerTest/ServerTest.csproj b/Server/ServerTest/ServerTest.csproj
new file mode 100644
index 0000000..bda4699
--- /dev/null
+++ b/Server/ServerTest/ServerTest.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tool/webtool/src/App.vue b/Tool/webtool/src/App.vue
index 341dbf0..52568c8 100644
--- a/Tool/webtool/src/App.vue
+++ b/Tool/webtool/src/App.vue
@@ -13,7 +13,8 @@ import HelloWorld from './components/HelloWorld.vue'
-
+