/Utilities/Helpers/FileHelper.cs
C# | 2070 lines | 1228 code | 189 blank | 653 comment | 104 complexity | b7a0f155bd58030704580436144e2d15 MD5 | raw file
Possible License(s): Apache-2.0
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Security.Cryptography;
5using System.Text;
6using System.Threading;
7using NUnit.Framework;
8
9namespace Delta.Utilities.Helpers
10{
11 /// <summary>
12 /// File helper class to get text lines, number of text lines, etc. This
13 /// extends the existing File and Path functionality of .NET.
14 /// <para />
15 /// This class also abstracts the use of File.Exists and File.Open for
16 /// IsolatedStorage classes are used, which just allow access to the
17 /// current directory, which is all we need in the engine internally).
18 /// </summary>
19 public static class FileHelper
20 {
21 #region Constants
22 /// <summary>
23 /// Represents the separator for folders in file paths on microsoft
24 /// platforms.
25 /// </summary>
26 public static readonly char FolderSeparator = '\\';
27
28 /// <summary>
29 /// Represents the separator for folders in file paths on Unix platforms.
30 /// </summary>
31 public static readonly char AlternateFolderSeparator = '/';
32
33 /// <summary>
34 /// Both folder separators together in one char array, used for GetFilename
35 /// </summary>
36 public static readonly char[] AllFolderSeparators =
37 new[]
38 {
39 FolderSeparator, AlternateFolderSeparator
40 };
41
42 /// <summary>
43 /// The typical file extension separator is just a dot: .
44 /// </summary>
45 public const char FileExtensionSeperator = '.';
46 #endregion
47
48 #region Delegates
49 internal delegate bool ExistsDelegate(string path);
50
51 internal delegate void CopyDelegate(string sourceFile,
52 string destinationFile);
53
54 internal delegate void MoveDelegate(string sourceFile,
55 string destinationFile);
56
57 internal delegate FileStream OpenDelegate(string filePath, FileMode mode,
58 FileAccess access, FileShare share);
59
60 internal delegate void DeleteDelegate(string filePath);
61
62 internal delegate bool FileIsNewerDelegate(string fileToCheckIfNewer,
63 string originalFile);
64
65 /// <summary>
66 /// Compare two files with the help of the MD5-checksum
67 /// </summary>
68 /// <param name="fileName1">Filename 1</param>
69 /// <param name="fileName2">Filename 2</param>
70 /// <returns>True if both file contents are the same</returns>
71 internal delegate bool CompareFilesDelegate(string fileName1,
72 string fileName2);
73
74 /// <summary>
75 /// Check if file exists and is not written to recently. This is mostly
76 /// used for update checks to see if we can assume this file is complete
77 /// and we can now open it, etc. (e.g. used in the RestartTool).
78 /// </summary>
79 /// <param name="filename">File to check</param>
80 /// <returns>
81 /// True if the file exists and was not written to recently.
82 /// </returns>
83 internal delegate bool FileNotWrittenToDelegate(string filename);
84 #endregion
85
86 #region Exists (Static)
87 /// <summary>
88 /// Check if the file exists.
89 /// </summary>
90 /// <param name="filename">Filename</param>
91 /// <returns>filename</returns>
92 public static bool Exists(string filename)
93 {
94 return ExistsCallback(filename);
95 }
96 #endregion
97
98 #region Copy (Static)
99 /// <summary>
100 /// Copy the source file to the destination file.
101 /// Note: this method overwrites existing destination files!
102 /// </summary>
103 /// <param name="sourceFile">Source filepath.</param>
104 /// <param name="destinationFile">Destination Filepath.</param>
105 public static void Copy(string sourceFile, string destinationFile)
106 {
107 CopyCallback(sourceFile, destinationFile);
108 }
109 #endregion
110
111 #region Move (Static)
112 /// <summary>
113 /// Move the source file to the destination file.
114 /// </summary>
115 /// <param name="sourceFile">Source filepath.</param>
116 /// <param name="destinationFile">Destination filepath.</param>
117 public static void Move(string sourceFile, string destinationFile)
118 {
119 MoveCallback(sourceFile, destinationFile);
120 }
121 #endregion
122
123 #region Open (Static)
124 /// <summary>
125 /// Opens a the given file (will not create it if it doesn't exists) on
126 /// platforms where this is possible.
127 /// <para />
128 /// Note: The file-mode is "Open" only and the file-access + file-share
129 /// mode is "Read".
130 /// </summary>
131 public static FileStream Open(string filePath)
132 {
133 return Open(filePath, FileMode.Open, FileAccess.Read,
134 FileShare.Read);
135 }
136
137 /// <summary>
138 /// Open, will use File.Open on platforms where this is possible.
139 /// </summary>
140 public static FileStream Open(string filePath, FileMode mode,
141 FileAccess access, FileShare share)
142 {
143 return OpenCallback(filePath, mode, access, share);
144 }
145 #endregion
146
147 #region Rename (Static)
148 /// <summary>
149 /// Rename the file.
150 /// </summary>
151 /// <param name="sourceFile">Path to the file to rename.</param>
152 /// <param name="destFile">Path to the file after rename.</param>
153 /// <returns>True if succeeded otherwise false.</returns>
154 public static bool Rename(string sourceFile, string destFile)
155 {
156 if (Exists(sourceFile) == false)
157 {
158 return false;
159 }
160
161 try
162 {
163 Move(sourceFile, destFile);
164 }
165 catch
166 {
167 return false;
168 }
169
170 return true;
171 }
172 #endregion
173
174 #region GetFilename (Static)
175 /// <summary>
176 /// Extracts filename from full path+filename, but don't cuts off the
177 /// extension. Can be also used to cut of directories from a path (only
178 /// last one will remain).
179 /// </summary>
180 /// <returns>filename</returns>
181 public static string GetFilename(string filePath)
182 {
183 // Similar code as GetFilename(..), but this method is called more often!
184
185 #region Validation
186 if (String.IsNullOrEmpty(filePath))
187 {
188 return "";
189 }
190 #endregion
191
192 // At first we try to find any folder separators (incl. alternate ones)
193 int lastSeparatorIndex = filePath.LastIndexOf(FolderSeparator);
194 int lastAlternateSeperatorIndex = filePath.LastIndexOf(
195 AlternateFolderSeparator);
196 // to pick the backmost one
197 int backMostIndex = (lastSeparatorIndex > lastAlternateSeperatorIndex)
198 ? lastSeparatorIndex
199 : lastAlternateSeperatorIndex;
200
201 // to get only the filename
202 return (backMostIndex != MathHelper.InvalidIndex)
203 ? // -> "Path\SubPath\File.tst" -> "File.tst"
204 // -> "Path/SubPath\File.tst" -> "File.tst"
205 // -> "Path\SubPath/File.tst" -> "File.tst"
206 filePath.Substring(backMostIndex + 1)
207 : // -> "File.tst" -> "File.tst"
208 filePath;
209 }
210
211 /// <summary>
212 /// Extracts filename from full path+filename, but don't cuts off the
213 /// extension. Can be also used to cut of directories from a path (only
214 /// last one will remain).
215 /// </summary>
216 public static string GetFilename(string filePath, out string pathOnly)
217 {
218 #region Validation
219 if (String.IsNullOrEmpty(filePath))
220 {
221 pathOnly = "";
222 return "";
223 }
224 #endregion
225
226 // At first we try to find any folder separators (incl. alternate ones)
227 int lastSeparatorIndex = filePath.LastIndexOf(FolderSeparator);
228 int lastAlternateSeperatorIndex = filePath.LastIndexOf(
229 AlternateFolderSeparator);
230 // to pick the backmost one
231 int backMostIndex = lastSeparatorIndex > lastAlternateSeperatorIndex
232 ? lastSeparatorIndex
233 : lastAlternateSeperatorIndex;
234
235 if (backMostIndex != MathHelper.InvalidIndex)
236 {
237 // -> "Path\SubPath\File.tst" -> "File.tst"
238 // -> "Path/SubPath\File.tst" -> "File.tst"
239 // -> "Path\SubPath/File.tst" -> "File.tst"
240 pathOnly = filePath.Substring(0, backMostIndex);
241 return filePath.Substring(backMostIndex + 1);
242 }
243 else
244 {
245 // -> "File.tst" -> "File.tst"
246 pathOnly = "";
247 return filePath;
248 }
249 }
250
251 /// <summary>
252 /// Extracts filename from full path+filename, cuts of extension
253 /// if cutExtension is true. Can be also used to cut of directories
254 /// from a path (only last one will remain).
255 /// </summary>
256 public static string GetFilename(string filePath, bool cutExtension)
257 {
258 #region Validation
259 if (String.IsNullOrEmpty(filePath))
260 {
261 return "";
262 }
263 #endregion
264
265 // At first we try to find any folder separators (incl. alternate ones)
266 int lastSeparatorIndex = filePath.LastIndexOf(FolderSeparator);
267 int lastAlternateSeperatorIndex = filePath.LastIndexOf(
268 AlternateFolderSeparator);
269 // to pick the backmost one
270 int backMostIndex =
271 lastSeparatorIndex > lastAlternateSeperatorIndex
272 ? lastSeparatorIndex
273 : lastAlternateSeperatorIndex;
274
275 // Convert to filename, examples:
276 // -> "Path\SubPath\File.tst" -> "File.tst"
277 // -> "Path/SubPath\File.tst" -> "File.tst"
278 // -> "Path\SubPath/File.tst" -> "File.tst"
279 // -> "File.tst" -> "File.tst"
280 string filename =
281 backMostIndex != MathHelper.InvalidIndex
282 ? filePath.Substring(backMostIndex + 1)
283 : filePath;
284
285 // Return the filename without path (and optionally cut of the extension)
286 // -> "File.tst" -> "File"
287 return
288 cutExtension
289 ? CutExtension(filename)
290 : filename;
291 }
292 #endregion
293
294 #region IsFilenameOnly (Static)
295 /// <summary>
296 /// Is filename only
297 /// </summary>
298 /// <param name="anyFilePath">Any file path</param>
299 public static bool IsFilenameOnly(string anyFilePath)
300 {
301 if (String.IsNullOrEmpty(anyFilePath) ||
302 // If this is a filename, it better has an extension. Note: This is
303 // also used for GetContentName, so all content follows this rule too.
304 HasFileNoExtension(anyFilePath))
305 {
306 return false;
307 }
308
309 // Make sure the filename does not contain any path elements
310 if (anyFilePath.IndexOf(FolderSeparator) >= 0 ||
311 anyFilePath.IndexOf(AlternateFolderSeparator) >= 0)
312 {
313 return false;
314 }
315
316 return true;
317 }
318 #endregion
319
320 #region HasFileNoExtension (Static)
321 /// <summary>
322 /// Has a filename no extension
323 /// </summary>
324 /// <param name="anyFilePath">Any file path</param>
325 /// <returns>True if the given file path has no extension.</returns>
326 public static bool HasFileNoExtension(string anyFilePath)
327 {
328 if (String.IsNullOrEmpty(anyFilePath))
329 {
330 return false;
331 }
332
333 // The file extension is always separated with the '.' defined above.
334 // If we cannot find it, this file has no extension.
335 return anyFilePath.LastIndexOf(FileExtensionSeperator) < 0;
336 }
337 #endregion
338
339 #region GetDirectoryPath (Static)
340 /// <summary>
341 /// Get directory of path+File, if only a path is given we will cut off
342 /// the last sub path!
343 /// </summary>
344 /// <param name="filePath">File path</param>
345 /// <returns>Directory path</returns>
346 public static string GetDirectoryPath(string filePath)
347 {
348 // pathFile must be valid
349 if (String.IsNullOrEmpty(filePath))
350 {
351 return "";
352 }
353
354 // If pathFile ends with a folder separator, cut that off!
355 if (filePath.EndsWith(FolderSeparator.ToString()) ||
356 filePath.EndsWith(AlternateFolderSeparator.ToString()))
357 {
358 filePath = filePath.Substring(0, filePath.Length - 1);
359 }
360
361 // Normal directory check
362 int index = filePath.LastIndexOf(FolderSeparator);
363 int alternateIndex = filePath.LastIndexOf(AlternateFolderSeparator);
364
365 if (alternateIndex > index)
366 {
367 index = alternateIndex;
368 }
369
370 if (index >= 0 &&
371 index < filePath.Length)
372 {
373 // Return directory
374 return filePath.Substring(0, index);
375 }
376
377 // No sub directory found (parent of some directory is "")
378 // Only exception is when we are on the file system root level (e.g. c:)
379 return
380 filePath.Contains(":")
381 ? filePath
382 : "";
383 }
384
385 /// <summary>
386 /// Get directory sub path of the given path or file path.
387 /// (-> e.g. "FileHelper.GetDirectoryPath(@"C:\Ab\cde\App.exe", 2)" will
388 /// return "C:\Ab")
389 /// </summary>
390 /// <param name="filePath">File path</param>
391 /// <param name="jumpbackCount">
392 /// The number of folders it should be "jumped" back.
393 /// </param>
394 /// <returns>Directory path</returns>
395 public static string GetDirectoryPath(string filePath, int jumpbackCount)
396 {
397 // pathFile must be valid
398 if (String.IsNullOrEmpty(filePath))
399 {
400 return "";
401 }
402
403 // At first split the path in its folder (+ filename) parts
404 string[] pathParts = filePath.Split(AllFolderSeparators);
405
406 // If we less or equal (folder) parts than folders we want to jump back
407 int remainingPartCount = pathParts.Length - jumpbackCount;
408 if (remainingPartCount <= 0)
409 {
410 // then we will just return an empty path
411 return "";
412 }
413
414 // Now just count the number of characters we have to cut off
415 int cutOffCount = 0;
416 for (int index = remainingPartCount; index < pathParts.Length; index++)
417 {
418 cutOffCount += pathParts[index].Length + 1;
419 }
420
421 // And finally just return the remaining path
422 return filePath.Substring(0, filePath.Length - cutOffCount);
423 }
424 #endregion
425
426 #region GetFirstDirectoryName (Static)
427 /// <summary>
428 /// Get first directory of path or file path! Returns "c:" for "C:\test.x"
429 /// or "bla" for "bla\blub\honk.txt".
430 /// </summary>
431 /// <param name="pathFile">Path file to search</param>
432 /// <returns>First found directory part</returns>
433 public static string GetFirstDirectoryName(string pathFile)
434 {
435 if (String.IsNullOrEmpty(pathFile))
436 {
437 return "";
438 }
439
440 string[] parts = pathFile.Split(new char[]
441 {
442 '/', '\\'
443 });
444 // Just return the first part
445 return parts[0];
446 }
447 #endregion
448
449 #region GetLastDirectoryName (Static)
450 /// <summary>
451 /// Get last directory of path+File, if only a path is given we will cut
452 /// off the last sub path! Returns "Models" for "C:\aoeus\Models\test.x".
453 /// Warning: If you just use a path, the results might be different from
454 /// what you want (use GetFilename instead): ""C:\aoeus\Models" will return
455 /// "aoeus" and not "Models".
456 /// </summary>
457 /// <param name="pathFile">Path file to search</param>
458 /// <returns>Last found directory</returns>
459 public static string GetLastDirectoryName(string pathFile)
460 {
461 if (String.IsNullOrEmpty(pathFile))
462 {
463 return "";
464 }
465
466 string dir1 = GetDirectoryPath(pathFile);
467 string dir2 = GetDirectoryPath(dir1);
468
469 // No other sub directory found?
470 if (String.IsNullOrEmpty(dir2))
471 {
472 return dir1;
473 }
474
475 // Remove dir2 part including "\\" or "/"
476 return dir1.Remove(0, dir2.Length + 1);
477 }
478 #endregion
479
480 #region RemoveFirstDirectory (Static)
481 /// <summary>
482 /// Remove first directory of path if one exists. "maps\\mymaps\\hehe.map"
483 /// becomes "mymaps\\hehe.map". Also used to cut first folder off,
484 /// especially useful for paths. e.g. "maps\\test" becomes "test".
485 /// </summary>
486 /// <param name="path">Path</param>
487 /// <returns>Reduced directory path</returns>
488 public static string RemoveFirstDirectory(string path)
489 {
490 int index = path.IndexOf(FolderSeparator);
491 if (index >= 0 &&
492 index < path.Length)
493 {
494 // Return rest of path
495 return path.Substring(index + 1);
496 }
497 index = path.IndexOf(AlternateFolderSeparator);
498 if (index >= 0 &&
499 index < path.Length)
500 {
501 // Return rest of path
502 return path.Substring(index + 1);
503 }
504
505 // No first directory found, just return original path
506 return path;
507 }
508 #endregion
509
510 #region CutExtension (Static)
511 /// <summary>
512 /// Cut of extension, e.g. "hi.txt" becomes "hi"
513 /// </summary>
514 /// <param name="file">File</param>
515 /// <returns>Filename without extension</returns>
516 public static string CutExtension(string file)
517 {
518 if (String.IsNullOrEmpty(file))
519 {
520 return "";
521 }
522
523 // Don't cut below any directory structure (which may contain points)
524 int maxLastSeparatorIndex = file.LastIndexOf(FolderSeparator);
525 int alternateSeparatorIndex = file.LastIndexOf(AlternateFolderSeparator);
526 if (alternateSeparatorIndex > maxLastSeparatorIndex)
527 {
528 maxLastSeparatorIndex = alternateSeparatorIndex;
529 }
530
531 // Get extension position.
532 int lastPointPos = file.LastIndexOf(FileExtensionSeperator);
533
534 // Only cut off stuff if we have valid indices, else return the input
535 return
536 lastPointPos > 0 &&
537 lastPointPos > maxLastSeparatorIndex
538 ? file.Remove(lastPointPos, file.Length - lastPointPos)
539 : file;
540 }
541 #endregion
542
543 #region GetExtension (Static)
544 /// <summary>
545 /// Get extension (whatever is behind that '.'), e.g. "test.bmp" will
546 /// return "bmp". Only a filename will return "", e.g. "Test" returns ""
547 /// </summary>
548 /// <param name="file">Filename to get the extension from</param>
549 /// <returns>Returns the extension string of the file.</returns>
550 public static string GetExtension(string file)
551 {
552 if (String.IsNullOrEmpty(file))
553 {
554 return "";
555 }
556
557 int lastPointPos = file.LastIndexOf(FileExtensionSeperator);
558
559 // Don't cut below any directory structure (which may contain points)
560 int lastDirectoryPos = file.LastIndexOf(FolderSeparator);
561 int alternateSeparatorIndex = file.LastIndexOf(AlternateFolderSeparator);
562 if (alternateSeparatorIndex > lastDirectoryPos)
563 {
564 lastDirectoryPos = alternateSeparatorIndex;
565 }
566 if (lastPointPos > 0 &&
567 lastPointPos > lastDirectoryPos &&
568 lastPointPos < file.Length)
569 {
570 return file.Remove(0, lastPointPos + 1);
571 }
572
573 // No extension found, return empty string (filename without extension).
574 return "";
575 }
576 #endregion
577
578 #region TryToUseRelativePath (Static)
579 /// <summary>
580 /// Helper function for saving, we check if path starts with same as
581 /// our application. If so, better use relative path, then we can use
582 /// them even if application is moved or copied over network!
583 /// </summary>
584 /// <param name="fullPath">Full path to check against</param>
585 /// <returns>Relative path starting at fullPath if possible</returns>
586 public static string TryToUseRelativePath(string fullPath)
587 {
588 return TryToUseRelativePath(Environment.CurrentDirectory, fullPath);
589 }
590
591 /// <summary>
592 /// Helper function for saving, we check if path starts with same as
593 /// our application. If so, better use relative path, then we can use
594 /// them even if application is moved or copied over network!
595 /// </summary>
596 /// <param name="pathToCheckFrom">Path to check from (e.g. from our
597 /// application) and the origin for the relative path that is returned
598 /// </param>
599 /// <param name="fullPath">Full path to check against</param>
600 /// <returns>Relative path starting at fullPath if possible</returns>
601 public static string TryToUseRelativePath(string pathToCheckFrom,
602 string fullPath)
603 {
604 if (fullPath != null &&
605 fullPath.StartsWith(pathToCheckFrom))
606 {
607 int len = pathToCheckFrom.Length;
608 // +1 to remove '/' too
609 if (!pathToCheckFrom.EndsWith(@"\"))
610 {
611 len++;
612 }
613 return fullPath.Remove(0, len);
614 } // if (fullPath)
615 // Startup path not found, so either its relative already or
616 // we can't use relative path that easy!
617 return fullPath;
618 }
619 #endregion
620
621 #region TryToUseAbsolutePath (Static)
622 /// <summary>
623 /// Helper method to use an absolute path. If the path is already absolute
624 /// nothing will change, but if the path is relative the basePath is added.
625 /// </summary>
626 /// <param name="basePath">Path to check from</param>
627 /// <param name="possibleFullPath">Full or relative path to check.</param>
628 /// <returns>Absolute path to the file</returns>
629 public static string TryToUseAbsolutePath(string basePath,
630 string possibleFullPath)
631 {
632 // If the path is relative, combine it
633 if (Path.IsPathRooted(possibleFullPath) == false)
634 {
635 return Path.Combine(basePath, possibleFullPath);
636 }
637 // Otherwise just return the original absolute path.
638 return possibleFullPath;
639 }
640 #endregion
641
642 #region IsDirectSubFolder (Static)
643 /// <summary>
644 /// Check if a folder is a direct sub folder of a main folder.
645 /// True is only returned if this is a direct sub folder, not if
646 /// it is some sub folder few levels below.
647 /// </summary>
648 /// <param name="mainFolder">MainFolder</param>
649 /// <param name="subFolder">SubFolder</param>
650 /// <returns>
651 /// True if the subFolder is a direct sub folder of mainFolder.
652 /// </returns>
653 public static bool IsDirectSubFolder(string subFolder, string mainFolder)
654 {
655 // First check if subFolder is really a sub folder of mainFolder
656 if (subFolder != null &&
657 subFolder.StartsWith(mainFolder))
658 {
659 // Same order?
660 if (subFolder.Length <
661 mainFolder.Length + 1)
662 {
663 // Then it ain't a sub folder!
664 return false;
665 }
666
667 // Ok, now check if this is direct sub folder or some sub folder
668 // of mainFolder sub folder
669 string folder = subFolder.Remove(0, mainFolder.Length + 1);
670
671 // Check if this is really a direct sub folder
672 if (folder.IndexOf(FolderSeparator) >= 0 ||
673 folder.IndexOf(AlternateFolderSeparator) >= 0)
674 {
675 // No, this is a sub folder of mainFolder sub folder
676 return false;
677 }
678
679 // Ok, this is a direct sub folder of mainFolder!
680 return true;
681 }
682
683 // Not even any sub folder!
684 return false;
685 }
686 #endregion
687
688 #region CleanupWindowsPath (Static)
689 /// <summary>
690 /// Cleans up the given path, so that it's in an homogenous unix/web path.
691 /// E.g. ".\MyPath\With/Subfolder" => "My/Path/With/Subfolder"
692 /// Also used by our whole content pipeline to make searching easier.
693 /// </summary>
694 /// <param name="anyPath">Any path in windows or unix format</param>
695 /// <returns>Path in windows format</returns>
696 public static string CleanupWindowsPath(string anyPath)
697 {
698 // Validation check
699 if (String.IsNullOrEmpty(anyPath))
700 {
701 return "";
702 }
703
704 // First we make sure the we have an homogenous windows path format
705 if (anyPath.StartsWith("file:///"))
706 {
707 anyPath = anyPath.Substring("file:///".Length);
708 }
709 anyPath = anyPath.Replace("/", @"\");
710
711 // If the path begins with "./" like e.g. ".\MyFolder\With\Subfolder",
712 // then we cut this away, because we don't need that and it makes the
713 // path more tight, might happen on windows
714 // else return the cleaned path
715 return
716 anyPath.StartsWith(@".\")
717 ? anyPath.Substring(2)
718 : anyPath;
719 }
720 #endregion
721
722 #region CleanupLinuxPath (Static)
723 /// <summary>
724 /// Cleans up the given path, so that it's in an homogenous unix/web path.
725 /// E.g. ".\MyPath\With/Subfolder" => "My/Path/With/Subfolder"
726 /// Also used by our whole content pipeline to make searching easier.
727 /// </summary>
728 /// <param name="anyPath">Any path in windows or unix format</param>
729 /// <returns>Path in unix format</returns>
730 public static string CleanupLinuxPath(string anyPath)
731 {
732 // Validation check
733 if (String.IsNullOrEmpty(anyPath))
734 {
735 return "";
736 }
737
738 // First we make sure the we have an homogenous unix/web path format
739 if (anyPath.StartsWith(@"\\"))
740 {
741 anyPath = "file://///" + anyPath.Substring(2);
742 }
743 anyPath = anyPath.Replace(@"\", "/");
744
745 // If the path begins with "./" like e.g. "./MyFolder/With/Subfolder",
746 // then we cut this away, because we don't need that and it makes the
747 // else return the cleaned path
748 return
749 anyPath.StartsWith("./")
750 ? anyPath.Substring(2)
751 : anyPath;
752 }
753 #endregion
754
755 #region GetRelativeFilePath (Static)
756 /// <summary>
757 /// Get relative file path of a given absolute file path based on basePath.
758 /// If the basePath and the absoluteFilePath share something in common this
759 /// method will try to remove the common part (e.g. "c:\code\Delta\bla"
760 /// in the basePath "c:\code" will be reduced to "Delta\bla" or in the
761 /// basePath "c:\code\DeltaEngine" will be "..\Delta\bla").
762 /// </summary>
763 /// <param name="absoluteFilePath">
764 /// Absolute file path, might already be relative or even be on a different
765 /// drive, then it will just be returned.
766 /// </param>
767 /// <param name="basePath">Base path</param>
768 /// <returns>Relative path if possible</returns>
769 public static string GetRelativeFilePath(string absoluteFilePath,
770 string basePath)
771 {
772 #region Validation checks
773 if (String.IsNullOrEmpty(absoluteFilePath))
774 {
775 return "";
776 }
777
778 if (String.IsNullOrEmpty(basePath))
779 {
780 return absoluteFilePath;
781 }
782 #endregion
783
784 // We can only easily "build" the relative content path, if the given
785 // file path starts with the given content base path.
786 string relativeContentFilePath =
787 absoluteFilePath.StartsWith(basePath)
788 ? absoluteFilePath.Substring(basePath.Length)
789 : absoluteFilePath;
790
791 // If that did not worked, we can also check folder by folder if
792 // the two path strings are equal up to a certain point.
793 if (relativeContentFilePath == absoluteFilePath)
794 {
795 string[] basePathParts = basePath.Split(new char[]
796 {
797 '\\', '/'
798 });
799 string[] absoluteFilePathParts = absoluteFilePath.Split(new char[]
800 {
801 '\\', '/'
802 });
803
804 // Continue checking as long as the parts are equal. Note: This will
805 // abort in the first loop when the drives are different or one part
806 // is not absolute.
807 int num = 0;
808 for (; num < basePathParts.Length &&
809 num < absoluteFilePathParts.Length; num++)
810 {
811 if (basePathParts[num] != absoluteFilePathParts[num])
812 {
813 break;
814 }
815 // Remove this part as it is equal (plus the separator)!
816 relativeContentFilePath =
817 relativeContentFilePath.Substring(basePathParts[num].Length + 1);
818 }
819
820 // Nothing matched? Then just return the absolute path again!
821 if (num == 0)
822 {
823 return absoluteFilePath;
824 }
825
826 // Otherwise add the number of parts we are away from the base path.
827 for (; num < basePathParts.Length; num++)
828 {
829 relativeContentFilePath = "..\\" + relativeContentFilePath;
830 }
831 }
832
833 // If the path starts with a "folder backslash" like "\Folder" then cut
834 // it off
835 return
836 relativeContentFilePath.StartsWith(@"\")
837 ? relativeContentFilePath.Substring(1)
838 : relativeContentFilePath;
839 }
840 #endregion
841
842 #region TrimPath (Static)
843 /// <summary>
844 /// Removes all unnecessary relative path fragments and will return a
845 /// trimmed path. Useful in combination with GetRelativeFilePath.
846 /// <para />
847 /// Example: "C:\Path\SubPath1\..\SubPath2\file.exe" will become to
848 /// "C:\Path\SubPath2\file.exe". Also works with relative paths and unix
849 /// paths (e.g. '/SomePath/../SomeFile.xml' will become 'SomeFile.xml').
850 /// </summary>
851 /// <param name="filePath">Path to be shortened if possible.</param>
852 public static string TrimPath(string filePath)
853 {
854 // If the filePath has no ..\ in it, ignore it and just return it!
855 if (String.IsNullOrEmpty(filePath) ||
856 (filePath.Contains("..\\") == false &&
857 filePath.Contains("../") == false))
858 {
859 return filePath;
860 }
861
862 // Network paths need the \\ added again after we are done processing
863 bool isNetworkPath = filePath.StartsWith(@"\\");
864
865 // Go through the path and collect all parts.
866 string[] pathParts = filePath.Split(new char[]
867 {
868 '\\', '/'
869 });
870
871 // Now collect all .. parts and cut off previous directories!
872 List<string> result = new List<string>();
873 foreach (string part in pathParts)
874 {
875 // Also ignore the . and empty parts (e.g. \ or at the beginning).
876 if (String.IsNullOrEmpty(part))
877 {
878 continue;
879 }
880 // For .. entries remove the last real path entry we added
881 if (result.Count > 0 &&
882 result[result.Count - 1] != ".." &&
883 part == "..")
884 {
885 result.RemoveAt(result.Count - 1);
886 }
887 else
888 {
889 // Otherwise add the path part normally (might even be .. if there
890 // is nothing more we can cut off).
891 result.Add(part);
892 }
893 }
894
895 // Return the result in windows path mode if the incoming path contained
896 // backslashes, otherwise return it in linux format.
897 return
898 (isNetworkPath
899 ? @"\\"
900 : "") +
901 ArrayHelper.Write(result,
902 filePath.Contains("\\")
903 ? "\\"
904 : "/");
905 }
906 #endregion
907
908 #region GetFileSize (Static)
909 /// <summary>
910 /// Get file size. Returns 0 if file does not exists instead of throwing
911 /// an exception. Else it returns file size as a long number.
912 /// </summary>
913 /// <param name="filename">Filename</param>
914 /// <returns>Long</returns>
915 public static long GetFileSize(string filename)
916 {
917 if (Exists(filename))
918 {
919 using (FileStream fileStream = Open(filename,
920 FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
921 {
922 return fileStream.Length;
923 }
924 }
925
926 return 0;
927 }
928 #endregion
929
930 #region GetLines (Static)
931 /// <summary>
932 /// Returns the text lines we got in a file.
933 /// </summary>
934 /// <param name="filename">Filename</param>
935 /// <returns>
936 /// All text lines from the file or null if no file was found.
937 /// </returns>
938 public static string[] GetLines(string filename)
939 {
940 return GetLines(filename, Encoding.UTF8);
941 }
942
943 /// <summary>
944 /// Returns the text lines we got in a file. Will not crash! If file does
945 /// not exist, we will just return null, same for an unreadable file.
946 /// </summary>
947 /// <param name="filename">Filename</param>
948 /// <param name="textEncoding">Text encoding</param>
949 /// <returns>
950 /// All text lines from the file or null if no file was found.
951 /// </returns>
952 public static string[] GetLines(string filename, Encoding textEncoding)
953 {
954 // If filename is invalid or file does not exist, just abort!
955 if (String.IsNullOrEmpty(filename) ||
956 Exists(filename) == false)
957 {
958 return null;
959 }
960
961 try
962 {
963 List<string> lines = new List<string>();
964
965 using (FileStream stream = Open(filename, FileMode.Open,
966 FileAccess.Read, FileShare.ReadWrite))
967 {
968 StreamReader reader = new StreamReader(stream, textEncoding);
969 do
970 {
971 lines.Add(reader.ReadLine());
972 } while (reader.Peek() >
973 -1);
974 }
975
976 return lines.ToArray();
977 }
978 catch (Exception ex)
979 {
980 Log.Warning(String.Format("Failed to GetLines of file={0}: {1}",
981 filename, ex));
982 // Failed to read, just return null!
983 return null;
984 }
985 }
986 #endregion
987
988 #region GetText (Static)
989 /// <summary>
990 /// Gets all the text of a file (e.g. if you just want to the text of a
991 /// text file). Will not crash! If file does not exist, we will just return
992 /// an empty string. If file is somehow unreadable, we will log an warning
993 /// (because this shouldn't happen, we should always open files in a way
994 /// that allows reading) and return an empty string too.
995 /// </summary>
996 /// <param name="filename">Filename for the file to load</param>
997 /// <returns>String with all the text from the file</returns>
998 public static string GetText(string filename)
999 {
1000 string error;
1001 string text = GetText(filename, out error);
1002
1003 // Log out the error if there was one
1004 if (String.IsNullOrEmpty(error) == false)
1005 {
1006 Log.Warning(error);
1007 }
1008
1009 return text;
1010 }
1011
1012 /// <summary>
1013 /// Gets all the text of a file (e.g. if you just want to the text of a
1014 /// text file). Will not crash! If file does not exist, we will just return
1015 /// an empty string. If file is somehow unreadable, we will log an warning
1016 /// (because this shouldn't happen, we should always open files in a way
1017 /// that allows reading) and return an empty string too.
1018 /// </summary>
1019 /// <param name="filename">Filename for the file to load</param>
1020 /// <returns>String with all the text from the file</returns>
1021 public static string GetText(string filename, out string error)
1022 {
1023 // If filename is invalid or file does not exist, just abort!
1024 if (String.IsNullOrEmpty(filename) ||
1025 Exists(filename) == false)
1026 {
1027 error = "GetText failed to get non existing file: '" + filename + "'";
1028 return "";
1029 }
1030
1031 try
1032 {
1033 using (StreamReader reader =
1034 new StreamReader(OpenFileAsStream(filename), Encoding.UTF8))
1035 {
1036 error = "";
1037 return reader.ReadToEnd();
1038 }
1039 }
1040 catch (Exception ex)
1041 {
1042 error = "Failed to GetText of file='" + filename + "': " + ex.Message;
1043 // Failed to read, just return an empty string!
1044 return "";
1045 }
1046 }
1047 #endregion
1048
1049 #region PathCombine (Static)
1050 /// <summary>
1051 /// Path combining methods to ensure each path is compatible. This is
1052 /// more powerful than Path.Combine and handles all platform cases and
1053 /// should be used if you need your code to run on all platforms.
1054 /// </summary>
1055 /// <param name="path1">Path 1</param>
1056 /// <param name="path2">Path 2</param>
1057 /// <returns>Combined path</returns>
1058 public static string PathCombine(string path1, string path2)
1059 {
1060 // If either one of the two paths are empty/null, return nothing.
1061 if ((path1 == null) ||
1062 (path2 == null))
1063 {
1064 return "";
1065 }
1066
1067 // If either path contains the wrong "slash", revert to the correct one
1068 if ((path1.Contains(@"\")) ||
1069 (path2.Contains(@"\")))
1070 {
1071 path1 = path1.Replace(@"\", "/");
1072 path2 = path2.Replace(@"\", "/");
1073 }
1074
1075 // If either path starts with either a "/" or a ".", remove them
1076 if (path1.StartsWith(@"/") ||
1077 path1.StartsWith("."))
1078 {
1079 path1 = path1.Remove(0, 1);
1080 }
1081 if (path2.StartsWith(@"/") ||
1082 path2.StartsWith("."))
1083 {
1084 path2 = path2.Remove(0, 1);
1085 }
1086
1087 // If either path ends with either a "/" or a ".", remove them.
1088 if (path1.EndsWith(@"/") ||
1089 path1.EndsWith("."))
1090 {
1091 path1 = path1.Substring(0, path1.Length - 1);
1092 }
1093 if (path2.EndsWith(@"/") ||
1094 path2.EndsWith("."))
1095 {
1096 path2 = path2.Substring(0, path2.Length - 1);
1097 }
1098
1099 // Return our combined path with the correct seperator
1100 return (path1 + "/" + path2);
1101 }
1102 #endregion
1103
1104 #region GetLinesCount (Static)
1105 /// <summary>
1106 /// Returns the number of text lines we got in a text file.
1107 /// </summary>
1108 /// <param name="filename">Filename</param>
1109 /// <returns>Number of lines in the given text file.</returns>
1110 public static int GetLinesCount(string filename)
1111 {
1112 int lines = 0;
1113
1114 try
1115 {
1116 using (StreamReader reader = new StreamReader(Open(
1117 filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite),
1118 Encoding.UTF8))
1119 {
1120 do
1121 {
1122 reader.ReadLine();
1123 lines++;
1124 } while (reader.Peek() >
1125 -1);
1126 }
1127 }
1128 catch
1129 {
1130 } // catch
1131
1132 // Failed to read, just return 0 lines!
1133 return lines;
1134 }
1135 #endregion
1136
1137 #region CreateTextFile (Static)
1138 /// <summary>
1139 /// Create text file or overwrite an existing file. Will store all the
1140 /// given text into it with the optional Encoding (default is UTF8).
1141 /// </summary>
1142 /// <param name="filename">Filename to save text to</param>
1143 /// <param name="textForFile">Text to store in the new file</param>
1144 /// <param name="encoding">
1145 /// Encoding to use for file, by default UTF8
1146 /// </param>
1147 /// <exception cref="IOException">
1148 /// Will be thrown if file already exists and could not be overwritten or
1149 /// if creating the file failed for other reasons (e.g. unable to write).
1150 /// </exception>
1151 public static void CreateTextFile(string filename, string textForFile,
1152 Encoding encoding = null)
1153 {
1154 using (TextWriter textWriter = new StreamWriter(Open(
1155 filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite),
1156 encoding ?? Encoding.UTF8))
1157 {
1158 /*this is nice, but slow, it should not be required anymore!
1159 string[] textLines = StringHelper.SplitMultiLineText(textForFile);
1160 foreach (string line in textLines)
1161 {
1162 textWriter.WriteLine(line);
1163 }
1164 */
1165 textWriter.Write(textForFile);
1166 textWriter.Close();
1167 }
1168 }
1169
1170 /// <summary>
1171 /// Create text file. Will create a new file and store all the given text
1172 /// lines into it with the optional Encoding (default is UTF8).
1173 /// </summary>
1174 /// <param name="filename">Filename to save text to</param>
1175 /// <param name="textLines">Text lines to store</param>
1176 /// <param name="encoding">
1177 /// Encoding to use for file, by default UTF8
1178 /// </param>
1179 /// <exception cref="IOException">
1180 /// Will be thrown if file already exists and could not be overwritten or
1181 /// if creating the file failed for other reasons (e.g. unable to write).
1182 /// </exception>
1183 public static void CreateTextFile(string filename, string[] textLines,
1184 Encoding encoding = null)
1185 {
1186 using (StreamWriter textWriter = new StreamWriter(Open(
1187 filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite),
1188 encoding ?? Encoding.UTF8))
1189 {
1190 foreach (string line in textLines)
1191 {
1192 textWriter.WriteLine(line);
1193 }
1194 textWriter.Close();
1195 }
1196 }
1197 #endregion
1198
1199 #region CreateBinaryFile (Static)
1200 /// <summary>
1201 /// Helper method to create a binary file with the given byte data array.
1202 /// /// <para />
1203 /// Note: If the file exists, it will just be overwritten. The path to
1204 /// the filename must exist however, else this will throw an exception.
1205 /// Also if writing this file is not possible an access denied IOException
1206 /// will be thrown.
1207 /// </summary>
1208 /// <param name="filename">Filename to save binary data to</param>
1209 /// <param name="binaryData">The data to save</param>
1210 public static void CreateBinaryFile(string filename, byte[] binaryData)
1211 {
1212 using (FileStream fileStream = Open(
1213 filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
1214 {
1215 fileStream.Write(binaryData, 0, binaryData.Length);
1216 }
1217 }
1218
1219 /// <summary>
1220 /// Helper method to create a binary file with the given memory stream.
1221 /// <para />
1222 /// Note: If the file exists, it will just be overwritten. The path to
1223 /// the filename must exist however, else this will throw an exception.
1224 /// Also if writing this file is not possible an access denied IOException
1225 /// will be thrown.
1226 /// </summary>
1227 /// <param name="filename">Filename to save binary data to</param>
1228 /// <param name="data">The data to save</param>
1229 public static void CreateBinaryFile(string filename, MemoryStream data)
1230 {
1231 using (FileStream fileStream = Open(
1232 filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
1233 {
1234 fileStream.Write(data.ToArray(), 0, (int)data.Length);
1235 }
1236 }
1237 #endregion
1238
1239 #region GetBytes (Static)
1240 /// <summary>
1241 /// Helper method to grab all bytes of a binary file and then close it
1242 /// again. Will not crash! If file does not exist, we will just return
1243 /// an empty array. If file is somehow unreadable, we will log an warning
1244 /// (because this shouldn't happen, we should always open files in a way
1245 /// that allows reading) and return an empty array too.
1246 /// </summary>
1247 /// <param name="filename">File to load</param>
1248 /// <returns>Array of bytes from the file</returns>
1249 public static byte[] GetBytes(string filename)
1250 {
1251 // If filename is invalid or file does not exist, just abort!
1252 if (String.IsNullOrEmpty(filename) ||
1253 Exists(filename) == false)
1254 {
1255 Log.Warning("GetBytes failed to get non existing file: " + filename);
1256 return new byte[0];
1257 }
1258
1259 try
1260 {
1261 using (Stream fileStream = OpenFileAsStream(filename))
1262 {
1263 byte[] bytes = new byte[fileStream.Length];
1264 fileStream.Read(bytes, 0, bytes.Length);
1265 return bytes;
1266 }
1267 }
1268 catch (Exception ex)
1269 {
1270 Log.Warning("Failed to GetBytes of file '" + filename + "': " + ex);
1271 // Failed to read, just return an empty array!
1272 return new byte[0];
1273 }
1274 }
1275 #endregion
1276
1277 #region SafeDelete (Static)
1278 /// <summary>
1279 /// Safely deletes a file.
1280 /// </summary>
1281 /// <param name="filePath">File to delete</param>
1282 /// <returns>True if file was deleted or did not exist anymore</returns>
1283 public static bool SafeDelete(string filePath)
1284 {
1285 if (String.IsNullOrEmpty(filePath) ||
1286 Exists(filePath) == false)
1287 {
1288 // File did not exist, so no deletion was required
1289 // (return true to mark that this file is gone now because it
1290 // was already gone).
1291 return true;
1292 }
1293
1294 try
1295 {
1296 DeleteCallback(filePath);
1297 return true;
1298 }
1299 catch (IOException ioEx)
1300 {
1301 if (ioEx.Message.Contains("The process cannot access the file"))
1302 {
1303 // Try to wait a short while, maybe it is free soon
1304 Thread.Sleep(50);
1305
1306 // And try to delete it again
1307 try
1308 {
1309 DeleteCallback(filePath);
1310 // Everything is fine now, quit without logging an error
1311 return true;
1312 }
1313 catch (Exception ex)
1314 {
1315 Log.Warning("Couldn't safely delete file even with waiting '" +
1316 filePath + "' because of: " + ex.Message);
1317 }
1318 }
1319 }
1320 catch (Exception ex)
1321 {
1322 Log.Warning("Couldn't safely delete file '" + filePath +
1323 "' because of: " + ex.Message);
1324 }
1325
1326 // File was not deleted
1327 return false;
1328 }
1329 #endregion
1330
1331 #region SafeDeleteFiles (Static)
1332 /// <summary>
1333 /// Safely delete the files matching the search pattern in the base
1334 /// directory and if wanted recursive in sub directories as well.
1335 /// </summary>
1336 /// <param name="basePath">Base Path</param>
1337 /// <param name="recursive">Recursive</param>
1338 /// <param name="searchPattern">Search Pattern</param>
1339 /// <returns>True if deleting the files worked (no matter if it actually
1340 /// deleted something or not), false otherwise (warnings for failed file
1341 /// deletes will be outputted via the SafeDelete method)</returns>
1342 public static bool SafeDeleteFiles(string basePath, string searchPattern,
1343 bool recursive)
1344 {
1345 if (DirectoryHelper.Exists(basePath) == false)
1346 {
1347 return true;
1348 }
1349
1350 string[] files = recursive
1351 ? DirectoryHelper.GetFilesRecursive(basePath, searchPattern)
1352 : DirectoryHelper.GetFiles(basePath, searchPattern);
1353
1354 foreach (string file in files)
1355 {
1356 SafeDelete(file);
1357 }
1358
1359 return true;
1360 }
1361 #endregion
1362
1363 #region CheckIfFileIsNewer (Static)
1364 /// <summary>
1365 /// Check if file is newer. Not the fastest method ever as it needs
1366 /// to load both files and compare their last write times.
1367 /// </summary>
1368 /// <param name="fileToCheckIfNewer">File to check if newer</param>
1369 /// <param name="originalFile">Original File</param>
1370 /// <returns>True if the file is newer than the original.</returns>
1371 public static bool CheckIfFileIsNewer(string fileToCheckIfNewer,
1372 string originalFile)
1373 {
1374 // Got no file to check, then we can't compare and should not copy.
1375 if (Exists(fileToCheckIfNewer) == false)
1376 {
1377 return false;
1378 }
1379 // For no original file, the new file is obviously newer than nothing!
1380 if (Exists(originalFile) == false)
1381 {
1382 return true;
1383 }
1384
1385 return FileIsNewerCallback(fileToCheckIfNewer, originalFile);
1386 }
1387 #endregion
1388
1389 #region ExistsPath (Static)
1390 /// <summary>
1391 /// Exist path, will check if a full file path or just a directory path
1392 /// exists. This is different from the DirectoryHelper.Exists method,
1393 /// which just checks if a directory path exists (this one checks for
1394 /// files too if a valid extension for a file path was given).
1395 /// </summary>
1396 /// <param name="path">A path from a file or a directory</param>
1397 /// <returns>True if the path was found as a file or directory</returns>
1398 public static bool ExistsPath(string path)
1399 {
1400 if (Path.HasExtension(path))
1401 {
1402 return Exists(path);
1403 }
1404 return DirectoryHelper.Exists(path);
1405 }
1406 #endregion
1407
1408 #region CutPath (Static)
1409 /// <summary>
1410 /// Cuts off the given "cut path" completely from the given filePath.
1411 /// </summary>
1412 /// <param name="filePath">
1413 /// Any absolute or relative file or folder path.
1414 /// </param>
1415 /// <param name="pathToCutOff">
1416 /// The path (or folder) which should be cut off completely.
1417 /// </param>
1418 /// <returns>FilePath without pathToCutOff</returns>
1419 public static string CutPath(string filePath, string pathToCutOff)
1420 {
1421 #region Validation
1422 if (String.IsNullOrEmpty(filePath))
1423 {
1424 return "";
1425 }
1426 #endregion
1427
1428 // IF we have a valid path which should be cut off
1429 if (String.IsNullOrEmpty(pathToCutOff) == false)
1430 {
1431 // then try to find that (sub)path or folder in the given file path
1432 int cutOffIndex = filePath.IndexOf(pathToCutOff);
1433 // If it really exists in the file path
1434 if (cutOffIndex != MathHelper.InvalidIndex)
1435 {
1436 cutOffIndex += pathToCutOff.Length;
1437 // then just check if the "cut off path" ends already with an "\"
1438 if (pathToCutOff[pathToCutOff.Length - 1] != '\\')
1439 {
1440 // because if not then we have to increase the index by that
1441 cutOffIndex++;
1442 }
1443
1444 // before we can finally cut off the subpath, but only if we
1445 // don't will cut off a filename
1446 return (filePath[cutOffIndex - 1] != '.')
1447 ? filePath.Substring(cutOffIndex)
1448 : filePath;
1449 }
1450 }
1451
1452 return filePath;
1453 }
1454 #endregion
1455
1456 #region CompareFiles (Static)
1457 /// <summary>
1458 /// Compare two files with the help of the MD5-checksum
1459 /// </summary>
1460 /// <param name="fileName1">Filename 1</param>
1461 /// <param name="fileName2">Filename 2</param>
1462 /// <returns>True if both file contents are the same</returns>
1463 public static bool CompareFiles(string fileName1, string fileName2)
1464 {
1465 return CompareFilesCallback(fileName1, fileName2);
1466 }
1467 #endregion
1468
1469 #region AppendTextToFile (Static)
1470 /// <summary>
1471 /// Append text to file, does the same as File.AppendAllText, but it
1472 /// won't crash and can handle already open files :)
1473 /// </summary>
1474 /// <param name="filename">Filename</param>
1475 /// <param name="text">Text</param>
1476 public static void AppendTextToFile(string filename, string text)
1477 {
1478 try
1479 {
1480 using (StreamWriter writer = new StreamWriter(Open(
1481 filename, FileMode.OpenOrCreate, FileAccess.Write,
1482 FileShare.ReadWrite), Encoding.UTF8))
1483 {
1484 // Go to end of file
1485 writer.BaseStream.Seek(0, SeekOrigin.End);
1486 // And write our new text
1487 writer.Write(text);
1488 }
1489 }
1490 catch (Exception ex)
1491 {
1492 Log.Warning("Failed to AppendTextToFile of file " + filename + ": " +
1493 ex);
1494 }
1495 }
1496
1497 /// <summary>
1498 /// Append text to file, does the same as File.AppendAllText, but it
1499 /// won't crash and can handle already open files :) This overload checks
1500 /// if the existing data is more than maxTextFileSizeBeforeAppending
1501 /// already, then remove data from the beginning before appending.
1502 /// </summary>
1503 /// <param name="filename">Filename</param>
1504 /// <param name="maxTextFileSizeBeforeAppending">Max Text File Size before
1505 /// appending</param>
1506 /// <param name="text">Text</param>
1507 public static void AppendTextToFile(string filename, string text,
1508 int maxTextFileSizeBeforeAppending)
1509 {
1510 //this suxx: File.AppendAllText(filename, text, UTF8);
1511 try
1512 {
1513 FileStream stream = Open(filename, FileMode.OpenOrCreate,
1514 FileAccess.ReadWrite, FileShare.ReadWrite);
1515 // File is too big, remove data at the beginning (happens rarely).
1516 if (stream.Length > maxTextFileSizeBeforeAppending)
1517 {
1518 stream.Close();
1519 // Just read it all into lines and build it up again from bottom up!
1520 string[] oldLines = GetLines(filename, Encoding.UTF8);
1521 // Always grab the first 100 lines and keep them!
1522 List<string> newLines = new List<string>();
1523 int newSize = 0;
1524 for (int line = 0; line < 100 && line < oldLines.Length; line++)
1525 {
1526 string newLine = oldLines[line];
1527 newLines.Add(newLine);
1528 newSize += newLine.Length;
1529 }
1530 newLines.Add(
1531 "\n" +
1532 "****************************\n" +
1533 "This text file was exceeding " +
1534 StringHelper.WriteKbMbGbNumber(
1535 maxTextFileSizeBeforeAppending) + ", old data will be " +
1536 "removed from the beginning.\nSkipped the first 100 lines and " +
1537 "the new text is appended at the end as usually.\n" +
1538 "****************************\n");
1539 // Next fill up the lines until we reach the max size.
1540 int insertLineNum = newLines.Count;
1541 for (int line = oldLines.Length - 1; line >= 100; line--)
1542 {
1543 string newLine = oldLines[line];
1544 // Always insert at 101 (100 presevered lines + our message),
1545 // this way we go bottom up
1546 newLines.Insert(insertLineNum, newLine);
1547 newSize += newLine.Length;
1548 // Abort if we reach the max size / 2 (this way we don't have to
1549 // do this costly operation too often).
1550 if (newSize > maxTextFileSizeBeforeAppending / 2)
1551 {
1552 break;
1553 }
1554 }
1555
1556 // And finally create the new file and get another stream handle
1557 CreateTextFile(filename, newLines.ToArray(),
1558 Encoding.UTF8);
1559 stream = Open(filename, FileMode.OpenOrCreate,
1560 FileAccess.ReadWrite, FileShare.ReadWrite);
1561 }
1562
1563 using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8))
1564 {
1565 // Go to end of file
1566 writer.BaseStream.Seek(0, SeekOrigin.End);
1567 // And write our new text
1568 writer.Write(text);
1569 }
1570 }
1571 catch (Exception ex)
1572 {
1573 Log.Warning("Failed to AppendTextToFile of file " + filename + ": " +
1574 ex);
1575 }
1576 }
1577 #endregion
1578
1579 #region CheckIfFileExistsAndIsNotWrittenTo (Static)
1580 /// <summary>
1581 /// Check if file exists and is not written to recently. This is mostly
1582 /// used for update checks to see if we can assume this file is complete
1583 /// and we can now open it, etc. (e.g. used in the RestartTool).
1584 /// </summary>
1585 /// <param name="filename">File to check</param>
1586 /// <returns>
1587 /// True if the file exists and was not written to recently.
1588 /// </returns>
1589 public static bool CheckIfFileExistsAndIsNotWrittenTo(string filename)
1590 {
1591 // First of all, make sure this file exists at all
1592 if (Exists(filename) == false)
1593 {
1594 return false;
1595 }
1596
1597 return FileNotWrittenToCallback(filename);
1598 }
1599 #endregion
1600
1601 #region OpenFileAsStream (Static)
1602 /// <summary>
1603 /// Open file as stream. Make sure the filePath is in the correct format
1604 /// for this platform!
1605 /// </summary>
1606 /// <param name="filePath">File path</param>
1607 /// <returns>Stream of the opened file for reading</returns>
1608 public static Stream OpenFileAsStream(string filePath)
1609 {
1610
1611 return Open(filePath, FileMode.Open, FileAccess.Read,
1612 FileShare.ReadWrite);
1613 }
1614 #endregion
1615
1616 #region Load (Static)
1617 /// <summary>
1618 /// Helper method to load data into any ISaveLoadBinary object just with
1619 /// a given filePath. This helps reducing writing the same code over and
1620 /// over again in each content class or ISaveLoadBinary data class.
1621 /// </summary>
1622 /// <typeparam name="T">Type to load into, must be derived from
1623 /// ISaveLoadBinary</typeparam>
1624 /// <param name="filePath">File path to load the binary data from</param>
1625 /// <param name="objectToLoad">Object to load all binary data into</param>
1626 public static void Load<T>(string filePath, T objectToLoad)
1627 where T : ISaveLoadBinary
1628 {
1629 using (Stream file = Open(filePath, FileMode.Open,
1630 FileAccess.Read, FileShare.Read))
1631 {
1632 objectToLoad.Load(new BinaryReader(file));
1633 }
1634 }
1635 #endregion
1636
1637 #region Save (Static)
1638 /// <summary>
1639 /// Helper method to save data from any ISaveLoadBinary object into a
1640 /// given filePath. This helps reducing writing the same code over and
1641 /// over again in each content class or ISaveLoadBinary data class.
1642 /// </summary>
1643 /// <typeparam name="T">Type to save into, must be derived from
1644 /// ISaveLoadBinary</typeparam>
1645 /// <param name="filePath">File path to save the binary data to</param>
1646 /// <param name="objectToSave">Object we want to save</param>
1647 public static void Save<T>(string filePath, T objectToSave)
1648 where T : ISaveLoadBinary
1649 {
1650 using (Stream file = Open(filePath, FileMode.Create,
1651 FileAccess.Write, FileShare.Read))
1652 {
1653 objectToSave.Save(new BinaryWriter(file));
1654 }
1655 }
1656 #endregion
1657
1658 #region GetFileDate (Static)
1659 /// <summary>
1660 /// Get file date via File.GetLastWriteTime if supported by this platform!
1661 /// </summary>
1662 /// <param name="filePath">File path to check</param>
1663 /// <returns>Last time this file was written to.</returns>
1664 public static DateTime GetFileDate(string filePath)
1665 {
1666 return File.GetLastWriteTime(filePath);
1667 }
1668 #endregion
1669
1670 #region Internal
1671
1672 #region ExistsCallback (Internal)
1673 internal static ExistsDelegate ExistsCallback;
1674 #endregion
1675
1676 #region CopyCallback (Internal)
1677 internal static CopyDelegate CopyCallback;
1678 #endregion
1679
1680 #region MoveCallback (Internal)
1681 internal static MoveDelegate MoveCallback;
1682 #endregion
1683
1684 #region OpenCallback (Internal)
1685 internal static OpenDelegate OpenCallback;
1686 #endregion
1687
1688 #region DeleteCallback (Internal)
1689 internal static DeleteDelegate DeleteCallback;
1690 #endregion
1691
1692 #region FileIsNewerCallback (Internal)
1693 internal static FileIsNewerDelegate FileIsNewerCallback;
1694 #endregion
1695
1696 #region CompareFilesCallback (Internal)
1697 /// <summary>
1698 /// Callback for comparing files. Some platforms won't support this as
1699 /// efficiently as others. Normally MD5 Checksums are used.
1700 /// </summary>
1701 internal static CompareFilesDelegate CompareFilesCallback;
1702 #endregion
1703
1704 #region FileNotWrittenToCallback (Internal)
1705 /// <summary>
1706 /// Delegate for CheckIfFileExistsAndIsNotWrittenTo.
1707 /// </summary>
1708 internal static FileNotWrittenToDelegate FileNotWrittenToCallback;
1709 #endregion
1710
1711 #endregion
1712
1713 #region Constructors
1714 /// <summary>
1715 /// Static FileHelper constructor to setup all the callbacks used here!
1716 /// </summary>
1717 static FileHelper()
1718 {
1719 ExistsCallback = File.Exists;
1720 MoveCallback = File.Move;
1721 OpenCallback = File.Open;
1722 DeleteCallback = File.Delete;
1723
1724 CopyCallback = delegate(string sourceFile, string destinationFile)
1725 {
1726 File.Copy(sourceFile, destinationFile, true);
1727 };
1728
1729 CompareFilesCallback = delegate(string fileName1, string fileName2)
1730 {
1731 MD5 md5 = new MD5CryptoServiceProvider();
1732
1733 byte[] md5Hash;
1734 // Compute checksum 1
1735 using (FileStream fileCheck = File.OpenRead(fileName1))
1736 {
1737 md5Hash = md5.ComputeHash(fileCheck);
1738 }
1739
1740 byte[] md5Hash2;
1741 // Now compute checksum 2
1742 using (FileStream fileCheck2 = File.OpenRead(fileName2))
1743 {
1744 md5Hash2 = md5.ComputeHash(fileCheck2);
1745 }
1746
1747 string checksum1 =
1748 BitConverter.ToString(md5Hash).Replace("-", "").ToLower();
1749 string checksum2 =
1750 BitConverter.ToString(md5Hash2).Replace("-", "").ToLower();
1751
1752 // Compare checksums
1753 return checksum1 == checksum2;
1754 };
1755
1756 FileIsNewerCallback = delegate(string fileToCheckIfNewer,
1757 string originalFile)
1758 {
1759 return
1760 File.GetLastWriteTime(fileToCheckIfNewer) >
1761 File.GetLastWriteTime(originalFile);
1762 };
1763
1764 FileNotWrittenToCallback = delegate(string filename)
1765 {
1766 DateTime now = DateTime.Now;
1767 // Next make sure this file is really written and completed
1768 // If there is still an FTP upload happening, we have to wait!
1769 if ((now - File.GetLastAccessTime(filename)).TotalSeconds < 1 ||
1770 (now - File.GetLastWriteTime(filename)).TotalSeconds < 1)
1771 {
1772 // Wait a little longer, <1s since last write action is pretty short
1773 return false;
1774 }
1775
1776 // And finally check if we can write to the file. If another process
1777 // is writing to it, it wont allow us to write!
1778 try
1779 {
1780 using (FileStream testFile = Open(filename, FileMode.Open,
1781 FileAccess.ReadWrite, FileShare.ReadWrite))
1782 {
1783 // Nothing to do here, close the file again!
1784 }
1785 return true;
1786 }
1787 catch
1788 {
1789 // That failed, this file is still open in FTP or some program ..
1790 return false;
1791 }
1792 };
1793 }
1794 #endregion
1795
1796 /// <summary>
1797 /// Test file helper
1798 /// </summary>
1799 [Category("LongRunning")]
1800 internal class FileHelperTests
1801 {
1802 #region TestCreateTextFileAndSafeDelete (Static)
1803 /// <summary>
1804 /// Test create text file and safe delete
1805 /// </summary>
1806 [Test]
1807 public static void TestCreateTextFileAndSafeDelete()
1808 {
1809 const string TestFilename = "TestTextFile.txt";
1810 const string SomeText = "Just some text for the test file ...";
1811
1812 try
1813 {
1814 if (File.Exists(TestFilename))
1815 {
1816 SafeDelete(TestFilename);
1817 //throw new Exception("Unable to execute this unit test if " +
1818 // TestFilename + " does already exists!");
1819 }
1820
1821 CreateTextFile(TestFilename, SomeText);
1822 // File contains now SomeText and "\n\r\0" characters.
1823 // Check if file size matches.
1824 Assert.Equal(SomeText.Length + 3,
1825 GetFileSize(TestFilename));
1826 }
1827 finally
1828 {
1829 // And now delete it safely again
1830 SafeDelete(TestFilename);
1831 // And make sure its gone which may take some millisecs on slow
1832 // hardware!
1833 Thread.Sleep(10);
1834 // There should be nothing left now
1835 Assert.False(File.Exists(TestFilename), "File exists");
1836 }
1837 }
1838 #endregion
1839
1840 #region TestCompareFiles (Static)
1841 /// <summary>
1842 /// Test check if file is newer. Note: Too slow for a dynamic unit test.
1843 /// </summary>
1844 [Test]
1845 public static void TestCompareFiles()
1846 {
1847 CreateTextFile("test1.txt", "Hello world!");
1848 CreateTextFile("test2.txt", "Hello!");
1849
1850 Assert.True(CompareFiles("test1.txt", "test1.txt"));
1851 Assert.False(CompareFiles("test1.txt", "test2.txt"));
1852
1853 SafeDelete("test1.txt");
1854 SafeDelete("test2.txt");
1855 }
1856 #endregion
1857
1858 #region TestPathCombine (Static)
1859 /// <summary>
1860 /// Test to see if our path combine methods correctly output the right path
1861 /// </summary>
1862 [Test]
1863 public static void TestPathCombine()
1864 {
1865 Assert.Equal(PathCombine("path1", "path2"), "path1/path2");
1866 Assert.Equal(PathCombine("path1", "/path2"), "path1/path2");
1867 Assert.Equal(PathCombine(@"path1\", "/path2"), "path1/path2");
1868 Assert.Equal(PathCombine(@"path1\", @"\path2"), "path1/path2");
1869 Assert.Equal(PathCombine(".path1/", "/path2."), "path1/path2");
1870 Assert.Equal(PathCombine(@"C:\path1", "/path2"), "C:/path1/path2");
1871 }
1872 #endregion
1873
1874 #region OpenFileAsStream (Static)
1875 /// <summary>
1876 /// Test OpenFileAsStream
1877 /// </summary>
1878 [Test]
1879 public static void OpenFileAsStream()
1880 {
1881 const string filepath = @"C:\Windows\winhlp32.exe";
1882 Stream stream = FileHelper.OpenFileAsStream(filepath);
1883 Assert.Equal(GetFileSize(filepath), stream.Length);
1884 stream.Close();
1885 stream = null;
1886 }
1887 #endregion
1888
1889 #region SafeDeleteFiles (Static)
1890 /// <summary>
1891 /// Test SafeDeleteFiles with some xml documentation files in the
1892 /// current directory.
1893 /// </summary>
1894 [Test]
1895 public static void SafeDeleteFiles()
1896 {
1897 FileHelper.SafeDeleteFiles(
1898 DirectoryHelper.GetCurrentDirectory(), "Delta.*.xml", true);
1899 }
1900 #endregion
1901
1902 #region TrimPath (Static)
1903 /// <summary>
1904 /// Trim path
1905 /// </summary>
1906 [Test]
1907 public static void TrimPath()
1908 {
1909 // Existing file path
1910 Assert.Equal(FileHelper.TrimPath(
1911 @"C:\Windows\Temp\..\System32\notepad.exe"),
1912 @"C:\Windows\System32\notepad.exe");
1913
1914 // Fictional absolute file path
1915 Assert.Equal(FileHelper.TrimPath(
1916 @"C:\Path\SubPath1\..\SubPath2\file.exe"),
1917 @"C:\Path\SubPath2\file.exe");
1918
1919 // Fictional relative file path
1920 Assert.Equal(FileHelper.TrimPath(
1921 @"SubPath1\..\SubPath2\file.exe"),
1922 @"SubPath2\file.exe");
1923
1924 // Test with network path
1925 Assert.Equal(FileHelper.TrimPath(
1926 @"\\blub\Bla\Honk\..\..\ContentSystem\Delta.ContentSystem.csproj"),
1927 @"\\blub\ContentSystem\Delta.ContentSystem.csproj");
1928
1929 // Test unix path and also add a trailing slash into the mix!
1930 Assert.Equal(FileHelper.TrimPath(
1931 "/SomePath/../SomeFile.xml"),
1932 "SomeFile.xml");
1933
1934 // Trying to trim a part away without providing a full path should not
1935 // kill the relative structure. We just want to trim after all.
1936 Assert.Equal(FileHelper.TrimPath(
1937 @"..\..\..\file.exe"),
1938 @"..\..\..\file.exe");
1939
1940 // Going all the way down 4 levels.
1941 Assert.Equal(FileHelper.TrimPath(
1942 @"C:\SomePath\AnotherPath\ThirdPath\ForthPath\..\..\..\..\file.exe"),
1943 @"C:\file.exe");
1944
1945 // Do the same with a relative path, we should not get an absolute path
1946 // back. Note: Very long path to test if converting the absolute path
1947 // to a relative one actually works.
1948 Assert.Equal(FileHelper.TrimPath(
1949 @"SomePath\AnotherPath\ThirdParth\ForthPath\..\..\..\..\file.exe"),
1950 @"file.exe");
1951 }
1952 #endregion
1953
1954 #region CleanupWindowsPath
1955 [Test]
1956 public static void CleanupWindowsPath()
1957 {
1958 // Test windows paths
1959 Assert.Equal(FileHelper.CleanupWindowsPath(
1960 @"C:\Windows\System32\notepad.exe"),
1961 @"C:\Windows\System32\notepad.exe");
1962 Assert.Equal(FileHelper.CleanupWindowsPath(
1963 @"SomePath/SomeFile.xml"),
1964 @"SomePath\SomeFile.xml");
1965 }
1966 #endregion
1967
1968 #region CleanupLinuxPath
1969 [Test]
1970 public static void CleanupLinuxPath()
1971 {
1972 // And do the same with Linux paths
1973 Assert.Equal(FileHelper.CleanupLinuxPath(
1974 @"C:\Windows\System32\notepad.exe"),
1975 @"C:/Windows/System32/notepad.exe");
1976 Assert.Equal(FileHelper.CleanupLinuxPath(
1977 @"SomePath/SomeFile.xml"),
1978 @"SomePath/SomeFile.xml");
1979 }
1980 #endregion
1981
1982 #region FileExists (Static)
1983 /// <summary>
1984 /// Test the File Exists method.
1985 /// </summary>
1986 [Test]
1987 public static void FileExists()
1988 {
1989 Assert.True(Exists("C:\\Windows\\regedit.exe"));
1990 Assert.False(Exists("C:\\awdawdok.test"));
1991 }
1992 #endregion
1993
1994 #region TestCutAndGetExtension
1995 /// <summary>
1996 /// Test cut and get extension
1997 /// </summary>
1998 [Test]
1999 public void TestCutAndGetExtension()
2000 {
2001 // Test cut extension, it should only remove extensions, nothing more.
2002 Assert.Equal("hi", CutExtension("hi.txt"));
2003 Assert.Equal("hi", CutExtension("hi"));
2004 Assert.Equal("SomeApplication.exe",
2005 CutExtension("SomeApplication.exe.config"));
2006 Assert.Equal(@"..\..\Yaya\Honk",
2007 CutExtension(@"..\..\Yaya\Honk.exe"));
2008 Assert.Equal(@"..\..\Yaya\Honk",
2009 CutExtension(@"..\..\Yaya\Honk"));
2010
2011 // Test get extension, should return the extension the file has.
2012 // Should work the same way as CutExtension.
2013 Assert.Equal("txt", GetExtension("hi.txt"));
2014 Assert.Equal("", GetExtension("hi"));
2015 Assert.Equal("config",
2016 GetExtension("SomeApplication.exe.config"));
2017 Assert.Equal("exe", GetExtension(@"..\..\Yaya\Honk.exe"));
2018 }
2019 #endregion
2020
2021 #region GetFilename
2022 /// <summary>
2023 /// Get filename
2024 /// </summary>
2025 [Test]
2026 public void GetFilename()
2027 {
2028 Assert.Equal("File.tst",
2029 FileHelper.GetFilename(@"Path\SubPath\File.tst"));
2030
2031 Assert.Equal("File.tst",
2032 FileHelper.GetFilename(@"Path\SubPath/File.tst"));
2033
2034 Assert.Equal("File",
2035 FileHelper.GetFilename(@"Path\SubPath\File.tst", true));
2036
2037 string pathOnly;
2038 Assert.Equal("File.tst",
2039 FileHelper.GetFilename(@"Path\SubPath\File.tst", out pathOnly));
2040 Assert.Equal(@"Path\SubPath", pathOnly);
2041 }
2042 #endregion
2043
2044 #region IsFilenameOnly
2045 /// <summary>
2046 /// Is filename only
2047 /// </summary>
2048 [Test]
2049 public void IsFilenameOnly()
2050 {
2051 Assert.True(FileHelper.IsFilenameOnly("File123.tst"));
2052 Assert.False(FileHelper.IsFilenameOnly("Folder"));
2053 }
2054 #endregion
2055
2056 #region TestCheckIfFileIsNewer
2057 /// <summary>
2058 /// Test check if file is newer
2059 /// </summary>
2060 [Test]
2061 public void TestCheckIfFileIsNewer()
2062 {
2063 //does not work this way, a full rebuild changes the dates around :(
2064 //// Delta.Utilities.Helpers.dll is the newest file of all, because we
2065 //// just compiled it to start this test. Delta.Utilities.Testing.dll
2066 //// doesn't change that often (and even if, it was compiled before).
2067 //Assert.True(FileHelper.CheckIfFileIsNewer(
2068 // "Delta.Utilities.Helpers.dll", "Delta.Utilities.Testing.dll"));
2069 //Assert.False(FileHelper.CheckIfFileIsNewer(
2070 // "Delta.Utilities.Testing.dll", "Delta.U