12
12
using System . Collections . Generic ;
13
13
using System . IO ;
14
14
using System . Linq ;
15
+ using System . Runtime . InteropServices ;
15
16
using System . Security . Cryptography ;
16
17
using System . Text ;
17
18
using System . Threading ;
@@ -33,10 +34,13 @@ internal static class Global
33
34
public static List < string > ExcludedExtensions = new List < string > ( ) { "*~" , "tmp" } ;
34
35
public static List < string > IgnorePathsStartingWith = new List < string > ( ) ;
35
36
public static List < string > IgnorePathsContaining = new List < string > ( ) ;
36
-
37
+
37
38
public static string AsyncPath = "" ;
38
39
public static string SyncPath = "" ;
39
40
41
+ public static long AsyncPathMinFreeSpace = 0 ;
42
+ public static long SyncPathMinFreeSpace = 0 ;
43
+
40
44
public static bool Bidirectional = true ;
41
45
42
46
@@ -114,6 +118,9 @@ private static void Main()
114
118
Global . AsyncPath = fileConfig . GetTextUpperOnWindows ( "AsyncPath" ) ;
115
119
Global . SyncPath = fileConfig . GetTextUpperOnWindows ( "SyncPath" ) ;
116
120
121
+ Global . AsyncPathMinFreeSpace = fileConfig . GetLong ( "AsyncPathMinFreeSpace" ) ?? 0 ;
122
+ Global . SyncPathMinFreeSpace = fileConfig . GetLong ( "SyncPathMinFreeSpace" ) ?? 0 ;
123
+
117
124
Global . WatchedCodeExtension = fileConfig . GetListUpperOnWindows ( "WatchedCodeExtensions" , "WatchedCodeExtension" ) ;
118
125
Global . WatchedResXExtension = fileConfig . GetListUpperOnWindows ( "WatchedResXExtensions" , "WatchedResXExtension" ) ;
119
126
@@ -180,7 +187,8 @@ private static async Task MainTask()
180
187
181
188
var messageContext = new Context (
182
189
eventObj : null ,
183
- token : new CancellationToken ( )
190
+ token : new CancellationToken ( ) ,
191
+ isSyncPath : false //unused here
184
192
) ;
185
193
186
194
@@ -333,8 +341,9 @@ private static void WaitForCtrlC()
333
341
334
342
internal class Context
335
343
{
336
- public IFileSystemEvent Event ;
337
- public CancellationToken Token ;
344
+ public readonly IFileSystemEvent Event ;
345
+ public readonly CancellationToken Token ;
346
+ public readonly bool IsSyncPath ;
338
347
339
348
public DateTime Time
340
349
{
@@ -344,10 +353,13 @@ public DateTime Time
344
353
}
345
354
}
346
355
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
348
359
{
349
360
Event = eventObj ;
350
361
Token = token ;
362
+ IsSyncPath = isSyncPath ;
351
363
}
352
364
}
353
365
@@ -372,7 +384,9 @@ internal class ConsoleWatch
372
384
private static readonly AsyncLockQueueDictionary FileEventLocks = new AsyncLockQueueDictionary ( ) ;
373
385
374
386
387
+ #pragma warning disable S1118 //Warning S1118 Hide this public constructor by making it 'protected'.
375
388
public ConsoleWatch ( IWatcher3 watch )
389
+ #pragma warning restore S1118
376
390
{
377
391
//_consoleColor = Console.ForegroundColor;
378
392
@@ -411,6 +425,11 @@ public static async Task WriteException(Exception ex, Context context)
411
425
await AddMessage ( ConsoleColor . Red , message . ToString ( ) , context ) ;
412
426
}
413
427
428
+ public static bool IsSyncPath ( string fullNameInvariant )
429
+ {
430
+ return fullNameInvariant . StartsWith ( Global . SyncPath ) ;
431
+ }
432
+
414
433
public static string GetNonFullName ( string fullName )
415
434
{
416
435
var fullNameInvariant = fullName . ToUpperInvariantOnWindows ( ) ;
@@ -419,7 +438,7 @@ public static string GetNonFullName(string fullName)
419
438
{
420
439
return fullName . Substring ( Global . AsyncPath . Length ) ;
421
440
}
422
- else if ( fullNameInvariant . StartsWith ( Global . SyncPath ) )
441
+ else if ( IsSyncPath ( fullNameInvariant ) )
423
442
{
424
443
return fullName . Substring ( Global . SyncPath . Length ) ;
425
444
}
@@ -438,7 +457,7 @@ public static string GetOtherFullName(string fullName)
438
457
{
439
458
return Path . Combine ( Global . SyncPath , nonFullName ) ;
440
459
}
441
- else if ( fullNameInvariant . StartsWith ( Global . SyncPath ) )
460
+ else if ( IsSyncPath ( fullNameInvariant ) )
442
461
{
443
462
return Path . Combine ( Global . AsyncPath , nonFullName ) ;
444
463
}
@@ -546,7 +565,7 @@ public static async Task FileUpdated(string fullName, Context context)
546
565
{
547
566
await AsyncToSyncConverter . AsyncFileUpdated ( fullName , context ) ;
548
567
}
549
- else if ( fullNameInvariant . StartsWith ( Global . SyncPath ) ) //NB!
568
+ else if ( IsSyncPath ( fullNameInvariant ) ) //NB!
550
569
{
551
570
await SyncToAsyncConverter . SyncFileUpdated ( fullName , context ) ;
552
571
}
@@ -611,7 +630,7 @@ private static bool IsWatchedFile(string fullName)
611
630
|| Global . WatchedCodeExtension . Contains ( "*" )
612
631
|| Global . WatchedResXExtension . Contains ( "*" )
613
632
)
614
- &&
633
+ &&
615
634
Global . ExcludedExtensions . All ( x =>
616
635
617
636
! fullNameInvariant . EndsWith ( "." + x )
@@ -644,44 +663,78 @@ private static bool IsWatchedFile(string fullName)
644
663
#pragma warning disable AsyncFixer01
645
664
private static async Task OnRenamedAsync ( IRenamedFileSystemEvent fse , CancellationToken token )
646
665
{
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 ) ) ;
648
673
649
674
try
650
675
{
651
676
if ( fse . IsFile )
652
677
{
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 )
655
683
{
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 ) ;
657
685
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
659
687
if ( ! fse . FileSystemInfo . FullName . EndsWith ( "~" ) )
660
688
{
661
689
//using (await Global.FileOperationLocks.LockAsync(rfse.FileSystemInfo.FullName, rfse.PreviousFileSystemInfo.FullName, context.Token)) //comment-out: prevent deadlock
662
690
{
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
+ }
665
717
}
666
718
}
667
719
}
668
720
}
669
721
else
670
722
{
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 ) ;
672
724
673
725
//TODO trigger update / delete event for all files in new folder
674
726
}
675
727
}
676
728
catch ( Exception ex )
677
729
{
678
- await WriteException ( ex , context ) ;
730
+ await WriteException ( ex , newContext ) ;
679
731
}
680
732
} //private static async Task OnRenamedAsync(IRenamedFileSystemEvent fse, CancellationToken token)
681
733
682
734
private static async Task OnRemovedAsync ( IFileSystemEvent fse , CancellationToken token )
683
735
{
684
- var context = new Context ( fse , token ) ;
736
+ var fullNameInvariant = fse . FileSystemInfo . FullName . ToUpperInvariantOnWindows ( ) ;
737
+ var context = new Context ( fse , token , isSyncPath : IsSyncPath ( fullNameInvariant ) ) ;
685
738
686
739
try
687
740
{
@@ -710,7 +763,8 @@ private static async Task OnRemovedAsync(IFileSystemEvent fse, CancellationToken
710
763
711
764
public static async Task OnAddedAsync ( IFileSystemEvent fse , CancellationToken token )
712
765
{
713
- var context = new Context ( fse , token ) ;
766
+ var fullNameInvariant = fse . FileSystemInfo . FullName . ToUpperInvariantOnWindows ( ) ;
767
+ var context = new Context ( fse , token , isSyncPath : IsSyncPath ( fullNameInvariant ) ) ;
714
768
715
769
try
716
770
{
@@ -739,7 +793,8 @@ public static async Task OnAddedAsync(IFileSystemEvent fse, CancellationToken to
739
793
740
794
private static async Task OnTouchedAsync ( IFileSystemEvent fse , CancellationToken token )
741
795
{
742
- var context = new Context ( fse , token ) ;
796
+ var fullNameInvariant = fse . FileSystemInfo . FullName . ToUpperInvariantOnWindows ( ) ;
797
+ var context = new Context ( fse , token , isSyncPath : IsSyncPath ( fullNameInvariant ) ) ;
743
798
744
799
try
745
800
{
@@ -818,10 +873,20 @@ public static async Task SaveFileModifications(string fullName, string fileData,
818
873
: null ;
819
874
820
875
if (
821
- ( otherFileData ? . Length ?? - 1 ) != fileData . Length
876
+ ( otherFileData ? . Length ?? - 1 ) != fileData . Length
822
877
|| otherFileData != fileData
823
878
)
824
879
{
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
+
825
890
await DeleteFile ( otherFullName , context ) ;
826
891
827
892
var otherDirName = Path . GetDirectoryName ( otherFullName ) ;
@@ -867,10 +932,20 @@ public static async Task SaveFileModifications(string fullName, byte[] fileData,
867
932
: null ;
868
933
869
934
if (
870
- ( otherFileData ? . Length ?? - 1 ) != fileData . Length
935
+ ( otherFileData ? . Length ?? - 1 ) != fileData . Length
871
936
|| ! FileExtensions . BinaryEqual ( otherFileData , fileData )
872
937
)
873
938
{
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
+
874
949
await DeleteFile ( otherFullName , context ) ;
875
950
876
951
var otherDirName = Path . GetDirectoryName ( otherFullName ) ;
@@ -886,7 +961,7 @@ public static async Task SaveFileModifications(string fullName, byte[] fileData,
886
961
887
962
await AddMessage ( ConsoleColor . Magenta , $ "Synchronised updates from file { fullName } ", context ) ;
888
963
}
889
- else if ( false )
964
+ else if ( false ) //TODO: config
890
965
{
891
966
//touch the file
892
967
var now = DateTime . UtcNow ; //NB! compute common now for ConverterSavedFileDates
@@ -904,6 +979,35 @@ public static async Task SaveFileModifications(string fullName, byte[] fileData,
904
979
}
905
980
} //public static async Task SaveFileModifications(string fullName, byte[] fileData, byte[] originalData, Context context)
906
981
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
+
907
1000
#pragma warning restore AsyncFixer01
908
1001
}
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
+ }
909
1013
}
0 commit comments