Skip to content

Commit 928b240

Browse files
JusterZhuclaude
andcommitted
refactor: rename Configuration entities and merge UpdateOptions into Option
Class renames: - BaseConfigInfo → UpdateConfiguration (abstract base) - Configinfo → UpdateRequest (user-facing entry) - GlobalConfigInfo → UpdateContext (runtime pipeline state) - ProcessInfo → ProcessContract (IPC serialization contract) - VersionInfo → VersionEntry (server response DTO) - Packet → PushPayload (push notification payload) - VersionOss → OssVersionRecord (OSS persistence) - GlobalConfigInfoOss → OssConfiguration (OSS config) - BlackListConfig → BlackPolicy (exclusion policy) - ConfiginfoBuilder → UpdateRequestBuilder - ConfigurationMapper (restored name) - Hooks.UpdateContext → HookContext Structural consolidation: - Promote UpdateUrl/UpgradeClientVersion/ProductId to UpdateConfiguration base class - Extract VersionIdentity base class for PushPayload/VersionEntry/OssVersionRecord - Add ToBlackPolicy() bridge from UpdateConfiguration to BlackPolicy - Merge UpdateOptions static catalog into Option class, delete UpdateOptions.cs - Rename Option<T> method to SetOption<T> to avoid class name collision Blacklist naming cleanup: - UpdateConfiguration: BlackFiles→Files, BlackFormats→Formats, SkipDirectorys→Directories - ProcessContract: same, removed legacy JsonPropertyName attributes - BlackPolicy record fields: Files, Formats, Directories - DefaultBlackListMatcher → BlackMatcher - IBlackListMatcher → IBlackMatcher - BlackListDefaults → BlackDefaults Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c11cad1 commit 928b240

92 files changed

Lines changed: 1595 additions & 1746 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/c#/GeneralUpdate.Bowl/Bowl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ internal static MonitorParameter CreateParameter()
296296
"ProcessInfo environment variable not set.");
297297
}
298298

299-
var processInfo = JsonSerializer.Deserialize<ProcessInfo>(json);
299+
var processInfo = JsonSerializer.Deserialize<ProcessContract>(json);
300300
if (processInfo == null)
301301
{
302302
GeneralTracer.Fatal("Bowl.CreateParameter: failed to deserialize ProcessInfo JSON.");

src/c#/GeneralUpdate.Bowl/Configuration/ProcessInfo.cs renamed to src/c#/GeneralUpdate.Bowl/Configuration/ProcessContract.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
namespace GeneralUpdate.Bowl.Configuration;
44

55
/// <summary>
6-
/// Minimal ProcessInfo for Bowl — only the fields needed for crash monitoring and rollback.
6+
/// Minimal ProcessContract for Bowl — only the fields needed for crash monitoring and rollback.
77
/// </summary>
8-
public class ProcessInfo
8+
public class ProcessContract
99
{
1010
[JsonPropertyName("AppName")]
1111
public string AppName { get; set; } = string.Empty;

src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ namespace GeneralUpdate.Core;
2525
/// <remarks>
2626
/// <para><b>Core flow:</b></para>
2727
/// <para>
28-
/// 1. <b>Configuration</b> — <see cref="SetConfig(Configinfo)"/> loads parameters (versions, paths, URLs).<br/>
28+
/// 1. <b>Configuration</b> — <see cref="SetConfig(UpdateRequest)"/> loads parameters (versions, paths, URLs).<br/>
2929
/// 2. <b>Extension resolution</b> — <see cref="LaunchWithStrategy"/> resolves all registered
3030
/// extension points (strategy, hooks, download components, network policies) and injects
3131
/// them into the role strategy.<br/>
@@ -44,7 +44,7 @@ namespace GeneralUpdate.Core;
4444
/// → PatchMiddleware</c> for each update version.<br/>
4545
/// 6. <b>App launch</b> — the OS strategy starts the updated main application.
4646
/// </para>
47-
/// <para><b>Silent mode:</b> when <c>Option(UpdateOptions.Silent, true)</c> is set on
47+
/// <para><b>Silent mode:</b> when <c>Option(Option.Silent, true)</c> is set on
4848
/// <see cref="AppType.Client"/>, launches a background poll loop via
4949
/// <see cref="Silent.SilentPollOrchestrator"/> and returns immediately. Updates are
5050
/// prepared on process exit.</para>
@@ -55,20 +55,20 @@ namespace GeneralUpdate.Core;
5555
/// <example>
5656
/// <code>
5757
/// var result = await new GeneralUpdateBootstrap()
58-
/// .SetConfig(new Configinfo {
58+
/// .SetConfig(new UpdateRequest {
5959
/// UpdateUrl = "https://api.example.com",
6060
/// ClientVersion = "1.0.0",
6161
/// InstallPath = @"C:\MyApp",
6262
/// AppSecretKey = "my-key"
6363
/// })
64-
/// .Option(UpdateOptions.AppType, AppType.Client)
64+
/// .SetOption(Option.AppType, AppType.Client)
6565
/// .Hooks&lt;MyCustomHooks&gt;()
6666
/// .LaunchAsync();
6767
/// </code>
6868
/// </example>
6969
public class GeneralUpdateBootstrap : AbstractBootstrap<GeneralUpdateBootstrap, IStrategy>
7070
{
71-
private GlobalConfigInfo _configInfo = new();
71+
private UpdateContext _configInfo = new();
7272
private Func<UpdateInfoEventArgs, bool>? _updatePrecheck;
7373
private CancellationTokenSource? _cts;
7474
private DiffPipelineBuilder? _diffPipelineBuilder;
@@ -100,11 +100,11 @@ public void Cancel()
100100
/// <returns>This bootstrap instance for chaining.</returns>
101101
public override async Task<GeneralUpdateBootstrap> LaunchAsync()
102102
{
103-
var appType = GetOption(UpdateOptions.AppType);
103+
var appType = GetOption(Option.AppType);
104104
_configInfo.AppType = appType;
105105

106106
// Silent mode: start background poll and return immediately
107-
if (appType == AppType.Client && GetOption(UpdateOptions.Silent))
107+
if (appType == AppType.Client && GetOption(Option.Silent))
108108
{
109109
await LaunchSilentAsync().ConfigureAwait(false);
110110
return this;
@@ -160,23 +160,23 @@ private async Task<GeneralUpdateBootstrap> LaunchWithStrategy(IStrategy roleStra
160160

161161
/// <summary>
162162
/// Applies the primary configuration object. Validates required fields, maps to
163-
/// the internal <see cref="GlobalConfigInfo"/>, and initialises the blacklist
163+
/// the internal <see cref="UpdateContext"/>, and initialises the blacklist
164164
/// matcher for file exclusion during update operations.
165165
/// </summary>
166166
/// <param name="configInfo">User-facing configuration. All required fields must
167167
/// be set explicitly — no auto-discovery is performed. Use
168168
/// <see cref="SetSource"/> for the zero-config path.</param>
169169
/// <returns>This bootstrap instance for chaining.</returns>
170-
public GeneralUpdateBootstrap SetConfig(Configinfo configInfo)
170+
public GeneralUpdateBootstrap SetConfig(UpdateRequest configInfo)
171171
{
172172
configInfo.Validate();
173-
_configInfo = ConfigurationMapper.MapToGlobalConfigInfo(configInfo);
173+
_configInfo = ConfigurationMapper.MapToUpdateContext(configInfo);
174174

175-
var appType = GetOption(UpdateOptions.AppType);
175+
var appType = GetOption(Option.AppType);
176176
if (appType != AppType.Upgrade)
177177
{
178178
_configInfo.TempPath = StorageManager.GetTempDirectory("upgrade_temp");
179-
InitBlackList();
179+
InitBlackPolicy();
180180
}
181181

182182
return this;
@@ -193,11 +193,11 @@ public GeneralUpdateBootstrap SetConfig(Configinfo configInfo)
193193
/// <returns>This bootstrap instance for chaining.</returns>
194194
/// <exception cref="ArgumentNullException">Thrown when <paramref name="filePath"/> is <c>null</c> or whitespace.</exception>
195195
/// <exception cref="FileNotFoundException">Thrown when the specified file does not exist.</exception>
196-
/// <exception cref="InvalidOperationException">Thrown when the JSON file cannot be deserialised into a valid <see cref="Configinfo"/>.</exception>
196+
/// <exception cref="InvalidOperationException">Thrown when the JSON file cannot be deserialised into a valid <see cref="UpdateRequest"/>.</exception>
197197
/// <remarks>
198-
/// Reads the file content as UTF-8 JSON, deserialises it into a <see cref="Configinfo"/>
198+
/// Reads the file content as UTF-8 JSON, deserialises it into a <see cref="UpdateRequest"/>
199199
/// using the source-generated JSON serialisation context (<see cref="JsonContext.HttpParameterJsonContext"/>),
200-
/// then delegates to <see cref="SetConfig(Configinfo)"/> for validation and mapping.
200+
/// then delegates to <see cref="SetConfig(UpdateRequest)"/> for validation and mapping.
201201
/// </remarks>
202202
public GeneralUpdateBootstrap SetConfig(string filePath)
203203
{
@@ -215,7 +215,7 @@ public GeneralUpdateBootstrap SetConfig(string filePath)
215215
throw new FileNotFoundException($"Config file not found: {fullPath}");
216216

217217
var json = File.ReadAllText(fullPath);
218-
var config = JsonSerializer.Deserialize(json, JsonContext.HttpParameterJsonContext.Default.Configinfo);
218+
var config = JsonSerializer.Deserialize(json, JsonContext.HttpParameterJsonContext.Default.UpdateRequest);
219219
if (config == null)
220220
throw new InvalidOperationException($"Failed to parse config file: {fullPath}");
221221

@@ -234,7 +234,7 @@ public GeneralUpdateBootstrap SetSource(
234234
string? scheme = null,
235235
string? token = null)
236236
{
237-
var config = new Configinfo
237+
var config = new UpdateRequest
238238
{
239239
UpdateUrl = updateUrl,
240240
AppSecretKey = appSecretKey,
@@ -287,14 +287,14 @@ public GeneralUpdateBootstrap AddListenerUpdatePrecheck(Func<UpdateInfoEventArgs
287287

288288
private void InitializeFromEnvironment()
289289
{
290-
// Read ProcessInfo via AES-encrypted file IPC.
290+
// Read ProcessContract via AES-encrypted file IPC.
291291
// The Upgrade process is only ever launched by the Client — no IPC means
292292
// there is nothing to do. The Client's manifest.json flows through IPC,
293293
// so the Upgrade never needs to load one directly.
294-
var processInfo = new EncryptedFileProcessInfoProvider().Receive();
294+
var processInfo = new EncryptedFileProcessContractProvider().Receive();
295295
if (processInfo == null) return;
296296

297-
_configInfo = new GlobalConfigInfo
297+
_configInfo = new UpdateContext
298298
{
299299
MainAppName = processInfo.AppName,
300300
InstallPath = processInfo.InstallPath,
@@ -315,41 +315,41 @@ private void InitializeFromEnvironment()
315315
UpdatePath = processInfo.UpdatePath,
316316
LaunchClientAfterUpdate = processInfo.LaunchClientAfterUpdate,
317317
ReportType = processInfo.ReportType,
318-
BlackFiles = processInfo.BlackFiles ?? BlackListDefaults.DefaultBlackFiles,
319-
BlackFormats = processInfo.BlackFileFormats ?? BlackListDefaults.DefaultBlackFormats,
320-
SkipDirectorys = processInfo.SkipDirectorys ?? BlackListDefaults.DefaultSkipDirectories
318+
Files = processInfo.Files ?? BlackDefaults.DefaultFiles,
319+
Formats = processInfo.Formats ?? BlackDefaults.DefaultFormats,
320+
Directories = processInfo.Directories ?? BlackDefaults.DefaultDirectories
321321
};
322322

323-
StorageManager.BlackListMatcher = DefaultBlackListMatcher.FromConfigInfo(_configInfo);
323+
StorageManager.BlackMatcher = BlackMatcher.FromConfigInfo(_configInfo);
324324
}
325325

326326
/// <summary>
327-
/// Applies UpdateOptions to _configInfo.
327+
/// Applies Option to _configInfo.
328328
/// Uses ??= only for values that InitializeFromEnvironment() may have already
329329
/// populated on the Upgrade path (Encoding, Format, DownloadTimeOut).
330-
/// All other options are always applied from UpdateOptions — their defaults
330+
/// All other options are always applied from Option — their defaults
331331
/// are already functionally reasonable (e.g. MaxConcurrency=3, RetryCount=3).
332332
/// </summary>
333333
private void ApplyRuntimeOptions()
334334
{
335335
// Preserve Upgrade path values set by InitializeFromEnvironment()
336-
_configInfo.Encoding ??= GetOption(UpdateOptions.Encoding);
337-
_configInfo.Format = GetOption(UpdateOptions.Format);
336+
_configInfo.Encoding ??= GetOption(Option.Encoding);
337+
_configInfo.Format = GetOption(Option.Format);
338338
if (_configInfo.DownloadTimeOut <= 0)
339-
_configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;
339+
_configInfo.DownloadTimeOut = GetOption(Option.DownloadTimeout) ?? 60;
340340

341341
// bool? options: use ??= so user-configured false is preserved
342-
_configInfo.PatchEnabled ??= GetOption(UpdateOptions.PatchEnabled);
343-
_configInfo.BackupEnabled ??= GetOption(UpdateOptions.BackupEnabled);
342+
_configInfo.PatchEnabled ??= GetOption(Option.PatchEnabled);
343+
_configInfo.BackupEnabled ??= GetOption(Option.BackupEnabled);
344344

345-
// Always apply from UpdateOptions — no other code sets these before
345+
// Always apply from Option — no other code sets these before
346346
// ApplyRuntimeOptions() runs. Defaults are functionally reasonable.
347-
_configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency);
348-
_configInfo.EnableResume = GetOption(UpdateOptions.EnableResume);
349-
_configInfo.RetryCount = GetOption(UpdateOptions.RetryCount);
350-
_configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval);
351-
_configInfo.VerifyChecksum = GetOption(UpdateOptions.VerifyChecksum);
352-
_configInfo.DiffMode = GetOption(UpdateOptions.DiffMode);
347+
_configInfo.MaxConcurrency = GetOption(Option.MaxConcurrency);
348+
_configInfo.EnableResume = GetOption(Option.EnableResume);
349+
_configInfo.RetryCount = GetOption(Option.RetryCount);
350+
_configInfo.RetryInterval = GetOption(Option.RetryInterval);
351+
_configInfo.VerifyChecksum = GetOption(Option.VerifyChecksum);
352+
_configInfo.DiffMode = GetOption(Option.DiffMode);
353353
}
354354

355355
/// <summary>
@@ -374,8 +374,8 @@ private async Task<GeneralUpdateBootstrap> LaunchSilentAsync()
374374

375375
strategy.LaunchAfterPrepare = false;
376376

377-
var pollMinutes = GetOption(UpdateOptions.SilentPollIntervalMinutes);
378-
var launchClient = GetOption(UpdateOptions.LaunchClientAfterUpdate);
377+
var pollMinutes = GetOption(Option.SilentPollIntervalMinutes);
378+
var launchClient = GetOption(Option.LaunchClientAfterUpdate);
379379
var silentOptions = new Silent.SilentOptions
380380
{
381381
PollInterval = TimeSpan.FromMinutes(pollMinutes),
@@ -477,18 +477,18 @@ private static Format ParseFormat(string? compressFormat)
477477
};
478478
}
479479

480-
private void InitBlackList()
480+
private void InitBlackPolicy()
481481
{
482-
// Build blacklist matcher from GlobalConfigInfo and set on StorageManager.
482+
// Build blacklist matcher from UpdateContext and set on StorageManager.
483483
// The matcher combines user config with system defaults.
484-
var effectiveConfig = new BlackListConfig(
485-
_configInfo.BlackFiles?.Count > 0 ? _configInfo.BlackFiles : BlackListDefaults.DefaultBlackFiles,
486-
_configInfo.BlackFormats?.Count > 0 ? _configInfo.BlackFormats : BlackListDefaults.DefaultBlackFormats,
487-
_configInfo.SkipDirectorys?.Count > 0
488-
? _configInfo.SkipDirectorys
489-
: BlackListDefaults.DefaultSkipDirectories
484+
var effectiveConfig = new BlackPolicy(
485+
_configInfo.Files?.Count > 0 ? _configInfo.Files : BlackDefaults.DefaultFiles,
486+
_configInfo.Formats?.Count > 0 ? _configInfo.Formats : BlackDefaults.DefaultFormats,
487+
_configInfo.Directories?.Count > 0
488+
? _configInfo.Directories
489+
: BlackDefaults.DefaultDirectories
490490
);
491-
StorageManager.BlackListMatcher = new DefaultBlackListMatcher(effectiveConfig);
491+
StorageManager.BlackMatcher = new BlackMatcher(effectiveConfig);
492492
}
493493

494494
private async Task CallSmallBowlHomeAsync(string processName)

src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ namespace GeneralUpdate.Core.Configuration
2121
/// <c>.DownloadSource&lt;T&gt;()</c> and are lazily instantiated on demand through
2222
/// <see cref="ResolveExtension{TExtension}"/>.<br/>
2323
/// - The <c>_instances</c> dictionary stores already-instantiated singleton objects
24-
/// (e.g., <c>BlackListConfig</c>). These take precedence over lazy registrations
24+
/// (e.g., <c>BlackPolicy</c>). These take precedence over lazy registrations
2525
/// in <c>_extensions</c>.<br/>
26-
/// - The <see cref="Option{T}(UpdateOption{T}, T)"/> method provides fluent configuration
27-
/// options, read via <see cref="GetOption{T}(UpdateOption{T}?)"/>, with a default-value
26+
/// - The <see cref="Option{T}(Option{T}, T)"/> method provides fluent configuration
27+
/// options, read via <see cref="GetOption{T}(Option{T}?)"/>, with a default-value
2828
/// fallback mechanism.
2929
/// </para>
3030
/// <para>Typical usage: chain extension registrations together, then call
@@ -34,17 +34,17 @@ public abstract class AbstractBootstrap<TBootstrap, TStrategy>
3434
where TBootstrap : AbstractBootstrap<TBootstrap, TStrategy>
3535
where TStrategy : IStrategy
3636
{
37-
private readonly ConcurrentDictionary<UpdateOption, UpdateOptionValue> _options;
37+
private readonly ConcurrentDictionary<Option, OptionValue> _options;
3838

3939
/// <summary>User-registered extension type mappings (interface type → implementation type), used for lazy instantiation.</summary>
4040
private readonly Dictionary<Type, Type> _extensions = new();
4141

42-
/// <summary>Registered singleton instances (e.g., <c>BlackListConfig</c>).</summary>
42+
/// <summary>Registered singleton instances (e.g., <c>BlackPolicy</c>).</summary>
4343
private readonly Dictionary<Type, object> _instances = new();
4444

4545
protected internal AbstractBootstrap()
4646
{
47-
_options = new ConcurrentDictionary<UpdateOption, UpdateOptionValue>();
47+
_options = new ConcurrentDictionary<Option, OptionValue>();
4848
}
4949

5050
public abstract Task<TBootstrap> LaunchAsync();
@@ -58,17 +58,17 @@ protected internal AbstractBootstrap()
5858
/// from the dictionary so that subsequent reads fall back to the default value.</param>
5959
/// <returns>The current <typeparamref name="TBootstrap"/> instance for chaining.</returns>
6060
/// <remarks>
61-
/// Options are stored in a <c>ConcurrentDictionary</c> to guarantee thread safety.
61+
/// Option are stored in a <c>ConcurrentDictionary</c> to guarantee thread safety.
6262
/// When <paramref name="value"/> is <c>null</c>, the entry is removed, causing
63-
/// <see cref="GetOption{T}(UpdateOption{T}?)"/> to return
64-
/// <see cref="UpdateOption{T}.DefaultValue"/>.
63+
/// <see cref="GetOption{T}(Option{T}?)"/> to return
64+
/// <see cref="Option{T}.DefaultValue"/>.
6565
/// </remarks>
66-
public TBootstrap Option<T>(UpdateOption<T> option, T value)
66+
public TBootstrap SetOption<T>(Option<T> option, T value)
6767
{
6868
if (value == null)
6969
_options.TryRemove(option, out _);
7070
else
71-
_options[option] = new UpdateOptionValue<T>(option, value);
71+
_options[option] = new OptionValue<T>(option, value);
7272
return (TBootstrap)this;
7373
}
7474

@@ -79,14 +79,14 @@ public TBootstrap Option<T>(UpdateOption<T> option, T value)
7979
/// <typeparam name="T">The type of the option value.</typeparam>
8080
/// <param name="option">The option key to retrieve; can be <c>null</c>.</param>
8181
/// <returns>
82-
/// The registered value if found; otherwise, <see cref="UpdateOption{T}.DefaultValue"/>.
82+
/// The registered value if found; otherwise, <see cref="Option{T}.DefaultValue"/>.
8383
/// </returns>
8484
/// <remarks>
8585
/// First attempts to look up the option in the <c>_options</c> dictionary.
86-
/// If not found, falls back to <see cref="UpdateOption{T}.DefaultValue"/>.
87-
/// This is the companion read method for <see cref="Option{T}(UpdateOption{T}, T)"/>.
86+
/// If not found, falls back to <see cref="Option{T}.DefaultValue"/>.
87+
/// This is the companion read method for <see cref="Option{T}(Option{T}, T)"/>.
8888
/// </remarks>
89-
protected T GetOption<T>(UpdateOption<T>? option)
89+
protected T GetOption<T>(Option<T>? option)
9090
{
9191
if (option == null) return default!;
9292
if (_options.TryGetValue(option, out var val) && val != null)

src/c#/GeneralUpdate.Core/Configuration/AppMetadataDiscoverer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@
33
namespace GeneralUpdate.Core.Configuration;
44

55
/// <summary>
6-
/// Fills missing identity fields in a <see cref="Configinfo"/> by probing
6+
/// Fills missing identity fields in a <see cref="UpdateRequest"/> by probing
77
/// <c>generalupdate.manifest.json</c>, then falling back to hard-coded defaults.
88
/// </summary>
99
public static class AppMetadataDiscoverer
1010
{
1111
/// <summary>
1212
/// Discover and fill every null-or-empty identity field in <paramref name="seed"/>.
1313
/// </summary>
14-
public static Configinfo Discover(Configinfo seed)
14+
public static UpdateRequest Discover(UpdateRequest seed)
1515
{
1616
if (seed == null)
1717
throw new System.ArgumentNullException(nameof(seed));
1818

1919
var manifest = ManifestInfo.Load();
2020

2121
// Identity fields — manifest overrides defaults, not just nulls.
22-
// (BaseConfigInfo pre-fills UpdateAppName with "Update.exe", so simple
22+
// (UpdateConfiguration pre-fills UpdateAppName with "Update.exe", so simple
2323
// null-coalescing would never pick up the manifest value.)
2424
if (manifest != null)
2525
{

src/c#/GeneralUpdate.Core/Configuration/BlackListConfig.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Collections.Generic;
2+
3+
namespace GeneralUpdate.Core.Configuration;
4+
5+
public sealed record BlackPolicy(
6+
IReadOnlyList<string>? Files = null,
7+
IReadOnlyList<string>? Formats = null,
8+
IReadOnlyList<string>? Directories = null
9+
)
10+
{
11+
public static BlackPolicy Empty { get; } = new();
12+
public bool HasRules =>
13+
(Files?.Count > 0) || (Formats?.Count > 0) || (Directories?.Count > 0);
14+
}

0 commit comments

Comments
 (0)