diff --git a/Directory.Packages.props b/Directory.Packages.props
index fe3463c..9e44184 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,8 +10,8 @@
-
-
+
+
-
\ No newline at end of file
+
diff --git a/src/Ramstack.FileProviders.Globbing/GlobbingFileProvider.cs b/src/Ramstack.FileProviders.Globbing/GlobbingFileProvider.cs
index 6021836..89f8986 100644
--- a/src/Ramstack.FileProviders.Globbing/GlobbingFileProvider.cs
+++ b/src/Ramstack.FileProviders.Globbing/GlobbingFileProvider.cs
@@ -1,4 +1,4 @@
-using Ramstack.Globbing;
+using Ramstack.FileProviders.Internal;
namespace Ramstack.FileProviders;
@@ -60,10 +60,9 @@ public GlobbingFileProvider(IFileProvider provider, string[] patterns, string[]?
public IFileInfo GetFileInfo(string subpath)
{
subpath = FilePath.Normalize(subpath);
- if (!IsExcluded(subpath) && IsIncluded(subpath))
- return _provider.GetFileInfo(subpath);
-
- return new NotFoundFileInfo(subpath);
+ return IsFileIncluded(subpath)
+ ? _provider.GetFileInfo(subpath)
+ : new NotFoundFileInfo(subpath);
}
///
@@ -71,7 +70,7 @@ public IDirectoryContents GetDirectoryContents(string subpath)
{
subpath = FilePath.Normalize(subpath);
- if (!IsExcluded(subpath))
+ if (IsDirectoryIncluded(subpath))
{
var directory = _provider.GetDirectoryContents(subpath);
if (directory is not NotFoundDirectoryContents)
@@ -85,23 +84,26 @@ public IDirectoryContents GetDirectoryContents(string subpath)
public IChangeToken Watch(string filter) =>
_provider.Watch(filter);
- private bool IsIncluded(string path)
- {
- foreach (var pattern in _patterns)
- if (Matcher.IsMatch(path, pattern, MatchFlags.Unix))
- return true;
-
- return false;
- }
-
- private bool IsExcluded(string path)
- {
- foreach (var pattern in _excludes)
- if (Matcher.IsMatch(path, pattern, MatchFlags.Unix))
- return true;
+ ///
+ /// Determines if a file is included based on the specified patterns and exclusions.
+ ///
+ /// The path of the file.
+ ///
+ /// if the file is included;
+ /// otherwise, .
+ ///
+ private bool IsFileIncluded(string path) =>
+ !PathHelper.IsMatch(path, _excludes) && PathHelper.IsMatch(path, _patterns);
- return false;
- }
+ ///
+ /// Determines if a directory is included based on the specified exclusions.
+ ///
+ /// The path of the directory.
+ ///
+ /// if the directory is included; otherwise, .
+ ///
+ private bool IsDirectoryIncluded(string path) =>
+ path == "/" || !PathHelper.IsMatch(path, _excludes) && PathHelper.IsPartialMatch(path, _patterns);
#region Inner type: GlobbingDirectoryContents
@@ -136,9 +138,8 @@ public IEnumerator GetEnumerator()
foreach (var file in _directory)
{
var path = FilePath.Join(_directoryPath, file.Name);
- if (!_provider.IsExcluded(path))
- if (file.IsDirectory || _provider.IsIncluded(path))
- yield return file;
+ if (file.IsDirectory ? _provider.IsDirectoryIncluded(path) : _provider.IsFileIncluded(path))
+ yield return file;
}
}
diff --git a/src/Ramstack.FileProviders.Globbing/Internal/PathHelper.cs b/src/Ramstack.FileProviders.Globbing/Internal/PathHelper.cs
new file mode 100644
index 0000000..ea0a405
--- /dev/null
+++ b/src/Ramstack.FileProviders.Globbing/Internal/PathHelper.cs
@@ -0,0 +1,339 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.Arm;
+using System.Runtime.Intrinsics.X86;
+
+using Ramstack.Globbing;
+
+namespace Ramstack.FileProviders.Internal;
+
+///
+/// Provides helper methods for path manipulations.
+///
+internal static class PathHelper
+{
+ ///
+ /// Determines whether the specified path matches any of the specified patterns.
+ ///
+ /// The path to match for a match.
+ /// An array of patterns to match against the path.
+ ///
+ /// if the path matches any of the patterns;
+ /// otherwise, .
+ ///
+ public static bool IsMatch(scoped ReadOnlySpan path, string[] patterns)
+ {
+ foreach (var pattern in patterns)
+ if (Matcher.IsMatch(path, pattern, MatchFlags.Unix))
+ return true;
+
+ return false;
+ }
+
+ ///
+ /// Determines whether the specified path partially matches any of the specified patterns.
+ ///
+ /// The path to be partially matched.
+ /// An array of patterns to match against the path.
+ ///
+ /// if the path partially matches any of the patterns;
+ /// otherwise, .
+ ///
+ public static bool IsPartialMatch(scoped ReadOnlySpan path, string[] patterns)
+ {
+ Debug.Assert(path is not "/");
+
+ var count = CountPathSegments(path);
+
+ foreach (var pattern in patterns)
+ if (Matcher.IsMatch(path, GetPartialPattern(pattern, count), MatchFlags.Unix))
+ return true;
+
+ return false;
+ }
+
+ ///
+ /// Counts the number of segments in the specified path.
+ ///
+ /// The path to count segments for.
+ ///
+ /// The number of segments in the path.
+ ///
+ private static int CountPathSegments(scoped ReadOnlySpan path)
+ {
+ var count = 0;
+ var iterator = new PathSegmentIterator();
+ ref var s = ref Unsafe.AsRef(in MemoryMarshal.GetReference(path));
+ var length = path.Length;
+
+ while (true)
+ {
+ var r = iterator.GetNext(ref s, length);
+
+ if (r.start != r.final)
+ count++;
+
+ if (r.final == length)
+ break;
+ }
+
+ if (count == 0)
+ count = 1;
+
+ return count;
+ }
+
+ ///
+ /// Returns a partial pattern from the specified pattern string based on the specified depth.
+ ///
+ /// The pattern string to extract from.
+ /// The depth level to extract the partial pattern up to.
+ ///
+ /// A representing the partial pattern.
+ ///
+ private static ReadOnlySpan GetPartialPattern(string pattern, int depth)
+ {
+ Debug.Assert(depth >= 1);
+
+ var iterator = new PathSegmentIterator();
+ ref var s = ref Unsafe.AsRef(in pattern.GetPinnableReference());
+ var length = pattern.Length;
+
+ while (true)
+ {
+ var r = iterator.GetNext(ref s, length);
+ if (r.start != r.final)
+ depth--;
+
+ if (depth < 1
+ || r.final == length
+ || IsGlobStar(ref s, r.start, r.final))
+ return MemoryMarshal.CreateReadOnlySpan(ref s, r.final);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static bool IsGlobStar(ref char s, int index, int final) =>
+ index + 2 == final && Unsafe.ReadUnaligned(
+ ref Unsafe.As(
+ ref Unsafe.Add(ref s, (nint)(uint)index))) == ('*' << 16 | '*');
+ }
+
+ #region Vector helper methods
+
+ ///
+ /// Loads a 256-bit vector from the specified source.
+ ///
+ /// The source from which the vector will be loaded.
+ /// The offset from the from which the vector will be loaded.
+ ///
+ /// The loaded 256-bit vector.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 LoadVector256(ref char source, nint offset) =>
+ Unsafe.ReadUnaligned>(
+ ref Unsafe.As(ref Unsafe.Add(ref source, offset)));
+
+ ///
+ /// Loads a 128-bit vector from the specified source.
+ ///
+ /// The source from which the vector will be loaded.
+ /// The offset from from which the vector will be loaded.
+ ///
+ /// The loaded 128-bit vector.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 LoadVector128(ref char source, nint offset) =>
+ Unsafe.ReadUnaligned>(
+ ref Unsafe.As(
+ ref Unsafe.Add(ref source, offset)));
+
+ #endregion
+
+ #region Inner type: PathSegmentIterator
+
+ ///
+ /// Provides functionality to iterate over segments of a path.
+ ///
+ private struct PathSegmentIterator
+ {
+ private int _last;
+ private nint _position;
+ private uint _mask;
+
+ ///
+ /// Initializes a new instance of the structure.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PathSegmentIterator() =>
+ _last = -1;
+
+ ///
+ /// Retrieves the next segment of the path.
+ ///
+ /// A reference to the starting character of the path.
+ /// The total number of characters in the input path starting from .
+ ///
+ /// A tuple containing the start and end indices of the next path segment.
+ /// start indicates the beginning of the segment, and final satisfies
+ /// the condition that final - start equals the length of the segment.
+ /// The end of the iteration is indicated by final being equal to the length of the path.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public (int start, int final) GetNext(ref char source, int length)
+ {
+ var start = _last + 1;
+
+ while ((int)_position < length)
+ {
+ if ((Avx2.IsSupported || Sse2.IsSupported || AdvSimd.Arm64.IsSupported) && _mask != 0)
+ {
+ var offset = BitOperations.TrailingZeroCount(_mask);
+ if (AdvSimd.IsSupported)
+ {
+ //
+ // On ARM, ExtractMostSignificantBits returns a mask where each bit
+ // represents one vector element (1 bit per ushort), so offset
+ // directly corresponds to the element index
+ //
+ _last = (int)(_position + (nint)(uint)offset);
+
+ //
+ // Clear the bits for the current separator
+ //
+ _mask &= ~(1u << offset);
+ }
+ else
+ {
+ //
+ // On x86, MoveMask (and ExtractMostSignificantBits on byte-based vectors)
+ // returns a mask where each bit represents one byte (2 bits per ushort),
+ // so we need to divide offset by 2 to get the actual element index
+ //
+ _last = (int)(_position + (nint)((uint)offset >> 1));
+
+ //
+ // Clear the bits for the current separator
+ //
+ _mask &= ~(0b_11u << offset);
+ }
+
+ //
+ // Advance position to the next chunk when no separators remain in the mask
+ //
+ if (_mask == 0)
+ {
+ //
+ // https://github.com/dotnet/runtime/issues/117416
+ //
+ // Precompute the stride size instead of calculating it inline
+ // to avoid stack spilling. For some unknown reason, the JIT
+ // fails to optimize properly when this is written inline, like so:
+ // _position += Avx2.IsSupported
+ // ? Vector256.Count
+ // : Vector128.Count;
+ //
+
+ var stride = Avx2.IsSupported
+ ? Vector256.Count
+ : Vector128.Count;
+
+ _position += stride;
+ }
+
+ return (start, _last);
+ }
+
+ if (Avx2.IsSupported && (int)_position + Vector256.Count <= length)
+ {
+ var chunk = LoadVector256(ref source, _position);
+ var slash = Vector256.Create('/');
+ var comparison = Avx2.CompareEqual(chunk, slash);
+
+ //
+ // Store the comparison bitmask and reuse it across iterations
+ // as long as it contains non-zero bits.
+ // This avoids reloading SIMD registers and repeating comparisons
+ // on the same chunk of data.
+ //
+ _mask = (uint)Avx2.MoveMask(comparison.AsByte());
+
+ //
+ // Advance position to the next chunk when no separators found
+ //
+ if (_mask == 0)
+ _position += Vector256.Count;
+ }
+ else if (Sse2.IsSupported && !Avx2.IsSupported && (int)_position + Vector128.Count <= length)
+ {
+ var chunk = LoadVector128(ref source, _position);
+ var slash = Vector128.Create('/');
+ var comparison = Sse2.CompareEqual(chunk, slash);
+
+ //
+ // Store the comparison bitmask and reuse it across iterations
+ // as long as it contains non-zero bits.
+ // This avoids reloading SIMD registers and repeating comparisons
+ // on the same chunk of data.
+ //
+ _mask = (uint)Sse2.MoveMask(comparison.AsByte());
+
+ //
+ // Advance position to the next chunk when no separators found
+ //
+ if (_mask == 0)
+ _position += Vector128.Count;
+ }
+ else if (AdvSimd.Arm64.IsSupported && (int)_position + Vector128.Count <= length)
+ {
+ var chunk = LoadVector128(ref source, _position);
+ var slash = Vector128.Create('/');
+ var comparison = AdvSimd.CompareEqual(chunk, slash);
+
+ //
+ // Store the comparison bitmask and reuse it across iterations
+ // as long as it contains non-zero bits.
+ // This avoids reloading SIMD registers and repeating comparisons
+ // on the same chunk of data.
+ //
+ _mask = ExtractMostSignificantBits(comparison);
+
+ //
+ // Advance position to the next chunk when no separators found
+ //
+ if (_mask == 0)
+ _position += Vector128.Count;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static uint ExtractMostSignificantBits(Vector128 v)
+ {
+ var sum = AdvSimd.Arm64.AddAcross(
+ AdvSimd.ShiftLogical(
+ AdvSimd.And(v, Vector128.Create((ushort)0x8000)),
+ Vector128.Create(-15, -14, -13, -12, -11, -10, -9, -8)));
+ return sum.ToScalar();
+ }
+ }
+ else
+ {
+ for (; (int)_position < length; _position++)
+ {
+ var ch = Unsafe.Add(ref source, _position);
+ if (ch == '/')
+ {
+ _last = (int)_position;
+ _position++;
+
+ return (start, _last);
+ }
+ }
+ }
+ }
+
+ return (start, length);
+ }
+ }
+
+ #endregion
+}
diff --git a/tests/Ramstack.FileProviders.Tests/AbstractFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/AbstractFileProviderTests.cs
index 68a80a4..d74c2ef 100644
--- a/tests/Ramstack.FileProviders.Tests/AbstractFileProviderTests.cs
+++ b/tests/Ramstack.FileProviders.Tests/AbstractFileProviderTests.cs
@@ -61,12 +61,9 @@ public void GetFileInfo_NonExistingFile()
{
using var provider = CreateFileProvider();
- var name = $"{Guid.NewGuid()}.txt";
- var info = provider.GetFileInfo(name);
+ var info = provider.GetFileInfo("/project/8f723faf0ee0.txt");
- Assert.That(
- FilePath.GetFileName(info.Name),
- Is.EqualTo(name));
+ Assert.That(info.Name, Is.EqualTo("8f723faf0ee0.txt").Or.EqualTo("/project/8f723faf0ee0.txt"));
Assert.That(info.IsDirectory, Is.False);
Assert.That(info.Exists, Is.False);
}
@@ -76,8 +73,7 @@ public void GetDirectoryContents_NonExistingDirectory()
{
using var provider = CreateFileProvider();
- var name = Guid.NewGuid().ToString();
- var info = provider.GetDirectoryContents($"/{name}");
+ var info = provider.GetDirectoryContents("/project/c800f57d/7c71");
Assert.That(info.Exists, Is.False);
}
diff --git a/tests/Ramstack.FileProviders.Tests/FilePathTests.cs b/tests/Ramstack.FileProviders.Tests/FilePathTests.cs
index fd274f3..25febd8 100644
--- a/tests/Ramstack.FileProviders.Tests/FilePathTests.cs
+++ b/tests/Ramstack.FileProviders.Tests/FilePathTests.cs
@@ -45,6 +45,7 @@ public void GetFileName(string path, string expected)
[TestCase("", "")]
[TestCase("/", "")]
+ [TestCase("dir", "")]
[TestCase("/dir", "/")]
[TestCase("/dir/file", "/dir")]
[TestCase("/dir/dir/", "/dir/dir")]
diff --git a/tests/Ramstack.FileProviders.Tests/GlobbingFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/GlobbingFileProviderTests.cs
index 1c6f7fd..b605cde 100644
--- a/tests/Ramstack.FileProviders.Tests/GlobbingFileProviderTests.cs
+++ b/tests/Ramstack.FileProviders.Tests/GlobbingFileProviderTests.cs
@@ -5,33 +5,85 @@ namespace Ramstack.FileProviders;
[TestFixture]
public class GlobbingFileProviderTests : AbstractFileProviderTests
{
- private readonly TempFileStorage _storage = new TempFileStorage();
+ private readonly TempFileStorage _storage1;
+ private readonly TempFileStorage _storage2;
+
+ public GlobbingFileProviderTests()
+ {
+ _storage1 = new TempFileStorage();
+ _storage2 = new TempFileStorage(_storage1);
+ }
[OneTimeSetUp]
public void Setup()
{
- var path = Path.Join(_storage.Root, "project");
- var directory = new DirectoryInfo(path);
+ var directory = new DirectoryInfo(
+ Path.Join(_storage1.Root, "project"));
foreach (var di in directory.GetDirectories("*", SearchOption.TopDirectoryOnly))
- if (di.Name != "docs")
+ if (di.Name != "assets")
di.Delete(recursive: true);
foreach (var fi in directory.GetFiles("*", SearchOption.TopDirectoryOnly))
- fi.Delete();
-
- File.Delete(Path.Join(_storage.Root, "project/docs/troubleshooting/common_issues.txt"));
+ if (fi.Name != "README.md")
+ fi.Delete();
}
[OneTimeTearDown]
- public void Cleanup() =>
- _storage.Dispose();
+ public void Cleanup()
+ {
+ _storage1.Dispose();
+ _storage2.Dispose();
+ }
+
+ [Test]
+ public void Glob_MatchStructures()
+ {
+ using var provider = CreateFileProvider();
+
+ Assert.That(
+ provider
+ .EnumerateDirectories("/", "**")
+ .OrderBy(f => f.FullName)
+ .Select(f => f.FullName)
+ .ToArray(),
+ Is.EquivalentTo(
+ [
+ "/project",
+ "/project/assets",
+ "/project/assets/fonts",
+ "/project/assets/images",
+ "/project/assets/images/backgrounds",
+ "/project/assets/styles"
+ ]));
+
+ Assert.That(
+ provider
+ .EnumerateFiles("/", "**")
+ .OrderBy(f => f.FullName)
+ .Select(f => f.FullName)
+ .ToArray(),
+ Is.EquivalentTo(
+ [
+ "/project/assets/fonts/Arial.ttf",
+ "/project/assets/fonts/Roboto.ttf",
+ "/project/assets/images/backgrounds/dark.jpeg",
+ "/project/assets/images/backgrounds/light.jpg",
+ "/project/assets/images/icon.svg",
+ "/project/assets/images/logo.png",
+ "/project/assets/styles/main.css",
+ "/project/assets/styles/print.css",
+ "/project/README.md"
+ ]));
+ }
[Test]
public void ExcludedDirectory_HasNoFileNodes()
{
- using var storage = new TempFileStorage();
- var fs = new GlobbingFileProvider(new PhysicalFileProvider(storage.Root), "**", exclude: "/project/src/**");
+ var provider = new GlobbingFileProvider(
+ new PhysicalFileProvider(_storage2.Root),
+ pattern: "**",
+ exclude: "/project/src/**");
var directories = new[]
{
@@ -45,15 +97,15 @@ public void ExcludedDirectory_HasNoFileNodes()
foreach (var path in directories)
{
- var directory = fs.GetDirectory(path);
+ var directory = provider.GetDirectoryContents(path);
Assert.That(
directory.Exists,
Is.False);
Assert.That(
- directory.EnumerateFileNodes("**").Count(),
- Is.Zero);
+ directory.Any(),
+ Is.False);
}
var files = new[]
@@ -73,17 +125,19 @@ public void ExcludedDirectory_HasNoFileNodes()
foreach (var path in files)
{
- var file = fs.GetFile(path);
+ var file = provider.GetFileInfo(path);
Assert.That(file.Exists, Is.False);
}
}
protected override IFileProvider GetFileProvider()
{
- var provider = new PhysicalFileProvider(_storage.Root);
- return new GlobbingFileProvider(provider, "project/docs/**", exclude: "**/*.txt");
+ var provider = new PhysicalFileProvider(_storage2.Root);
+ return new GlobbingFileProvider(provider,
+ patterns: ["project/assets/**", "project/README.md", "project/global.json"],
+ excludes: ["project/*.json"]);
}
protected override DirectoryInfo GetDirectoryInfo() =>
- new DirectoryInfo(_storage.Root);
+ new DirectoryInfo(_storage1.Root);
}
diff --git a/tests/Ramstack.FileProviders.Tests/PrefixedFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/PrefixedFileProviderTests.cs
index 6e9f741..f18f2f8 100644
--- a/tests/Ramstack.FileProviders.Tests/PrefixedFileProviderTests.cs
+++ b/tests/Ramstack.FileProviders.Tests/PrefixedFileProviderTests.cs
@@ -12,12 +12,12 @@ public sealed class PrefixedFileProviderTests : AbstractFileProviderTests
.GetMethod("ResolveGlobFilter", BindingFlags.Static | BindingFlags.NonPublic)!
.CreateDelegate>();
- private const string Prefix = "solution/app";
-
- private readonly TempFileStorage _storage = new TempFileStorage(Prefix);
+ private readonly TempFileStorage _storage = new TempFileStorage();
protected override IFileProvider GetFileProvider() =>
- new PrefixedFileProvider(Prefix, new PhysicalFileProvider(_storage.PrefixedPath, ExclusionFilters.None));
+ new PrefixedFileProvider("/project",
+ new PhysicalFileProvider(
+ Path.Join(_storage.Root, "project")));
protected override DirectoryInfo GetDirectoryInfo() =>
new DirectoryInfo(_storage.Root);
diff --git a/tests/Ramstack.FileProviders.Tests/SubFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/SubFileProviderTests.cs
index a551628..270f0a0 100644
--- a/tests/Ramstack.FileProviders.Tests/SubFileProviderTests.cs
+++ b/tests/Ramstack.FileProviders.Tests/SubFileProviderTests.cs
@@ -12,8 +12,10 @@ public void Cleanup() =>
_storage.Dispose();
protected override IFileProvider GetFileProvider() =>
- new SubFileProvider("/project/docs", new PhysicalFileProvider(_storage.Root));
+ new SubFileProvider("/bin/app",
+ new PrefixedFileProvider("/bin/app",
+ new PhysicalFileProvider(_storage.Root)));
protected override DirectoryInfo GetDirectoryInfo() =>
- new DirectoryInfo(Path.Join(_storage.Root, "project", "docs"));
+ new DirectoryInfo(Path.Join(_storage.Root));
}
diff --git a/tests/Ramstack.FileProviders.Tests/Utilities/TempFileStorage.cs b/tests/Ramstack.FileProviders.Tests/Utilities/TempFileStorage.cs
index ddf3001..574736c 100644
--- a/tests/Ramstack.FileProviders.Tests/Utilities/TempFileStorage.cs
+++ b/tests/Ramstack.FileProviders.Tests/Utilities/TempFileStorage.cs
@@ -2,19 +2,11 @@ namespace Ramstack.FileProviders.Utilities;
public sealed class TempFileStorage : IDisposable
{
- public string Root { get; }
- public string PrefixedPath { get; }
+ public string Root { get; } = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- public TempFileStorage(string prefix = "")
+ public TempFileStorage()
{
- var root = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- var path = prefix.Length != 0
- ? Path.GetFullPath(Path.Join(root, prefix))
- : root;
-
- Root = root;
- PrefixedPath = path;
-
+ var path = Root;
var list = new[]
{
"project/docs/user_manual.pdf",
@@ -53,10 +45,6 @@ public TempFileStorage(string prefix = "")
"project/data/temp/temp_file1.tmp",
"project/data/temp/temp_file2.tmp",
"project/data/temp/ac/b2/34/2d/7e/temp_file2.tmp",
- "project/data/temp/hidden-folder/temp_1.tmp",
- "project/data/temp/hidden-folder/temp_2.tmp",
- "project/data/temp/hidden/temp_hidden3.dat",
- "project/data/temp/hidden/temp_hidden4.dat",
"project/scripts/setup.p1",
"project/scripts/deploy.ps1",
@@ -78,13 +66,13 @@ public TempFileStorage(string prefix = "")
"project/assets/images/backgrounds/light.jpg",
"project/assets/images/backgrounds/dark.jpeg",
- "project/assets/fonts/opensans.ttf",
- "project/assets/fonts/roboto.ttf",
+ "project/assets/fonts/Arial.ttf",
+ "project/assets/fonts/Roboto.ttf",
"project/assets/styles/main.css",
"project/assets/styles/print.css",
"project/packages/Ramstack.Globbing.2.1.0/lib/net60/Ramstack.Globbing.dll",
- "project/packages/Ramstack.Globbing.2.1.0/Ramstack.Globbing.2.1.0.nupkg",
+ "project/packages/Ramstack.Globbing.2.1.0/Ramstack.Globbing.2.1.0.zip",
"project/.gitignore",
"project/.editorconfig",
@@ -106,12 +94,29 @@ public TempFileStorage(string prefix = "")
foreach (var f in list)
File.WriteAllText(f, $"Automatically generated on {DateTime.Now:s}\n\nId:{Guid.NewGuid()}");
+ }
- var hiddenFolder = directories.First(p => p.Contains("hidden-folder"));
- File.SetAttributes(hiddenFolder, FileAttributes.Hidden);
+ public TempFileStorage(TempFileStorage storage)
+ {
+ CopyDirectory(storage.Root, Root);
- foreach (var hiddenFile in list.Where(p => p.Contains("temp_hidden")))
- File.SetAttributes(hiddenFile, FileAttributes.Hidden);
+ static void CopyDirectory(string sourcePath, string destinationPath)
+ {
+ Directory.CreateDirectory(destinationPath);
+
+ foreach (var sourceFileName in Directory.GetFiles(sourcePath))
+ {
+ var name = Path.GetFileName(sourceFileName);
+ var destFileName = Path.Join(destinationPath, name);
+ File.Copy(sourceFileName, destFileName);
+ }
+
+ foreach (var directoryPath in Directory.GetDirectories(sourcePath))
+ {
+ var destination = Path.Join(destinationPath, Path.GetFileName(directoryPath));
+ CopyDirectory(directoryPath, destination);
+ }
+ }
}
public void Dispose() =>
diff --git a/tests/Ramstack.FileProviders.Tests/ZipFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/ZipFileProviderTests.cs
index 9fed1d7..eee0c4e 100644
--- a/tests/Ramstack.FileProviders.Tests/ZipFileProviderTests.cs
+++ b/tests/Ramstack.FileProviders.Tests/ZipFileProviderTests.cs
@@ -8,7 +8,11 @@ namespace Ramstack.FileProviders;
public class ZipFileProviderTests : AbstractFileProviderTests
{
private readonly TempFileStorage _storage = new TempFileStorage();
- private readonly string _path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ private readonly string _path =
+ Path.Combine(
+ Path.GetTempPath(),
+ Path.GetRandomFileName()
+ ) + ".zip";
[OneTimeSetUp]
public void Setup()