Client LuaCsForBarotrauma
SaveUtil.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.IO.Compression;
5 using System.Linq;
6 using System.Text;
7 using System.Threading;
8 using System.Xml.Linq;
9 using System.Text.RegularExpressions;
10 using Barotrauma.IO;
11 using Microsoft.Xna.Framework;
12 using System.Collections.Immutable;
13 using System.Diagnostics.CodeAnalysis;
14 
15 namespace Barotrauma
16 {
17  static class SaveUtil
18  {
19  public const string GameSessionFileName = "gamesession.xml";
20 
21  private static readonly string LegacySaveFolder = Path.Combine("Data", "Saves");
22  private static readonly string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder, "Multiplayer");
23 
24 #if OSX
25  //"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac
26  public static readonly string DefaultSaveFolder = Path.Combine(
27  Environment.GetFolderPath(Environment.SpecialFolder.Personal),
28  "Library",
29  "Application Support",
30  "Daedalic Entertainment GmbH",
31  "Barotrauma");
32 #else
33  //"C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/" on Windows
34  //"/home/*user*/.local/share/Daedalic Entertainment GmbH/" on Linux
35  public static readonly string DefaultSaveFolder = Path.Combine(
36  Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
37  "Daedalic Entertainment GmbH",
38  "Barotrauma");
39 #endif
40 
41  public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder, "Multiplayer");
42 
43  public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded");
44  public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded");
45 
46  public static string TempPath
47  {
48 #if SERVER
49  get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp_server"); }
50 #else
51  get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); }
52 #endif
53  }
54 
55  public enum SaveType
56  {
57  Singleplayer,
58  Multiplayer
59  }
60 
61  public static void SaveGame(string filePath)
62  {
63  DebugConsole.Log("Saving the game to: " + filePath);
64  Directory.CreateDirectory(TempPath);
65  try
66  {
67  ClearFolder(TempPath, new string[] { GameMain.GameSession.SubmarineInfo.FilePath });
68  }
69  catch (Exception e)
70  {
71  DebugConsole.ThrowError("Failed to clear folder", e);
72  return;
73  }
74 
75  try
76  {
77  GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName));
78  }
79  catch (Exception e)
80  {
81  DebugConsole.ThrowError("Error saving gamesession", e);
82  return;
83  }
84 
85  try
86  {
87  string? mainSubPath = null;
88  if (GameMain.GameSession.SubmarineInfo != null)
89  {
90  mainSubPath = Path.Combine(TempPath, GameMain.GameSession.SubmarineInfo.Name + ".sub");
91  GameMain.GameSession.SubmarineInfo.SaveAs(mainSubPath);
92  for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
93  {
94  if (GameMain.GameSession.OwnedSubmarines[i].Name == GameMain.GameSession.SubmarineInfo.Name)
95  {
96  GameMain.GameSession.OwnedSubmarines[i] = GameMain.GameSession.SubmarineInfo;
97  }
98  }
99  }
100 
101  if (GameMain.GameSession.OwnedSubmarines != null)
102  {
103  for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
104  {
105  SubmarineInfo storedInfo = GameMain.GameSession.OwnedSubmarines[i];
106  string subPath = Path.Combine(TempPath, storedInfo.Name + ".sub");
107  if (mainSubPath == subPath) { continue; }
108  storedInfo.SaveAs(subPath);
109  }
110  }
111  }
112  catch (Exception e)
113  {
114  DebugConsole.ThrowError("Error saving submarine", e);
115  return;
116  }
117 
118  try
119  {
120  CompressDirectory(TempPath, filePath);
121  }
122  catch (Exception e)
123  {
124  DebugConsole.ThrowError("Error compressing save file", e);
125  }
126  }
127 
128  public static void LoadGame(string filePath)
129  {
130  //ensure there's no gamesession/sub loaded because it'd lead to issues when starting a new one (e.g. trying to determine which level to load based on the placement of the sub)
131  //can happen if a gamesession is interrupted ungracefully (exception during loading)
132  Submarine.Unload();
133  GameMain.GameSession = null;
134  DebugConsole.Log("Loading save file: " + filePath);
135  DecompressToDirectory(filePath, TempPath);
136 
137  XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, GameSessionFileName));
138  if (doc == null) { return; }
139 
140  if (!IsSaveFileCompatible(doc))
141  {
142  throw new Exception($"The save file \"{filePath}\" is not compatible with this version of Barotrauma.");
143  }
144 
145  var ownedSubmarines = LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub);
146  GameMain.GameSession = new GameSession(selectedSub, ownedSubmarines, doc, filePath);
147  }
148 
149  public static List<SubmarineInfo> LoadOwnedSubmarines(XDocument saveDoc, out SubmarineInfo selectedSub)
150  {
151  string subPath = Path.Combine(TempPath, saveDoc.Root.GetAttributeString("submarine", "")) + ".sub";
152  selectedSub = new SubmarineInfo(subPath);
153 
154  List<SubmarineInfo> ownedSubmarines = new List<SubmarineInfo>();
155 
156  var ownedSubsElement = saveDoc.Root?.Element("ownedsubmarines");
157  if (ownedSubsElement == null) { return ownedSubmarines; }
158 
159  foreach (var subElement in ownedSubsElement.Elements())
160  {
161  string subName = subElement.GetAttributeString("name", "");
162  string ownedSubPath = Path.Combine(TempPath, subName + ".sub");
163  if (!File.Exists(ownedSubPath))
164  {
165  DebugConsole.ThrowError($"Could not find the submarine \"{subName}\" ({ownedSubPath})! The save file may be corrupted. Removing the submarine from owned submarines...");
166  }
167  else
168  {
169  ownedSubmarines.Add(new SubmarineInfo(ownedSubPath));
170  }
171  }
172  return ownedSubmarines;
173  }
174 
175  public static bool IsSaveFileCompatible(XDocument? saveDoc)
176  => IsSaveFileCompatible(saveDoc?.Root);
177 
178  public static bool IsSaveFileCompatible(XElement? saveDocRoot)
179  {
180  if (saveDocRoot?.Attribute("version") == null) { return false; }
181  return true;
182  }
183 
184  public static void DeleteSave(string filePath)
185  {
186  try
187  {
188  File.Delete(filePath);
189  }
190  catch (Exception e)
191  {
192  DebugConsole.ThrowError("ERROR: deleting save file \"" + filePath + "\" failed.", e);
193  }
194 
195  //deleting a multiplayer save file -> also delete character data
196  var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath) ?? "");
197 
198  if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) ||
199  fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer)))
200  {
201  string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath);
202  if (File.Exists(characterDataSavePath))
203  {
204  try
205  {
206  File.Delete(characterDataSavePath);
207  }
208  catch (Exception e)
209  {
210  DebugConsole.ThrowError("ERROR: deleting character data file \"" + characterDataSavePath + "\" failed.", e);
211  }
212  }
213  }
214  }
215 
216  public static string GetSaveFolder(SaveType saveType)
217  {
218  string folder = string.Empty;
219 
220  if (!string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath))
221  {
222  folder = GameSettings.CurrentConfig.SavePath;
223  if (saveType == SaveType.Multiplayer)
224  {
225  folder = Path.Combine(folder, "Multiplayer");
226  }
227  if (!Directory.Exists(folder))
228  {
229  DebugConsole.AddWarning($"Could not find the custom save folder \"{folder}\", creating the folder...");
230  try
231  {
232  Directory.CreateDirectory(folder);
233  }
234  catch (Exception e)
235  {
236  DebugConsole.ThrowError($"Could not find the custom save folder \"{folder}\". Using the default save path instead.", e);
237  folder = string.Empty;
238  }
239  }
240  }
241  if (string.IsNullOrEmpty(folder))
242  {
243  folder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
244  }
245  return folder;
246  }
247 
248  public static IReadOnlyList<CampaignMode.SaveInfo> GetSaveFiles(SaveType saveType, bool includeInCompatible = true, bool logLoadErrors = true)
249  {
250  string defaultFolder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
251  if (!Directory.Exists(defaultFolder))
252  {
253  DebugConsole.Log("Save folder \"" + defaultFolder + " not found! Attempting to create a new folder...");
254  try
255  {
256  Directory.CreateDirectory(defaultFolder);
257  }
258  catch (Exception e)
259  {
260  DebugConsole.ThrowError("Failed to create the folder \"" + defaultFolder + "\"!", e);
261  }
262  }
263 
264  List<string> files = Directory.GetFiles(defaultFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList();
265 
266  var folder = GetSaveFolder(saveType);
267  if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder))
268  {
269  files.AddRange(Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly));
270  }
271 
272  string legacyFolder = saveType == SaveType.Singleplayer ? LegacySaveFolder : LegacyMultiplayerSaveFolder;
273  if (Directory.Exists(legacyFolder))
274  {
275  files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly));
276  }
277 
278  files = files.Distinct().ToList();
279 
280  List<CampaignMode.SaveInfo> saveInfos = new List<CampaignMode.SaveInfo>();
281  foreach (string file in files)
282  {
283  var docRoot = ExtractGameSessionRootElementFromSaveFile(file, logLoadErrors);
284  if (!includeInCompatible && !IsSaveFileCompatible(docRoot))
285  {
286  continue;
287  }
288  if (docRoot == null)
289  {
290  saveInfos.Add(new CampaignMode.SaveInfo(
291  FilePath: file,
292  SaveTime: Option.None,
293  SubmarineName: "",
294  EnabledContentPackageNames: ImmutableArray<string>.Empty));
295  }
296  else
297  {
298  List<string> enabledContentPackageNames = new List<string>();
299 
300  //backwards compatibility
301  string enabledContentPackagePathsStr = docRoot.GetAttributeStringUnrestricted("selectedcontentpackages", string.Empty);
302  foreach (string packagePath in enabledContentPackagePathsStr.Split('|'))
303  {
304  if (string.IsNullOrEmpty(packagePath)) { continue; }
305  //change paths to names
306  string fileName = Path.GetFileNameWithoutExtension(packagePath);
307  if (fileName == "filelist")
308  {
309  enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath) ?? ""));
310  }
311  else
312  {
313  enabledContentPackageNames.Add(fileName);
314  }
315  }
316 
317  string enabledContentPackageNamesStr = docRoot.GetAttributeStringUnrestricted("selectedcontentpackagenames", string.Empty);
318  //split on pipes, excluding pipes preceded by \
319  foreach (string packageName in Regex.Split(enabledContentPackageNamesStr, @"(?<!(?<!\\‍)*\\‍)\|"))
320  {
321  if (string.IsNullOrEmpty(packageName)) { continue; }
322  enabledContentPackageNames.Add(packageName.Replace(@"\|", "|"));
323  }
324 
325  saveInfos.Add(new CampaignMode.SaveInfo(
326  FilePath: file,
327  SaveTime: docRoot.GetAttributeDateTime("savetime"),
328  SubmarineName: docRoot.GetAttributeStringUnrestricted("submarine", ""),
329  EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray()));
330  }
331  }
332 
333  return saveInfos;
334  }
335 
336  public static string CreateSavePath(SaveType saveType, string fileName = "Save_Default")
337  {
338  fileName = ToolBox.RemoveInvalidFileNameChars(fileName);
339 
340  string folder = GetSaveFolder(saveType);
341  if (fileName == "Save_Default")
342  {
343  fileName = TextManager.Get("SaveFile.DefaultName").Value;
344  if (fileName.Length == 0) fileName = "Save";
345  }
346 
347  if (!Directory.Exists(folder))
348  {
349  DebugConsole.Log("Save folder \"" + folder + "\" not found. Created new folder");
350  Directory.CreateDirectory(folder);
351  }
352 
353  string extension = ".save";
354  string pathWithoutExtension = Path.Combine(folder, fileName);
355 
356  if (!File.Exists(pathWithoutExtension + extension))
357  {
358  return pathWithoutExtension + extension;
359  }
360 
361  int i = 0;
362  while (File.Exists(pathWithoutExtension + " " + i + extension))
363  {
364  i++;
365  }
366 
367  return pathWithoutExtension + " " + i + extension;
368  }
369 
370  public static void CompressStringToFile(string fileName, string value)
371  {
372  // A.
373  // Convert the string to its byte representation.
374  byte[] b = Encoding.UTF8.GetBytes(value);
375 
376  // B.
377  // Use GZipStream to write compressed bytes to target file.
378  using FileStream f2 = File.Open(fileName, System.IO.FileMode.Create)
379  ?? throw new Exception($"Failed to create file \"{fileName}\"");;
380  using GZipStream gz = new GZipStream(f2, CompressionMode.Compress, false);
381  gz.Write(b, 0, b.Length);
382  }
383 
384  private static void CompressFile(string sDir, string sRelativePath, GZipStream zipStream)
385  {
386  //Compress file name
387  if (sRelativePath.Length > 255)
388  {
389  throw new Exception(
390  $"Failed to compress \"{sDir}\" (file name length > 255).");
391  }
392  // File name length is encoded as a 32-bit little endian integer here
393  zipStream.WriteByte((byte)sRelativePath.Length);
394  zipStream.WriteByte(0);
395  zipStream.WriteByte(0);
396  zipStream.WriteByte(0);
397  // File name content is encoded as little-endian UTF-16
398  var strBytes = Encoding.Unicode.GetBytes(sRelativePath.CleanUpPathCrossPlatform(correctFilenameCase: false));
399  zipStream.Write(strBytes, 0, strBytes.Length);
400 
401  //Compress file content
402  byte[] bytes = File.ReadAllBytes(Path.Combine(sDir, sRelativePath));
403  zipStream.Write(BitConverter.GetBytes(bytes.Length), 0, sizeof(int));
404  zipStream.Write(bytes, 0, bytes.Length);
405  }
406 
407  public static void CompressDirectory(string sInDir, string sOutFile)
408  {
409  IEnumerable<string> sFiles = Directory.GetFiles(sInDir, "*.*", System.IO.SearchOption.AllDirectories);
410  int iDirLen = sInDir[^1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1;
411 
412  using var outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write)
413  ?? throw new Exception($"Failed to create file \"{sOutFile}\"");
414  using GZipStream str = new GZipStream(outFile, CompressionMode.Compress);
415  foreach (string sFilePath in sFiles)
416  {
417  string sRelativePath = sFilePath.Substring(iDirLen);
418  CompressFile(sInDir, sRelativePath, str);
419  }
420  }
421 
422 
423  public static System.IO.Stream DecompressFileToStream(string fileName)
424  {
425  using FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.Read)
426  ?? throw new Exception($"Failed to open file \"{fileName}\"");
427  System.IO.MemoryStream streamToReturn = new System.IO.MemoryStream();
428 
429  using GZipStream gzipStream = new GZipStream(originalFileStream, CompressionMode.Decompress);
430  gzipStream.CopyTo(streamToReturn);
431 
432  streamToReturn.Position = 0;
433  return streamToReturn;
434  }
435 
436  private static bool IsExtractionPathValid(string rootDir, string fileDir)
437  {
438  string getFullPath(string dir)
439  => (string.IsNullOrEmpty(dir)
440  ? Directory.GetCurrentDirectory()
441  : Path.GetFullPath(dir))
442  .CleanUpPathCrossPlatform(correctFilenameCase: false);
443 
444  string rootDirFull = getFullPath(rootDir);
445  string fileDirFull = getFullPath(fileDir);
446 
447  return fileDirFull.StartsWith(rootDirFull, StringComparison.OrdinalIgnoreCase);
448  }
449 
450  private static bool DecompressFile(System.IO.BinaryReader reader, [NotNullWhen(returnValue: true)]out string? fileName, [NotNullWhen(returnValue: true)]out byte[]? fileContent)
451  {
452  fileName = null;
453  fileContent = null;
454 
455  if (reader.PeekChar() < 0) { return false; }
456 
457  //Decompress file name
458  int nameLen = reader.ReadInt32();
459  if (nameLen > 255)
460  {
461  throw new Exception(
462  $"Failed to decompress (file name length > 255). The file may be corrupted.");
463  }
464 
465  byte[] strBytes = reader.ReadBytes(nameLen * sizeof(char));
466  string sFileName = Encoding.Unicode.GetString(strBytes)
467  .Replace('\\', '/');
468 
469  fileName = sFileName;
470 
471  //Decompress file content
472  int contentLen = reader.ReadInt32();
473  fileContent = reader.ReadBytes(contentLen);
474 
475  return true;
476  }
477 
478  public static void DecompressToDirectory(string sCompressedFile, string sDir)
479  {
480  DebugConsole.Log("Decompressing " + sCompressedFile + " to " + sDir + "...");
481  const int maxRetries = 4;
482  for (int i = 0; i <= maxRetries; i++)
483  {
484  try
485  {
486  using var memStream = DecompressFileToStream(sCompressedFile);
487  using var reader = new System.IO.BinaryReader(memStream);
488  while (DecompressFile(reader, out var fileName, out var contentBytes))
489  {
490  string sFilePath = Path.Combine(sDir, fileName);
491  string sFinalDir = Path.GetDirectoryName(sFilePath) ?? "";
492 
493  if (!IsExtractionPathValid(sDir, sFinalDir))
494  {
495  throw new InvalidOperationException(
496  $"Error extracting \"{fileName}\": cannot be extracted to parent directory");
497  }
498 
499  Directory.CreateDirectory(sFinalDir);
500  using var outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)
501  ?? throw new Exception($"Failed to create file \"{sFilePath}\"");
502  outFile.Write(contentBytes, 0, contentBytes.Length);
503  }
504  break;
505  }
506  catch (System.IO.IOException e)
507  {
508  if (i >= maxRetries || !File.Exists(sCompressedFile)) { throw; }
509  DebugConsole.NewMessage("Failed decompress file \"" + sCompressedFile + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red);
510  Thread.Sleep(250);
511  }
512  }
513  }
514 
515  public static IEnumerable<string> EnumerateContainedFiles(string sCompressedFile)
516  {
517  const int maxRetries = 4;
518  HashSet<string> paths = new HashSet<string>();
519  for (int i = 0; i <= maxRetries; i++)
520  {
521  try
522  {
523  paths.Clear();
524  using var memStream = DecompressFileToStream(sCompressedFile);
525  using var reader = new System.IO.BinaryReader(memStream);
526  while (DecompressFile(reader, out var fileName, out _))
527  {
528  paths.Add(fileName);
529  }
530  break;
531  }
532  catch (System.IO.IOException e)
533  {
534  if (i >= maxRetries || !File.Exists(sCompressedFile)) { throw; }
535 
536  DebugConsole.NewMessage(
537  $"Failed to decompress file \"{sCompressedFile}\" for enumeration {{{e.Message}}}, retrying in 250 ms...",
538  Color.Red);
539  Thread.Sleep(250);
540  }
541  }
542 
543  return paths;
544  }
545 
552  public static XDocument? DecompressSaveAndLoadGameSessionDoc(string savePath)
553  {
554  DebugConsole.Log("Loading game session doc: " + savePath);
555  try
556  {
557  DecompressToDirectory(savePath, TempPath);
558  }
559  catch (Exception e)
560  {
561  DebugConsole.ThrowError("Error decompressing " + savePath, e);
562  return null;
563  }
564  return XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml"));
565  }
566 
571  public static XElement? ExtractGameSessionRootElementFromSaveFile(string savePath, bool logLoadErrors = true)
572  {
573  const int maxRetries = 4;
574  for (int i = 0; i <= maxRetries; i++)
575  {
576  try
577  {
578  using var memStream = DecompressFileToStream(savePath);
579  using var reader = new System.IO.BinaryReader(memStream);
580  while (DecompressFile(reader, out var fileName, out var fileContent))
581  {
582  if (fileName != GameSessionFileName) { continue; }
583 
584  // Found the file! Here's a quick byte-wise parser to find the root element
585  int tagOpenerStartIndex = -1;
586  for (int j = 0; j < fileContent.Length; j++)
587  {
588  if (fileContent[j] == '<')
589  {
590  // Found a tag opener: return null if we had already found one
591  if (tagOpenerStartIndex >= 0) { return null; }
592  tagOpenerStartIndex = j;
593  }
594  else if (j > 0 && fileContent[j] == '?' && fileContent[j - 1] == '<')
595  {
596  // Found the XML version element, skip this
597  tagOpenerStartIndex = -1;
598  }
599  else if (fileContent[j] == '>')
600  {
601  // Found a tag closer, if we know where the tag opener is then we've found the root element
602  if (tagOpenerStartIndex < 0) { continue; }
603 
604  string elemStr = Encoding.UTF8.GetString(fileContent.AsSpan()[tagOpenerStartIndex..j]) + "/>";
605  try
606  {
607  return XElement.Parse(elemStr);
608  }
609  catch (Exception e)
610  {
611  DebugConsole.NewMessage(
612  $"Failed to parse gamesession root in \"{savePath}\": {{{e.Message}}}.",
613  Color.Red);
614  // Parsing the element failed! Return null instead of crashing here
615  return null;
616  }
617  }
618  }
619  }
620  break;
621  }
622  catch (System.IO.IOException e)
623  {
624  if (i >= maxRetries || !File.Exists(savePath)) { throw; }
625 
626  DebugConsole.NewMessage(
627  $"Failed to decompress file \"{savePath}\" for root extraction ({e.Message}), retrying in 250 ms...",
628  Color.Red);
629  Thread.Sleep(250);
630  }
631  catch (System.IO.InvalidDataException e)
632  {
633  if (logLoadErrors)
634  {
635  DebugConsole.ThrowError($"Failed to decompress file \"{savePath}\" for root extraction.", e);
636  }
637  return null;
638  }
639  }
640  return null;
641  }
642 
643  public static void DeleteDownloadedSubs()
644  {
645  if (Directory.Exists(SubmarineDownloadFolder))
646  {
647  ClearFolder(SubmarineDownloadFolder);
648  }
649  }
650 
651  public static void CleanUnnecessarySaveFiles()
652  {
653  if (Directory.Exists(CampaignDownloadFolder))
654  {
655  ClearFolder(CampaignDownloadFolder);
656  Directory.Delete(CampaignDownloadFolder);
657  }
658  if (Directory.Exists(TempPath))
659  {
660  ClearFolder(TempPath);
661  Directory.Delete(TempPath);
662  }
663  }
664 
665  public static void ClearFolder(string folderName, string[]? ignoredFileNames = null)
666  {
667  DirectoryInfo dir = new DirectoryInfo(folderName);
668 
669  foreach (FileInfo fi in dir.GetFiles())
670  {
671  if (ignoredFileNames != null)
672  {
673  bool ignore = false;
674  foreach (string ignoredFile in ignoredFileNames)
675  {
676  if (Path.GetFileName(fi.FullName).Equals(Path.GetFileName(ignoredFile)))
677  {
678  ignore = true;
679  break;
680  }
681  }
682  if (ignore) continue;
683  }
684  fi.IsReadOnly = false;
685  fi.Delete();
686  }
687 
688  foreach (DirectoryInfo di in dir.GetDirectories())
689  {
690  ClearFolder(di.FullName, ignoredFileNames);
691  const int maxRetries = 4;
692  for (int i = 0; i <= maxRetries; i++)
693  {
694  try
695  {
696  di.Delete();
697  break;
698  }
699  catch (System.IO.IOException)
700  {
701  if (i >= maxRetries) { throw; }
702  Thread.Sleep(250);
703  }
704  }
705  }
706  }
707  }
708 }
IEnumerable< FileInfo > GetFiles()
Definition: SafeIO.cs:725
IEnumerable< DirectoryInfo > GetDirectories()
Definition: SafeIO.cs:716