From fa9666562dea3a2a3587aa73bc73fa52193ea1da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Polykanine?= Date: Wed, 5 Nov 2025 00:31:05 +0100 Subject: [PATCH 1/3] Modernize code, pass two --- src/SharpSync/Sync/SyncEngine.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/SharpSync/Sync/SyncEngine.cs b/src/SharpSync/Sync/SyncEngine.cs index d4d6dda..da02da8 100644 --- a/src/SharpSync/Sync/SyncEngine.cs +++ b/src/SharpSync/Sync/SyncEngine.cs @@ -216,7 +216,8 @@ private async Task ScanStorageAsync( Dictionary trackedItems, bool isLocal, ChangeSet changeSet, - CancellationToken cancellationToken) { + CancellationToken cancellationToken + ) { await ScanDirectoryRecursiveAsync(storage, "", trackedItems, isLocal, changeSet, cancellationToken); } @@ -226,7 +227,8 @@ private async Task ScanDirectoryRecursiveAsync( Dictionary trackedItems, bool isLocal, ChangeSet changeSet, - CancellationToken cancellationToken) { + CancellationToken cancellationToken + ) { try { var items = await storage.ListItemsAsync(dirPath, cancellationToken); var tasks = new List(); @@ -276,7 +278,13 @@ private async Task ScanDirectoryRecursiveAsync( /// /// Determines if an item has changed compared to tracked state /// - private async Task HasChangedAsync(ISyncStorage storage, SyncItem item, SyncState tracked, bool isLocal, CancellationToken cancellationToken) { + private async Task HasChangedAsync( + ISyncStorage storage, + SyncItem item, + SyncState tracked, + bool isLocal, + CancellationToken cancellationToken + ) { if (isLocal) { // Check local changes if (tracked.LocalModified is null) { From e1a4aa9c5e76479987f802fc392fe667a94928a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Polykanine?= Date: Wed, 5 Nov 2025 18:18:20 +0100 Subject: [PATCH 2/3] Modernize code style: null checks, braces, and using statements (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modernize null checks: `== null` → `is null`, `!= null` → `is not null` - Add braces to single-line if statements for consistency - Improve code readability and follow modern C# conventions Files modified: - src/SharpSync/Core/ConflictAnalysis.cs - src/SharpSync/Core/SmartConflictResolver.cs - src/SharpSync/Database/SqliteSyncDatabase.cs - src/SharpSync/Database/SqliteSyncTransaction.cs - src/SharpSync/Storage/LocalFileStorage.cs - src/SharpSync/Storage/WebDavStorage.cs - src/SharpSync/Sync/SyncFilter.cs - tests/SharpSync.Tests/Sync/SyncEngineTests.cs Co-authored-by: Claude --- src/SharpSync/Core/ConflictAnalysis.cs | 3 +- src/SharpSync/Core/SmartConflictResolver.cs | 6 +- src/SharpSync/Database/SqliteSyncDatabase.cs | 2 +- .../Database/SqliteSyncTransaction.cs | 6 +- src/SharpSync/Storage/LocalFileStorage.cs | 30 +++++---- src/SharpSync/Storage/WebDavStorage.cs | 61 ++++++++++++------- src/SharpSync/Sync/SyncFilter.cs | 30 ++++++--- tests/SharpSync.Tests/Sync/SyncEngineTests.cs | 2 +- 8 files changed, 89 insertions(+), 51 deletions(-) diff --git a/src/SharpSync/Core/ConflictAnalysis.cs b/src/SharpSync/Core/ConflictAnalysis.cs index e6e1d02..6c79776 100644 --- a/src/SharpSync/Core/ConflictAnalysis.cs +++ b/src/SharpSync/Core/ConflictAnalysis.cs @@ -95,8 +95,9 @@ public record ConflictAnalysis { public string FormattedSizeDifference => FormatFileSize(SizeDifference); private static string FormatFileSize(long bytes) { - if (bytes == 0) + if (bytes == 0) { return "0 B"; + } string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; int suffixIndex = 0; diff --git a/src/SharpSync/Core/SmartConflictResolver.cs b/src/SharpSync/Core/SmartConflictResolver.cs index d2f700a..f9d0835 100644 --- a/src/SharpSync/Core/SmartConflictResolver.cs +++ b/src/SharpSync/Core/SmartConflictResolver.cs @@ -32,7 +32,7 @@ public async Task ResolveConflictAsync(FileConflictEventArgs var analysis = await AnalyzeConflictAsync(conflict, cancellationToken); // If we have a UI handler, let it decide - if (_conflictHandler != null) { + if (_conflictHandler is not null) { return await _conflictHandler(analysis, cancellationToken); } @@ -56,14 +56,14 @@ private static async Task AnalyzeConflictAsync(FileConflictEve var reasoning = string.Empty; // Analyze file sizes - if (conflict.LocalItem != null && conflict.RemoteItem != null) { + if (conflict.LocalItem is not null && conflict.RemoteItem is not null) { localSize = conflict.LocalItem.Size; remoteSize = conflict.RemoteItem.Size; sizeDifference = Math.Abs(conflict.RemoteItem.Size - conflict.LocalItem.Size); } // Analyze timestamps - if (conflict.LocalItem?.LastModified != null && conflict.RemoteItem?.LastModified != null) { + if (conflict.LocalItem?.LastModified is not null && conflict.RemoteItem?.LastModified is not null) { localModified = conflict.LocalItem.LastModified; remoteModified = conflict.RemoteItem.LastModified; timeDifference = Math.Abs((conflict.RemoteItem.LastModified - conflict.LocalItem.LastModified).TotalSeconds); diff --git a/src/SharpSync/Database/SqliteSyncDatabase.cs b/src/SharpSync/Database/SqliteSyncDatabase.cs index 44bf36a..acc2426 100644 --- a/src/SharpSync/Database/SqliteSyncDatabase.cs +++ b/src/SharpSync/Database/SqliteSyncDatabase.cs @@ -141,7 +141,7 @@ public async Task GetStatsAsync(CancellationToken cancellationTok s.Status == SyncStatus.RemoteDeleted).CountAsync(); var lastSyncState = await _connection!.Table() - .Where(s => s.LastSyncTime != null) + .Where(s => s.LastSyncTime is not null) .OrderByDescending(s => s.LastSyncTime) .FirstOrDefaultAsync(); diff --git a/src/SharpSync/Database/SqliteSyncTransaction.cs b/src/SharpSync/Database/SqliteSyncTransaction.cs index 9420105..92913db 100644 --- a/src/SharpSync/Database/SqliteSyncTransaction.cs +++ b/src/SharpSync/Database/SqliteSyncTransaction.cs @@ -16,8 +16,9 @@ public SqliteSyncTransaction(SQLiteAsyncConnection connection) { } public async Task CommitAsync(CancellationToken cancellationToken = default) { - if (_disposed) + if (_disposed) { throw new ObjectDisposedException(nameof(SqliteSyncTransaction)); + } // SQLite-net handles transactions automatically for batch operations // For explicit transaction control, we could use BeginTransaction/Commit @@ -26,8 +27,9 @@ public async Task CommitAsync(CancellationToken cancellationToken = default) { } public async Task RollbackAsync(CancellationToken cancellationToken = default) { - if (_disposed) + if (_disposed) { throw new ObjectDisposedException(nameof(SqliteSyncTransaction)); + } // SQLite-net handles rollback automatically if transaction is not committed await Task.CompletedTask; diff --git a/src/SharpSync/Storage/LocalFileStorage.cs b/src/SharpSync/Storage/LocalFileStorage.cs index fc7f8d5..3eabd44 100644 --- a/src/SharpSync/Storage/LocalFileStorage.cs +++ b/src/SharpSync/Storage/LocalFileStorage.cs @@ -92,8 +92,9 @@ public async Task> ListItemsAsync(string path, Cancellatio public async Task ReadFileAsync(string path, CancellationToken cancellationToken = default) { var fullPath = GetFullPath(path); - if (!File.Exists(fullPath)) + if (!File.Exists(fullPath)) { throw new FileNotFoundException($"File not found: {path}"); + } return await Task.FromResult(File.OpenRead(fullPath)); } @@ -102,8 +103,9 @@ public async Task WriteFileAsync(string path, Stream content, CancellationToken var fullPath = GetFullPath(path); var directory = Path.GetDirectoryName(fullPath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); + } using var fileStream = File.Create(fullPath); await content.CopyToAsync(fileStream, cancellationToken); @@ -118,10 +120,11 @@ public async Task CreateDirectoryAsync(string path, CancellationToken cancellati public async Task DeleteAsync(string path, CancellationToken cancellationToken = default) { var fullPath = GetFullPath(path); - if (Directory.Exists(fullPath)) + if (Directory.Exists(fullPath)) { Directory.Delete(fullPath, recursive: true); - else if (File.Exists(fullPath)) + } else if (File.Exists(fullPath)) { File.Delete(fullPath); + } await Task.CompletedTask; } @@ -131,15 +134,17 @@ public async Task MoveAsync(string sourcePath, string targetPath, CancellationTo var targetFullPath = GetFullPath(targetPath); var targetDirectory = Path.GetDirectoryName(targetFullPath); - if (!string.IsNullOrEmpty(targetDirectory) && !Directory.Exists(targetDirectory)) + if (!string.IsNullOrEmpty(targetDirectory) && !Directory.Exists(targetDirectory)) { Directory.CreateDirectory(targetDirectory); + } - if (Directory.Exists(sourceFullPath)) + if (Directory.Exists(sourceFullPath)) { Directory.Move(sourceFullPath, targetFullPath); - else if (File.Exists(sourceFullPath)) + } else if (File.Exists(sourceFullPath)) { File.Move(sourceFullPath, targetFullPath, overwrite: true); - else + } else { throw new FileNotFoundException($"Source not found: {sourcePath}"); + } await Task.CompletedTask; } @@ -161,8 +166,9 @@ public async Task GetStorageInfoAsync(CancellationToken cancellatio public async Task ComputeHashAsync(string path, CancellationToken cancellationToken = default) { var fullPath = GetFullPath(path); - if (!File.Exists(fullPath)) + if (!File.Exists(fullPath)) { throw new FileNotFoundException($"File not found: {path}"); + } using var stream = File.OpenRead(fullPath); using var sha256 = SHA256.Create(); @@ -176,8 +182,9 @@ public async Task TestConnectionAsync(CancellationToken cancellationToken } private string GetFullPath(string relativePath) { - if (string.IsNullOrEmpty(relativePath) || relativePath == "/") + if (string.IsNullOrEmpty(relativePath) || relativePath == "/") { return _rootPath; + } // Normalize path separators and remove leading slash relativePath = relativePath.Replace('/', Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar); @@ -188,8 +195,9 @@ private string GetFullPath(string relativePath) { var normalizedFullPath = Path.GetFullPath(fullPath); var normalizedRoot = Path.GetFullPath(_rootPath); - if (!normalizedFullPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) + if (!normalizedFullPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)) { throw new UnauthorizedAccessException($"Path is outside root directory: {relativePath}"); + } return normalizedFullPath; } diff --git a/src/SharpSync/Storage/WebDavStorage.cs b/src/SharpSync/Storage/WebDavStorage.cs index e97e1e5..0f90a57 100644 --- a/src/SharpSync/Storage/WebDavStorage.cs +++ b/src/SharpSync/Storage/WebDavStorage.cs @@ -113,13 +113,15 @@ public WebDavStorage( /// Gets server capabilities for optimization /// public async Task GetServerCapabilitiesAsync(CancellationToken cancellationToken = default) { - if (_serverCapabilities != null) + if (_serverCapabilities is not null) { return _serverCapabilities; + } await _capabilitiesSemaphore.WaitAsync(cancellationToken); try { - if (_serverCapabilities != null) + if (_serverCapabilities is not null) { return _serverCapabilities; + } _serverCapabilities = await DetectServerCapabilitiesAsync(cancellationToken); return _serverCapabilities; @@ -132,8 +134,9 @@ public async Task GetServerCapabilitiesAsync(CancellationTok /// Authenticates using OAuth2 if configured /// public async Task AuthenticateAsync(CancellationToken cancellationToken = default) { - if (_oauth2Provider == null || _oauth2Config == null) + if (_oauth2Provider is null || _oauth2Config is null) { return true; // No OAuth2 configured, assume basic auth or anonymous + } await _authSemaphore.WaitAsync(cancellationToken); try { @@ -143,7 +146,7 @@ public async Task AuthenticateAsync(CancellationToken cancellationToken = } // Try refresh token first - if (_oauth2Result?.RefreshToken != null) { + if (_oauth2Result?.RefreshToken is not null) { try { _oauth2Result = await _oauth2Provider.RefreshTokenAsync(_oauth2Config, _oauth2Result.RefreshToken, cancellationToken); UpdateClientAuth(); @@ -163,7 +166,7 @@ public async Task AuthenticateAsync(CancellationToken cancellationToken = } private void UpdateClientAuth() { - if (_oauth2Result?.AccessToken != null) { + if (_oauth2Result?.AccessToken is not null) { // Recreate client with OAuth2 token var clientParams = new WebDavClientParams { BaseAddress = new Uri(_baseUrl), @@ -205,8 +208,9 @@ public async Task> ListItemsAsync(string path, Cancellatio }); if (!result.IsSuccessful) { - if (result.StatusCode == 404) + if (result.StatusCode == 404) { return Enumerable.Empty(); + } throw new HttpRequestException($"WebDAV request failed: {result.StatusCode}"); } @@ -236,12 +240,14 @@ public async Task> ListItemsAsync(string path, Cancellatio CancellationToken = cancellationToken }); - if (!result.IsSuccessful) + if (!result.IsSuccessful) { return null; + } var resource = result.Resources.FirstOrDefault(); - if (resource == null) + if (resource is null) { return null; + } return new SyncItem { Path = path, @@ -270,14 +276,15 @@ public async Task ReadFileAsync(string path, CancellationToken cancellat }); if (!response.IsSuccessful) { - if (response.StatusCode == 404) + if (response.StatusCode == 404) { throw new FileNotFoundException($"File not found: {path}"); + } throw new HttpRequestException($"WebDAV request failed: {response.StatusCode}"); } // For large files, wrap stream with progress reporting - if (needsProgress && item != null) { + if (needsProgress && item is not null) { return new ProgressStream(response.Stream, item.Size, (bytes, total) => RaiseProgressChanged(path, bytes, total, StorageOperation.Download)); } @@ -305,8 +312,9 @@ await ExecuteWithRetry(async () => { CancellationToken = cancellationToken }); - if (!result.IsSuccessful) + if (!result.IsSuccessful) { throw new HttpRequestException($"WebDAV upload failed: {result.StatusCode}"); + } return true; }, cancellationToken); @@ -350,8 +358,9 @@ await ExecuteWithRetry(async () => { CancellationToken = cancellationToken }); - if (!result.IsSuccessful) + if (!result.IsSuccessful) { throw new HttpRequestException($"WebDAV upload failed: {result.StatusCode}"); + } // Report completion RaiseProgressChanged(relativePath, totalSize, totalSize, StorageOperation.Upload); @@ -381,8 +390,9 @@ private async Task WriteFileNextcloudChunkedAsync(string fullPath, string relati while (uploadedBytes < totalSize) { var bytesRead = await content.ReadAsync(buffer, cancellationToken); - if (bytesRead == 0) + if (bytesRead == 0) { break; + } // Upload chunk var chunkPath = $"{chunkFolder}/{chunkNumber:D6}"; @@ -393,8 +403,9 @@ await ExecuteWithRetry(async () => { CancellationToken = cancellationToken }); - if (!result.IsSuccessful) + if (!result.IsSuccessful) { throw new HttpRequestException($"Chunk upload failed: {result.StatusCode}"); + } return true; }, cancellationToken); @@ -445,8 +456,9 @@ await ExecuteWithRetry(async () => { CancellationToken = cancellationToken }); - if (!result.IsSuccessful) + if (!result.IsSuccessful) { throw new HttpRequestException($"Chunk assembly failed: {result.StatusCode}"); + } return true; }, cancellationToken); @@ -464,8 +476,9 @@ await ExecuteWithRetry(async () => { CancellationToken = cancellationToken }); - if (existsResult.IsSuccessful) + if (existsResult.IsSuccessful) { return true; // Directory already exists + } var result = await _client.Mkcol(fullPath, new MkColParameters { CancellationToken = cancellationToken @@ -514,8 +527,9 @@ await ExecuteWithRetry(async () => { CancellationToken = cancellationToken }); - if (!result.IsSuccessful) + if (!result.IsSuccessful) { throw new HttpRequestException($"Move failed: {result.StatusCode}"); + } return true; }, cancellationToken); @@ -622,8 +636,9 @@ public async Task ComputeHashAsync(string path, CancellationToken cancel CancellationToken = cancellationToken }); - if (!result.IsSuccessful || result.Resources.Count == 0) + if (!result.IsSuccessful || result.Resources.Count == 0) { return null; + } var resource = result.Resources.First(); @@ -632,11 +647,12 @@ public async Task ComputeHashAsync(string path, CancellationToken cancel p.Name.LocalName == "checksum" && (p.Name.NamespaceName.Contains("owncloud") || p.Name.NamespaceName.Contains("nextcloud"))); - if (checksumProp != null && !string.IsNullOrEmpty(checksumProp.Value)) { + if (checksumProp is not null && !string.IsNullOrEmpty(checksumProp.Value)) { // Format is usually "SHA1:hash" or "MD5:hash" var parts = checksumProp.Value.Split(':'); - if (parts.Length == 2) + if (parts.Length == 2) { return parts[1]; + } } return null; @@ -658,7 +674,7 @@ private async Task DetectServerCapabilitiesAsync(Cancellatio using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; // Add auth header if using OAuth2 - if (_oauth2Result?.AccessToken != null) { + if (_oauth2Result?.AccessToken is not null) { httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _oauth2Result.AccessToken); } @@ -739,8 +755,9 @@ private string GetRelativePath(string fullUrl) { } private async Task EnsureAuthenticated(CancellationToken cancellationToken) { - if (_oauth2Provider != null) + if (_oauth2Provider is not null) { return await AuthenticateAsync(cancellationToken); + } return true; } diff --git a/src/SharpSync/Sync/SyncFilter.cs b/src/SharpSync/Sync/SyncFilter.cs index 01c6583..227e4ca 100644 --- a/src/SharpSync/Sync/SyncFilter.cs +++ b/src/SharpSync/Sync/SyncFilter.cs @@ -16,8 +16,9 @@ public class SyncFilter: ISyncFilter { /// Determines whether a file or directory should be synchronized /// public bool ShouldSync(string path) { - if (string.IsNullOrWhiteSpace(path)) + if (string.IsNullOrWhiteSpace(path)) { return false; + } // Normalize path separators path = path.Replace('\\', '/').Trim('/'); @@ -44,20 +45,23 @@ public bool ShouldSync(string path) { } } - if (!included) + if (!included) { return false; + } } // Check exclude patterns foreach (var pattern in _excludePatterns) { - if (MatchesWildcard(path, pattern)) + if (MatchesWildcard(path, pattern)) { return false; + } } // Check regex excludes foreach (var regex in _excludeRegexes) { - if (regex.IsMatch(path)) + if (regex.IsMatch(path)) { return false; + } } return true; @@ -67,14 +71,16 @@ public bool ShouldSync(string path) { /// Adds an exclusion pattern /// public void AddExclusionPattern(string pattern) { - if (string.IsNullOrWhiteSpace(pattern)) + if (string.IsNullOrWhiteSpace(pattern)) { return; + } // Replace backslashes with forward slashes but preserve trailing slash bool hasTrailingSlash = pattern.EndsWith('/') || pattern.EndsWith('\\'); pattern = pattern.Replace('\\', '/').Trim('/'); - if (hasTrailingSlash && !pattern.EndsWith('/')) + if (hasTrailingSlash && !pattern.EndsWith('/')) { pattern += '/'; + } // If it looks like a regex (contains regex special chars), compile it if (IsRegexPattern(pattern)) { @@ -94,14 +100,16 @@ public void AddExclusionPattern(string pattern) { /// Adds an inclusion pattern /// public void AddInclusionPattern(string pattern) { - if (string.IsNullOrWhiteSpace(pattern)) + if (string.IsNullOrWhiteSpace(pattern)) { return; + } // Replace backslashes with forward slashes but preserve trailing slash bool hasTrailingSlash = pattern.EndsWith('/') || pattern.EndsWith('\\'); pattern = pattern.Replace('\\', '/').Trim('/'); - if (hasTrailingSlash && !pattern.EndsWith('/')) + if (hasTrailingSlash && !pattern.EndsWith('/')) { pattern += '/'; + } // If it looks like a regex, compile it if (IsRegexPattern(pattern)) { @@ -184,10 +192,12 @@ private static bool MatchesWildcard(string path, string pattern) { // Handle simple directory patterns without wildcard (like "temp", ".git", "node_modules") if (!pattern.Contains('*') && !pattern.Contains('?')) { // Check exact match or if path is under this directory - if (path.Equals(pattern, StringComparison.OrdinalIgnoreCase)) + if (path.Equals(pattern, StringComparison.OrdinalIgnoreCase)) { return true; - if (path.StartsWith(pattern + "/", StringComparison.OrdinalIgnoreCase)) + } + if (path.StartsWith(pattern + "/", StringComparison.OrdinalIgnoreCase)) { return true; + } return false; } diff --git a/tests/SharpSync.Tests/Sync/SyncEngineTests.cs b/tests/SharpSync.Tests/Sync/SyncEngineTests.cs index c9e4bd1..8539423 100644 --- a/tests/SharpSync.Tests/Sync/SyncEngineTests.cs +++ b/tests/SharpSync.Tests/Sync/SyncEngineTests.cs @@ -80,7 +80,7 @@ public async Task SynchronizeAsync_EmptyDirectories_ReturnsSuccess() { // Assert Assert.NotNull(result); - if (!result.Success && result.Error != null) { + if (!result.Success && result.Error is not null) { throw new Exception($"Sync failed: {result.Error.Message}", result.Error); } Assert.True(result.Success); From 0af0445465ddd7ca46e777975ca4984d9f7f1d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Polykanine?= Date: Wed, 5 Nov 2025 18:51:12 +0100 Subject: [PATCH 3/3] Fix build errors --- src/SharpSync/Database/SqliteSyncDatabase.cs | 2 +- src/SharpSync/Storage/WebDavStorage.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/SharpSync/Database/SqliteSyncDatabase.cs b/src/SharpSync/Database/SqliteSyncDatabase.cs index acc2426..44bf36a 100644 --- a/src/SharpSync/Database/SqliteSyncDatabase.cs +++ b/src/SharpSync/Database/SqliteSyncDatabase.cs @@ -141,7 +141,7 @@ public async Task GetStatsAsync(CancellationToken cancellationTok s.Status == SyncStatus.RemoteDeleted).CountAsync(); var lastSyncState = await _connection!.Table() - .Where(s => s.LastSyncTime is not null) + .Where(s => s.LastSyncTime != null) .OrderByDescending(s => s.LastSyncTime) .FirstOrDefaultAsync(); diff --git a/src/SharpSync/Storage/WebDavStorage.cs b/src/SharpSync/Storage/WebDavStorage.cs index 0f90a57..104307b 100644 --- a/src/SharpSync/Storage/WebDavStorage.cs +++ b/src/SharpSync/Storage/WebDavStorage.cs @@ -217,8 +217,9 @@ public async Task> ListItemsAsync(string path, Cancellatio return result.Resources .Skip(1) // Skip the directory itself + .Where(resource => resource.Uri != null) .Select(resource => new SyncItem { - Path = GetRelativePath(resource.Uri), + Path = GetRelativePath(resource.Uri!), IsDirectory = resource.IsCollection, Size = resource.ContentLength ?? 0, LastModified = resource.LastModifiedDate?.ToUniversalTime() ?? DateTime.MinValue,