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' - +