Skip to content

Commit 047fc92

Browse files
committed
Implement disk free space check functionality. When file is moved between src and dest folders, ignore file delete in src folder.
1 parent a058048 commit 047fc92

File tree

4 files changed

+152
-25
lines changed

4 files changed

+152
-25
lines changed

ConfigParser.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ public static string ToUpperInvariantOnWindows(this string text)
2525
return text;
2626
}
2727

28+
public static long? GetLong(this IConfiguration config, params string[] sectionKeyAlternateNames)
29+
{
30+
foreach (var sectionKeyAlternateName in sectionKeyAlternateNames)
31+
{
32+
var text = config[sectionKeyAlternateName];
33+
if (text != null)
34+
{
35+
//long result;
36+
//if (long.TryParse(text, out result))
37+
// return result;
38+
return long.Parse(text); //NB! if the parameter exists then it must be in a proper numeric format
39+
}
40+
}
41+
42+
return null;
43+
}
44+
2845
public static string GetTextUpperOnWindows(this IConfiguration config, params string[] sectionKeyAlternateNames)
2946
{
3047
if (IsWindows) //assume NTFS or FAT filesystem which are usually case-insensitive
@@ -61,7 +78,7 @@ public static List<string> GetListUpperOnWindows(this IConfiguration config, par
6178
public static List<string> GetListUpper(this IConfiguration config, params string[] sectionKeyAlternateNames)
6279
{
6380
return config.GetList(sectionKeyAlternateNames)
64-
.Select(x => x.ToUpperInvariant())
81+
.Select(x => x?.ToUpperInvariant())
6582
.ToList();
6683
}
6784

Program.cs

Lines changed: 128 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Collections.Generic;
1313
using System.IO;
1414
using System.Linq;
15+
using System.Runtime.InteropServices;
1516
using System.Security.Cryptography;
1617
using System.Text;
1718
using 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
}

Synchronisation.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public class AsyncLockQueueDictionary
1717
public sealed class AsyncLockWithCount
1818
{
1919
public readonly AsyncLock LockEntry;
20+
#pragma warning disable S1104 //Warning S1104 Make this field 'private' and encapsulate it in a 'public' property.
2021
public int WaiterCount;
22+
#pragma warning restore S1104
2123

2224
public AsyncLockWithCount()
2325
{

appsettings.example.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
"AsyncPath": "C:\\yourpath\\yourproject\\",
99

1010

11+
"SyncPathMinFreeSpace": 100000000,
12+
"AsyncPathMinFreeSpace": 100000000,
13+
14+
1115
"WatchedCodeExtensions": [ "cs" ],
1216
"WatchedResXExtensions": [ "resx" ],
1317

0 commit comments

Comments
 (0)