diff --git a/src/ByteSync.Client/Business/Inventories/FileSystemEntryKind.cs b/src/ByteSync.Client/Business/Inventories/FileSystemEntryKind.cs
new file mode 100644
index 00000000..43fe8f84
--- /dev/null
+++ b/src/ByteSync.Client/Business/Inventories/FileSystemEntryKind.cs
@@ -0,0 +1,13 @@
+namespace ByteSync.Business.Inventories;
+
+public enum FileSystemEntryKind
+{
+ Unknown = 0,
+ RegularFile = 1,
+ Directory = 2,
+ BlockDevice = 3,
+ CharacterDevice = 4,
+ Fifo = 5,
+ Socket = 6,
+ Symlink = 7
+}
diff --git a/src/ByteSync.Client/ByteSync.Client.csproj b/src/ByteSync.Client/ByteSync.Client.csproj
index 232fbee3..436be7c5 100644
--- a/src/ByteSync.Client/ByteSync.Client.csproj
+++ b/src/ByteSync.Client/ByteSync.Client.csproj
@@ -1,260 +1,261 @@
-
- WinExe
- enable
- true
- Assets\ByteSync.ico
- net8.0
- win-x64;linux-x64
- true
- true
- true
- default
- ByteSync.Client
- ByteSync
- ByteSync
- false
- false
- true
- true
-
+
+ WinExe
+ enable
+ true
+ Assets\ByteSync.ico
+ net8.0
+ win-x64;linux-x64
+ true
+ true
+ true
+ default
+ ByteSync.Client
+ ByteSync
+ ByteSync
+ false
+ false
+ true
+ true
+
-
- WIN
-
+
+ WIN
+
-
- OSX
-
+
+ OSX
+
-
- LIN
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Never
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Resources.resx
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
-
-
-
-
-
-
-
-
-
-
- True
- True
- Resources.resx
-
-
-
-
- Designer
-
-
-
-
- Resources.resx
-
-
-
-
- ActivityIndicator.axaml
-
-
- CurrentCloudSessionView.axaml
- Code
-
-
- JoinCloudSessionView.axaml
- Code
-
-
- StartCloudSessionView.axaml
- Code
-
-
- StartOrJoinView.axaml
- Code
-
-
- ComparisonResultView.axaml
- Code
-
-
- StatusView.axaml
- Code
-
-
- SynchronizationActionView.axaml
- Code
-
-
- ManageSynchronizationRulesView.axaml
- Code
-
-
- ContentIdentityView.axaml
- Code
-
-
- AutomaticActionSummaryView.axaml
- Code
-
-
- AtomicActionEditView.axaml
- Code
-
-
- AtomicConditionEditView.axaml
- Code
-
-
- SynchronizationRulesGlobalView.axaml
- Code
-
-
- ManualActionEditionGlobalView.axaml
- Code
-
-
- SessionSettingsEditView.axaml
- Code
-
-
- HomeMainView.axaml
- Code
-
-
- SelectLocaleView.axaml
- Code
-
-
- AddTrustedClientView.axaml
- Code
-
-
- TrustedPublicKeysView.axaml
- Code
-
-
- ProfilesView.axaml
- Code
-
-
- AccountDetailsView.axaml
- Code
-
-
- UsageStatisticsView.axaml
- Code
-
-
- SessionSettingsEditView.axaml
- Code
-
-
- AnnouncementView.axaml
- Code
-
-
- DataNodeHeaderView.axaml
- Code
-
-
- DataNodeSourcesView.axaml
- Code
-
-
- DataNodeStatusView.axaml
- Code
-
-
- DataNodeView.axaml
- Code
-
-
- AddTrustedClientView.axaml
- Code
-
-
- AnnouncementView.axaml
- Code
-
-
+
+ LIN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Never
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Resources.resx
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+ Designer
+
+
+
+
+ Resources.resx
+
+
+
+
+ ActivityIndicator.axaml
+
+
+ CurrentCloudSessionView.axaml
+ Code
+
+
+ JoinCloudSessionView.axaml
+ Code
+
+
+ StartCloudSessionView.axaml
+ Code
+
+
+ StartOrJoinView.axaml
+ Code
+
+
+ ComparisonResultView.axaml
+ Code
+
+
+ StatusView.axaml
+ Code
+
+
+ SynchronizationActionView.axaml
+ Code
+
+
+ ManageSynchronizationRulesView.axaml
+ Code
+
+
+ ContentIdentityView.axaml
+ Code
+
+
+ AutomaticActionSummaryView.axaml
+ Code
+
+
+ AtomicActionEditView.axaml
+ Code
+
+
+ AtomicConditionEditView.axaml
+ Code
+
+
+ SynchronizationRulesGlobalView.axaml
+ Code
+
+
+ ManualActionEditionGlobalView.axaml
+ Code
+
+
+ SessionSettingsEditView.axaml
+ Code
+
+
+ HomeMainView.axaml
+ Code
+
+
+ SelectLocaleView.axaml
+ Code
+
+
+ AddTrustedClientView.axaml
+ Code
+
+
+ TrustedPublicKeysView.axaml
+ Code
+
+
+ ProfilesView.axaml
+ Code
+
+
+ AccountDetailsView.axaml
+ Code
+
+
+ UsageStatisticsView.axaml
+ Code
+
+
+ SessionSettingsEditView.axaml
+ Code
+
+
+ AnnouncementView.axaml
+ Code
+
+
+ DataNodeHeaderView.axaml
+ Code
+
+
+ DataNodeSourcesView.axaml
+ Code
+
+
+ DataNodeStatusView.axaml
+ Code
+
+
+ DataNodeView.axaml
+ Code
+
+
+ AddTrustedClientView.axaml
+ Code
+
+
+ AnnouncementView.axaml
+ Code
+
+
diff --git a/src/ByteSync.Client/Interfaces/Controls/Inventories/IPosixFileTypeClassifier.cs b/src/ByteSync.Client/Interfaces/Controls/Inventories/IPosixFileTypeClassifier.cs
new file mode 100644
index 00000000..a46b2f6b
--- /dev/null
+++ b/src/ByteSync.Client/Interfaces/Controls/Inventories/IPosixFileTypeClassifier.cs
@@ -0,0 +1,8 @@
+using ByteSync.Business.Inventories;
+
+namespace ByteSync.Interfaces.Controls.Inventories;
+
+public interface IPosixFileTypeClassifier
+{
+ FileSystemEntryKind ClassifyPosixEntry(string path);
+}
diff --git a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs
index 0afc7987..968c0e5c 100644
--- a/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs
+++ b/src/ByteSync.Client/Services/Inventories/InventoryBuilder.cs
@@ -26,7 +26,8 @@ public InventoryBuilder(SessionMember sessionMember, DataNode dataNode, SessionS
IInventoryFileAnalyzer inventoryFileAnalyzer,
IInventorySaver inventorySaver,
IInventoryIndexer inventoryIndexer,
- IFileSystemInspector? fileSystemInspector = null)
+ IFileSystemInspector? fileSystemInspector = null,
+ IPosixFileTypeClassifier? posixFileTypeClassifier = null)
{
_logger = logger;
@@ -45,6 +46,7 @@ public InventoryBuilder(SessionMember sessionMember, DataNode dataNode, SessionS
InventoryFileAnalyzer = inventoryFileAnalyzer;
FileSystemInspector = fileSystemInspector ?? new FileSystemInspector();
+ PosixFileTypeClassifier = posixFileTypeClassifier ?? new PosixFileTypeClassifier();
}
private Inventory InstantiateInventory()
@@ -87,6 +89,8 @@ private Inventory InstantiateInventory()
private IFileSystemInspector FileSystemInspector { get; }
+ private IPosixFileTypeClassifier PosixFileTypeClassifier { get; }
+
private bool IgnoreHidden
{
get { return SessionSettings is { ExcludeHiddenFiles: true }; }
@@ -305,7 +309,7 @@ private void AddInaccessibleDirectoryAndLog(InventoryPart inventoryPart, Directo
AddFileSystemDescription(inventoryPart, subDirectoryDescription);
_logger.LogWarning(ex, message, directoryInfo.FullName);
}
-
+
private bool IsRootPath(InventoryPart inventoryPart, FileSystemInfo fileSystemInfo)
{
var rootPath = NormalizePath(inventoryPart.RootPath);
@@ -359,6 +363,14 @@ private bool ShouldIgnoreHiddenDirectory(DirectoryInfo directoryInfo)
{
var isRoot = IsRootPath(inventoryPart, fileInfo);
+ var entryKind = PosixFileTypeClassifier.ClassifyPosixEntry(fileInfo.FullName);
+ if (IsPosixSpecialFile(entryKind))
+ {
+ AddPosixSpecialFileAndLog(inventoryPart, fileInfo, entryKind);
+
+ return;
+ }
+
if (!isRoot && ShouldIgnoreHiddenFile(fileInfo))
{
return;
@@ -527,6 +539,27 @@ private void AddInaccessibleFileAndLog(InventoryPart inventoryPart, FileInfo fil
_logger.LogWarning(ex, message, fileInfo.FullName);
}
+ private void AddPosixSpecialFileAndLog(InventoryPart inventoryPart, FileInfo fileInfo, FileSystemEntryKind entryKind)
+ {
+ inventoryPart.IsIncompleteDueToAccess = true;
+ var relativePath = BuildRelativePath(inventoryPart, fileInfo);
+ var fileDescription = new FileDescription(inventoryPart, relativePath)
+ {
+ IsAccessible = false
+ };
+ AddFileSystemDescription(inventoryPart, fileDescription);
+ _logger.LogWarning("File {File} is a POSIX special file ({EntryKind}) and will be skipped", fileInfo.FullName, entryKind);
+ }
+
+ private static bool IsPosixSpecialFile(FileSystemEntryKind entryKind)
+ {
+ return entryKind is
+ FileSystemEntryKind.BlockDevice or
+ FileSystemEntryKind.CharacterDevice or
+ FileSystemEntryKind.Fifo or
+ FileSystemEntryKind.Socket;
+ }
+
private string BuildRelativePath(InventoryPart inventoryPart, FileInfo fileInfo)
{
if (inventoryPart.InventoryPartType != FileSystemTypes.Directory)
@@ -584,4 +617,4 @@ private void AddFileSystemDescription(InventoryPart inventoryPart, FileSystemDes
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/ByteSync.Client/Services/Inventories/PosixFileTypeClassifier.cs b/src/ByteSync.Client/Services/Inventories/PosixFileTypeClassifier.cs
new file mode 100644
index 00000000..1b612c44
--- /dev/null
+++ b/src/ByteSync.Client/Services/Inventories/PosixFileTypeClassifier.cs
@@ -0,0 +1,150 @@
+using ByteSync.Business.Inventories;
+using ByteSync.Interfaces.Controls.Inventories;
+using Mono.Unix;
+using Mono.Unix.Native;
+
+namespace ByteSync.Services.Inventories;
+
+public class PosixFileTypeClassifier : IPosixFileTypeClassifier
+{
+ private readonly Func _unixFileInfoFactory;
+ private readonly Func _tryGetMode;
+
+ public PosixFileTypeClassifier()
+ : this(path => new UnixFileInfo(path), TryGetModeDefault)
+ {
+ }
+
+ public PosixFileTypeClassifier(Func unixFileInfoFactory)
+ : this(unixFileInfoFactory, TryGetModeDefault)
+ {
+ }
+
+ public PosixFileTypeClassifier(Func unixFileInfoFactory,
+ Func tryGetMode)
+ {
+ _unixFileInfoFactory = unixFileInfoFactory;
+ _tryGetMode = tryGetMode;
+ }
+
+ public FileSystemEntryKind ClassifyPosixEntry(string path)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ return FileSystemEntryKind.Unknown;
+ }
+
+ try
+ {
+ var modeResult = _tryGetMode(path);
+ if (!modeResult.Success)
+ {
+ return FileSystemEntryKind.Unknown;
+ }
+
+ var entryKind = MapFilePermissions(modeResult.Type);
+ if (entryKind == FileSystemEntryKind.RegularFile || entryKind == FileSystemEntryKind.Unknown)
+ {
+ var unixKind = TryClassifyWithUnixFileInfo(path);
+ if (unixKind != FileSystemEntryKind.Unknown)
+ {
+ return unixKind;
+ }
+ }
+
+ return entryKind;
+ }
+ catch (DllNotFoundException)
+ {
+ return FileSystemEntryKind.Unknown;
+ }
+ catch (EntryPointNotFoundException)
+ {
+ return FileSystemEntryKind.Unknown;
+ }
+ catch (PlatformNotSupportedException)
+ {
+ return FileSystemEntryKind.Unknown;
+ }
+ }
+
+ private static (bool Success, FilePermissions Type) TryGetModeDefault(string path)
+ {
+ if (Syscall.lstat(path, out var stat) != 0)
+ {
+ if (Syscall.stat(path, out stat) != 0)
+ {
+ return (false, 0);
+ }
+ }
+
+ var mode = stat.st_mode;
+ var type = mode & FilePermissions.S_IFMT;
+
+ return (true, type);
+ }
+
+ private static FileSystemEntryKind MapFilePermissions(FilePermissions type)
+ {
+ if (type == FilePermissions.S_IFREG)
+ {
+ return FileSystemEntryKind.RegularFile;
+ }
+
+ if (type == FilePermissions.S_IFDIR)
+ {
+ return FileSystemEntryKind.Directory;
+ }
+
+ if (type == FilePermissions.S_IFBLK)
+ {
+ return FileSystemEntryKind.BlockDevice;
+ }
+
+ if (type == FilePermissions.S_IFCHR)
+ {
+ return FileSystemEntryKind.CharacterDevice;
+ }
+
+ if (type == FilePermissions.S_IFIFO)
+ {
+ return FileSystemEntryKind.Fifo;
+ }
+
+ if (type == FilePermissions.S_IFSOCK)
+ {
+ return FileSystemEntryKind.Socket;
+ }
+
+ if (type == FilePermissions.S_IFLNK)
+ {
+ return FileSystemEntryKind.Symlink;
+ }
+
+ return FileSystemEntryKind.Unknown;
+ }
+
+ private FileSystemEntryKind TryClassifyWithUnixFileInfo(string path)
+ {
+ try
+ {
+ var info = _unixFileInfoFactory(path);
+
+ return info.FileType switch
+ {
+ FileTypes.BlockDevice => FileSystemEntryKind.BlockDevice,
+ FileTypes.CharacterDevice => FileSystemEntryKind.CharacterDevice,
+ FileTypes.Fifo => FileSystemEntryKind.Fifo,
+ FileTypes.Socket => FileSystemEntryKind.Socket,
+ FileTypes.Directory => FileSystemEntryKind.Directory,
+ FileTypes.RegularFile => FileSystemEntryKind.RegularFile,
+ FileTypes.SymbolicLink => FileSystemEntryKind.Symlink,
+ _ => FileSystemEntryKind.Unknown
+ };
+ }
+ catch (Exception)
+ {
+ return FileSystemEntryKind.Unknown;
+ }
+ }
+}
diff --git a/tests/ByteSync.Client.IntegrationTests/Services/Inventories/TestInventoryBuilder.cs b/tests/ByteSync.Client.IntegrationTests/Services/Inventories/TestInventoryBuilder.cs
index 9f729e5a..854fafbb 100644
--- a/tests/ByteSync.Client.IntegrationTests/Services/Inventories/TestInventoryBuilder.cs
+++ b/tests/ByteSync.Client.IntegrationTests/Services/Inventories/TestInventoryBuilder.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using Autofac;
using ByteSync.Business;
using ByteSync.Business.DataNodes;
@@ -716,6 +717,60 @@ public async Task Test_ReparsePoint()
inventory.InventoryParts[0].DirectoryDescriptions.Count.Should().Be(1);
inventory.InventoryParts[0].FileDescriptions.Count.Should().Be(2);
}
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public async Task Test_PosixFifo_IsSkipped()
+ {
+ InventoryBuilder inventoryBuilder;
+ Inventory inventory;
+
+ DirectoryInfo sourceA, unzipDir;
+ FileInfo fileInfo;
+
+ sourceA = new DirectoryInfo(IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, "sourceA"));
+ sourceA.Create();
+ fileInfo = new FileInfo(_testDirectoryService.CreateFileInDirectory(sourceA.FullName, "fileA.txt", "fileAContent").FullName);
+
+ var fifoPath = IOUtils.Combine(sourceA.FullName, "pipeA");
+ CreateFifo(fifoPath);
+
+ var classifier = new PosixFileTypeClassifier();
+ var fifoKind = classifier.ClassifyPosixEntry(fifoPath);
+ if (fifoKind == FileSystemEntryKind.Unknown)
+ {
+ Assert.Ignore($"POSIX classification returned Unknown for '{fifoPath}'.");
+ }
+
+ var inventoryAFilePath = IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, $"inventoryA.zip");
+
+ var sessionSettings = SessionSettings.BuildDefault();
+ var osPlatform = OperatingSystem.IsMacOS() ? OSPlatforms.MacOs : OSPlatforms.Linux;
+
+ inventoryBuilder = BuildInventoryBuilder(sessionSettings, null, null, osPlatform);
+ inventoryBuilder.AddInventoryPart(sourceA.FullName);
+ await inventoryBuilder.BuildBaseInventoryAsync(inventoryAFilePath);
+
+ File.Exists(inventoryAFilePath).Should().BeTrue();
+
+ unzipDir = new DirectoryInfo(IOUtils.Combine(_testDirectoryService.TestDirectory.FullName, "unzip"));
+ unzipDir.Create();
+
+ var fastZip = new FastZip();
+ fastZip.ExtractZip(inventoryAFilePath, unzipDir.FullName, null);
+
+ unzipDir.GetFiles("*", SearchOption.AllDirectories).Length.Should().Be(1);
+ File.Exists(IOUtils.Combine(unzipDir.FullName, $"inventory.json")).Should().BeTrue();
+
+ inventory = inventoryBuilder.Inventory!;
+ inventory.InventoryParts.Count.Should().Be(1);
+ inventory.InventoryParts[0].DirectoryDescriptions.Count.Should().Be(0);
+ inventory.InventoryParts[0].FileDescriptions.Count.Should().Be(2);
+
+ var fifoDescription = inventory.InventoryParts[0].FileDescriptions.Single(fd => fd.Name.Equals("pipeA"));
+ fifoDescription.IsAccessible.Should().BeFalse();
+ inventory.InventoryParts[0].IsIncompleteDueToAccess.Should().BeTrue();
+ }
[Test]
public async Task Test_GetBuildingStageData()
@@ -873,4 +928,21 @@ private InventoryBuilder BuildInventoryBuilder(SessionSettings? sessionSettings
saver,
new InventoryIndexer());
}
+
+ private static void CreateFifo(string path)
+ {
+ var startInfo = new ProcessStartInfo("mkfifo", path)
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false
+ };
+
+ using var process = Process.Start(startInfo);
+ process.Should().NotBeNull();
+ process!.WaitForExit();
+
+ var error = process.StandardError.ReadToEnd();
+ process.ExitCode.Should().Be(0, error);
+ }
}
diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/PosixFileTypeClassifierTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/PosixFileTypeClassifierTests.cs
new file mode 100644
index 00000000..f29975ba
--- /dev/null
+++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/PosixFileTypeClassifierTests.cs
@@ -0,0 +1,197 @@
+using System.IO;
+using ByteSync.Business.Inventories;
+using ByteSync.Services.Inventories;
+using FluentAssertions;
+using NUnit.Framework;
+
+namespace ByteSync.Client.UnitTests.Services.Inventories;
+
+public class PosixFileTypeClassifierTests
+{
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ [TestCase("/dev/null", FileSystemEntryKind.CharacterDevice)]
+ [TestCase("/dev/zero", FileSystemEntryKind.CharacterDevice)]
+ public void ClassifyPosixEntry_ReturnsExpected(string path, FileSystemEntryKind expected)
+ {
+ if (!File.Exists(path))
+ {
+ Assert.Ignore($"Path '{path}' not found on this system.");
+ }
+
+ var classifier = new PosixFileTypeClassifier();
+
+ var result = classifier.ClassifyPosixEntry(path);
+
+ if (result == FileSystemEntryKind.Unknown)
+ {
+ Assert.Ignore($"POSIX classification returned Unknown for '{path}'.");
+ }
+
+ result.Should().Be(expected);
+ }
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public void ClassifyPosixEntry_ReturnsRegularFile_ForTempFile()
+ {
+ var classifier = new PosixFileTypeClassifier();
+ var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDirectory);
+ var tempFile = Path.Combine(tempDirectory, "file.txt");
+ File.WriteAllText(tempFile, "data");
+
+ try
+ {
+ var result = classifier.ClassifyPosixEntry(tempFile);
+
+ if (result == FileSystemEntryKind.Unknown)
+ {
+ Assert.Ignore($"POSIX classification returned Unknown for '{tempFile}'.");
+ }
+
+ result.Should().Be(FileSystemEntryKind.RegularFile);
+ }
+ finally
+ {
+ Directory.Delete(tempDirectory, true);
+ }
+ }
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public void ClassifyPosixEntry_ReturnsDirectory_ForTempDirectory()
+ {
+ var classifier = new PosixFileTypeClassifier();
+ var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDirectory);
+
+ try
+ {
+ var result = classifier.ClassifyPosixEntry(tempDirectory);
+
+ if (result == FileSystemEntryKind.Unknown)
+ {
+ Assert.Ignore($"POSIX classification returned Unknown for '{tempDirectory}'.");
+ }
+
+ result.Should().Be(FileSystemEntryKind.Directory);
+ }
+ finally
+ {
+ Directory.Delete(tempDirectory, true);
+ }
+ }
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public void ClassifyPosixEntry_ReturnsUnknown_ForMissingPath()
+ {
+ var classifier = new PosixFileTypeClassifier();
+ var missingPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "missing");
+
+ var result = classifier.ClassifyPosixEntry(missingPath);
+
+ result.Should().Be(FileSystemEntryKind.Unknown);
+ }
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public void ClassifyPosixEntry_ReturnsSymlink_WhenSupported()
+ {
+ var classifier = new PosixFileTypeClassifier();
+ var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDirectory);
+ var targetFile = Path.Combine(tempDirectory, "target.txt");
+ File.WriteAllText(targetFile, "data");
+ var linkPath = Path.Combine(tempDirectory, "link.txt");
+
+ try
+ {
+ try
+ {
+ File.CreateSymbolicLink(linkPath, targetFile);
+ }
+ catch (Exception ex)
+ {
+ Assert.Ignore($"Symbolic link creation failed: {ex.GetType().Name}");
+ }
+
+ var result = classifier.ClassifyPosixEntry(linkPath);
+
+ if (result == FileSystemEntryKind.Unknown)
+ {
+ Assert.Ignore($"POSIX classification returned Unknown for '{linkPath}'.");
+ }
+
+ result.Should().Be(FileSystemEntryKind.Symlink);
+ }
+ finally
+ {
+ Directory.Delete(tempDirectory, true);
+ }
+ }
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public void ClassifyPosixEntry_ReturnsUnknown_WhenUnixFileInfoThrows()
+ {
+ var classifier = new PosixFileTypeClassifier(_ => throw new InvalidOperationException("fail"));
+ var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDirectory);
+ var tempFile = Path.Combine(tempDirectory, "file.txt");
+ File.WriteAllText(tempFile, "data");
+
+ try
+ {
+ var result = classifier.ClassifyPosixEntry(tempFile);
+
+ if (result == FileSystemEntryKind.Unknown)
+ {
+ Assert.Ignore($"POSIX classification returned Unknown for '{tempFile}'.");
+ }
+
+ result.Should().Be(FileSystemEntryKind.RegularFile);
+ }
+ finally
+ {
+ Directory.Delete(tempDirectory, true);
+ }
+ }
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public void ClassifyPosixEntry_ReturnsUnknown_WhenDllNotFound()
+ {
+ var classifier = new PosixFileTypeClassifier(_ => throw new InvalidOperationException("unused"),
+ _ => throw new DllNotFoundException("missing"));
+
+ var result = classifier.ClassifyPosixEntry("/tmp");
+
+ result.Should().Be(FileSystemEntryKind.Unknown);
+ }
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public void ClassifyPosixEntry_ReturnsUnknown_WhenEntryPointNotFound()
+ {
+ var classifier = new PosixFileTypeClassifier(_ => throw new InvalidOperationException("unused"),
+ _ => throw new EntryPointNotFoundException("missing"));
+
+ var result = classifier.ClassifyPosixEntry("/tmp");
+
+ result.Should().Be(FileSystemEntryKind.Unknown);
+ }
+
+ [Test]
+ [Platform(Include = "Linux,MacOsX")]
+ public void ClassifyPosixEntry_ReturnsUnknown_WhenPlatformNotSupported()
+ {
+ var classifier = new PosixFileTypeClassifier(_ => throw new InvalidOperationException("unused"),
+ _ => throw new PlatformNotSupportedException("missing"));
+
+ var result = classifier.ClassifyPosixEntry("/tmp");
+
+ result.Should().Be(FileSystemEntryKind.Unknown);
+ }
+}
diff --git a/tests/ByteSync.Functions.IntegrationTests/End2End/E2E_Environment_Setup.cs b/tests/ByteSync.Functions.IntegrationTests/End2End/E2E_Environment_Setup.cs
index 757edb2d..6b8873f6 100644
--- a/tests/ByteSync.Functions.IntegrationTests/End2End/E2E_Environment_Setup.cs
+++ b/tests/ByteSync.Functions.IntegrationTests/End2End/E2E_Environment_Setup.cs
@@ -70,6 +70,11 @@ string ResolveFunctionsProjectRoot()
var env = new Dictionary
{
["AzureWebJobsStorage"] = cfg["AzureWebJobsStorage"]!,
+ ["FUNCTIONS_WORKER_RUNTIME"] = "dotnet-isolated",
+ ["AzureWebJobsScriptRoot"] = "/home/site/wwwroot",
+ ["AZURE_FUNCTIONS_ENVIRONMENT"] = "Development",
+ ["AzureFunctionsJobHost__Logging__LogLevel__Default"] = "Information",
+ ["AzureFunctionsJobHost__Logging__LogLevel__Host"] = "Information",
["AppSettings__SkipClientsVersionCheck"] = "True",
["Redis__ConnectionString"] = cfg["Redis:ConnectionString"]!,
["SignalR__ConnectionString"] = cfg["SignalR:ConnectionString"]!,