chore: 添加sqlite 数据库,remote server 增加一些接口

添加了一些数据模型,添加了一些简单的单元测试
This commit is contained in:
zhaoyouya 2024-06-24 17:27:12 +08:00 committed by zerlei
parent df258598b6
commit 46315ba760
24 changed files with 1570 additions and 54 deletions

View file

@ -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"
]
}
}
}

18
Server/.vscode/launch.json vendored Normal file
View file

@ -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"
}
},
]
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

542
Server/Common/Dir.cs Normal file
View file

@ -0,0 +1,542 @@
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;
}
}

View file

@ -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;
/// <summary>
/// 全部为绝对路径... 占用资源会大一点但是完全OK
/// </summary>
///
private string Path = path;
/// <summary>
/// 相当于Path 包装天杀的windows在路径字符串中使用两种分隔符“/” 和“\”,这导致,即使两个字符串不相同,也可能是同一个路径。现在把它们统一起来
/// </summary>
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;
}
}
/// <summary>
/// 文件
/// </summary>
/// <param name="path">绝对路径</param>
/// <param name="mtime">文件的修改时间</param>/
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;
}
}
}

View file

@ -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

View file

@ -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
{
/// <summary>
/// 0 新增 1 修改 2 删除
/// </summary>
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; }
/// <summary>
/// 0 special 1 exclude
/// </summary>
public int Type { get; set; }
public List<FileInfo>? Files { get; set; }
}
public class ServerOpFiles
{
public required string ServerRootDirPath { get; set; }
public string? ClientRootDirPath { get; set; }
public List<OutputFileInfo>? 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 });
}
}
}

View file

@ -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<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> 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();
}
}
}

View file

@ -0,0 +1,117 @@
// <auto-generated />
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
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("RemoteServer.SyncGitCommit", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("CommitId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CommitMessage")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CommitTime")
.HasColumnType("TEXT");
b.Property<string>("CommitUserName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("HeadId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("SyncGitCommits");
});
modelBuilder.Entity("RemoteServer.SyncLogFile", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ClientRootPath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<Guid>("HeadId")
.HasColumnType("TEXT");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("ServerRootPath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("SyncLogFiles");
});
modelBuilder.Entity("RemoteServer.SyncLogHead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ClientID")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("ClientName")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Message")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTime>("SyncTime")
.HasColumnType("TEXT");
b.Property<string>("VersionsFromTag")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("SyncLogHeads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,76 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RemoteServer.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SyncGitCommits",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
HeadId = table.Column<Guid>(type: "TEXT", nullable: false),
CommitId = table.Column<string>(type: "TEXT", nullable: false),
CommitUserName = table.Column<string>(type: "TEXT", nullable: false),
CommitTime = table.Column<DateTime>(type: "TEXT", nullable: false),
CommitMessage = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SyncGitCommits", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SyncLogFiles",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
HeadId = table.Column<Guid>(type: "TEXT", nullable: false),
ClientRootPath = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
ServerRootPath = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
RelativePath = table.Column<string>(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<Guid>(type: "TEXT", nullable: false),
VersionsFromTag = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
SyncTime = table.Column<DateTime>(type: "TEXT", nullable: false),
ClientID = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
ClientName = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Status = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SyncLogHeads", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SyncGitCommits");
migrationBuilder.DropTable(
name: "SyncLogFiles");
migrationBuilder.DropTable(
name: "SyncLogHeads");
}
}
}

View file

@ -0,0 +1,114 @@
// <auto-generated />
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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("CommitId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CommitMessage")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CommitTime")
.HasColumnType("TEXT");
b.Property<string>("CommitUserName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("HeadId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("SyncGitCommits");
});
modelBuilder.Entity("RemoteServer.SyncLogFile", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ClientRootPath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<Guid>("HeadId")
.HasColumnType("TEXT");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("ServerRootPath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("SyncLogFiles");
});
modelBuilder.Entity("RemoteServer.SyncLogHead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ClientID")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("ClientName")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Message")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTime>("SyncTime")
.HasColumnType("TEXT");
b.Property<string>("VersionsFromTag")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("SyncLogHeads");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -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<SyncLogHead> SyncLogHeads { get; set; }
public DbSet<SyncLogFile> SyncLogFiles { get; set; }
public DbSet<SyncGitCommit> SyncGitCommits { get; set; }
}

View file

@ -0,0 +1,95 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RemoteServer;
public class SyncLogHead
{
[Key]
public Guid Id { get; set; }
/// <summary>
/// git versions
/// </summary>
[MaxLength(50)]
public required string VersionsFromTag { get; set; }
/// <summary>
/// 同步时间
/// </summary>
public DateTime SyncTime { get; set; }
/// <summary>
/// 客户端ID
/// </summary>
[MaxLength(200)]
public required string ClientID { get; set; }
/// <summary>
/// 客户端名称
/// </summary>
[MaxLength(50)]
public string? ClientName { get; set; }
/// <summary>
/// 状态 0 正在进行1 已完成2 失败有错误
/// </summary>
public int Status { get; set; }
/// <summary>
/// 同步消息
/// </summary>
public string? Message { get; set; }
}
public class SyncLogFile
{
[Key]
public Guid Id { get; set; }
/// <summary>
/// 头部Id
/// </summary>
public Guid HeadId { get; set; }
/// <summary>
/// 客户端文件全目录
/// </summary>
[MaxLength(500)]
public required string ClientRootPath { get; set; }
/// <summary>
/// 服务器文件全目录
/// </summary>
[MaxLength(500)]
public required string ServerRootPath { get; set; }
/// <summary>
/// 相对路径
/// </summary>
[MaxLength(500)]
public required string RelativePath { get; set; }
}
public class SyncGitCommit
{
[Key]
public Guid Id { get; set; }
public Guid HeadId { get; set; }
/// <summary>
/// git commit id
/// </summary>
public required string CommitId { get; set; }
/// <summary>
/// git commit 用户名
/// </summary>
public required string CommitUserName { get; set; }
/// <summary>
/// git commit 时间
/// </summary>
public DateTime CommitTime { get; set; }
/// <summary>
/// git 提交内容
/// </summary>
public required string CommitMessage { get; set; }
}

View file

@ -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<SqliteDbContext>(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();

View file

@ -14,7 +14,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5165",
"applicationUrl": "http://localhost:6888",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View file

@ -7,7 +7,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<ActiveDebugProfile>http</ActiveDebugProfile>
</PropertyGroup>
</Project>

Binary file not shown.

View file

@ -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; }
}
}

View file

@ -1,4 +1,7 @@
{
"ConnectionStrings": {
"DbPath": "Data Source=SyncFilesLog.db"
},
"Logging": {
"LogLevel": {
"Default": "Information",
@ -6,4 +9,4 @@
}
},
"AllowedHosts": "*"
}
}

View file

@ -0,0 +1,71 @@
namespace ServerTest;
using Common;
using Newtonsoft.Json;
using System.Text.Json.Nodes;
using Xunit;
/// <summary>
/// xUnit将会对每个测试方法创建一个测试上下文IClassFixture可以用来创建类中共享测试上下文
///
/// XUnit 的测试方法不是按照顺序执行,所以注意对象状态
/// </summary>
public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture<FilesSeed>
{
private readonly FilesSeed filesSeed = filesSeed;
/// <summary>
/// 测试文件目录写入和提取
/// </summary>
[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));
}
/// <summary>
/// 测试文件差异比较
/// </summary>
[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));
}
/// <summary>
/// 测试文件合并
/// </summary>
[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));
}
/// <summary>
/// 测试同步是否成功
/// </summary>
[Fact]
public void FinalSyncFileDir()
{
Assert.True(true);
}
}

View file

@ -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 ~");
// }
}

View file

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RemoteServer\RemoteServer.csproj" />
<ProjectReference Include="..\LocalServer\LocalServer.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View file

@ -13,7 +13,8 @@ import HelloWorld from './components/HelloWorld.vue'
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<script>
</script>
<style scoped>
.logo {
height: 6em;