feat(vtank): slice 1c step 1 — two-folder metas/navs layout, no nav_ marker
Owner decision 2026-09-07: Meta and Nav profiles both use metaf .af and are
told apart by living in two dedicated VtankProfiles subfolders (metas/,
navs/) instead of the flat-directory nav_/--nav_ marker scheme from slice 1
Part A, which was only ever how the owner happened to name files in their
own metas repo. VtankProfileDirectory.ListMetaProfiles/ListNavigationProfiles
now enumerate metas/ and navs/ respectively via a new folder-scoped
EnumerateFolderFileNames helper; the NavMarker constant and the marker
overload of AutoCharacterFileName are deleted. MossTankMetaProfileStore and
MossTankRouteProfileStore build every real storage key with their folder
prefix (CurrentFileName, Select, Create, TryImportLegacy, the legacy-roster
sweep) and strip it back off for display (StripAf/Strip). The .cdf's Nav/Meta
lines (4-5) now carry the folder-relative key ("metas/Name.af",
"navs/Name.af"); AD-122 and the ACDREAM_VTANK_PROFILE_DIR launch-option row
are updated to describe this.
Mutation demonstrated: reverting VtankProfileDirectory.cs,
MossTankMetaProfileStore.cs, and MossTankRouteProfileStore.cs to HEAD~ (the
flat nav_-marker layout) while keeping the updated tests reproduces 8 test
failures (KeyNotFoundException / Assert.True(false) against the new
"metas/…"/"navs/…" keys the tests now expect, e.g.
MetaSaveAcceptsAnAfSuffixedNameWithoutDoublingIt,
MetaAndRouteProfilesWithTheSameNameDoNotCollide,
NavCommandsImportLegacyAndExportAf, MetaCommandsImportLegacyAndExportAf,
MetaRosterSweepConvertsEveryNamedLegacyProfileOnce,
MetaStoreLeavesLegacyJsonUntouchedWhenAfCounterpartExists,
MetaStoreRefusesToSaveADisabledRuleAndKeepsThePriorAfContent,
NavSaveAcceptsAnAfSuffixedNameWithoutDoublingIt) — confirmed by running the
suite immediately after the production-code edit, before the test-file
updates landed. All 625 tests pass after both sides of the change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
8080a99eef
commit
812a533b83
8 changed files with 204 additions and 167 deletions
|
|
@ -14,7 +14,13 @@ namespace AcDream.Plugins.MossTank;
|
|||
/// Meta profiles are plain shared files (unlike the Settings picker, there
|
||||
/// is no per-character <c>[Char] suffix</c> sub-profile carve-out here),
|
||||
/// while the per-character auto file uses VTank's <c>--Name_Server.af</c>
|
||||
/// naming and is therefore hidden from that listing by construction.
|
||||
/// naming and is therefore hidden from that listing by construction. Owner
|
||||
/// decision 2026-09-07: every real file this store touches lives under
|
||||
/// <see cref="VtankProfileDirectory.MetaFolder"/> — <c>metas/Name.af</c>,
|
||||
/// auto <c>metas/--Name_Server.af</c> — a dedicated subfolder of
|
||||
/// <see cref="IPluginHost.VtankProfiles"/>, distinct from
|
||||
/// <see cref="MossTankRouteProfileStore"/>'s <c>navs/</c> folder. No
|
||||
/// file-name marker distinguishes the two anymore.
|
||||
/// </summary>
|
||||
internal sealed class MossTankMetaProfileStore
|
||||
{
|
||||
|
|
@ -53,12 +59,19 @@ internal sealed class MossTankMetaProfileStore
|
|||
private IPluginStorage VtankStorage => _host.VtankProfiles;
|
||||
private bool CanBindFiles => _character.Length > 0 && Server.Length > 0;
|
||||
|
||||
private static string StripAf(string name) => name.Equals(
|
||||
ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||
? name
|
||||
: name.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
? name[..^3]
|
||||
: name;
|
||||
private const string FolderPrefix = VtankProfileDirectory.MetaFolder + "/";
|
||||
|
||||
private static string StripAf(string name)
|
||||
{
|
||||
if (name.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||
return name;
|
||||
string value = name.StartsWith(FolderPrefix, StringComparison.Ordinal)
|
||||
? name[FolderPrefix.Length..]
|
||||
: name;
|
||||
return value.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
? value[..^3]
|
||||
: value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> AvailableNames
|
||||
{
|
||||
|
|
@ -164,9 +177,10 @@ internal sealed class MossTankMetaProfileStore
|
|||
return true;
|
||||
}
|
||||
|
||||
string plain = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
string bare = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
? normalized
|
||||
: normalized + ".af";
|
||||
string plain = $"{VtankProfileDirectory.MetaFolder}/{bare}";
|
||||
if (VtankStorage.IsAvailable && VtankStorage.ReadText(plain) is not null)
|
||||
{
|
||||
_selected = plain;
|
||||
|
|
@ -199,9 +213,10 @@ internal sealed class MossTankMetaProfileStore
|
|||
notice = "Enter a unique Meta profile name (1-64 characters).";
|
||||
return false;
|
||||
}
|
||||
string fileName = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
string bare = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
? normalized
|
||||
: normalized + ".af";
|
||||
string fileName = $"{VtankProfileDirectory.MetaFolder}/{bare}";
|
||||
MetaProfile document = copyCurrent ? Clone(current) : new MetaProfile();
|
||||
if (!SaveTo(fileName, document, out notice))
|
||||
return false;
|
||||
|
|
@ -209,8 +224,8 @@ internal sealed class MossTankMetaProfileStore
|
|||
_pendingLegacyBareName = null;
|
||||
WriteBinding();
|
||||
notice = copyCurrent
|
||||
? $"Copied Meta profile to {fileName}."
|
||||
: $"Created Meta profile {fileName}.";
|
||||
? $"Copied Meta profile to {bare}."
|
||||
: $"Created Meta profile {bare}.";
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -246,9 +261,10 @@ internal sealed class MossTankMetaProfileStore
|
|||
notice = $"Could not import {Path.GetFileName(key)}: {error}";
|
||||
return false;
|
||||
}
|
||||
string fileName = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
string bare = normalized.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
? normalized
|
||||
: normalized + ".af";
|
||||
string fileName = $"{VtankProfileDirectory.MetaFolder}/{bare}";
|
||||
if (!SaveTo(fileName, profile, out string saveNotice))
|
||||
{
|
||||
notice = saveNotice;
|
||||
|
|
@ -257,7 +273,7 @@ internal sealed class MossTankMetaProfileStore
|
|||
_selected = fileName;
|
||||
_pendingLegacyBareName = null;
|
||||
WriteBinding();
|
||||
notice = $"Imported VTank Meta profile {fileName}.";
|
||||
notice = $"Imported VTank Meta profile {bare}.";
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +297,7 @@ internal sealed class MossTankMetaProfileStore
|
|||
_selected = ByCharacter;
|
||||
_pendingLegacyBareName = null;
|
||||
WriteBinding();
|
||||
notice = $"Deleted Meta profile {fileName}.";
|
||||
notice = $"Deleted Meta profile {StripAf(fileName)}.";
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -330,9 +346,10 @@ internal sealed class MossTankMetaProfileStore
|
|||
if (legacy is null)
|
||||
continue; // already converted (or never existed); drop the row.
|
||||
|
||||
string fileName = name.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
string bare = name.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
? name
|
||||
: name + ".af";
|
||||
string fileName = $"{VtankProfileDirectory.MetaFolder}/{bare}";
|
||||
if (VtankStorage.IsAvailable && VtankStorage.ReadText(fileName) is null)
|
||||
{
|
||||
if (!SaveTo(fileName, legacy, out string notice))
|
||||
|
|
@ -476,7 +493,7 @@ internal sealed class MossTankMetaProfileStore
|
|||
// ------------------------------------------------------------------
|
||||
|
||||
private string CurrentFileName() => _selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||
? VtankProfileDirectory.AutoCharacterFileName(_character, Server, "af")
|
||||
? $"{VtankProfileDirectory.MetaFolder}/{VtankProfileDirectory.AutoCharacterFileName(_character, Server, "af")}"
|
||||
: _selected;
|
||||
|
||||
private void WriteBinding()
|
||||
|
|
|
|||
|
|
@ -22,16 +22,19 @@ namespace AcDream.Plugins.MossTank;
|
|||
/// VTank's own split between the Settings-scoped nav prefs and the
|
||||
/// per-route file.
|
||||
///
|
||||
/// Named route files carry a <c>nav_</c> prefix (metaf's own observed
|
||||
/// convention for a stand-alone nav-only <c>.af</c>, see the committed
|
||||
/// <c>nav_*.af</c> fixtures) so a route and a Meta profile sharing the same
|
||||
/// user-typed name never collide in the shared <see cref="IPluginHost.VtankProfiles"/>
|
||||
/// directory.
|
||||
/// Owner decision 2026-09-07: every real file this store touches lives
|
||||
/// under <see cref="VtankProfileDirectory.NavFolder"/> — <c>navs/Name.af</c>,
|
||||
/// auto <c>navs/--Name_Server.af</c> — a dedicated subfolder of
|
||||
/// <see cref="IPluginHost.VtankProfiles"/>, distinct from
|
||||
/// <see cref="MossTankMetaProfileStore"/>'s <c>metas/</c> folder. A route
|
||||
/// and a Meta profile sharing the same user-typed name never collide
|
||||
/// because they live in different folders; no file-name marker (the
|
||||
/// earlier flat-directory <c>nav_</c> prefix) is needed anymore.
|
||||
/// </summary>
|
||||
internal sealed class MossTankRouteProfileStore
|
||||
{
|
||||
public const string ByCharacter = "By char";
|
||||
private const string NavPrefix = "nav_";
|
||||
private const string FolderPrefix = VtankProfileDirectory.NavFolder + "/";
|
||||
// The pre-cutover roster (round 3 item 2): before the .af cutover this
|
||||
// store kept a flat Names list here (named routes were never
|
||||
// owner-scoped — one shared, globally-hashed key per name). The
|
||||
|
|
@ -63,12 +66,12 @@ internal sealed class MossTankRouteProfileStore
|
|||
{
|
||||
if (fileName.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase))
|
||||
return fileName;
|
||||
string value = fileName;
|
||||
if (value.StartsWith(NavPrefix, StringComparison.Ordinal))
|
||||
value = value[NavPrefix.Length..];
|
||||
if (value.EndsWith(".af", StringComparison.OrdinalIgnoreCase))
|
||||
value = value[..^3];
|
||||
return value;
|
||||
string value = fileName.StartsWith(FolderPrefix, StringComparison.Ordinal)
|
||||
? fileName[FolderPrefix.Length..]
|
||||
: fileName;
|
||||
return value.EndsWith(".af", StringComparison.OrdinalIgnoreCase)
|
||||
? value[..^3]
|
||||
: value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> AvailableNames
|
||||
|
|
@ -399,19 +402,16 @@ internal sealed class MossTankRouteProfileStore
|
|||
// File naming, storage plumbing.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// Round 3 item 3: the hidden "--" prefix MUST come before the "nav_"
|
||||
// kind marker (--nav_Name_Server.af) — putting the marker first
|
||||
// (nav_--Name_Server.af, the pre-fix shape) means the filename does not
|
||||
// start with "--" at all, defeating the StartsWith("--") hidden-file
|
||||
// check both ListNavigationProfiles and ListMetaProfiles rely on and
|
||||
// leaking this character's private per-character route to every other
|
||||
// character's picker.
|
||||
// Owner decision 2026-09-07: the auto per-character route file is the
|
||||
// bare "--Name_Server.af" hidden-prefix name (same as Meta's), living
|
||||
// under this store's own navs/ folder — the folder is what keeps it out
|
||||
// of ListMetaProfiles now, not a "--" + "nav_" combined marker.
|
||||
private string CurrentFileName() => _selected.Equals(ByCharacter, StringComparison.OrdinalIgnoreCase)
|
||||
? VtankProfileDirectory.AutoCharacterFileName(_characterName, Server, "af", NavPrefix)
|
||||
? $"{VtankProfileDirectory.NavFolder}/{VtankProfileDirectory.AutoCharacterFileName(_characterName, Server, "af")}"
|
||||
: _selected;
|
||||
|
||||
private static string ToFileName(string bareName) =>
|
||||
NavPrefix + bareName + ".af";
|
||||
$"{VtankProfileDirectory.NavFolder}/{bareName}.af";
|
||||
|
||||
private void WriteBinding()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,24 +36,27 @@ internal static class VtankProfileDirectory
|
|||
internal const string NoneLabel = "[None]";
|
||||
|
||||
/// <summary>
|
||||
/// The <c>nav_</c> marker (round 3 items 3/4) that distinguishes a
|
||||
/// stand-alone route <c>.af</c> from a Meta profile sharing the same
|
||||
/// flat <see cref="IPluginHost.VtankProfiles"/> directory and extension
|
||||
/// — metaf's own observed convention for a nav-only <c>.af</c> (see the
|
||||
/// committed <c>nav_*.af</c> fixtures). MUST be checked AFTER stripping
|
||||
/// <see cref="HiddenPrefix"/> when the marker is combined with it (see
|
||||
/// <see cref="AutoCharacterFileName(string,string,string,string)"/>'s
|
||||
/// <c>marker</c> parameter): a hidden per-character route file is named
|
||||
/// <c>--nav_Name_Server.af</c> — hidden prefix FIRST, marker SECOND —
|
||||
/// never <c>nav_--Name_Server.af</c>, which does not start with
|
||||
/// <see cref="HiddenPrefix"/> at all and so defeats every
|
||||
/// <c>StartsWith("--")</c> hidden-file check in this class.
|
||||
/// Owner decision 2026-09-07 (Campaign VT slice 1c): Meta and Navigation
|
||||
/// profiles both use metaf's <c>.af</c> grammar and are told apart by
|
||||
/// living in two separate subfolders of <see cref="IPluginHost.VtankProfiles"/>,
|
||||
/// with NO file-name marker — <see cref="MetaFolder"/> for
|
||||
/// <see cref="MossTankMetaProfileStore"/>, <see cref="NavFolder"/> for
|
||||
/// <see cref="MossTankRouteProfileStore"/>. This replaces the earlier
|
||||
/// flat-directory <c>nav_</c> marker scheme (round 3 items 3/4), which
|
||||
/// was only ever how the owner happened to name files in their own metas
|
||||
/// repo, not a real VTank convention.
|
||||
/// </summary>
|
||||
internal const string NavMarker = "nav_";
|
||||
internal const string MetaFolder = "metas";
|
||||
|
||||
/// <summary>See <see cref="MetaFolder"/>.</summary>
|
||||
internal const string NavFolder = "navs";
|
||||
|
||||
/// <summary>
|
||||
/// VTank's single per-character default filename
|
||||
/// (<c>uTank2/PluginCore.cs:3863-3865</c>): <c>--Name_Server.ext</c>.
|
||||
/// Callers that store this file inside a subfolder (Meta/Nav, see
|
||||
/// <see cref="MetaFolder"/>/<see cref="NavFolder"/>) prepend that folder
|
||||
/// themselves — this method only ever produces the bare file name.
|
||||
/// </summary>
|
||||
public static string AutoCharacterFileName(
|
||||
string characterName,
|
||||
|
|
@ -61,21 +64,6 @@ internal static class VtankProfileDirectory
|
|||
string extension) =>
|
||||
$"{HiddenPrefix}{characterName}_{server}.{extension.TrimStart('.')}";
|
||||
|
||||
/// <summary>
|
||||
/// Overload for a per-character auto file that ALSO carries a kind
|
||||
/// marker (currently only <see cref="NavMarker"/>, for
|
||||
/// <see cref="MossTankRouteProfileStore"/>'s route <c>.af</c>): the
|
||||
/// hidden prefix always comes first so the file stays hidden from every
|
||||
/// other character's picker exactly like the unmarked overload above —
|
||||
/// <c>--{marker}Name_Server.ext</c>, e.g. <c>--nav_Name_Server.af</c>.
|
||||
/// </summary>
|
||||
public static string AutoCharacterFileName(
|
||||
string characterName,
|
||||
string server,
|
||||
string extension,
|
||||
string marker) =>
|
||||
$"{HiddenPrefix}{marker}{characterName}_{server}.{extension.TrimStart('.')}";
|
||||
|
||||
/// <summary>
|
||||
/// The longer, trailing-underscore prefix
|
||||
/// (<c>uTank2/PluginCore.cs:933,3866</c>, field <c>dw</c>) that marks a
|
||||
|
|
@ -186,10 +174,12 @@ internal static class VtankProfileDirectory
|
|||
/// VTank's navigation-profile list (<c>l()</c>,
|
||||
/// <c>uTank2/PluginCore.cs:7156-7185</c>): seeds
|
||||
/// <see cref="NoneLabel"/>/<see cref="ByCharacterLabel"/>, then every
|
||||
/// <see cref="NavMarker"/>-marked <c>.af</c> file that starts with
|
||||
/// neither <c>--</c> nor <c>~~</c>. Round 3 item 4: a Meta profile and a
|
||||
/// route share this same flat, single-extension directory — without
|
||||
/// the marker check this returned every Meta <c>.af</c> too.
|
||||
/// <c>.af</c> file directly under <see cref="NavFolder"/> that starts
|
||||
/// with neither <c>--</c> nor <c>~~</c>. Owner decision 2026-09-07: the
|
||||
/// folder itself is what separates route profiles from Meta profiles
|
||||
/// now — see <see cref="MetaFolder"/>. <see cref="ProfileEntry.FileName"/>
|
||||
/// carries the folder-relative real storage key (<c>navs/Name.af</c>);
|
||||
/// <see cref="ProfileEntry.DisplayName"/> is the bare name.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ProfileEntry> ListNavigationProfiles(IPluginStorage storage)
|
||||
{
|
||||
|
|
@ -198,16 +188,14 @@ internal static class VtankProfileDirectory
|
|||
new(string.Empty, NoneLabel),
|
||||
new(string.Empty, ByCharacterLabel),
|
||||
};
|
||||
foreach (string fileName in EnumerateFileNames(storage, ".af"))
|
||||
foreach (string bareName in EnumerateFolderFileNames(storage, NavFolder, ".af"))
|
||||
{
|
||||
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal)
|
||||
|| fileName.StartsWith(NavHiddenPrefix, StringComparison.Ordinal))
|
||||
if (bareName.StartsWith(HiddenPrefix, StringComparison.Ordinal)
|
||||
|| bareName.StartsWith(NavHiddenPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!fileName.StartsWith(NavMarker, StringComparison.Ordinal))
|
||||
continue;
|
||||
entries.Add(new ProfileEntry(fileName, fileName));
|
||||
entries.Add(new ProfileEntry($"{NavFolder}/{bareName}", bareName));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
|
@ -216,12 +204,12 @@ internal static class VtankProfileDirectory
|
|||
/// VTank's meta-profile list (<c>ac()</c>,
|
||||
/// <c>uTank2/PluginCore.cs:7187+</c>): seeds
|
||||
/// <see cref="NoneLabel"/>/<see cref="ByCharacterLabel"/>, then every
|
||||
/// non-<c>--</c>, non-<see cref="NavMarker"/>-marked file (a meta and a
|
||||
/// nav profile share the same directory and extension here —
|
||||
/// <c>.af</c> — so the marker is what VTank's own separate
|
||||
/// <c>.met</c>/<c>.nav</c> extensions used to provide; round 3 item 4:
|
||||
/// without excluding <see cref="NavMarker"/> files this returned every
|
||||
/// route <c>.af</c> too).
|
||||
/// non-<c>--</c> <c>.af</c> file directly under <see cref="MetaFolder"/>.
|
||||
/// Owner decision 2026-09-07: the folder itself is what separates Meta
|
||||
/// profiles from route profiles now — see <see cref="NavFolder"/>.
|
||||
/// <see cref="ProfileEntry.FileName"/> carries the folder-relative real
|
||||
/// storage key (<c>metas/Name.af</c>); <see cref="ProfileEntry.DisplayName"/>
|
||||
/// is the bare name.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ProfileEntry> ListMetaProfiles(IPluginStorage storage)
|
||||
{
|
||||
|
|
@ -230,13 +218,11 @@ internal static class VtankProfileDirectory
|
|||
new(string.Empty, NoneLabel),
|
||||
new(string.Empty, ByCharacterLabel),
|
||||
};
|
||||
foreach (string fileName in EnumerateFileNames(storage, ".af"))
|
||||
foreach (string bareName in EnumerateFolderFileNames(storage, MetaFolder, ".af"))
|
||||
{
|
||||
if (fileName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
|
||||
if (bareName.StartsWith(HiddenPrefix, StringComparison.Ordinal))
|
||||
continue;
|
||||
if (fileName.StartsWith(NavMarker, StringComparison.Ordinal))
|
||||
continue;
|
||||
entries.Add(new ProfileEntry(fileName, fileName));
|
||||
entries.Add(new ProfileEntry($"{MetaFolder}/{bareName}", bareName));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
|
@ -302,6 +288,13 @@ internal static class VtankProfileDirectory
|
|||
/// <see langword="null"/> when the file is short (line 5 is present
|
||||
/// only when the stream isn't already at EOF, <c>da.cs:125-128</c> —
|
||||
/// an older <c>.cdf</c> predating meta support has no line 5 at all).
|
||||
/// This type is a generic pass-through: <see cref="TryReadCharacterBinding"/>/
|
||||
/// <see cref="WriteCharacterBinding"/> store and return whatever strings
|
||||
/// the two callers hand them. Since owner decision 2026-09-07,
|
||||
/// <c>NavFileName</c>/<c>MetaFileName</c> are the folder-relative real
|
||||
/// storage keys <see cref="MossTankRouteProfileStore"/>/
|
||||
/// <see cref="MossTankMetaProfileStore"/> write (<c>navs/Name.af</c>,
|
||||
/// <c>metas/Name.af</c>), not a bare file name.
|
||||
/// </summary>
|
||||
public readonly record struct VtankCharacterBinding(
|
||||
string SettingsFileName,
|
||||
|
|
@ -401,4 +394,33 @@ internal static class VtankProfileDirectory
|
|||
yield return key;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists BARE file names (folder prefix stripped) directly under
|
||||
/// <paramref name="folder"/> matching <paramref name="extension"/> —
|
||||
/// the <see cref="MetaFolder"/>/<see cref="NavFolder"/> counterpart of
|
||||
/// <see cref="EnumerateFileNames"/> above. A key nested one level
|
||||
/// deeper than <paramref name="folder"/> (a sub-directory of it) is
|
||||
/// skipped for the same reason <see cref="EnumerateFileNames"/> skips
|
||||
/// any key containing '/' — neither Meta nor Nav storage is ever
|
||||
/// nested past its one folder.
|
||||
/// </summary>
|
||||
private static IEnumerable<string> EnumerateFolderFileNames(
|
||||
IPluginStorage storage,
|
||||
string folder,
|
||||
string extension)
|
||||
{
|
||||
if (!storage.IsAvailable)
|
||||
yield break;
|
||||
string folderPrefix = folder + "/";
|
||||
foreach (string bareName in storage.List(folder)
|
||||
.Where(key => key.StartsWith(folderPrefix, StringComparison.Ordinal))
|
||||
.Select(key => key[folderPrefix.Length..])
|
||||
.Where(bareName => !bareName.Contains('/', StringComparison.Ordinal))
|
||||
.Where(bareName => bareName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(static bareName => bareName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
yield return bareName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue