chore: 添加sqlite 数据库,remote server 增加一些接口
添加了一些数据模型,添加了一些简单的单元测试
This commit is contained in:
parent
df258598b6
commit
46315ba760
24 changed files with 1570 additions and 54 deletions
18
Server/.config/dotnet-tools.json
Normal file
18
Server/.config/dotnet-tools.json
Normal 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
18
Server/.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
9
Server/Common/Common.csproj
Normal file
9
Server/Common/Common.csproj
Normal 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
542
Server/Common/Dir.cs
Normal 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;
|
||||
}
|
||||
}
|
79
Server/Common/FileDirBase.cs
Normal file
79
Server/Common/FileDirBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
128
Server/RemoteServer/Controllers/SyncFilesController.cs
Normal file
128
Server/RemoteServer/Controllers/SyncFilesController.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
117
Server/RemoteServer/Migrations/20240708101413_InitialCreate.Designer.cs
generated
Normal file
117
Server/RemoteServer/Migrations/20240708101413_InitialCreate.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
114
Server/RemoteServer/Migrations/SqliteDbContextModelSnapshot.cs
Normal file
114
Server/RemoteServer/Migrations/SqliteDbContextModelSnapshot.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
22
Server/RemoteServer/Models/SqliteDbContext.cs
Normal file
22
Server/RemoteServer/Models/SqliteDbContext.cs
Normal 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; }
|
||||
}
|
95
Server/RemoteServer/Models/SyncFilesLog.cs
Normal file
95
Server/RemoteServer/Models/SyncFilesLog.cs
Normal 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; }
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5165",
|
||||
"applicationUrl": "http://localhost:6888",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
9
Server/RemoteServer/RemoteServer.csproj.user
Normal file
9
Server/RemoteServer/RemoteServer.csproj.user
Normal 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>
|
BIN
Server/RemoteServer/SyncFilesLog.db
Normal file
BIN
Server/RemoteServer/SyncFilesLog.db
Normal file
Binary file not shown.
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"DbPath": "Data Source=SyncFilesLog.db"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
@ -6,4 +9,4 @@
|
|||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
}
|
71
Server/ServerTest/DirFileOpTest.cs
Normal file
71
Server/ServerTest/DirFileOpTest.cs
Normal 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);
|
||||
}
|
||||
}
|
206
Server/ServerTest/FilesSeed.cs
Normal file
206
Server/ServerTest/FilesSeed.cs
Normal 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 ~");
|
||||
// }
|
||||
}
|
29
Server/ServerTest/ServerTest.csproj
Normal file
29
Server/ServerTest/ServerTest.csproj
Normal 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>
|
|
@ -13,7 +13,8 @@ import HelloWorld from './components/HelloWorld.vue'
|
|||
</div>
|
||||
<HelloWorld msg="Vite + Vue" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
|
|
Loading…
Reference in a new issue