Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 65 additions & 31 deletions src/EmptyFiles/AllFiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ static AllFiles()
sheets = AddCategory(sheetExtensions, Category.Sheet);
slides = AddCategory(slideExtensions, Category.Slide);
binary = AddCategory(binaryExtensions, Category.Binary);
var all = new Dictionary<string, EmptyFile>();
var all = new Dictionary<string, EmptyFile>(StringComparer.OrdinalIgnoreCase);
Append(archives);
Append(documents);
Append(images);
Append(sheets);
Append(slides);
Append(binary);

files = all.ToFrozenDictionary();
files = all.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);

void Append(FrozenDictionary<string, EmptyFile> files)
{
Expand All @@ -59,7 +59,7 @@ void Append(FrozenDictionary<string, EmptyFile> files)

static FrozenDictionary<string, EmptyFile> AddCategory(FrozenSet<string> extensions, Category category)
{
Dictionary<string, EmptyFile> items = [];
Dictionary<string, EmptyFile> items = new(StringComparer.OrdinalIgnoreCase);
var categoryName = category
.ToString()
.ToLowerInvariant();
Expand All @@ -69,47 +69,58 @@ static FrozenDictionary<string, EmptyFile> AddCategory(FrozenSet<string> extensi
items[extension] = EmptyFile.Embedded(category, extension, resourceName);
}

return items.ToFrozenDictionary();
return items.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
}

static readonly Lazy<string> extractDirectory = new(
() =>
{
// AssemblyVersion and FileVersion are both pinned (1.0.0), so use the
// informational version, which tracks the package version, to keep
// extracted templates isolated per release. Strip any "+sha" suffix
// appended by source control integration.
var assembly = typeof(AllFiles).Assembly;
var version = assembly.GetName().Version?.ToString() ?? "unknown";
var informational = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion;
var version = "unknown";
if (informational != null)
{
var plusIndex = informational.IndexOf('+');
version = plusIndex == -1 ? informational : informational[..plusIndex];
}

return Path.Combine(Path.GetTempPath(), "EmptyFiles", version);
});

internal static string ExtractDirectory => extractDirectory.Value;

static void ExtractAll(string directory)
{
var assembly = typeof(AllFiles).Assembly;
ExtractCategory(assembly, directory, "archive", archiveExtensions);
ExtractCategory(assembly, directory, "binary", binaryExtensions);
ExtractCategory(assembly, directory, "document", documentExtensions);
ExtractCategory(assembly, directory, "image", imageExtensions);
ExtractCategory(assembly, directory, "sheet", sheetExtensions);
ExtractCategory(assembly, directory, "slide", slideExtensions);
ExtractCategory(directory, "archive", archives);
ExtractCategory(directory, "binary", binary);
ExtractCategory(directory, "document", documents);
ExtractCategory(directory, "image", images);
ExtractCategory(directory, "sheet", sheets);
ExtractCategory(directory, "slide", slides);
}

static void ExtractCategory(Assembly assembly, string root, string categoryName, FrozenSet<string> extensions)
static void ExtractCategory(string root, string categoryName, FrozenDictionary<string, EmptyFile> items)
{
var categoryDirectory = Path.Combine(root, categoryName);
Directory.CreateDirectory(categoryDirectory);
foreach (var extension in extensions)
foreach (var emptyFile in items.Values)
{
var resourceName = $"EmptyFiles.{categoryName}.empty{extension}";
using var resource = assembly.GetManifestResourceStream(resourceName) ??
throw new($"Embedded resource not found: {resourceName}");
var target = Path.Combine(categoryDirectory, $"empty{extension}");
if (File.Exists(target) && new FileInfo(target).Length == resource.Length)
using var resource = emptyFile.OpenRead();
var target = Path.Combine(categoryDirectory, $"empty{emptyFile.Extension}");
if (File.Exists(target) &&
new FileInfo(target).Length == resource.Length)
{
continue;
}

using var file = File.Create(target);
resource.CopyTo(file);
using var fileStream = File.Create(target);
resource.CopyTo(fileStream);
}
}

Expand Down Expand Up @@ -149,21 +160,27 @@ public static void UseFile(Category category, string file)
throw new($"Unknown category: {category}");
}

var mergedFiles = new Dictionary<string, EmptyFile>(files, StringComparer.OrdinalIgnoreCase)
{
[extension] = emptyFile
};
files = mergedFiles.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);

void Init(ref FrozenDictionary<string, EmptyFile> emptyFiles, ref FrozenSet<string> extensions)
{
var tempDictionary = new Dictionary<string, EmptyFile>();
var tempDictionary = new Dictionary<string, EmptyFile>(StringComparer.OrdinalIgnoreCase);
foreach (var (key, value) in emptyFiles)
{
tempDictionary[key] = value;
}

tempDictionary[extension] = emptyFile;
emptyFiles = tempDictionary.ToFrozenDictionary();
var tempSet = new HashSet<string>(extensions)
emptyFiles = tempDictionary.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
var tempSet = new HashSet<string>(extensions, StringComparer.OrdinalIgnoreCase)
{
extension
};
extensions = tempSet.ToFrozenSet();
extensions = tempSet.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
}
}

Expand All @@ -189,7 +206,8 @@ void Init(ref FrozenDictionary<string, EmptyFile> emptyFiles, ref FrozenSet<stri
".tar",
".xz",
".zip"
]);
],
StringComparer.OrdinalIgnoreCase);

public static IEnumerable<string> DocumentPaths => documents.Values.Select(_ => _.Path);

Expand All @@ -201,7 +219,8 @@ void Init(ref FrozenDictionary<string, EmptyFile> emptyFiles, ref FrozenSet<stri
".odt",
".pdf",
".rtf"
]);
],
StringComparer.OrdinalIgnoreCase);

public static IEnumerable<string> ImagePaths => images.Values.Select(_ => _.Path);

Expand Down Expand Up @@ -239,7 +258,8 @@ void Init(ref FrozenDictionary<string, EmptyFile> emptyFiles, ref FrozenSet<stri
".wdp",
".webp",
".wmp"
]);
],
StringComparer.OrdinalIgnoreCase);

public static IEnumerable<string> SheetPaths => sheets.Values.Select(_ => _.Path);

Expand All @@ -249,7 +269,8 @@ void Init(ref FrozenDictionary<string, EmptyFile> emptyFiles, ref FrozenSet<stri
[
".ods",
".xlsx"
]);
],
StringComparer.OrdinalIgnoreCase);

public static IEnumerable<string> SlidePaths => slides.Values.Select(_ => _.Path);

Expand All @@ -259,7 +280,8 @@ void Init(ref FrozenDictionary<string, EmptyFile> emptyFiles, ref FrozenSet<stri
[
".odp",
".pptx"
]);
],
StringComparer.OrdinalIgnoreCase);

public static IEnumerable<string> BinaryPaths => binary.Values.Select(_ => _.Path);

Expand All @@ -268,7 +290,8 @@ void Init(ref FrozenDictionary<string, EmptyFile> emptyFiles, ref FrozenSet<stri
static FrozenSet<string> binaryExtensions = FrozenSet.ToFrozenSet(
[
".bin"
]);
],
StringComparer.OrdinalIgnoreCase);

public static bool IsEmptyFile(string path)
{
Expand Down Expand Up @@ -319,6 +342,11 @@ public static bool TryCreateFile(string path, bool useEmptyStringForTextFiles =
Guard.AgainstNullOrEmpty(path);
var extension = Path.GetExtension(path);

if (extension.Length == 0)
{
return false;
}

if (useEmptyStringForTextFiles &&
FileExtensions.IsTextExtension(extension))
{
Expand Down Expand Up @@ -351,6 +379,12 @@ static void TryCreateDirectory(string path)

public static bool TryGetPathFor(string extension, [NotNullWhen(true)] out string? path)
{
if (extension.Length > 0 &&
extension[0] != '.')
{
extension = $".{extension}";
}

if (files.TryGetValue(extension, out var emptyFile))
{
path = emptyFile.Path;
Expand Down
2 changes: 1 addition & 1 deletion src/EmptyFiles/ContentTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public static bool TryGetExtension(string? mediaType, [NotNullWhen(true)] out st
"image/bmp", "bmp"
},
{
"image/heic", ".heic"
"image/heic", "heic"
},
{
"image/heic-sequence", "heics"
Expand Down
49 changes: 43 additions & 6 deletions src/EmptyFiles/EmptyFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public DateTime LastWriteTime
string? path;
DateTime lastWriteTime;
bool extracted;
readonly bool embedded;
readonly object sync = new();

internal static EmptyFile Embedded(Category category, string extension, string resourceName) =>
Expand All @@ -37,6 +38,7 @@ internal static EmptyFile Embedded(Category category, string extension, string r
Category = category;
Extension = extension;
ResourceName = resourceName;
embedded = true;
}

public EmptyFile(string path, in DateTime lastWriteTime, in Category category)
Expand Down Expand Up @@ -68,13 +70,11 @@ void EnsureExtracted()
var directory = System.IO.Path.Combine(AllFiles.ExtractDirectory, categoryName);
Directory.CreateDirectory(directory);
var target = System.IO.Path.Combine(directory, $"empty{Extension}");
var assembly = typeof(EmptyFile).Assembly;
using var resource = assembly.GetManifestResourceStream(ResourceName) ??
throw new($"Embedded resource not found: {ResourceName}");
if (!File.Exists(target) || new FileInfo(target).Length != resource.Length)
using var resource = OpenRead();
if (!File.Exists(target) ||
new FileInfo(target).Length != resource.Length)
{
using var fileStream = File.Create(target);
resource.CopyTo(fileStream);
ExtractTo(directory, target, resource);
}

path = target;
Expand All @@ -83,8 +83,45 @@ void EnsureExtracted()
}
}

// Extract via a unique temp file then move into place, so that parallel
// test processes sharing the temp directory cannot observe or write a
// partially written file. A concurrent process winning the race is treated
// as success, since every process writes byte-identical content.
static void ExtractTo(string directory, string target, Stream resource)
{
var temp = System.IO.Path.Combine(directory, $"{Guid.NewGuid():N}.tmp");
try
{
using (var fileStream = File.Create(temp))
{
resource.CopyTo(fileStream);
}

try
{
File.Move(temp, target);
}
catch (IOException) when (File.Exists(target))
{
// Another process published the file first.
}
}
finally
{
if (File.Exists(temp))
{
File.Delete(temp);
}
}
}

public Stream OpenRead()
{
if (!embedded)
{
return File.OpenRead(Path);
}

var assembly = typeof(EmptyFile).Assembly;
return assembly.GetManifestResourceStream(ResourceName) ??
throw new($"Embedded resource not found: {ResourceName}");
Expand Down
Loading
Loading