Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 156 additions & 16 deletions SyncTool/Client/SyncOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
using dotnetCampus.Cli;
using System;
using System.Configuration.Assemblies;
using System.Net;
using System.Reflection;

using dotnetCampus.Cli;

using SyncTool.Configurations;
using SyncTool.Context;
using SyncTool.Server;
using SyncTool.Utils;

namespace SyncTool.Client;

Expand Down Expand Up @@ -49,41 +57,84 @@ public async Task Run()

Console.WriteLine($"开始执行文件夹同步。同步地址:{Address} 同步文件夹{syncFolder}");

using var httpClient = new HttpClient();
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(Address);
// 客户端允许等着服务端慢慢返回,不要不断发送请求
httpClient.Timeout = ServerConfiguration.MaxFreeTime;

// 记录本地的字典值。首次同步的时候需要用到
Dictionary<string, SyncFileInfo> syncFileDictionary = InitLocalInfo(syncFolder);

ulong currentVersion = 0;
bool isFirstQuery = true;
var clientName = Environment.MachineName;
while (true)
{
try
{
var syncFolderInfo = await httpClient.GetFromJsonAsync<SyncFolderInfo>("/");
var queryFileStatusRequest = new QueryFileStatusRequest(clientName, currentVersion, isFirstQuery);
using var httpResponseMessage = await httpClient.PostAsJsonAsync("/", queryFileStatusRequest);
if (httpResponseMessage.StatusCode == HttpStatusCode.NotFound)
{
// 服务端是不是还没开启 是不是开启错版本了
var assemblyVersion =
GetType().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
Console.WriteLine($"服务器返回 404 可能访问错误的服务,或 SyncTool 服务器版本过低。当前 SyncTool 客户端版本:{assemblyVersion}");

// 同步结束
return;
}

httpResponseMessage.EnsureSuccessStatusCode();

var queryFileStatusResponse =
await httpResponseMessage.Content.ReadFromJsonAsync<QueryFileStatusResponse>();
var syncFolderInfo = queryFileStatusResponse?.SyncFolderInfo;

if (syncFolderInfo is null || syncFolderInfo.Version == currentVersion)
{
// 这里不需要等待,继续不断发起请求就可以
// 为什么不怕发送太多,影响性能?服务端不会立刻返回
//await Task.Delay(TimeSpan.FromSeconds(1));
continue;
}

isFirstQuery = false;
currentVersion = syncFolderInfo.Version;
Console.WriteLine($"[{currentVersion}] 开始同步");
await SyncFolderAsync(syncFolderInfo.SyncFileList, currentVersion);
Console.WriteLine($"[{currentVersion}] 开始同步 - {DateTimeHelper.DateTimeNowToLogMessage()}");
await SyncFolderAsync(syncFolderInfo.SyncFileList, syncFolderInfo.SyncFolderPathInfoList, currentVersion);

Console.WriteLine($"[{currentVersion}] 同步完成 - {DateTimeHelper.DateTimeNowToLogMessage()}");
Console.WriteLine($"同步地址:{Address} 同步文件夹{syncFolder}");
Console.WriteLine("==========");

// 更新本地字典信息
syncFileDictionary.Clear();
foreach (var syncFileInfo in syncFolderInfo.SyncFileList)
{
syncFileDictionary[syncFileInfo.RelativePath] = syncFileInfo;
}

_ = ReportCompleted(currentVersion);
}
catch (HttpRequestException e)
{
if (e.HttpRequestError == HttpRequestError.ConnectionError)
{
// 可能是服务器还没开启
Console.WriteLine($"【同步失败】连接服务器失败,同步地址:{Address} 同步文件夹{syncFolder}");
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
catch (Exception e)
{
// 大不了下次再继续
Console.WriteLine(e);
Console.WriteLine($"【同步失败】同步地址:{Address} 同步文件夹{syncFolder}\r\n{e}");
}
}

async Task SyncFolderAsync(List<SyncFileInfo> remote, ulong version)
async Task SyncFolderAsync(List<SyncFileInfo> remote, List<SyncFolderPathInfo> syncFolderPathInfoList, ulong version)
{
Dictionary<string/*RelativePath*/, SyncFileInfo> local = syncFileDictionary;

Expand Down Expand Up @@ -138,15 +189,26 @@ async Task SyncFolderAsync(List<SyncFileInfo> remote, ulong version)
Console.WriteLine($"同步 {remoteSyncFileInfo.RelativePath} 失败,正在重试");
}

await Task.Delay(200);
// 快速下载完成
//await Task.Delay(200);
}
}

await RemoveRedundantFile(remote, version);
foreach (var folderPathInfo in syncFolderPathInfoList)
{
if (version != currentVersion)
{
return;
}

// 如果文件夹不存在,则创建文件夹
var localFilePath = Path.Join(syncFolder, folderPathInfo.RelativePath);
Directory.CreateDirectory(localFilePath);
}

Console.WriteLine($"[{version}] 同步完成");
Console.WriteLine($"同步地址:{Address} 同步文件夹{syncFolder}");
Console.WriteLine("==========");
// 先删除多余的文件,再删除空文件夹,除非空文件夹是在记录里面的
await RemoveRedundantFile(remote, version);
await RemoveRedundantFolder(syncFolderPathInfoList, version);
}

async Task RemoveRedundantFile(List<SyncFileInfo> remote, ulong version)
Expand All @@ -166,18 +228,27 @@ async Task RemoveRedundantFile(List<SyncFileInfo> remote, ulong version)
return;
}

var relativePath = Path.GetRelativePath(syncFolder, file);
// 用来兼容 Linux 系统
relativePath = relativePath.Replace('\\', '/');

for (int i = 0; i < 1000; i++)
{
var relativePath = Path.GetRelativePath(syncFolder, file);
// 用来兼容 Linux 系统
relativePath = relativePath.Replace('\\', '/');
try
{
if (!updatedList.Contains(relativePath))
if (updatedList.Contains(relativePath))
{
break;
}
else
{
// 本地存在,远端不存在,删除
File.Delete(file);
Console.WriteLine($"删除 {relativePath}");
if (!File.Exists(file))
{
break;
}
}
}
catch (Exception e)
Expand All @@ -193,6 +264,62 @@ async Task RemoveRedundantFile(List<SyncFileInfo> remote, ulong version)
}
}

async Task RemoveRedundantFolder(List<SyncFolderPathInfo> syncFolderPathInfoList, ulong version)
{
var updatedList = new HashSet<string>(syncFolderPathInfoList.Count);
foreach (var syncFileInfo in syncFolderPathInfoList)
{
updatedList.Add(syncFileInfo.RelativePath);
}

foreach (var folder in Directory.GetDirectories(syncFolder, "*", SearchOption.AllDirectories))
{
if (version != currentVersion)
{
return;
}

if (Directory.EnumerateFiles(folder,"*",SearchOption.AllDirectories).Any())
{
// 如果存在文件,则不是空文件夹,不能删除
continue;
}

// 没有任何文件的空文件夹,如果不在列表里面,则需要删除文件夹
var relativePath = Path.GetRelativePath(syncFolder, folder);
// 用来兼容 Linux 系统
relativePath = relativePath.Replace('\\', '/');

for (int i = 0; i < 100; i++)
{
try
{
if (updatedList.Contains(relativePath))
{
break;
}
else
{
Directory.Delete(folder);
if (!Directory.Exists(folder))
{
break;
}
}
}
catch (Exception e)
{
if (i == 100 - 1)
{
Console.WriteLine($"第{i}次删除 {relativePath} 失败 {e}");
}

await Task.Delay(100);
}
}
}
}

async Task<string> DownloadFile(SyncFileInfo remoteSyncFileInfo)
{
// 发起请求,使用 Post 的方式,解决 GetURL 的字符不支持
Expand All @@ -209,6 +336,19 @@ async Task<string> DownloadFile(SyncFileInfo remoteSyncFileInfo)

return downloadFilePath;
}

async Task ReportCompleted(ulong version)
{
try
{
var syncCompletedRequest = new SyncCompletedRequest(clientName, version);
await httpClient.PostAsJsonAsync("/SyncCompleted", syncCompletedRequest);
}
catch
{
// 只是报告而已,失败就失败
}
}
}

/// <summary>
Expand Down
9 changes: 9 additions & 0 deletions SyncTool/Configurations/ServerConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SyncTool.Configurations;

static class ServerConfiguration
{
/// <summary>
/// 如果服务端没有更新,最多会空挂机 1 分钟以内
/// </summary>
public static TimeSpan MaxFreeTime => TimeSpan.FromMinutes(1);
}
11 changes: 11 additions & 0 deletions SyncTool/Context/QueryFileStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SyncTool.Context;

/// <summary>
/// 查询文件状态的请求
/// </summary>
record QueryFileStatusRequest(string ClientName, ulong CurrentVersion, bool IsFirstQuery);

/// <summary>
/// 查询文件状态的响应
/// </summary>
record QueryFileStatusResponse(SyncFolderInfo SyncFolderInfo);
4 changes: 4 additions & 0 deletions SyncTool/Context/SyncCompleted.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace SyncTool.Context;

record SyncCompletedRequest(string ClientName, ulong CurrentVersion);
record SyncCompletedResponse();
8 changes: 7 additions & 1 deletion SyncTool/Context/SyncFileInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@
/// <param name="RelativePath">文件的相对路径。这里为了兼容 Linux 系统,采用的是 / 字符</param>
/// <param name="FileSize"></param>
/// <param name="LastWriteTimeUtc"></param>
record SyncFileInfo(string RelativePath, long FileSize, DateTime LastWriteTimeUtc);
record SyncFileInfo(string RelativePath, long FileSize, DateTime LastWriteTimeUtc);

/// <summary>
/// 文件夹信息,防止空文件夹没有被同步过去
/// </summary>
/// <param name="RelativePath"></param>
record SyncFolderPathInfo(string RelativePath);
2 changes: 1 addition & 1 deletion SyncTool/Context/SyncFolderInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// </summary>
/// <param name="Version">同步版本</param>
/// <param name="SyncFileList">同步的文件列表</param>
record SyncFolderInfo(ulong Version, List<SyncFileInfo> SyncFileList)
record SyncFolderInfo(ulong Version, List<SyncFileInfo> SyncFileList, List<SyncFolderPathInfo> SyncFolderPathInfoList)
{
/// <summary>
/// 同步的文件字典,用来给服务端快速获取文件对应
Expand Down
6 changes: 4 additions & 2 deletions SyncTool/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
},
"Serve": {
"commandName": "Project",
"commandLineArgs": "serve -p 56621"
"commandLineArgs": "serve -p 5625",
"workingDirectory": "C:\\lindexi\\SyncFile_Serve\\"
},
"Sync": {
"commandName": "Project",
"commandLineArgs": "sync -a http://127.0.0.1:56621"
"commandLineArgs": "sync -a http://127.0.0.1:56622",
"workingDirectory": "C:\\lindexi\\SyncFile_Client\\"
}
}
}
Loading