-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathSyncOptions.cs
More file actions
409 lines (354 loc) · 16 KB
/
SyncOptions.cs
File metadata and controls
409 lines (354 loc) · 16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
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;
/// <summary>
/// 客户端的同步命令行参数
/// </summary>
[Verb("sync")]
internal class SyncOptions
{
/// <summary>
/// 同步服务的地址,如 http://127.0.0.1:56621
/// </summary>
[Option('a', "Address")]
public string? Address { set; get; }
/// <summary>
/// 本地同步的文件夹,不填默认为工作路径
/// </summary>
[Option('f', "Folder")]
public string? SyncFolder { set; get; }
public async Task Run()
{
var syncFolder = SyncFolder;
if (string.IsNullOrEmpty(syncFolder))
{
// 没有给明确的文件夹,使用工作文件夹
syncFolder = Environment.CurrentDirectory;
if (OperatingSystem.IsLinux())
{
// 不写 -f 禁止 ~ 路径
var homePath = Environment.GetEnvironmentVariable("HOME");
if (syncFolder == homePath)
{
Console.WriteLine($"禁止在 $HOME='{homePath}' 路径下进行同步,如果确定要在此路径下同步,请使用 -f 参数明确指定路径");
return;
}
}
}
if (OperatingSystem.IsLinux())
{
if (syncFolder == "/")
{
Console.WriteLine($"禁止使用 / 根路径作为同步文件夹");
return;
}
}
if (string.IsNullOrEmpty(Address))
{
Console.WriteLine($@"找不到同步地址,请确保传入正确参数。
参数列表:
- `-a` 或 `--Address` : 【必填】同步服务的地址,如 http://127.0.0.1:56621
- `-f` 或 `--Folder` : 【选填】本地同步的文件夹,不填默认为工作路径
参数例子:
SyncTool -a http://127.0.0.1:56621 -f lindexi");
return;
}
syncFolder = Path.GetFullPath(syncFolder);
Directory.CreateDirectory(syncFolder);
Console.WriteLine($"开始执行文件夹同步。同步地址:{Address} 同步文件夹{syncFolder}");
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 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}] 开始同步 - {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($"【同步失败】同步地址:{Address} 同步文件夹{syncFolder}\r\n{e}");
}
}
async Task SyncFolderAsync(List<SyncFileInfo> remote, List<SyncFolderPathInfo> syncFolderPathInfoList, ulong version)
{
Dictionary<string/*RelativePath*/, SyncFileInfo> local = syncFileDictionary;
foreach (var remoteSyncFileInfo in remote)
{
// ReSharper disable AccessToModifiedClosure
if (version != currentVersion)
{
return;
}
if (local.TryGetValue(remoteSyncFileInfo.RelativePath, out var localInfo))
{
// 如果能拿到本地的记录,判断一下是否需要更新
var localFilePath = Path.Join(syncFolder, localInfo.RelativePath);
var localFile = new FileInfo(localFilePath);
if (localFile.Exists
// 时间不能取本地时间,因为必定存在时间差
&& localInfo.LastWriteTimeUtc == remoteSyncFileInfo.LastWriteTimeUtc
&& localFile.Length == remoteSyncFileInfo.FileSize)
{
// 如果本地的记录不需要更新,那就跳过
continue;
}
}
Console.WriteLine($"正在同步 {remoteSyncFileInfo.RelativePath}");
// 先下载到一个新的文件,然后再重命名替换
// 如果原本的文件正在被占用,那失败的只有重命名部分,而不会导致重复下载
// 那如果下载失败呢?大概需要重新开始同步了
var downloadFilePath = await DownloadFile(remoteSyncFileInfo);
// 完成下载,移动下载的文件作为正式需要的文件
while (true)
{
if (version != currentVersion)
{
return;
}
try
{
var localFilePath = Path.Join(syncFolder, remoteSyncFileInfo.RelativePath);
File.Move(downloadFilePath, localFilePath, overwrite: true);
break;
}
catch
{
// 忽略
Console.WriteLine($"同步 {remoteSyncFileInfo.RelativePath} 失败,正在重试");
}
// 快速下载完成
//await Task.Delay(200);
}
}
foreach (var folderPathInfo in syncFolderPathInfoList)
{
if (version != currentVersion)
{
return;
}
// 如果文件夹不存在,则创建文件夹
var localFilePath = Path.Join(syncFolder, folderPathInfo.RelativePath);
Directory.CreateDirectory(localFilePath);
}
// 先删除多余的文件,再删除空文件夹,除非空文件夹是在记录里面的
await RemoveRedundantFile(remote, version);
await RemoveRedundantFolder(syncFolderPathInfoList, version);
}
async Task RemoveRedundantFile(List<SyncFileInfo> remote, ulong version)
{
// 删除多余的文件,也就是本地存在但是远程不存在的文件
// 记录已经更新的 RelativePath 哈希,用来记录哪些存在
var updatedList = new HashSet<string>(remote.Count);
foreach (var syncFileInfo in remote)
{
updatedList.Add(syncFileInfo.RelativePath);
}
foreach (var file in Directory.GetFiles(syncFolder, "*", SearchOption.AllDirectories))
{
if (version != currentVersion)
{
return;
}
var relativePath = Path.GetRelativePath(syncFolder, file);
// 用来兼容 Linux 系统
relativePath = relativePath.Replace('\\', '/');
for (int i = 0; i < 1000; i++)
{
try
{
if (updatedList.Contains(relativePath))
{
break;
}
else
{
// 本地存在,远端不存在,删除
File.Delete(file);
Console.WriteLine($"删除 {relativePath}");
if (!File.Exists(file))
{
break;
}
}
}
catch (Exception e)
{
if (i == 100)
{
Console.WriteLine($"第{i}次删除 {relativePath} 失败 {e}");
}
await Task.Delay(100);
}
}
}
}
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.Exists(folder))
{
// 如果文件夹不存在了,可能是之前删除了,不再继续
// 什么情况可能导致文件夹不存在?如有这样的路径 A\B\C 结构
// 之前进入到 A\B 时,将 B 删除了,那么 A\B\C 就会不存在
continue;
}
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
{
// 删除空文件夹的时候,可能遇到空文件夹里面还包含其他空文件夹,导致删除失败
// System.IO.IOException: Directory not empty
Directory.Delete(folder, true);
if (!Directory.Exists(folder))
{
break;
}
}
}
catch (Exception e)
{
if (i == 100 - 1)
{
Console.WriteLine($"第{i}次删除 {relativePath} 失败 {e}");
}
await Task.Delay(100);
}
}
}
foreach (var syncFolderPathInfo in syncFolderPathInfoList)
{
var folderPath = Path.Join(syncFolder, syncFolderPathInfo.RelativePath);
// 重新创建,防止误删空文件夹
Directory.CreateDirectory(folderPath);
}
}
async Task<string> DownloadFile(SyncFileInfo remoteSyncFileInfo)
{
// 发起请求,使用 Post 的方式,解决 GetURL 的字符不支持
var request = new DownloadFileRequest(remoteSyncFileInfo.RelativePath);
var response = await httpClient.PostAsJsonAsync("/Download", request);
await using var stream = await response.Content.ReadAsStreamAsync();
var downloadFilePath = Path.Join(syncFolder, $"{remoteSyncFileInfo.RelativePath}_{Path.GetRandomFileName()}");
// 下载之前先确保文件夹存在,防止下载炸了
Directory.CreateDirectory(Path.GetDirectoryName(downloadFilePath)!);
await using var fileStream = File.Create(downloadFilePath);
await stream.CopyToAsync(fileStream);
return downloadFilePath;
}
async Task ReportCompleted(ulong version)
{
try
{
var syncCompletedRequest = new SyncCompletedRequest(clientName, version);
await httpClient.PostAsJsonAsync("/SyncCompleted", syncCompletedRequest);
}
catch
{
// 只是报告而已,失败就失败
}
}
}
/// <summary>
/// 初始化本地文件的信息
/// </summary>
/// <param name="syncFolder"></param>
/// <returns></returns>
private static Dictionary<string /*RelativePath*/, SyncFileInfo> InitLocalInfo(string syncFolder)
{
var syncFileDictionary =
new Dictionary<string /*RelativePath*/, SyncFileInfo>();
foreach (var file in Directory.EnumerateFiles(syncFolder, "*", SearchOption.AllDirectories))
{
var fileInfo = new FileInfo(file);
var relativePath = Path.GetRelativePath(syncFolder, file);
var syncFileInfo = new SyncFileInfo(relativePath, fileInfo.Length, fileInfo.LastWriteTimeUtc);
syncFileDictionary[relativePath] = syncFileInfo;
}
return syncFileDictionary;
}
}