PatchTransport.cs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. using System.Net;
  2. using System.Net.Http.Headers;
  3. using System.Xml.Linq;
  4. using Renci.SshNet;
  5. using Renci.SshNet.Sftp;
  6. namespace AutoPatcherAdmin
  7. {
  8. public sealed class PatchTransferProgress
  9. {
  10. public string FileName { get; init; } = string.Empty;
  11. public double OverallProgress { get; init; }
  12. public double FileProgress { get; init; }
  13. public long BytesPerSecond { get; init; }
  14. public int RemainingFiles { get; init; }
  15. public string Action { get; init; } = string.Empty;
  16. }
  17. public interface IPatchTransport : IDisposable
  18. {
  19. event EventHandler<PatchTransferProgress>? ProgressChanged;
  20. bool FileExists(string remotePath);
  21. bool DirectoryExists(string remotePath);
  22. IReadOnlyList<string> EnumerateFiles(string remoteDirectory, CancellationToken cancellationToken = default);
  23. IReadOnlyList<string> EnumerateDirectories(string remoteDirectory, CancellationToken cancellationToken = default);
  24. long GetFileLength(string remotePath);
  25. byte[] DownloadFile(string remotePath);
  26. void DownloadFiles(IEnumerable<string> remotePaths, string localRoot, CancellationToken cancellationToken = default);
  27. void UploadDirectory(string localRoot, string remoteRoot, IReadOnlyList<string> relativePaths, string action, CancellationToken cancellationToken = default);
  28. void EnsureDirectory(string remoteDirectory);
  29. void MoveFile(string sourcePath, string destinationPath);
  30. void DeleteFile(string remotePath);
  31. void DeleteDirectory(string remoteDirectory);
  32. void DeleteEmptyDirectory(string remoteDirectory);
  33. }
  34. public static class PatchTransportFactory
  35. {
  36. public static IPatchTransport Create()
  37. {
  38. var protocol = (Settings.Protocol ?? string.Empty).Trim();
  39. var uri = new Uri(Settings.Host);
  40. if (protocol.Equals("SFtp", StringComparison.OrdinalIgnoreCase) ||
  41. protocol.Equals("Sftp", StringComparison.OrdinalIgnoreCase) ||
  42. uri.Scheme.Equals("sftp", StringComparison.OrdinalIgnoreCase))
  43. {
  44. return new SftpPatchTransport(uri, Settings.Login, Settings.Password, Settings.Port);
  45. }
  46. if (protocol.Equals("Http", StringComparison.OrdinalIgnoreCase) ||
  47. protocol.Equals("Https", StringComparison.OrdinalIgnoreCase) ||
  48. uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ||
  49. uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
  50. {
  51. return new HttpPatchTransport(uri, Settings.Login, Settings.Password, Settings.Port);
  52. }
  53. return new FtpPatchTransport(uri, Settings.Login, Settings.Password, Settings.Port);
  54. }
  55. }
  56. internal abstract class PatchTransportBase : IPatchTransport
  57. {
  58. public event EventHandler<PatchTransferProgress>? ProgressChanged;
  59. public abstract bool FileExists(string remotePath);
  60. public abstract bool DirectoryExists(string remotePath);
  61. public abstract IReadOnlyList<string> EnumerateFiles(string remoteDirectory, CancellationToken cancellationToken = default);
  62. public virtual IReadOnlyList<string> EnumerateDirectories(string remoteDirectory, CancellationToken cancellationToken = default) => Array.Empty<string>();
  63. public abstract long GetFileLength(string remotePath);
  64. public abstract byte[] DownloadFile(string remotePath);
  65. public abstract void EnsureDirectory(string remoteDirectory);
  66. public abstract void MoveFile(string sourcePath, string destinationPath);
  67. public abstract void DeleteFile(string remotePath);
  68. public abstract void DeleteDirectory(string remoteDirectory);
  69. public abstract void DeleteEmptyDirectory(string remoteDirectory);
  70. public abstract void Dispose();
  71. public virtual void DownloadFiles(IEnumerable<string> remotePaths, string localRoot, CancellationToken cancellationToken = default)
  72. {
  73. var paths = remotePaths.ToList();
  74. int remaining = paths.Count;
  75. int completed = 0;
  76. string hostRoot = new Uri(Settings.Host).AbsolutePath.Trim('/');
  77. foreach (string remotePath in paths)
  78. {
  79. cancellationToken.ThrowIfCancellationRequested();
  80. string relativePath = NormalizeRemotePath(remotePath).TrimStart('/');
  81. if (!string.IsNullOrEmpty(hostRoot) &&
  82. relativePath.StartsWith(hostRoot + "/", StringComparison.OrdinalIgnoreCase))
  83. {
  84. relativePath = relativePath.Substring(hostRoot.Length + 1);
  85. }
  86. string localPath = Path.Combine(localRoot, relativePath);
  87. string? localDirectory = Path.GetDirectoryName(localPath);
  88. if (!string.IsNullOrEmpty(localDirectory) && !Directory.Exists(localDirectory))
  89. Directory.CreateDirectory(localDirectory);
  90. byte[] data = DownloadFile(remotePath);
  91. File.WriteAllBytes(localPath, data);
  92. completed++;
  93. remaining--;
  94. Report("Downloading", remotePath, completed / (double)Math.Max(paths.Count, 1), 1, 0, remaining);
  95. }
  96. }
  97. public virtual void UploadDirectory(string localRoot, string remoteRoot, IReadOnlyList<string> relativePaths, string action, CancellationToken cancellationToken = default)
  98. {
  99. int remaining = relativePaths.Count;
  100. int completed = 0;
  101. foreach (string relativePath in relativePaths)
  102. {
  103. cancellationToken.ThrowIfCancellationRequested();
  104. string localPath = Path.Combine(localRoot, relativePath);
  105. string remotePath = CombineRemotePath(remoteRoot, relativePath);
  106. string remoteDirectory = GetRemoteDirectory(remotePath);
  107. EnsureDirectory(remoteDirectory);
  108. UploadFile(localPath, remotePath, action, completed, relativePaths.Count, remaining);
  109. completed++;
  110. remaining--;
  111. Report(action, relativePath, completed / (double)Math.Max(relativePaths.Count, 1), 1, 0, remaining);
  112. }
  113. }
  114. protected abstract void UploadFile(string localPath, string remotePath, string action, int completedFiles, int totalFiles, int remainingFiles);
  115. protected void Report(string action, string fileName, double overall, double fileProgress, long bytesPerSecond, int remainingFiles)
  116. {
  117. ProgressChanged?.Invoke(this, new PatchTransferProgress
  118. {
  119. Action = action,
  120. FileName = fileName,
  121. OverallProgress = overall,
  122. FileProgress = fileProgress,
  123. BytesPerSecond = bytesPerSecond,
  124. RemainingFiles = remainingFiles
  125. });
  126. }
  127. protected static string CombineRemotePath(string rootPath, string fileName)
  128. {
  129. rootPath = (rootPath ?? string.Empty).Replace('\\', '/').TrimEnd('/');
  130. fileName = (fileName ?? string.Empty).Replace('\\', '/').TrimStart('/');
  131. return string.IsNullOrEmpty(rootPath) ? "/" + fileName : rootPath + "/" + fileName;
  132. }
  133. protected static string GetRemoteDirectory(string remotePath)
  134. {
  135. remotePath = NormalizeRemotePath(remotePath);
  136. int index = remotePath.LastIndexOf('/');
  137. return index <= 0 ? "/" : remotePath.Substring(0, index);
  138. }
  139. protected static string NormalizeRemotePath(string path)
  140. {
  141. path = (path ?? string.Empty).Replace('\\', '/');
  142. return path.StartsWith("/") ? path : "/" + path;
  143. }
  144. protected static string SafeNormalizePath(string path)
  145. {
  146. string p = NormalizeRemotePath(path).TrimEnd('/');
  147. return string.IsNullOrEmpty(p) ? "/" : p;
  148. }
  149. }
  150. internal sealed class SftpPatchTransport : PatchTransportBase
  151. {
  152. private readonly SftpClient _client;
  153. public SftpPatchTransport(Uri host, string userName, string password, int portOverride)
  154. {
  155. int port = portOverride > 0 ? portOverride : host.IsDefaultPort ? 22 : host.Port;
  156. _client = new SftpClient(host.Host, port, userName, password);
  157. _client.Connect();
  158. }
  159. public override bool FileExists(string remotePath)
  160. {
  161. return _client.Exists(NormalizeRemotePath(remotePath));
  162. }
  163. public override bool DirectoryExists(string remotePath)
  164. {
  165. remotePath = SafeNormalizePath(remotePath);
  166. return _client.Exists(remotePath) && _client.GetAttributes(remotePath).IsDirectory;
  167. }
  168. public override IReadOnlyList<string> EnumerateFiles(string remoteDirectory, CancellationToken cancellationToken = default)
  169. {
  170. var files = new List<string>();
  171. EnumerateFilesRecursive(SafeNormalizePath(remoteDirectory), files, cancellationToken);
  172. return files;
  173. }
  174. public override long GetFileLength(string remotePath)
  175. {
  176. return _client.GetAttributes(NormalizeRemotePath(remotePath)).Size;
  177. }
  178. public override byte[] DownloadFile(string remotePath)
  179. {
  180. using var stream = new MemoryStream();
  181. _client.DownloadFile(NormalizeRemotePath(remotePath), stream);
  182. return stream.ToArray();
  183. }
  184. public override void EnsureDirectory(string remoteDirectory)
  185. {
  186. remoteDirectory = NormalizeRemotePath(remoteDirectory).TrimEnd('/');
  187. if (remoteDirectory == string.Empty || remoteDirectory == "/") return;
  188. string current = "/";
  189. foreach (string part in remoteDirectory.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
  190. {
  191. current = current == "/" ? current + part : current + "/" + part;
  192. if (!_client.Exists(current))
  193. _client.CreateDirectory(current);
  194. }
  195. }
  196. public override void MoveFile(string sourcePath, string destinationPath)
  197. {
  198. sourcePath = NormalizeRemotePath(sourcePath);
  199. destinationPath = NormalizeRemotePath(destinationPath);
  200. if (_client.Exists(destinationPath))
  201. _client.DeleteFile(destinationPath);
  202. _client.RenameFile(sourcePath, destinationPath);
  203. }
  204. public override void DeleteFile(string remotePath)
  205. {
  206. remotePath = NormalizeRemotePath(remotePath);
  207. if (_client.Exists(remotePath))
  208. _client.DeleteFile(remotePath);
  209. }
  210. public override void DeleteDirectory(string remoteDirectory)
  211. {
  212. remoteDirectory = SafeNormalizePath(remoteDirectory);
  213. if (!_client.Exists(remoteDirectory)) return;
  214. DeleteDirectoryRecursive(remoteDirectory);
  215. }
  216. public override void DeleteEmptyDirectory(string remoteDirectory)
  217. {
  218. remoteDirectory = SafeNormalizePath(remoteDirectory);
  219. if (_client.Exists(remoteDirectory))
  220. _client.DeleteDirectory(remoteDirectory);
  221. }
  222. protected override void UploadFile(string localPath, string remotePath, string action, int completedFiles, int totalFiles, int remainingFiles)
  223. {
  224. using var stream = File.OpenRead(localPath);
  225. long started = Environment.TickCount64;
  226. _client.UploadFile(stream, NormalizeRemotePath(remotePath), true, uploaded =>
  227. {
  228. double fileProgress = stream.Length == 0 ? 1 : uploaded / (double)stream.Length;
  229. double overall = (completedFiles + fileProgress) / Math.Max(totalFiles, 1);
  230. long elapsed = Math.Max(Environment.TickCount64 - started, 1);
  231. long cps = (long)(uploaded / (elapsed / 1000.0));
  232. Report(action, localPath, overall, fileProgress, cps, remainingFiles);
  233. });
  234. }
  235. public override void Dispose()
  236. {
  237. _client.Dispose();
  238. }
  239. private void DeleteDirectoryRecursive(string remoteDirectory)
  240. {
  241. foreach (SftpFile file in _client.ListDirectory(remoteDirectory))
  242. {
  243. if (file.Name == "." || file.Name == "..") continue;
  244. if (file.IsDirectory)
  245. DeleteDirectoryRecursive(file.FullName);
  246. else
  247. _client.DeleteFile(file.FullName);
  248. }
  249. _client.DeleteDirectory(remoteDirectory);
  250. }
  251. public override IReadOnlyList<string> EnumerateDirectories(string remoteDirectory, CancellationToken cancellationToken = default)
  252. {
  253. var dirs = new List<string>();
  254. EnumerateDirectoriesRecursive(SafeNormalizePath(remoteDirectory), dirs, cancellationToken);
  255. return dirs;
  256. }
  257. private void EnumerateFilesRecursive(string remoteDirectory, List<string> files, CancellationToken cancellationToken)
  258. {
  259. cancellationToken.ThrowIfCancellationRequested();
  260. foreach (SftpFile file in _client.ListDirectory(remoteDirectory))
  261. {
  262. cancellationToken.ThrowIfCancellationRequested();
  263. if (file.Name == "." || file.Name == "..") continue;
  264. if (file.IsDirectory)
  265. EnumerateFilesRecursive(file.FullName, files, cancellationToken);
  266. else
  267. files.Add(NormalizeRemotePath(file.FullName));
  268. }
  269. }
  270. private void EnumerateDirectoriesRecursive(string remoteDirectory, List<string> dirs, CancellationToken cancellationToken)
  271. {
  272. cancellationToken.ThrowIfCancellationRequested();
  273. foreach (SftpFile file in _client.ListDirectory(remoteDirectory))
  274. {
  275. if (file.Name == "." || file.Name == "..") continue;
  276. if (!file.IsDirectory) continue;
  277. EnumerateDirectoriesRecursive(file.FullName, dirs, cancellationToken);
  278. dirs.Add(NormalizeRemotePath(file.FullName)); // deepest first
  279. }
  280. }
  281. }
  282. internal sealed class FtpPatchTransport : PatchTransportBase
  283. {
  284. private readonly Uri _host;
  285. private readonly NetworkCredential _credentials;
  286. private readonly int _portOverride;
  287. public FtpPatchTransport(Uri host, string userName, string password, int portOverride)
  288. {
  289. _host = host;
  290. _credentials = new NetworkCredential(userName, password);
  291. _portOverride = portOverride;
  292. }
  293. public override bool FileExists(string remotePath)
  294. {
  295. try
  296. {
  297. GetFileLength(remotePath);
  298. return true;
  299. }
  300. catch (WebException ex) when ((ex.Response as FtpWebResponse)?.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable)
  301. {
  302. return false;
  303. }
  304. }
  305. public override bool DirectoryExists(string remotePath)
  306. {
  307. try
  308. {
  309. using var response = (FtpWebResponse)CreateRequest(remotePath, WebRequestMethods.Ftp.ListDirectory).GetResponse();
  310. return true;
  311. }
  312. catch (WebException ex) when ((ex.Response as FtpWebResponse)?.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable)
  313. {
  314. return false;
  315. }
  316. }
  317. public override IReadOnlyList<string> EnumerateFiles(string remoteDirectory, CancellationToken cancellationToken = default)
  318. {
  319. var files = new List<string>();
  320. EnumerateFilesRecursive(NormalizeRemotePath(remoteDirectory).TrimEnd('/'), files, cancellationToken);
  321. return files;
  322. }
  323. public override long GetFileLength(string remotePath)
  324. {
  325. var request = CreateRequest(remotePath, WebRequestMethods.Ftp.GetFileSize);
  326. using var response = (FtpWebResponse)request.GetResponse();
  327. return response.ContentLength;
  328. }
  329. public override byte[] DownloadFile(string remotePath)
  330. {
  331. var request = CreateRequest(remotePath, WebRequestMethods.Ftp.DownloadFile);
  332. using var response = (FtpWebResponse)request.GetResponse();
  333. using var stream = response.GetResponseStream();
  334. using var output = new MemoryStream();
  335. stream?.CopyTo(output);
  336. return output.ToArray();
  337. }
  338. public override void EnsureDirectory(string remoteDirectory)
  339. {
  340. remoteDirectory = NormalizeRemotePath(remoteDirectory).TrimEnd('/');
  341. if (remoteDirectory == string.Empty || remoteDirectory == "/") return;
  342. string current = "/";
  343. foreach (string part in remoteDirectory.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
  344. {
  345. current = current == "/" ? current + part : current + "/" + part;
  346. try
  347. {
  348. using var response = (FtpWebResponse)CreateRequest(current, WebRequestMethods.Ftp.MakeDirectory).GetResponse();
  349. }
  350. catch (WebException ex) when ((ex.Response as FtpWebResponse)?.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable)
  351. {
  352. }
  353. }
  354. }
  355. public override void MoveFile(string sourcePath, string destinationPath)
  356. {
  357. DeleteFile(destinationPath);
  358. var request = CreateRequest(sourcePath, WebRequestMethods.Ftp.Rename);
  359. request.RenameTo = NormalizeRemotePath(destinationPath);
  360. using var response = (FtpWebResponse)request.GetResponse();
  361. }
  362. public override void DeleteFile(string remotePath)
  363. {
  364. if (!FileExists(remotePath)) return;
  365. using var response = (FtpWebResponse)CreateRequest(remotePath, WebRequestMethods.Ftp.DeleteFile).GetResponse();
  366. }
  367. public override void DeleteDirectory(string remoteDirectory)
  368. {
  369. DeleteDirectoryRecursive(SafeNormalizePath(remoteDirectory));
  370. }
  371. public override void DeleteEmptyDirectory(string remoteDirectory)
  372. {
  373. remoteDirectory = SafeNormalizePath(remoteDirectory);
  374. if (!DirectoryExists(remoteDirectory)) return;
  375. using var response = (FtpWebResponse)CreateRequest(remoteDirectory, WebRequestMethods.Ftp.RemoveDirectory).GetResponse();
  376. }
  377. private void DeleteDirectoryRecursive(string remoteDirectory)
  378. {
  379. foreach (string name in ListDirectoryNames(remoteDirectory))
  380. {
  381. if (string.IsNullOrWhiteSpace(name) || name == "." || name == "..") continue;
  382. string child = name.StartsWith("/", StringComparison.Ordinal)
  383. ? NormalizeRemotePath(name)
  384. : CombineRemotePath(remoteDirectory, name);
  385. if (DirectoryExists(child))
  386. DeleteDirectoryRecursive(child);
  387. else
  388. DeleteFile(child);
  389. }
  390. using var response = (FtpWebResponse)CreateRequest(remoteDirectory, WebRequestMethods.Ftp.RemoveDirectory).GetResponse();
  391. }
  392. protected override void UploadFile(string localPath, string remotePath, string action, int completedFiles, int totalFiles, int remainingFiles)
  393. {
  394. byte[] data = File.ReadAllBytes(localPath);
  395. var request = CreateRequest(remotePath, WebRequestMethods.Ftp.UploadFile);
  396. request.ContentLength = data.Length;
  397. long started = Environment.TickCount64;
  398. using (var stream = request.GetRequestStream())
  399. {
  400. int offset = 0;
  401. while (offset < data.Length)
  402. {
  403. int count = Math.Min(64 * 1024, data.Length - offset);
  404. stream.Write(data, offset, count);
  405. offset += count;
  406. double fileProgress = data.Length == 0 ? 1 : offset / (double)data.Length;
  407. double overall = (completedFiles + fileProgress) / Math.Max(totalFiles, 1);
  408. long elapsed = Math.Max(Environment.TickCount64 - started, 1);
  409. long cps = (long)(offset / (elapsed / 1000.0));
  410. Report(action, localPath, overall, fileProgress, cps, remainingFiles);
  411. }
  412. }
  413. using var response = (FtpWebResponse)request.GetResponse();
  414. }
  415. public override void Dispose()
  416. {
  417. }
  418. private FtpWebRequest CreateRequest(string remotePath, string method)
  419. {
  420. var builder = new UriBuilder(_host)
  421. {
  422. Path = NormalizeRemotePath(remotePath)
  423. };
  424. if (_portOverride > 0)
  425. builder.Port = _portOverride;
  426. var uri = builder.Uri;
  427. #pragma warning disable SYSLIB0014 // FtpWebRequest is the built-in .NET FTP client API.
  428. var request = (FtpWebRequest)WebRequest.Create(uri);
  429. #pragma warning restore SYSLIB0014
  430. request.Method = method;
  431. request.Credentials = _credentials;
  432. request.UseBinary = true;
  433. request.KeepAlive = false;
  434. return request;
  435. }
  436. public override IReadOnlyList<string> EnumerateDirectories(string remoteDirectory, CancellationToken cancellationToken = default)
  437. {
  438. var dirs = new List<string>();
  439. EnumerateDirectoriesRecursive(SafeNormalizePath(remoteDirectory), dirs, cancellationToken);
  440. return dirs;
  441. }
  442. private void EnumerateFilesRecursive(string remoteDirectory, List<string> files, CancellationToken cancellationToken)
  443. {
  444. cancellationToken.ThrowIfCancellationRequested();
  445. foreach (string name in ListDirectoryNames(remoteDirectory))
  446. {
  447. cancellationToken.ThrowIfCancellationRequested();
  448. if (string.IsNullOrWhiteSpace(name) || name == "." || name == "..") continue;
  449. string child = name.StartsWith("/", StringComparison.Ordinal)
  450. ? NormalizeRemotePath(name)
  451. : CombineRemotePath(remoteDirectory, name);
  452. if (FileExists(child))
  453. {
  454. files.Add(NormalizeRemotePath(child));
  455. continue;
  456. }
  457. if (DirectoryExists(child))
  458. EnumerateFilesRecursive(child, files, cancellationToken);
  459. }
  460. }
  461. private void EnumerateDirectoriesRecursive(string remoteDirectory, List<string> dirs, CancellationToken cancellationToken)
  462. {
  463. cancellationToken.ThrowIfCancellationRequested();
  464. foreach (string name in ListDirectoryNames(remoteDirectory))
  465. {
  466. if (string.IsNullOrWhiteSpace(name) || name == "." || name == "..") continue;
  467. string child = name.StartsWith("/", StringComparison.Ordinal)
  468. ? NormalizeRemotePath(name)
  469. : CombineRemotePath(remoteDirectory, name);
  470. if (!DirectoryExists(child)) continue;
  471. EnumerateDirectoriesRecursive(child, dirs, cancellationToken);
  472. dirs.Add(NormalizeRemotePath(child)); // deepest first
  473. }
  474. }
  475. private IEnumerable<string> ListDirectoryNames(string remoteDirectory)
  476. {
  477. var request = CreateRequest(remoteDirectory, WebRequestMethods.Ftp.ListDirectory);
  478. using var response = (FtpWebResponse)request.GetResponse();
  479. using var stream = response.GetResponseStream();
  480. using var reader = new StreamReader(stream ?? Stream.Null);
  481. while (!reader.EndOfStream)
  482. {
  483. string? line = reader.ReadLine();
  484. if (line != null)
  485. yield return line.Trim();
  486. }
  487. }
  488. }
  489. internal sealed class HttpPatchTransport : PatchTransportBase
  490. {
  491. private readonly Uri _host;
  492. private readonly HttpClient _client;
  493. private readonly int _portOverride;
  494. public HttpPatchTransport(Uri host, string userName, string password, int portOverride)
  495. {
  496. _host = host;
  497. _portOverride = portOverride;
  498. var handler = new HttpClientHandler();
  499. if (!string.IsNullOrEmpty(userName))
  500. handler.Credentials = new NetworkCredential(userName, password);
  501. _client = new HttpClient(handler);
  502. if (!string.IsNullOrEmpty(userName))
  503. {
  504. string token = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"{userName}:{password}"));
  505. _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);
  506. }
  507. }
  508. public override bool FileExists(string remotePath)
  509. {
  510. using var response = _client.Send(new HttpRequestMessage(HttpMethod.Head, BuildUri(remotePath)));
  511. if (response.StatusCode == HttpStatusCode.NotFound)
  512. return false;
  513. response.EnsureSuccessStatusCode();
  514. return true;
  515. }
  516. public override bool DirectoryExists(string remotePath)
  517. {
  518. using var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), BuildUri(remotePath));
  519. request.Headers.TryAddWithoutValidation("Depth", "0");
  520. using var response = _client.Send(request);
  521. return response.IsSuccessStatusCode;
  522. }
  523. public override IReadOnlyList<string> EnumerateFiles(string remoteDirectory, CancellationToken cancellationToken = default)
  524. {
  525. using var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), BuildUri(remoteDirectory));
  526. request.Headers.TryAddWithoutValidation("Depth", "infinity");
  527. using var response = _client.Send(request, cancellationToken);
  528. response.EnsureSuccessStatusCode();
  529. string rootPath = BuildUri(remoteDirectory).AbsolutePath.TrimEnd('/');
  530. string xml = response.Content.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult();
  531. XDocument document = XDocument.Parse(xml);
  532. var files = new List<string>();
  533. foreach (var href in document.Descendants().Where(x => x.Name.LocalName.Equals("href", StringComparison.OrdinalIgnoreCase)))
  534. {
  535. cancellationToken.ThrowIfCancellationRequested();
  536. string value = Uri.UnescapeDataString((href.Value ?? string.Empty).Trim());
  537. if (string.IsNullOrWhiteSpace(value) || value.EndsWith("/", StringComparison.Ordinal))
  538. continue;
  539. if (Uri.TryCreate(value, UriKind.Absolute, out var absolute))
  540. value = absolute.AbsolutePath;
  541. if (!value.StartsWith(rootPath + "/", StringComparison.OrdinalIgnoreCase) &&
  542. !string.Equals(value, rootPath, StringComparison.OrdinalIgnoreCase))
  543. continue;
  544. files.Add(NormalizeRemotePath(value));
  545. }
  546. return files.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
  547. }
  548. public override long GetFileLength(string remotePath)
  549. {
  550. using var response = _client.Send(new HttpRequestMessage(HttpMethod.Head, BuildUri(remotePath)));
  551. response.EnsureSuccessStatusCode();
  552. return response.Content.Headers.ContentLength ?? 0;
  553. }
  554. public override byte[] DownloadFile(string remotePath)
  555. {
  556. return _client.GetByteArrayAsync(BuildUri(remotePath)).GetAwaiter().GetResult();
  557. }
  558. public override void EnsureDirectory(string remoteDirectory)
  559. {
  560. // HTTP(S) publishing expects a PUT/WebDAV-capable endpoint that creates parent paths,
  561. // or an endpoint that treats path creation as implicit.
  562. }
  563. public override void MoveFile(string sourcePath, string destinationPath)
  564. {
  565. var request = new HttpRequestMessage(new HttpMethod("MOVE"), BuildUri(sourcePath));
  566. request.Headers.TryAddWithoutValidation("Destination", BuildUri(destinationPath).ToString());
  567. using var response = _client.Send(request);
  568. response.EnsureSuccessStatusCode();
  569. }
  570. public override void DeleteFile(string remotePath)
  571. {
  572. using var response = _client.DeleteAsync(BuildUri(remotePath)).GetAwaiter().GetResult();
  573. if (response.StatusCode != HttpStatusCode.NotFound)
  574. response.EnsureSuccessStatusCode();
  575. }
  576. public override void DeleteDirectory(string remoteDirectory)
  577. {
  578. using var response = _client.DeleteAsync(BuildUri(remoteDirectory)).GetAwaiter().GetResult();
  579. if (response.StatusCode != HttpStatusCode.NotFound)
  580. response.EnsureSuccessStatusCode();
  581. }
  582. public override void DeleteEmptyDirectory(string remoteDirectory)
  583. {
  584. if (!IsDirectoryEmpty(remoteDirectory)) return;
  585. DeleteDirectory(remoteDirectory);
  586. }
  587. private bool IsDirectoryEmpty(string remoteDirectory)
  588. {
  589. using var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), BuildUri(remoteDirectory));
  590. request.Headers.TryAddWithoutValidation("Depth", "1");
  591. using var response = _client.Send(request);
  592. if (response.StatusCode == HttpStatusCode.NotFound)
  593. return false;
  594. response.EnsureSuccessStatusCode();
  595. string rootPath = BuildUri(remoteDirectory).AbsolutePath.TrimEnd('/');
  596. string xml = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
  597. XDocument document = XDocument.Parse(xml);
  598. foreach (var href in document.Descendants().Where(x => x.Name.LocalName.Equals("href", StringComparison.OrdinalIgnoreCase)))
  599. {
  600. string value = Uri.UnescapeDataString((href.Value ?? string.Empty).Trim()).TrimEnd('/');
  601. if (string.IsNullOrWhiteSpace(value))
  602. continue;
  603. if (Uri.TryCreate(value, UriKind.Absolute, out var absolute))
  604. value = absolute.AbsolutePath.TrimEnd('/');
  605. if (!string.Equals(value, rootPath, StringComparison.OrdinalIgnoreCase))
  606. return false;
  607. }
  608. return true;
  609. }
  610. protected override void UploadFile(string localPath, string remotePath, string action, int completedFiles, int totalFiles, int remainingFiles)
  611. {
  612. byte[] data = File.ReadAllBytes(localPath);
  613. long started = Environment.TickCount64;
  614. using var content = new ByteArrayContent(data);
  615. using var response = _client.PutAsync(BuildUri(remotePath), content).GetAwaiter().GetResult();
  616. response.EnsureSuccessStatusCode();
  617. long elapsed = Math.Max(Environment.TickCount64 - started, 1);
  618. long cps = (long)(data.Length / (elapsed / 1000.0));
  619. double overall = (completedFiles + 1) / (double)Math.Max(totalFiles, 1);
  620. Report(action, localPath, overall, 1, cps, remainingFiles);
  621. }
  622. public override void Dispose()
  623. {
  624. _client.Dispose();
  625. }
  626. private Uri BuildUri(string remotePath)
  627. {
  628. var builder = new UriBuilder(_host)
  629. {
  630. Path = NormalizeRemotePath(remotePath)
  631. };
  632. if (_portOverride > 0)
  633. builder.Port = _portOverride;
  634. return builder.Uri;
  635. }
  636. }
  637. }