You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

754 lines
28 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. using Apewer.Internals;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Reflection;
  6. using System.Security.Permissions;
  7. namespace Apewer
  8. {
  9. /// <summary>存储实用工具。</summary>
  10. public static class StorageUtility
  11. {
  12. private static Exception Try(Action action)
  13. {
  14. try { action?.Invoke(); return null; }
  15. catch (Exception ex) { return ex; }
  16. }
  17. /// <summary>文件操作线程锁。</summary>
  18. public static object Locker = new object();
  19. #region delete
  20. /// <summary>删除文件。</summary>
  21. public static Exception DeleteFile(string path, bool useTemp = false)
  22. {
  23. if (useTemp)
  24. {
  25. try
  26. {
  27. if (string.IsNullOrEmpty(path)) return new ArgumentException();
  28. if (!File.Exists(path)) return new FileNotFoundException();
  29. Try(() => new FileInfo(path).Attributes = FileAttributes.Normal);
  30. var temp = Environment.GetEnvironmentVariable("TEMP");
  31. var name = Path.GetFileName(path);
  32. var dest = null as string;
  33. while (dest == null || File.Exists(dest))
  34. {
  35. var guid = Guid.NewGuid().ToString("n").Substring(0, 8);
  36. dest = $"trash_{guid}_{name}";
  37. }
  38. File.Move(path, dest);
  39. File.Delete(dest);
  40. return null;
  41. }
  42. catch (Exception ex) { return ex; }
  43. }
  44. try
  45. {
  46. File.Delete(path);
  47. return null;
  48. }
  49. catch (Exception ex) { return ex; }
  50. }
  51. /// <summary>删除目录、子目录和子文件。</summary>
  52. public static Exception DeleteDirectory(string path, bool useTemp = false)
  53. {
  54. if (useTemp)
  55. {
  56. try
  57. {
  58. if (string.IsNullOrEmpty(path)) return new ArgumentException();
  59. if (!Directory.Exists(path)) return new DirectoryNotFoundException();
  60. var temp = Environment.GetEnvironmentVariable("TEMP");
  61. var name = Path.GetDirectoryName(path);
  62. var dest = null as string;
  63. while (dest == null || Directory.Exists(dest))
  64. {
  65. var guid = Guid.NewGuid().ToString("n").Substring(0, 8);
  66. dest = $"trash_{guid}_{name}";
  67. }
  68. Directory.Move(path, dest);
  69. Directory.Delete(dest, true);
  70. return null;
  71. }
  72. catch (Exception ex) { return ex; }
  73. }
  74. try
  75. {
  76. Directory.Delete(path, true);
  77. return null;
  78. }
  79. catch (Exception ex) { return ex; }
  80. }
  81. #endregion
  82. #region path
  83. /// <summary>无效的路径字符。</summary>
  84. public static char[] InvalidPathChars
  85. {
  86. get => new char[] {
  87. '\\', '/', '\'', '"', ':', '*', '?', '<', '>', '|',
  88. '\0', '\a', '\b', '\t', '\n', '\v', '\f', '\r',
  89. '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\u000e', '\u000f',
  90. '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017',
  91. '\u0018', '\u0019', '\u001a', '\u001b', '\u001c', '\u001d', '\u001e', '\u001f'
  92. };
  93. }
  94. /// <summary>合并路径。</summary>
  95. public static string CombinePath(params string[] paths)
  96. {
  97. if (paths == null || paths.Length < 1) return "";
  98. try
  99. {
  100. #if NET20
  101. var result = paths[0];
  102. for (var i = 0; i < paths.Length; i++)
  103. {
  104. result = Path.Combine(result, paths[i]);
  105. }
  106. return result;
  107. #else
  108. return Path.Combine(paths);
  109. #endif
  110. }
  111. catch { return ""; }
  112. }
  113. /// <summary>获取文件或目录的存在状态。</summary>
  114. public static bool PathExists(string path)
  115. {
  116. if (string.IsNullOrEmpty(path)) return false;
  117. if (File.Exists(path)) return true;
  118. if (Directory.Exists(path)) return true;
  119. return false;
  120. }
  121. /// <summary>获取目录的存在状态。</summary>
  122. public static bool DirectoryExists(string path) => string.IsNullOrEmpty(path) ? false : Directory.Exists(path);
  123. /// <summary>获取文件的存在状态。</summary>
  124. public static bool FileExists(string path) => string.IsNullOrEmpty(path) ? false : File.Exists(path);
  125. /// <summary>获取文件或目录的上级目录完整路径,失败时返回 NULL 值。。</summary>
  126. public static string GetParentPath(string path)
  127. {
  128. try
  129. {
  130. if (File.Exists(path)) return Path.GetDirectoryName(path);
  131. if (Directory.Exists(path)) return Directory.GetParent(path).FullName;
  132. }
  133. catch { }
  134. return null;
  135. }
  136. /// <summary>修正文件名,去除不允许的字符。</summary>
  137. public static string FixFileName(string fileName)
  138. {
  139. var result = fileName;
  140. if (string.IsNullOrEmpty(result))
  141. {
  142. result = Constant.EmptyString;
  143. }
  144. else
  145. {
  146. var invalid = InvalidPathChars;
  147. foreach (var c in invalid) result = result.Replace(c.ToString(), "");
  148. }
  149. if (result.Length > 0) result = result.Trim();
  150. return result;
  151. }
  152. /// <summary>获取指定目录的子目录,可指定递归子目录。</summary>
  153. public static List<string> GetSubDirectories(string dirPath, bool recursive = false, bool recurPrecedence = true) => GetSubDirectories(dirPath, recursive ? -1 : 0, recurPrecedence);
  154. /// <summary>获取指定目录下子目录的路径。</summary>
  155. /// <param name="dirPath">顶级目录。</param>
  156. /// <param name="recurDepth">子目录递归深度,指定为 0 时不递归,指定为 -1 时无限递归。</param>
  157. /// <param name="recurPrecedence">优先排列递归项。</param>
  158. public static List<string> GetSubDirectories(string dirPath, int recurDepth, bool recurPrecedence = true)
  159. {
  160. var list = new List<string>();
  161. if (string.IsNullOrEmpty(dirPath)) return list;
  162. var directories = new string[0];
  163. try
  164. {
  165. directories = Directory.GetDirectories(dirPath);
  166. }
  167. catch { }
  168. foreach (var dir in directories)
  169. {
  170. var recurlist = new List<string>();
  171. if (recurDepth != 0)
  172. {
  173. var depth = (recurDepth > 0) ? recurDepth - 1 : recurDepth;
  174. var subrecur = GetSubDirectories(dir, depth, recurPrecedence);
  175. recurlist.AddRange(subrecur);
  176. }
  177. if (recurPrecedence)
  178. {
  179. list.AddRange(recurlist);
  180. list.Add(dir);
  181. }
  182. else
  183. {
  184. list.Add(dir);
  185. list.AddRange(recurlist);
  186. }
  187. }
  188. return list;
  189. }
  190. /// <summary>获取指定目录的子文件,可指定递归子目录。</summary>
  191. public static List<string> GetSubFiles(string dirPath, bool recursive = false, bool recurPrecedence = true) => GetSubFiles(dirPath, recursive ? -1 : 0, recurPrecedence);
  192. /// <summary>获取指定目录下子文件的路径。</summary>
  193. /// <param name="dirPath">顶级目录。</param>
  194. /// <param name="recurDepth">子目录递归深度,指定为 0 时不递归,指定为 -1 时无限递归。</param>
  195. /// <param name="recurPrecedence">优先排列递归项。</param>
  196. public static List<string> GetSubFiles(string dirPath, int recurDepth, bool recurPrecedence = true)
  197. {
  198. var list = new List<string>();
  199. if (string.IsNullOrEmpty(dirPath)) return list;
  200. var dirs = new List<string>();
  201. if (recurDepth == 0)
  202. {
  203. dirs.Add(dirPath);
  204. }
  205. else
  206. {
  207. var recurdicrotylist = GetSubDirectories(dirPath, recurDepth, recurPrecedence);
  208. if (recurPrecedence)
  209. {
  210. dirs.AddRange(recurdicrotylist);
  211. dirs.Add(dirPath);
  212. }
  213. else
  214. {
  215. dirs.Add(dirPath);
  216. dirs.AddRange(recurdicrotylist);
  217. }
  218. }
  219. foreach (var d in dirs)
  220. {
  221. try
  222. {
  223. var files = System.IO.Directory.GetFiles(d);
  224. list.AddRange(files);
  225. }
  226. catch { }
  227. }
  228. return list;
  229. }
  230. #endregion
  231. #region directory
  232. /// <summary>确信指定存在指定目录。</summary>
  233. public static bool AssureDirectory(string path)
  234. {
  235. if (string.IsNullOrEmpty(path)) return false;
  236. try
  237. {
  238. if (File.Exists(path)) return false;
  239. if (Directory.Exists(path)) return true;
  240. var created = Directory.CreateDirectory(path);
  241. return created.Exists;
  242. }
  243. catch { }
  244. return false;
  245. }
  246. /// <summary>确信指定存在指定路径所在的上级目录。</summary>
  247. public static bool AssureParent(string path)
  248. {
  249. if (string.IsNullOrEmpty(path)) return false;
  250. var parent = Constant.EmptyString;
  251. try { parent = Directory.GetParent(path).FullName; } catch { }
  252. var result = AssureDirectory(parent);
  253. return result;
  254. }
  255. #endregion
  256. #region file
  257. /// <summary>打开文件,并获取文件流。若文件不存在,则先创建文件;若获取失败,则返回 NULL 值。可选文件的锁定状态。</summary>
  258. public static FileStream OpenFile(string path, bool share = true)
  259. {
  260. try
  261. {
  262. if (string.IsNullOrEmpty(path)) return null;
  263. if (DirectoryExists(path)) return null;
  264. if (AssureParent(path))
  265. {
  266. var m = FileMode.OpenOrCreate;
  267. var a = FileAccess.ReadWrite;
  268. var s = share ? FileShare.ReadWrite : FileShare.None;
  269. var stream = new FileStream(path, m, a, s);
  270. return stream;
  271. }
  272. }
  273. catch { }
  274. return null;
  275. }
  276. /// <summary>创建一个空文件且不保留句柄。</summary>
  277. /// <param name="path">文件路径,若已存在则返回失败。</param>
  278. /// <param name="length">文件长度(字节数)。</param>
  279. /// <param name="replace">替换现有文件。</param>
  280. /// <returns>创建成功。</returns>
  281. public static bool CreateFile(string path, long length = 0, bool replace = false)
  282. {
  283. if (string.IsNullOrEmpty(path)) return false;
  284. if (!replace && File.Exists(path)) return false;
  285. if (!AssureParent(path)) return false;
  286. lock (Locker)
  287. {
  288. var file = null as FileStream;
  289. var success = false;
  290. try
  291. {
  292. var m = replace ? FileMode.Create : FileMode.OpenOrCreate;
  293. file = new FileStream(path, m, FileAccess.ReadWrite, FileShare.ReadWrite);
  294. if (length > 0) file.SetLength(length);
  295. success = true;
  296. }
  297. catch { }
  298. RuntimeUtility.Dispose(file);
  299. return success;
  300. }
  301. }
  302. /// <summary>复制文件。</summary>
  303. /// <param name="source">旧路径。</param>
  304. /// <param name="destination">新路径。</param>
  305. /// <param name="replace">新路径存在时,替换新文件。</param>
  306. public static bool CopyFile(string source, string destination, bool replace = true)
  307. {
  308. if (string.IsNullOrEmpty(source)) return false;
  309. if (string.IsNullOrEmpty(destination)) return false;
  310. lock (Locker)
  311. {
  312. if (!FileExists(source)) return false;
  313. try
  314. {
  315. File.Copy(source, destination, replace);
  316. return true;
  317. }
  318. catch { return false; }
  319. }
  320. }
  321. /// <summary>向文件追加数据。文件不存在时将创建。</summary>
  322. public static bool AppendFile(string path, params byte[] bytes)
  323. {
  324. if (bytes == null || bytes.Length < 1) return false;
  325. lock (Locker)
  326. {
  327. var assured = AssureParent(path);
  328. if (!assured) return false;
  329. using (var file = OpenFile(path, true))
  330. {
  331. if (file == null) return false;
  332. try
  333. {
  334. file.Position = file.Length;
  335. file.Write(bytes, 0, bytes.Length);
  336. file.Flush();
  337. return true;
  338. }
  339. catch { return false; }
  340. }
  341. }
  342. }
  343. /// <summary>将数据写入新文件,若文件已存在则覆盖。</summary>
  344. public static bool WriteFile(string path, params byte[] bytes) => WriteFile(path, false, bytes);
  345. /// <summary>将数据写入新文件,若文件已存在则覆盖。</summary>
  346. public static bool WriteFile(string path, bool bom, params byte[] bytes)
  347. {
  348. if (string.IsNullOrEmpty(path)) return false;
  349. if (bom)
  350. {
  351. lock (Locker)
  352. {
  353. if (bytes == null || bytes.Length < 1)
  354. {
  355. try
  356. {
  357. File.WriteAllBytes(path, TextUtility.Bom);
  358. return true;
  359. }
  360. catch
  361. {
  362. return false;
  363. }
  364. }
  365. else
  366. {
  367. var file = OpenFile(path, true);
  368. var success = false;
  369. try
  370. {
  371. var write1 = BytesUtility.Write(file, TextUtility.Bom);
  372. if (write1 == TextUtility.Bom.Length)
  373. {
  374. var write2 = BytesUtility.Write(file, bytes);
  375. if (bytes != null && bytes.LongLength > 0L)
  376. {
  377. }
  378. success = true;
  379. }
  380. }
  381. catch { }
  382. RuntimeUtility.Dispose(file);
  383. return success;
  384. }
  385. }
  386. }
  387. else
  388. {
  389. lock (Locker)
  390. {
  391. try
  392. {
  393. File.WriteAllBytes(path, bytes);
  394. return true;
  395. }
  396. catch
  397. {
  398. return false;
  399. }
  400. }
  401. }
  402. }
  403. /// <summary>将数据写入新文件,若文件已存在则覆盖。</summary>
  404. public static bool WriteFile(string path, Json json, bool bom = false)
  405. {
  406. if (string.IsNullOrEmpty(path)) return false;
  407. if (json == null || json.IsNull || json.IsNone) return false;
  408. var text = json.ToString(true);
  409. var bytes = TextUtility.Bytes(text);
  410. return WriteFile(path, bom, bytes);
  411. }
  412. /// <summary>读取文件,获取文件内容。当文件为 UTF-8 文本文件时,可去除 BOM 头。</summary>
  413. /// <remarks>注:<br />字节数组最大为 2GB;<br />此方法不抛出异常,读取失时返回空字节数组。</remarks>
  414. public static byte[] ReadFile(string path, bool wipeBom = false)
  415. {
  416. const int Buffer = 1048576;
  417. if (!FileExists(path)) return Constant.EmptyBytes;
  418. lock (Locker)
  419. {
  420. try
  421. {
  422. var result = new byte[0];
  423. using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
  424. {
  425. if (!stream.CanRead) return result;
  426. stream.Position = 0;
  427. var length = stream.Length;
  428. if (length < 1) return result;
  429. if (length > int.MaxValue) return result;
  430. var offset = 0L;
  431. if (wipeBom && length >= 3)
  432. {
  433. var head = new byte[3];
  434. stream.Read(head, 0, 3);
  435. if (TextUtility.ContainsBOM(head))
  436. {
  437. var capacity = length - 3;
  438. result = new byte[capacity];
  439. }
  440. else
  441. {
  442. result = new byte[length];
  443. result[0] = head[0];
  444. result[1] = head[1];
  445. result[2] = head[2];
  446. offset = 3;
  447. }
  448. }
  449. else
  450. {
  451. result = new byte[length];
  452. }
  453. var block = new byte[Buffer];
  454. while (true)
  455. {
  456. var read = stream.Read(block, 0, Buffer);
  457. if (read < 1) break;
  458. Array.Copy(block, 0, result, offset, read);
  459. offset += read;
  460. }
  461. }
  462. return result;
  463. }
  464. catch { }
  465. }
  466. return Constant.EmptyBytes;
  467. }
  468. /// <summary>获取指定文件的字节数,获取失败时返回 -1 值。</summary>
  469. public static long GetFileLength(string path)
  470. {
  471. try
  472. {
  473. var info = new FileInfo(path);
  474. return info.Length;
  475. }
  476. catch { }
  477. return -1;
  478. }
  479. #endregion
  480. /// <summary>获取文件的最后写入时间。</summary>
  481. public static DateTime GetFileLastWriteTime(string path, bool utc = false)
  482. {
  483. try
  484. {
  485. var info = new FileInfo(path);
  486. return utc ? info.LastWriteTimeUtc : info.LastWriteTime;
  487. }
  488. catch { }
  489. return new DateTime();
  490. }
  491. /// <summary>设置文件的最后写入时间。</summary>
  492. public static string SetFileLastWriteTime(string path, DateTime value)
  493. {
  494. try
  495. {
  496. var info = new FileInfo(path);
  497. info.LastWriteTime = value;
  498. return null;
  499. }
  500. catch (Exception ex)
  501. {
  502. return ex.Message;
  503. }
  504. }
  505. /// <summary>压缩文件到流。</summary>
  506. /// <param name="files">Key 为 ZIP 内的文件名,Value 为原始文件路径。</param>
  507. /// <param name="output">要输出的 ZIP 流。</param>
  508. public static Exception ToZip(Dictionary<string, string> files, Stream output)
  509. {
  510. if (files == null) return new ArgumentNullException(nameof(files));
  511. var streams = new List<Stream>();
  512. var ex = BytesUtility.ToZip(files.Keys, output, (name) =>
  513. {
  514. if (name.IsEmpty()) return null;
  515. var path = files[name];
  516. if (path.IsEmpty()) return null;
  517. if (!FileExists(path)) return null;
  518. var input = OpenFile(path);
  519. streams.Add(input);
  520. return input;
  521. }, (name) => GetFileLastWriteTime(files[name]), true);
  522. RuntimeUtility.Dispose(streams);
  523. return ex;
  524. }
  525. /// <summary>压缩文件到流。</summary>
  526. /// <param name="files">Key 为 ZIP 内的文件名,Value 为原始文件路径。</param>
  527. /// <param name="output">要输出的 ZIP 文件路径,已存在的文件将被删除。</param>
  528. public static Exception ToZip(Dictionary<string, string> files, string output)
  529. {
  530. if (output.IsEmpty()) return new ArgumentException(nameof(output));
  531. if (FileExists(output)) DeleteFile(output);
  532. if (FileExists(output)) return new IOException();
  533. var stream = OpenFile(output);
  534. if (stream == null) return new IOException();
  535. var ex = ToZip(files, stream);
  536. RuntimeUtility.Dispose(stream, true);
  537. return ex;
  538. }
  539. /// <summary>压缩文件到流。</summary>
  540. /// <param name="directory">原始文件目录。</param>
  541. /// <param name="output">要输出的 ZIP 流。</param>
  542. public static Exception ToZip(string directory, Stream output)
  543. {
  544. if (!DirectoryExists(directory)) return new DirectoryNotFoundException();
  545. var dict = new Dictionary<string, string>();
  546. foreach (var path in GetSubFiles(directory, true))
  547. {
  548. var name = path.Substring(directory.Length);
  549. if (name.StartsWith("\\")) name = name.Substring(1);
  550. if (name.StartsWith("/")) name = name.Substring(1);
  551. dict.Add(name, path);
  552. }
  553. return ToZip(dict, output);
  554. }
  555. /// <summary>压缩文件到流。</summary>
  556. /// <param name="directory">原始文件目录。</param>
  557. /// <param name="output">要输出的 ZIP 文件路径,已存在的旧文件将被删除。</param>
  558. public static Exception ToZip(string directory, string output)
  559. {
  560. if (output.IsEmpty()) return new ArgumentException(nameof(output));
  561. if (FileExists(output)) DeleteFile(output);
  562. if (FileExists(output)) return new IOException("无法删除现有文件。");
  563. var stream = OpenFile(output);
  564. if (stream == null) return new IOException("无法创建文件。");
  565. var ex = ToZip(directory, stream);
  566. RuntimeUtility.Dispose(stream, true);
  567. return ex;
  568. }
  569. /// <summary>解压 ZIP 文件到目标目录。已存在的旧文件将被删除。</summary>
  570. public static Exception FromZip(string zipPath, string outputDirectory = null)
  571. {
  572. if (!FileExists(zipPath)) return new FileNotFoundException("指定的 ZIP 文件路径无效。");
  573. var directory = outputDirectory;
  574. if (directory.IsEmpty())
  575. {
  576. if (zipPath.Lower().EndsWith(".zip"))
  577. {
  578. directory = zipPath.Substring(0, zipPath.Length - 4);
  579. //if (directory.EndsWith("/") || directory.EndsWith("\\"))
  580. //if (PathExists(directory)) directory = null;
  581. }
  582. else
  583. {
  584. directory = GetParentPath(zipPath);
  585. }
  586. }
  587. if (directory.IsEmpty()) return new ArgumentException("无法判断目录路径。");
  588. if (!AssureDirectory(directory)) return new DirectoryNotFoundException("无法创建目录。");
  589. var input = OpenFile(zipPath);
  590. if (input == null) return new IOException("无法打开 ZIP 文件。");
  591. var info = new Dictionary<string, DateTime>();
  592. var ex = BytesUtility.FromZip(input, (name, size, modifield) =>
  593. {
  594. var path = Path.Combine(directory, name);
  595. if (FileExists(path)) DeleteFile(path);
  596. if (DirectoryExists(path)) DeleteDirectory(path, true);
  597. if (PathExists(path)) throw new IOException("无法删除旧文件或旧目录。");
  598. var output = OpenFile(path);
  599. if (output == null) throw new IOException("无法创建文件 " + path + "。");
  600. if (info.ContainsKey(path))
  601. {
  602. RuntimeUtility.Dispose(output);
  603. return null;
  604. }
  605. info.Add(path, modifield);
  606. return output;
  607. }, (name, modified) =>
  608. {
  609. var path = Path.Combine(directory, name);
  610. var exists = AssureDirectory(path);
  611. if (!exists) throw new IOException("无法创建 ZIP 中对应的目录。");
  612. }, true);
  613. // 恢复文件的修改时间。
  614. foreach (var i in info) SetFileLastWriteTime(i.Key, i.Value);
  615. return ex;
  616. }
  617. ///// <summary>解压 ZIP 文件到字典。失败且不允许异常时返回 NULL 值。</summary>
  618. //public static Dictionary<string, byte[]> FromZip(string zipPath, bool allowException = false)
  619. //{
  620. // if (FileExists(zipPath))
  621. // {
  622. // var ex = new FileNotFoundException("文件不存在。");
  623. // if (allowException) throw ex;
  624. // return null;
  625. // }
  626. // var input = OpenFile(zipPath);
  627. // if (input == null)
  628. // {
  629. // var ex = new IOException("无法打开 ZIP 文件。");
  630. // if (allowException) throw ex;
  631. // return null;
  632. // }
  633. // return BinaryUtility.FromZip(input, ;
  634. //}
  635. /// <summary>获取指定程序集的资源。</summary>
  636. public static Stream GetResource(string name, Assembly assembly = null)
  637. {
  638. try
  639. {
  640. if (assembly == null) assembly = Assembly.GetExecutingAssembly();
  641. var stream = assembly.GetManifestResourceStream(name);
  642. return stream;
  643. }
  644. catch { }
  645. return null;
  646. }
  647. /// <summary>获取当前进程程序集的资源。读取失败时返回 NULL 值。</summary>
  648. public static byte[] ReadResource(string name, Assembly assembly = null)
  649. {
  650. var stream = GetResource(name, assembly);
  651. if (stream == null) return null;
  652. var bytes = BytesUtility.Read(stream, true);
  653. return bytes;
  654. }
  655. /// <summary>监视文件夹的变化。</summary>
  656. public static FileSystemWatcher NewFileSystemWatcher(string directory, FileSystemEventHandler action)
  657. {
  658. var watcher = new FileSystemWatcher();
  659. watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite | NotifyFilters.Size;
  660. watcher.IncludeSubdirectories = true;
  661. watcher.Path = directory;
  662. watcher.Created += (s, e) => action?.Invoke(watcher, e);
  663. watcher.Deleted += (s, e) => action?.Invoke(watcher, e);
  664. watcher.Changed += (s, e) => action?.Invoke(watcher, e);
  665. watcher.Renamed += (s, e) => action?.Invoke(watcher, e);
  666. watcher.EnableRaisingEvents = true;
  667. return watcher;
  668. }
  669. }
  670. }