mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-04-20 07:56:50 +00:00
feat: Add asset loading functionality and related tests for improved resource management
This commit is contained in:
parent
4ed5e648c0
commit
b85f1dab6b
8 changed files with 733 additions and 197 deletions
|
|
@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core.Tests", "sr
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Core.Tests", "src\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{F508E0BA-E885-424F-9D4C-359CF0011DEF}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Core.Tests", "src\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{F508E0BA-E885-424F-9D4C-359CF0011DEF}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Tests", "src\PepperDash.Essentials.Tests\PepperDash.Essentials.Tests.csproj", "{841EE676-7784-4456-8F76-3697C6D432A6}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
|
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
|
||||||
|
|
@ -181,6 +183,24 @@ Global
|
||||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU
|
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU
|
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -194,6 +214,7 @@ Global
|
||||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
{F508E0BA-E885-424F-9D4C-359CF0011DEF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
|
{841EE676-7784-4456-8F76-3697C6D432A6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
|
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
|
||||||
|
|
|
||||||
334
src/PepperDash.Essentials.Tests/ControlSystem/LoadAssetsTests.cs
Normal file
334
src/PepperDash.Essentials.Tests/ControlSystem/LoadAssetsTests.cs
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
using System.IO.Compression;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Tests.ControlSystem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for <see cref="AssetLoader.Load"/>.
|
||||||
|
/// <see cref="AssetLoader"/> is the <c>System.IO</c>-based implementation that backs
|
||||||
|
/// <see cref="AssetLoader.Load"/>. Tests run against a
|
||||||
|
/// temporary directory tree so no Crestron runtime is required.
|
||||||
|
/// Debug is initialised with fakes via TestInitializer.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LoadAssetsTests : IDisposable
|
||||||
|
{
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fixture: each test gets an isolated temp directory tree
|
||||||
|
//
|
||||||
|
// _rootDir/
|
||||||
|
// appdir/ ← applicationDirectoryPath (where the loader scans)
|
||||||
|
// user/
|
||||||
|
// program1/ ← filePathPrefix (where assets land)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private readonly string _rootDir;
|
||||||
|
private readonly string _appDir;
|
||||||
|
private readonly string _filePathPrefix;
|
||||||
|
|
||||||
|
public LoadAssetsTests()
|
||||||
|
{
|
||||||
|
_rootDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||||
|
_appDir = Path.Combine(_rootDir, "appdir");
|
||||||
|
_filePathPrefix = Path.Combine(_rootDir, "user", "program1") + Path.DirectorySeparatorChar;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_appDir);
|
||||||
|
Directory.CreateDirectory(_filePathPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_rootDir))
|
||||||
|
Directory.Delete(_rootDir, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static byte[] EmptyZip()
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using (var _ = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) { }
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ZipWithFile(string entryName, string content = "test")
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
||||||
|
{
|
||||||
|
var entry = archive.CreateEntry(entryName);
|
||||||
|
using var sw = new StreamWriter(entry.Open());
|
||||||
|
sw.Write(content);
|
||||||
|
}
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ZipWithDirectory(string directoryEntryName)
|
||||||
|
{
|
||||||
|
// Directory entries have a trailing slash and an empty Name
|
||||||
|
var normalised = directoryEntryName.TrimEnd('/') + '/';
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
||||||
|
{
|
||||||
|
archive.CreateEntry(normalised);
|
||||||
|
}
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ZipWithTraversalEntry()
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
|
||||||
|
{
|
||||||
|
var entry = archive.CreateEntry("../traversal.txt");
|
||||||
|
using var sw = new StreamWriter(entry.Open());
|
||||||
|
sw.Write("should not appear");
|
||||||
|
}
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteToAppDir(string fileName, byte[] contents) =>
|
||||||
|
File.WriteAllBytes(Path.Combine(_appDir, fileName), contents);
|
||||||
|
|
||||||
|
private void WriteTextToAppDir(string fileName, string text) =>
|
||||||
|
File.WriteAllText(Path.Combine(_appDir, fileName), text);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// No-op cases — nothing in the application directory
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_EmptyApplicationDirectory_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var act = () => AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
act.Should().NotThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// assets*.zip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_MultipleAssetsZips_ThrowsException()
|
||||||
|
{
|
||||||
|
WriteToAppDir("assets1.zip", EmptyZip());
|
||||||
|
WriteToAppDir("assets2.zip", EmptyZip());
|
||||||
|
|
||||||
|
var act = () => AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
act.Should().Throw<Exception>().WithMessage("*Multiple assets zip files*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleAssetsZip_ExtractsFileToFilePathPrefix()
|
||||||
|
{
|
||||||
|
WriteToAppDir("assets.zip", ZipWithFile("config.json", "{}"));
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
File.Exists(Path.Combine(_filePathPrefix, "config.json")).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleAssetsZip_FileContentsArePreserved()
|
||||||
|
{
|
||||||
|
const string expected = "{\"key\":\"value\"}";
|
||||||
|
WriteToAppDir("assets.zip", ZipWithFile("data.json", expected));
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
File.ReadAllText(Path.Combine(_filePathPrefix, "data.json")).Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleAssetsZip_ZipIsDeletedAfterExtraction()
|
||||||
|
{
|
||||||
|
var zipPath = Path.Combine(_appDir, "assets.zip");
|
||||||
|
WriteToAppDir("assets.zip", ZipWithFile("file.txt"));
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
File.Exists(zipPath).Should().BeFalse("assets zip should be cleaned up after extraction");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_AssetsZipWithDirectoryEntry_CreatesDirectory()
|
||||||
|
{
|
||||||
|
WriteToAppDir("assets.zip", ZipWithDirectory("subdir/"));
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
Directory.Exists(Path.Combine(_filePathPrefix, "subdir/")).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_AssetsZipWithPathTraversal_ThrowsInvalidOperationException()
|
||||||
|
{
|
||||||
|
WriteToAppDir("assets.zip", ZipWithTraversalEntry());
|
||||||
|
|
||||||
|
var act = () => AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("*trying to extract outside of the target directory*");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// htmlassets*.zip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_MultipleHtmlAssetsZips_ThrowsException()
|
||||||
|
{
|
||||||
|
WriteToAppDir("htmlassets1.zip", EmptyZip());
|
||||||
|
WriteToAppDir("htmlassets2.zip", EmptyZip());
|
||||||
|
|
||||||
|
var act = () => AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
act.Should().Throw<Exception>().WithMessage("*Multiple htmlassets zip files*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleHtmlAssetsZip_ExtractsToHtmlDirectory()
|
||||||
|
{
|
||||||
|
// htmlDir = rootDir/html
|
||||||
|
WriteToAppDir("htmlassets.zip", ZipWithFile("index.html", "<html/>"));
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
var expectedHtmlDir = Path.Combine(_rootDir, "html");
|
||||||
|
File.Exists(Path.Combine(expectedHtmlDir, "index.html")).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleHtmlAssetsZip_ZipIsDeletedAfterExtraction()
|
||||||
|
{
|
||||||
|
var zipPath = Path.Combine(_appDir, "htmlassets.zip");
|
||||||
|
WriteToAppDir("htmlassets.zip", ZipWithFile("page.html"));
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
File.Exists(zipPath).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_HtmlAssetsZipWithPathTraversal_ThrowsInvalidOperationException()
|
||||||
|
{
|
||||||
|
WriteToAppDir("htmlassets.zip", ZipWithTraversalEntry());
|
||||||
|
|
||||||
|
var act = () => AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("*trying to extract outside of the target directory*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_HtmlAssetsZip_ShallowFilePathPrefixThrowsWhenRootCannotBeDetermined()
|
||||||
|
{
|
||||||
|
// A path that is exactly ONE level below the filesystem root has no grandparent,
|
||||||
|
// so rootDir (programDir.Parent.Parent) is null and the guard should throw.
|
||||||
|
// DirectoryInfo works with non-existent paths, so we don't create the directory.
|
||||||
|
var filesystemRoot = Path.GetPathRoot(Path.GetTempPath())!;
|
||||||
|
var shallowPrefix = Path.Combine(filesystemRoot, "pepperDashTestShallow") + Path.DirectorySeparatorChar;
|
||||||
|
WriteToAppDir("htmlassets.zip", ZipWithFile("page.html"));
|
||||||
|
|
||||||
|
var act = () => AssetLoader.Load(_appDir, shallowPrefix);
|
||||||
|
act.Should().Throw<Exception>().WithMessage("*Unable to determine root directory for html extraction*");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// essentials-devtools*.zip
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_MultipleDevToolsZips_ThrowsException()
|
||||||
|
{
|
||||||
|
WriteToAppDir("essentials-devtools1.zip", EmptyZip());
|
||||||
|
WriteToAppDir("essentials-devtools2.zip", EmptyZip());
|
||||||
|
|
||||||
|
var act = () => AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
act.Should().Throw<Exception>().WithMessage("*Multiple essentials-devtools zip files*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleDevToolsZip_ExtractsToHtmlDebugDirectory()
|
||||||
|
{
|
||||||
|
// debugDir = rootDir/html/debug
|
||||||
|
WriteToAppDir("essentials-devtools.zip", ZipWithFile("app.js", "console.log('hi');"));
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
var expectedDebugDir = Path.Combine(_rootDir, "html", "debug");
|
||||||
|
File.Exists(Path.Combine(expectedDebugDir, "app.js")).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleDevToolsZip_ZipIsDeletedAfterExtraction()
|
||||||
|
{
|
||||||
|
var zipPath = Path.Combine(_appDir, "essentials-devtools.zip");
|
||||||
|
WriteToAppDir("essentials-devtools.zip", ZipWithFile("tool.js"));
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
File.Exists(zipPath).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_DevToolsZipWithPathTraversal_ThrowsInvalidOperationException()
|
||||||
|
{
|
||||||
|
WriteToAppDir("essentials-devtools.zip", ZipWithTraversalEntry());
|
||||||
|
|
||||||
|
var act = () => AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
act.Should().Throw<InvalidOperationException>()
|
||||||
|
.WithMessage("*trying to extract outside of the target directory*");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// *configurationFile*.json
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_MultipleJsonConfigFiles_ThrowsException()
|
||||||
|
{
|
||||||
|
WriteTextToAppDir("abcconfigurationFile1.json", "{}");
|
||||||
|
WriteTextToAppDir("abcconfigurationFile2.json", "{}");
|
||||||
|
|
||||||
|
var act = () => AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
act.Should().Throw<Exception>().WithMessage("*Multiple configuration files found*");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleJsonConfigFile_IsMovedToFilePathPrefix()
|
||||||
|
{
|
||||||
|
const string fileName = "myconfigurationFile.json";
|
||||||
|
WriteTextToAppDir(fileName, "{}");
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
File.Exists(Path.Combine(_appDir, fileName)).Should().BeFalse("source file should be moved, not copied");
|
||||||
|
File.Exists(Path.Combine(_filePathPrefix, fileName)).Should().BeTrue("file should exist at the file path prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleJsonConfigFile_ExistingDestinationIsReplaced()
|
||||||
|
{
|
||||||
|
const string fileName = "myconfigurationFile.json";
|
||||||
|
WriteTextToAppDir(fileName, "new content");
|
||||||
|
|
||||||
|
// Pre-populate the destination with stale content
|
||||||
|
File.WriteAllText(Path.Combine(_filePathPrefix, fileName), "old content");
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
File.ReadAllText(Path.Combine(_filePathPrefix, fileName)).Should().Be("new content");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadAssets_SingleJsonConfigFile_ContentIsPreserved()
|
||||||
|
{
|
||||||
|
const string content = "{\"devices\":[]}";
|
||||||
|
const string fileName = "myconfigurationFile.json";
|
||||||
|
WriteTextToAppDir(fileName, content);
|
||||||
|
|
||||||
|
AssetLoader.Load(_appDir, _filePathPrefix);
|
||||||
|
|
||||||
|
File.ReadAllText(Path.Combine(_filePathPrefix, fileName)).Should().Be(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/PepperDash.Essentials.Tests/Fakes/Fakes.cs
Normal file
61
src/PepperDash.Essentials.Tests/Fakes/Fakes.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
using PepperDash.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Tests.Fakes;
|
||||||
|
|
||||||
|
internal class FakeCrestronEnvironment : ICrestronEnvironment
|
||||||
|
{
|
||||||
|
public DevicePlatform DevicePlatform { get; set; } = DevicePlatform.Appliance;
|
||||||
|
public RuntimeEnvironment RuntimeEnvironment { get; set; } = RuntimeEnvironment.SimplSharpPro;
|
||||||
|
public string NewLine { get; set; } = "\r\n";
|
||||||
|
public uint ApplicationNumber { get; set; } = 1;
|
||||||
|
public uint RoomId { get; set; } = 0;
|
||||||
|
|
||||||
|
public event EventHandler<ProgramStatusEventArgs>? ProgramStatusChanged
|
||||||
|
{
|
||||||
|
add { }
|
||||||
|
remove { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<PepperDashEthernetEventArgs>? EthernetEventReceived
|
||||||
|
{
|
||||||
|
add { }
|
||||||
|
remove { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath();
|
||||||
|
public bool IsHardwareRuntime => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NoOpCrestronConsole : ICrestronConsole
|
||||||
|
{
|
||||||
|
public void PrintLine(string message) { }
|
||||||
|
public void Print(string message) { }
|
||||||
|
public void ConsoleCommandResponse(string message) { }
|
||||||
|
public void AddNewConsoleCommand(Action<string> _, string __, string ___, ConsoleAccessLevel ____) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class InMemoryCrestronDataStore : ICrestronDataStore
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, object> _store = new();
|
||||||
|
|
||||||
|
public void InitStore() { }
|
||||||
|
|
||||||
|
public bool TryGetLocalInt(string key, out int value)
|
||||||
|
{
|
||||||
|
if (_store.TryGetValue(key, out var raw) && raw is int i) { value = i; return true; }
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetLocalInt(string key, int value) { _store[key] = value; return true; }
|
||||||
|
public bool SetLocalUint(string key, uint value) { _store[key] = (int)value; return true; }
|
||||||
|
|
||||||
|
public bool TryGetLocalBool(string key, out bool value)
|
||||||
|
{
|
||||||
|
if (_store.TryGetValue(key, out var raw) && raw is bool b) { value = b; return true; }
|
||||||
|
value = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetLocalBool(string key, bool value) { _store[key] = value; return true; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||||
|
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Provides access to ControlSystem.LoadAssets (internal) after InternalsVisibleTo is set -->
|
||||||
|
<ProjectReference Include="..\PepperDash.Essentials\PepperDash.Essentials.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
28
src/PepperDash.Essentials.Tests/TestInitializer.cs
Normal file
28
src/PepperDash.Essentials.Tests/TestInitializer.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using PepperDash.Core.Abstractions;
|
||||||
|
using PepperDash.Essentials.Tests.Fakes;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs once before any type in this assembly is accessed.
|
||||||
|
/// Registers fake Crestron service implementations so that the <c>Debug</c> static
|
||||||
|
/// constructor never tries to reach the real Crestron SDK.
|
||||||
|
/// </summary>
|
||||||
|
internal static class TestInitializer
|
||||||
|
{
|
||||||
|
[ModuleInitializer]
|
||||||
|
internal static void Initialize()
|
||||||
|
{
|
||||||
|
DebugServiceRegistration.Register(
|
||||||
|
new FakeCrestronEnvironment
|
||||||
|
{
|
||||||
|
DevicePlatform = DevicePlatform.Server,
|
||||||
|
RuntimeEnvironment = RuntimeEnvironment.Other,
|
||||||
|
},
|
||||||
|
new NoOpCrestronConsole(),
|
||||||
|
new InMemoryCrestronDataStore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
256
src/PepperDash.Essentials/AssetLoader.cs
Normal file
256
src/PepperDash.Essentials/AssetLoader.cs
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using PepperDash.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles extracting embedded asset bundles and moving configuration files from the
|
||||||
|
/// application directory to the program file-path prefix at startup.
|
||||||
|
/// Implemented using <c>System.IO</c> types so it can run (and be tested) outside
|
||||||
|
/// of a Crestron runtime environment.
|
||||||
|
/// </summary>
|
||||||
|
internal static class AssetLoader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scans <paramref name="applicationDirectoryPath"/> for well-known zip bundles and
|
||||||
|
/// JSON configuration files and deploys them to <paramref name="filePathPrefix"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationDirectoryPath">
|
||||||
|
/// The directory to scan (typically the Crestron application root).
|
||||||
|
/// </param>
|
||||||
|
/// <param name="filePathPrefix">
|
||||||
|
/// The program's runtime working directory (e.g. <c>/nvram/program1/</c>).
|
||||||
|
/// </param>
|
||||||
|
internal static void Load(string applicationDirectoryPath, string filePathPrefix)
|
||||||
|
{
|
||||||
|
var applicationDirectory = new DirectoryInfo(applicationDirectoryPath);
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Searching: {applicationDirectory:l} for embedded assets - {Destination}",
|
||||||
|
applicationDirectory.FullName, filePathPrefix);
|
||||||
|
|
||||||
|
ExtractAssetsZip(applicationDirectory, filePathPrefix);
|
||||||
|
ExtractHtmlAssetsZip(applicationDirectory, filePathPrefix);
|
||||||
|
ExtractDevToolsZip(applicationDirectory, filePathPrefix);
|
||||||
|
MoveConfigurationFile(applicationDirectory, filePathPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static void ExtractAssetsZip(DirectoryInfo applicationDirectory, string filePathPrefix)
|
||||||
|
{
|
||||||
|
var zipFiles = applicationDirectory.GetFiles("assets*.zip");
|
||||||
|
|
||||||
|
if (zipFiles.Length > 1)
|
||||||
|
throw new Exception("Multiple assets zip files found. Cannot continue.");
|
||||||
|
|
||||||
|
if (zipFiles.Length == 1)
|
||||||
|
{
|
||||||
|
var zipFile = zipFiles[0];
|
||||||
|
var assetsRoot = Path.GetFullPath(filePathPrefix);
|
||||||
|
if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
||||||
|
!assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
||||||
|
{
|
||||||
|
assetsRoot += Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName);
|
||||||
|
|
||||||
|
using (var archive = ZipFile.OpenRead(zipFile.FullName))
|
||||||
|
{
|
||||||
|
foreach (var entry in archive.Entries)
|
||||||
|
{
|
||||||
|
var destinationPath = Path.Combine(filePathPrefix, entry.FullName);
|
||||||
|
var fullDest = Path.GetFullPath(destinationPath);
|
||||||
|
if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(entry.Name))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destinationPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(destinationPath))
|
||||||
|
Directory.Delete(destinationPath, recursive: true);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
||||||
|
entry.ExtractToFile(destinationPath, overwrite: true);
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in zipFiles)
|
||||||
|
File.Delete(file.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExtractHtmlAssetsZip(DirectoryInfo applicationDirectory, string filePathPrefix)
|
||||||
|
{
|
||||||
|
var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip");
|
||||||
|
|
||||||
|
if (htmlZipFiles.Length > 1)
|
||||||
|
throw new Exception(
|
||||||
|
"Multiple htmlassets zip files found in application directory. " +
|
||||||
|
"Please ensure only one htmlassets*.zip file is present and retry.");
|
||||||
|
|
||||||
|
if (htmlZipFiles.Length == 1)
|
||||||
|
{
|
||||||
|
var htmlZipFile = htmlZipFiles[0];
|
||||||
|
var programDir = new DirectoryInfo(
|
||||||
|
filePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||||
|
var userOrNvramDir = programDir.Parent;
|
||||||
|
var rootDir = userOrNvramDir?.Parent;
|
||||||
|
if (rootDir == null)
|
||||||
|
throw new Exception(
|
||||||
|
$"Unable to determine root directory for html extraction. Current path: {filePathPrefix}");
|
||||||
|
|
||||||
|
var htmlDir = Path.Combine(rootDir.FullName, "html");
|
||||||
|
var htmlRoot = Path.GetFullPath(htmlDir);
|
||||||
|
if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
||||||
|
!htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
||||||
|
{
|
||||||
|
htmlRoot += Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName);
|
||||||
|
|
||||||
|
using (var archive = ZipFile.OpenRead(htmlZipFile.FullName))
|
||||||
|
{
|
||||||
|
foreach (var entry in archive.Entries)
|
||||||
|
{
|
||||||
|
var destinationPath = Path.Combine(htmlDir, entry.FullName);
|
||||||
|
var fullDest = Path.GetFullPath(destinationPath);
|
||||||
|
if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(entry.Name))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destinationPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(destinationPath))
|
||||||
|
File.Delete(destinationPath);
|
||||||
|
|
||||||
|
var parentDir = Path.GetDirectoryName(destinationPath);
|
||||||
|
if (!string.IsNullOrEmpty(parentDir))
|
||||||
|
Directory.CreateDirectory(parentDir);
|
||||||
|
|
||||||
|
entry.ExtractToFile(destinationPath, overwrite: true);
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in htmlZipFiles)
|
||||||
|
File.Delete(file.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExtractDevToolsZip(DirectoryInfo applicationDirectory, string filePathPrefix)
|
||||||
|
{
|
||||||
|
var devToolsZipFiles = applicationDirectory.GetFiles("essentials-devtools*.zip");
|
||||||
|
|
||||||
|
if (devToolsZipFiles.Length > 1)
|
||||||
|
throw new Exception(
|
||||||
|
"Multiple essentials-devtools zip files found in application directory. " +
|
||||||
|
"Please ensure only one essentials-devtools*.zip file is present and retry.");
|
||||||
|
|
||||||
|
if (devToolsZipFiles.Length == 1)
|
||||||
|
{
|
||||||
|
var devToolsZipFile = devToolsZipFiles[0];
|
||||||
|
var programDir = new DirectoryInfo(
|
||||||
|
filePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||||
|
var userOrNvramDir = programDir.Parent;
|
||||||
|
var rootDir = userOrNvramDir?.Parent;
|
||||||
|
if (rootDir == null)
|
||||||
|
throw new Exception(
|
||||||
|
$"Unable to determine root directory for debug html extraction. Current path: {filePathPrefix}");
|
||||||
|
|
||||||
|
var debugDir = Path.Combine(rootDir.FullName, "html", "debug");
|
||||||
|
var debugRoot = Path.GetFullPath(debugDir);
|
||||||
|
if (!debugRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
||||||
|
!debugRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
||||||
|
{
|
||||||
|
debugRoot += Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Found essentials-devtools zip file: {zipFile:l}... Unzipping to {Destination}...",
|
||||||
|
devToolsZipFile.FullName, debugDir);
|
||||||
|
|
||||||
|
using (var archive = ZipFile.OpenRead(devToolsZipFile.FullName))
|
||||||
|
{
|
||||||
|
foreach (var entry in archive.Entries)
|
||||||
|
{
|
||||||
|
var destinationPath = Path.Combine(debugDir, entry.FullName);
|
||||||
|
var fullDest = Path.GetFullPath(destinationPath);
|
||||||
|
if (!fullDest.StartsWith(debugRoot, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(entry.Name))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destinationPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(destinationPath))
|
||||||
|
File.Delete(destinationPath);
|
||||||
|
|
||||||
|
var parentDir = Path.GetDirectoryName(destinationPath);
|
||||||
|
if (!string.IsNullOrEmpty(parentDir))
|
||||||
|
Directory.CreateDirectory(parentDir);
|
||||||
|
|
||||||
|
entry.ExtractToFile(destinationPath, overwrite: true);
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var file in devToolsZipFiles)
|
||||||
|
File.Delete(file.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MoveConfigurationFile(DirectoryInfo applicationDirectory, string filePathPrefix)
|
||||||
|
{
|
||||||
|
var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json");
|
||||||
|
|
||||||
|
if (jsonFiles.Length > 1)
|
||||||
|
{
|
||||||
|
Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}",
|
||||||
|
jsonFiles.Select(f => f.FullName).ToArray());
|
||||||
|
throw new Exception("Multiple configuration files found. Cannot continue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonFiles.Length == 1)
|
||||||
|
{
|
||||||
|
var jsonFile = jsonFiles[0];
|
||||||
|
var finalPath = Path.Combine(filePathPrefix, jsonFile.Name);
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Found configuration file: {jsonFile:l}... Moving to: {Destination}",
|
||||||
|
jsonFile.FullName, finalPath);
|
||||||
|
|
||||||
|
if (File.Exists(finalPath))
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Information,
|
||||||
|
"Removing existing configuration file: {Destination}", finalPath);
|
||||||
|
File.Delete(finalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonFile.MoveTo(finalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -271,7 +271,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
|
||||||
|
|
||||||
_ = new Core.DeviceFactory();
|
_ = new Core.DeviceFactory();
|
||||||
|
|
||||||
LoadAssets();
|
LoadAssets(Global.ApplicationDirectoryPathPrefix, Global.FilePathPrefix);
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration");
|
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration");
|
||||||
|
|
||||||
|
|
@ -520,201 +520,7 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void LoadAssets()
|
internal static void LoadAssets(string applicationDirectoryPath, string filePathPrefix) =>
|
||||||
{
|
AssetLoader.Load(applicationDirectoryPath, filePathPrefix);
|
||||||
var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.FilePathPrefix);
|
|
||||||
|
|
||||||
var zipFiles = applicationDirectory.GetFiles("assets*.zip");
|
|
||||||
|
|
||||||
if (zipFiles.Length > 1)
|
|
||||||
{
|
|
||||||
throw new Exception("Multiple assets zip files found. Cannot continue.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zipFiles.Length == 1)
|
|
||||||
{
|
|
||||||
var zipFile = zipFiles[0];
|
|
||||||
var assetsRoot = System.IO.Path.GetFullPath(Global.FilePathPrefix);
|
|
||||||
if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
|
||||||
{
|
|
||||||
assetsRoot += Path.DirectorySeparatorChar;
|
|
||||||
}
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName);
|
|
||||||
using (var archive = ZipFile.OpenRead(zipFile.FullName))
|
|
||||||
{
|
|
||||||
foreach (var entry in archive.Entries)
|
|
||||||
{
|
|
||||||
var destinationPath = Path.Combine(Global.FilePathPrefix, entry.FullName);
|
|
||||||
var fullDest = System.IO.Path.GetFullPath(destinationPath);
|
|
||||||
if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase))
|
|
||||||
throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(entry.Name))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(destinationPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a directory exists where a file should go, delete it
|
|
||||||
if (Directory.Exists(destinationPath))
|
|
||||||
Directory.Delete(destinationPath, true);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
|
|
||||||
entry.ExtractToFile(destinationPath, true);
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleaning up zip files
|
|
||||||
foreach (var file in zipFiles)
|
|
||||||
{
|
|
||||||
File.Delete(file.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip");
|
|
||||||
|
|
||||||
if (htmlZipFiles.Length > 1)
|
|
||||||
{
|
|
||||||
throw new Exception("Multiple htmlassets zip files found in application directory. Please ensure only one htmlassets*.zip file is present and retry.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (htmlZipFiles.Length == 1)
|
|
||||||
{
|
|
||||||
var htmlZipFile = htmlZipFiles[0];
|
|
||||||
var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
|
||||||
var userOrNvramDir = programDir.Parent;
|
|
||||||
var rootDir = userOrNvramDir?.Parent;
|
|
||||||
if (rootDir == null)
|
|
||||||
{
|
|
||||||
throw new Exception($"Unable to determine root directory for html extraction. Current path: {Global.FilePathPrefix}");
|
|
||||||
}
|
|
||||||
var htmlDir = Path.Combine(rootDir.FullName, "html");
|
|
||||||
var htmlRoot = System.IO.Path.GetFullPath(htmlDir);
|
|
||||||
if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
|
||||||
!htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
|
||||||
{
|
|
||||||
htmlRoot += Path.DirectorySeparatorChar;
|
|
||||||
}
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName);
|
|
||||||
using (var archive = ZipFile.OpenRead(htmlZipFile.FullName))
|
|
||||||
{
|
|
||||||
foreach (var entry in archive.Entries)
|
|
||||||
{
|
|
||||||
var destinationPath = Path.Combine(htmlDir, entry.FullName);
|
|
||||||
var fullDest = System.IO.Path.GetFullPath(destinationPath);
|
|
||||||
if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase))
|
|
||||||
throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(entry.Name))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(destinationPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only delete the file if it exists and is a file, not a directory
|
|
||||||
if (File.Exists(destinationPath))
|
|
||||||
File.Delete(destinationPath);
|
|
||||||
|
|
||||||
var parentDir = Path.GetDirectoryName(destinationPath);
|
|
||||||
if (!string.IsNullOrEmpty(parentDir))
|
|
||||||
Directory.CreateDirectory(parentDir);
|
|
||||||
|
|
||||||
entry.ExtractToFile(destinationPath, true);
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleaning up html zip files
|
|
||||||
foreach (var file in htmlZipFiles)
|
|
||||||
{
|
|
||||||
File.Delete(file.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var devToolsZipFiles = applicationDirectory.GetFiles("essentials-devtools*.zip");
|
|
||||||
|
|
||||||
if (devToolsZipFiles.Length > 1)
|
|
||||||
{
|
|
||||||
throw new Exception("Multiple essentials-devtools zip files found in application directory. Please ensure only one essentials-devtools*.zip file is present and retry.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (devToolsZipFiles.Length == 1)
|
|
||||||
{
|
|
||||||
var devToolsZipFile = devToolsZipFiles[0];
|
|
||||||
var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
|
||||||
var userOrNvramDir = programDir.Parent;
|
|
||||||
var rootDir = userOrNvramDir?.Parent;
|
|
||||||
if (rootDir == null)
|
|
||||||
{
|
|
||||||
throw new Exception($"Unable to determine root directory for debug html extraction. Current path: {Global.FilePathPrefix}");
|
|
||||||
}
|
|
||||||
var debugDir = Path.Combine(Path.Combine(rootDir.FullName, "html"), "debug");
|
|
||||||
var debugRoot = System.IO.Path.GetFullPath(debugDir);
|
|
||||||
if (!debugRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
|
|
||||||
!debugRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
|
|
||||||
{
|
|
||||||
debugRoot += Path.DirectorySeparatorChar;
|
|
||||||
}
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Found essentials-devtools zip file: {zipFile:l}... Unzipping to {Destination}...", devToolsZipFile.FullName, debugDir);
|
|
||||||
using (var archive = ZipFile.OpenRead(devToolsZipFile.FullName))
|
|
||||||
{
|
|
||||||
foreach (var entry in archive.Entries)
|
|
||||||
{
|
|
||||||
var destinationPath = Path.Combine(debugDir, entry.FullName);
|
|
||||||
var fullDest = System.IO.Path.GetFullPath(destinationPath);
|
|
||||||
if (!fullDest.StartsWith(debugRoot, StringComparison.OrdinalIgnoreCase))
|
|
||||||
throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(entry.Name))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(destinationPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(destinationPath))
|
|
||||||
File.Delete(destinationPath);
|
|
||||||
|
|
||||||
var parentDir = Path.GetDirectoryName(destinationPath);
|
|
||||||
if (!string.IsNullOrEmpty(parentDir))
|
|
||||||
Directory.CreateDirectory(parentDir);
|
|
||||||
|
|
||||||
entry.ExtractToFile(destinationPath, true);
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleaning up devtools zip files
|
|
||||||
foreach (var file in devToolsZipFiles)
|
|
||||||
{
|
|
||||||
File.Delete(file.FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json");
|
|
||||||
|
|
||||||
if (jsonFiles.Length > 1)
|
|
||||||
{
|
|
||||||
Debug.LogError("Multiple configuration files found in application directory: {@jsonFiles}", jsonFiles.Select(f => f.FullName).ToArray());
|
|
||||||
throw new Exception("Multiple configuration files found. Cannot continue.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonFiles.Length == 1)
|
|
||||||
{
|
|
||||||
var jsonFile = jsonFiles[0];
|
|
||||||
var finalPath = Path.Combine(Global.FilePathPrefix, jsonFile.Name);
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Found configuration file: {jsonFile:l}... Moving to: {Destination}", jsonFile.FullName, finalPath);
|
|
||||||
|
|
||||||
if (File.Exists(finalPath))
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Removing existing configuration file: {Destination}", finalPath);
|
|
||||||
File.Delete(finalPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonFile.MoveTo(finalPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@
|
||||||
<DebugType>pdbonly</DebugType>
|
<DebugType>pdbonly</DebugType>
|
||||||
<DocumentationFile>bin\$(Configuration)\PepperDashEssentials.xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\PepperDashEssentials.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||||
|
<_Parameter1>PepperDash.Essentials.Tests</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.128" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.128" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue