1212using System . Collections . Generic ;
1313using System . IO ;
1414using System . Linq ;
15+ using System . Runtime . InteropServices ;
1516using System . Security . Cryptography ;
1617using System . Text ;
1718using System . Threading ;
@@ -33,10 +34,13 @@ internal static class Global
3334 public static List < string > ExcludedExtensions = new List < string > ( ) { "*~" , "tmp" } ;
3435 public static List < string > IgnorePathsStartingWith = new List < string > ( ) ;
3536 public static List < string > IgnorePathsContaining = new List < string > ( ) ;
36-
37+
3738 public static string AsyncPath = "" ;
3839 public static string SyncPath = "" ;
3940
41+ public static long AsyncPathMinFreeSpace = 0 ;
42+ public static long SyncPathMinFreeSpace = 0 ;
43+
4044 public static bool Bidirectional = true ;
4145
4246
@@ -114,6 +118,9 @@ private static void Main()
114118 Global . AsyncPath = fileConfig . GetTextUpperOnWindows ( "AsyncPath" ) ;
115119 Global . SyncPath = fileConfig . GetTextUpperOnWindows ( "SyncPath" ) ;
116120
121+ Global . AsyncPathMinFreeSpace = fileConfig . GetLong ( "AsyncPathMinFreeSpace" ) ?? 0 ;
122+ Global . SyncPathMinFreeSpace = fileConfig . GetLong ( "SyncPathMinFreeSpace" ) ?? 0 ;
123+
117124 Global . WatchedCodeExtension = fileConfig . GetListUpperOnWindows ( "WatchedCodeExtensions" , "WatchedCodeExtension" ) ;
118125 Global . WatchedResXExtension = fileConfig . GetListUpperOnWindows ( "WatchedResXExtensions" , "WatchedResXExtension" ) ;
119126
@@ -180,7 +187,8 @@ private static async Task MainTask()
180187
181188 var messageContext = new Context (
182189 eventObj : null ,
183- token : new CancellationToken ( )
190+ token : new CancellationToken ( ) ,
191+ isSyncPath : false //unused here
184192 ) ;
185193
186194
@@ -333,8 +341,9 @@ private static void WaitForCtrlC()
333341
334342 internal class Context
335343 {
336- public IFileSystemEvent Event ;
337- public CancellationToken Token ;
344+ public readonly IFileSystemEvent Event ;
345+ public readonly CancellationToken Token ;
346+ public readonly bool IsSyncPath ;
338347
339348 public DateTime Time
340349 {
@@ -344,10 +353,13 @@ public DateTime Time
344353 }
345354 }
346355
347- public Context ( IFileSystemEvent eventObj , CancellationToken token )
356+ #pragma warning disable CA1068 //should take CancellationToken as the last parameter
357+ public Context ( IFileSystemEvent eventObj , CancellationToken token , bool isSyncPath )
358+ #pragma warning restore CA1068
348359 {
349360 Event = eventObj ;
350361 Token = token ;
362+ IsSyncPath = isSyncPath ;
351363 }
352364 }
353365
@@ -372,7 +384,9 @@ internal class ConsoleWatch
372384 private static readonly AsyncLockQueueDictionary FileEventLocks = new AsyncLockQueueDictionary ( ) ;
373385
374386
387+ #pragma warning disable S1118 //Warning S1118 Hide this public constructor by making it 'protected'.
375388 public ConsoleWatch ( IWatcher3 watch )
389+ #pragma warning restore S1118
376390 {
377391 //_consoleColor = Console.ForegroundColor;
378392
@@ -411,6 +425,11 @@ public static async Task WriteException(Exception ex, Context context)
411425 await AddMessage ( ConsoleColor . Red , message . ToString ( ) , context ) ;
412426 }
413427
428+ public static bool IsSyncPath ( string fullNameInvariant )
429+ {
430+ return fullNameInvariant . StartsWith ( Global . SyncPath ) ;
431+ }
432+
414433 public static string GetNonFullName ( string fullName )
415434 {
416435 var fullNameInvariant = fullName . ToUpperInvariantOnWindows ( ) ;
@@ -419,7 +438,7 @@ public static string GetNonFullName(string fullName)
419438 {
420439 return fullName . Substring ( Global . AsyncPath . Length ) ;
421440 }
422- else if ( fullNameInvariant . StartsWith ( Global . SyncPath ) )
441+ else if ( IsSyncPath ( fullNameInvariant ) )
423442 {
424443 return fullName . Substring ( Global . SyncPath . Length ) ;
425444 }
@@ -438,7 +457,7 @@ public static string GetOtherFullName(string fullName)
438457 {
439458 return Path . Combine ( Global . SyncPath , nonFullName ) ;
440459 }
441- else if ( fullNameInvariant . StartsWith ( Global . SyncPath ) )
460+ else if ( IsSyncPath ( fullNameInvariant ) )
442461 {
443462 return Path . Combine ( Global . AsyncPath , nonFullName ) ;
444463 }
@@ -546,7 +565,7 @@ public static async Task FileUpdated(string fullName, Context context)
546565 {
547566 await AsyncToSyncConverter . AsyncFileUpdated ( fullName , context ) ;
548567 }
549- else if ( fullNameInvariant . StartsWith ( Global . SyncPath ) ) //NB!
568+ else if ( IsSyncPath ( fullNameInvariant ) ) //NB!
550569 {
551570 await SyncToAsyncConverter . SyncFileUpdated ( fullName , context ) ;
552571 }
@@ -611,7 +630,7 @@ private static bool IsWatchedFile(string fullName)
611630 || Global . WatchedCodeExtension . Contains ( "*" )
612631 || Global . WatchedResXExtension . Contains ( "*" )
613632 )
614- &&
633+ &&
615634 Global . ExcludedExtensions . All ( x =>
616635
617636 ! fullNameInvariant . EndsWith ( "." + x )
@@ -644,44 +663,78 @@ private static bool IsWatchedFile(string fullName)
644663#pragma warning disable AsyncFixer01
645664 private static async Task OnRenamedAsync ( IRenamedFileSystemEvent fse , CancellationToken token )
646665 {
647- var context = new Context ( fse , token ) ;
666+ //NB! create separate context to properly handle disk free space checks on cases where file is renamed from src path to dest path (not a recommended practice though!)
667+
668+ var previousFullNameInvariant = fse . PreviousFileSystemInfo . FullName . ToUpperInvariantOnWindows ( ) ;
669+ var previousContext = new Context ( fse , token , isSyncPath : IsSyncPath ( previousFullNameInvariant ) ) ;
670+
671+ var newFullNameInvariant = fse . FileSystemInfo . FullName . ToUpperInvariantOnWindows ( ) ;
672+ var newContext = new Context ( fse , token , isSyncPath : IsSyncPath ( newFullNameInvariant ) ) ;
648673
649674 try
650675 {
651676 if ( fse . IsFile )
652677 {
653- if ( IsWatchedFile ( fse . PreviousFileSystemInfo . FullName )
654- || IsWatchedFile ( fse . FileSystemInfo . FullName ) )
678+ var prevFileIsWatchedFile = IsWatchedFile ( fse . PreviousFileSystemInfo . FullName ) ;
679+ var newFileIsWatchedFile = IsWatchedFile ( fse . FileSystemInfo . FullName ) ;
680+
681+ if ( prevFileIsWatchedFile
682+ || newFileIsWatchedFile )
655683 {
656- await AddMessage ( ConsoleColor . Cyan , $ "[{ ( fse . IsFile ? "F" : "D" ) } ][R]:{ fse . PreviousFileSystemInfo . FullName } > { fse . FileSystemInfo . FullName } ", context ) ;
684+ await AddMessage ( ConsoleColor . Cyan , $ "[{ ( fse . IsFile ? "F" : "D" ) } ][R]:{ fse . PreviousFileSystemInfo . FullName } > { fse . FileSystemInfo . FullName } ", newContext ) ;
657685
658- //NB! if file is renamed to cs~ or resx~ then that means there will be yet another write to same file, so lets skip this event here
686+ //NB! if file is renamed to cs~ or resx~ then that means there will be yet another write to same file, so lets skip this event here - NB! skip the event here, including delete event of the previous file
659687 if ( ! fse . FileSystemInfo . FullName . EndsWith ( "~" ) )
660688 {
661689 //using (await Global.FileOperationLocks.LockAsync(rfse.FileSystemInfo.FullName, rfse.PreviousFileSystemInfo.FullName, context.Token)) //comment-out: prevent deadlock
662690 {
663- await FileUpdated ( fse . FileSystemInfo . FullName , context ) ;
664- await FileDeleted ( fse . PreviousFileSystemInfo . FullName , context ) ;
691+ if ( newFileIsWatchedFile )
692+ {
693+ await FileUpdated ( fse . FileSystemInfo . FullName , newContext ) ;
694+ }
695+
696+ if ( prevFileIsWatchedFile )
697+ {
698+ if (
699+ newFileIsWatchedFile //both files were watched files
700+ && previousContext . IsSyncPath != newContext . IsSyncPath
701+ &&
702+ (
703+ Global . Bidirectional //move in either direction between sync and async
704+ || previousContext . IsSyncPath //sync -> async move
705+ )
706+ )
707+ {
708+ //the file was moved from one watched path to another watched path, which is illegal, lets ignore the file move
709+
710+ await AddMessage ( ConsoleColor . Red , $ "Ignoring file delete in the source path since the move was to the other managed path : { fse . PreviousFileSystemInfo . FullName } > { fse . FileSystemInfo . FullName } ", previousContext ) ;
711+ }
712+ else
713+ {
714+ await FileDeleted ( fse . PreviousFileSystemInfo . FullName , previousContext ) ;
715+ }
716+ }
665717 }
666718 }
667719 }
668720 }
669721 else
670722 {
671- await AddMessage ( ConsoleColor . Cyan , $ "[{ ( fse . IsFile ? "F" : "D" ) } ][R]:{ fse . PreviousFileSystemInfo . FullName } > { fse . FileSystemInfo . FullName } ", context ) ;
723+ await AddMessage ( ConsoleColor . Cyan , $ "[{ ( fse . IsFile ? "F" : "D" ) } ][R]:{ fse . PreviousFileSystemInfo . FullName } > { fse . FileSystemInfo . FullName } ", newContext ) ;
672724
673725 //TODO trigger update / delete event for all files in new folder
674726 }
675727 }
676728 catch ( Exception ex )
677729 {
678- await WriteException ( ex , context ) ;
730+ await WriteException ( ex , newContext ) ;
679731 }
680732 } //private static async Task OnRenamedAsync(IRenamedFileSystemEvent fse, CancellationToken token)
681733
682734 private static async Task OnRemovedAsync ( IFileSystemEvent fse , CancellationToken token )
683735 {
684- var context = new Context ( fse , token ) ;
736+ var fullNameInvariant = fse . FileSystemInfo . FullName . ToUpperInvariantOnWindows ( ) ;
737+ var context = new Context ( fse , token , isSyncPath : IsSyncPath ( fullNameInvariant ) ) ;
685738
686739 try
687740 {
@@ -710,7 +763,8 @@ private static async Task OnRemovedAsync(IFileSystemEvent fse, CancellationToken
710763
711764 public static async Task OnAddedAsync ( IFileSystemEvent fse , CancellationToken token )
712765 {
713- var context = new Context ( fse , token ) ;
766+ var fullNameInvariant = fse . FileSystemInfo . FullName . ToUpperInvariantOnWindows ( ) ;
767+ var context = new Context ( fse , token , isSyncPath : IsSyncPath ( fullNameInvariant ) ) ;
714768
715769 try
716770 {
@@ -739,7 +793,8 @@ public static async Task OnAddedAsync(IFileSystemEvent fse, CancellationToken to
739793
740794 private static async Task OnTouchedAsync ( IFileSystemEvent fse , CancellationToken token )
741795 {
742- var context = new Context ( fse , token ) ;
796+ var fullNameInvariant = fse . FileSystemInfo . FullName . ToUpperInvariantOnWindows ( ) ;
797+ var context = new Context ( fse , token , isSyncPath : IsSyncPath ( fullNameInvariant ) ) ;
743798
744799 try
745800 {
@@ -818,10 +873,20 @@ public static async Task SaveFileModifications(string fullName, string fileData,
818873 : null ;
819874
820875 if (
821- ( otherFileData ? . Length ?? - 1 ) != fileData . Length
876+ ( otherFileData ? . Length ?? - 1 ) != fileData . Length
822877 || otherFileData != fileData
823878 )
824879 {
880+ var minDiskFreeSpace = context . IsSyncPath ? Global . AsyncPathMinFreeSpace : Global . SyncPathMinFreeSpace ;
881+ var actualFreeSpace = minDiskFreeSpace > 0 ? CheckDiskSpace ( otherFullName ) : 0 ;
882+ if ( minDiskFreeSpace > actualFreeSpace )
883+ {
884+ await AddMessage ( ConsoleColor . Red , $ "Error synchronising updates from file { fullName } : minDiskFreeSpace > actualFreeSpace : { minDiskFreeSpace } > { actualFreeSpace } ", context ) ;
885+
886+ return ;
887+ }
888+
889+
825890 await DeleteFile ( otherFullName , context ) ;
826891
827892 var otherDirName = Path . GetDirectoryName ( otherFullName ) ;
@@ -867,10 +932,20 @@ public static async Task SaveFileModifications(string fullName, byte[] fileData,
867932 : null ;
868933
869934 if (
870- ( otherFileData ? . Length ?? - 1 ) != fileData . Length
935+ ( otherFileData ? . Length ?? - 1 ) != fileData . Length
871936 || ! FileExtensions . BinaryEqual ( otherFileData , fileData )
872937 )
873938 {
939+ var minDiskFreeSpace = context . IsSyncPath ? Global . AsyncPathMinFreeSpace : Global . SyncPathMinFreeSpace ;
940+ var actualFreeSpace = minDiskFreeSpace > 0 ? CheckDiskSpace ( otherFullName ) : 0 ;
941+ if ( minDiskFreeSpace > actualFreeSpace )
942+ {
943+ await AddMessage ( ConsoleColor . Red , $ "Error synchronising updates from file { fullName } : minDiskFreeSpace > actualFreeSpace : { minDiskFreeSpace } > { actualFreeSpace } ", context ) ;
944+
945+ return ;
946+ }
947+
948+
874949 await DeleteFile ( otherFullName , context ) ;
875950
876951 var otherDirName = Path . GetDirectoryName ( otherFullName ) ;
@@ -886,7 +961,7 @@ public static async Task SaveFileModifications(string fullName, byte[] fileData,
886961
887962 await AddMessage ( ConsoleColor . Magenta , $ "Synchronised updates from file { fullName } ", context ) ;
888963 }
889- else if ( false )
964+ else if ( false ) //TODO: config
890965 {
891966 //touch the file
892967 var now = DateTime . UtcNow ; //NB! compute common now for ConverterSavedFileDates
@@ -904,6 +979,35 @@ public static async Task SaveFileModifications(string fullName, byte[] fileData,
904979 }
905980 } //public static async Task SaveFileModifications(string fullName, byte[] fileData, byte[] originalData, Context context)
906981
982+ public static long CheckDiskSpace ( string path )
983+ {
984+ long freeBytes ;
985+
986+ if ( ! ConfigParser . IsWindows )
987+ {
988+ //NB! DriveInfo works on paths well in Linux //TODO: what about Mac?
989+ var drive = new DriveInfo ( path ) ;
990+ freeBytes = drive . AvailableFreeSpace ;
991+ }
992+ else
993+ {
994+ WindowsDllImport . GetDiskFreeSpaceEx ( path , out freeBytes , out var _ , out var __ ) ;
995+ }
996+
997+ return freeBytes ;
998+ }
999+
9071000#pragma warning restore AsyncFixer01
9081001 }
1002+
1003+ internal static class WindowsDllImport //keep in a separate class just in case to ensure that dllimport is not attempted during application loading under non-Windows OS
1004+ {
1005+ //https://stackoverflow.com/questions/61037184/find-out-free-and-total-space-on-a-network-unc-path-in-netcore-3-x
1006+ [ DllImport ( "kernel32.dll" , SetLastError = true , CharSet = CharSet . Unicode ) ]
1007+ [ return : MarshalAs ( UnmanagedType . Bool ) ]
1008+ internal static extern bool GetDiskFreeSpaceEx ( string lpDirectoryName ,
1009+ out long lpFreeBytesAvailable ,
1010+ out long lpTotalNumberOfBytes ,
1011+ out long lpTotalNumberOfFreeBytes ) ;
1012+ }
9091013}
0 commit comments