Files
duplicati/Duplicati/WebserverCore/Middlewares/StaticFilesMiddleware.Debug.cs
T
Kenneth Skovhede ca1c765b61 Fixed developer setup for ngclient on Windows.
This fixes two issues that prevented the debug loader to auto-fetch and set up ngclient on Windows when running debug builds.
2025-02-18 11:04:25 +01:00

196 lines
8.7 KiB
C#

// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#if DEBUG
using System.Formats.Tar;
using System.IO.Compression;
using System.Text.Json;
using Duplicati.Library.Logging;
using Duplicati.Library.Utility;
namespace Duplicati.WebserverCore.Middlewares;
internal static class NpmSpaHelper
{
private static readonly string LOGTAG = Log.LogTagFromType(typeof(NpmSpaHelper));
private sealed record PackageJson(Dictionary<string, string>? dependencies, string? packageId);
private sealed record PackageLockJson(Dictionary<string, PackageInfo>? packages);
private sealed record PackageInfo(string version, string resolved, string? integrity);
private sealed record InstalledPackageJson(string version);
public sealed record SpaConfig(FileInfo IndexFile, string BasePath);
public static SpaConfig? InstallNpmPackage(string packageUrl, string targetPath)
{
var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
// Handle move not working across drive boundaries on Windows
if (OperatingSystem.IsWindows() && Path.GetPathRoot(tempPath) != Path.GetPathRoot(targetPath))
{
tempPath = Path.GetFullPath(targetPath);
if (targetPath.EndsWith(Path.DirectorySeparatorChar))
tempPath = tempPath[..-1];
tempPath += "-tmp";
if (Directory.Exists(tempPath))
Directory.Delete(tempPath, true);
}
try
{
// Download and extract the package
Directory.CreateDirectory(tempPath);
var tgzFile = Path.Combine(tempPath, "package.tgz");
var tarFile = Path.ChangeExtension(tgzFile, ".tar");
using (var client = new HttpClient())
{
var response = client.GetAsync(packageUrl).Await();
response.EnsureSuccessStatusCode();
using (var stream = response.Content.ReadAsStream())
using (var fileStream = File.Create(tgzFile))
stream.CopyTo(fileStream);
}
// Extract the zipped contents
using (var fsource = new FileStream(tgzFile, FileMode.Open, FileAccess.Read))
using (var ftarget = new FileStream(tarFile, FileMode.Create, FileAccess.Write))
using (var gzip = new GZipStream(new FileStream(tgzFile, FileMode.Open, FileAccess.Read), CompressionMode.Decompress))
gzip.CopyTo(ftarget);
var dest = Path.Combine(tempPath, "package");
// Extract the tar entries into the temp folder
using (var fsource = new FileStream(tarFile, FileMode.Open, FileAccess.Read))
using (var reader = new TarReader(fsource))
while (reader.GetNextEntry() is TarEntry tar)
{
var path = Path.Combine(tempPath, tar.Name);
// Ensure the path is within the temp folder
if (!path.StartsWith(dest))
continue;
if (tar.EntryType == TarEntryType.Directory)
Directory.CreateDirectory(path);
else if (tar.EntryType == TarEntryType.RegularFile)
{
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Invalid path"));
tar.ExtractToFile(path, false);
}
}
// Read the package.json and check it looks correct
var packageJson = JsonSerializer.Deserialize<InstalledPackageJson>(File.ReadAllText(Path.Combine(dest, "package.json")));
if (packageJson == null || string.IsNullOrWhiteSpace(packageJson.version))
return null;
// Make room for the new package
if (Directory.Exists(targetPath))
Directory.Delete(targetPath, true);
var parentFolder = Path.GetDirectoryName(targetPath);
if (parentFolder != null && !Directory.Exists(parentFolder))
Directory.CreateDirectory(parentFolder);
// Move the package to the target path
Directory.Move(dest, targetPath);
// Clean up
Directory.Delete(tempPath, true);
return new SpaConfig(new FileInfo(Path.Combine(targetPath, "index.html")), targetPath);
}
catch (Exception ex)
{
Library.Logging.Log.WriteErrorMessage(LOGTAG, "SpaDebugHelperFail", ex, "NPM SPA package installation failed");
}
finally
{
if (Directory.Exists(tempPath))
Directory.Delete(tempPath, true);
}
return null;
}
/// <summary>
/// Probes for a missing/outdates SPA in the target folder and installs it if found.
/// </summary>
/// <param name="basepath">The base path to probe</param>
/// <returns>The SPA configuration if found, otherwise null</returns>
public static SpaConfig? ProbeForNpmSpa(string basepath)
{
try
{
// Check if the package.json and package-lock.json files exist
var packageFile = Path.Combine(basepath, "package.json");
var packagelockFile = Path.Combine(basepath, "package-lock.json");
if (!File.Exists(packageFile) || !File.Exists(packagelockFile))
return null;
var packageJson = JsonSerializer.Deserialize<PackageJson>(File.ReadAllText(packageFile));
var packageLockJson = JsonSerializer.Deserialize<PackageLockJson>(File.ReadAllText(packagelockFile));
var packageId = string.IsNullOrWhiteSpace(packageJson?.packageId) ? packageJson?.dependencies?.Keys.FirstOrDefault() : packageJson.packageId;
if (string.IsNullOrWhiteSpace(packageId) || packageJson?.dependencies == null)
return null;
var version = packageJson.dependencies.GetValueOrDefault(packageId);
if (string.IsNullOrWhiteSpace(version))
return null;
// Remove version prefix
if (!char.IsAsciiDigit(version.First()))
version = version[1..];
var packageUrl = packageLockJson?.packages?.GetValueOrDefault($"node_modules/{packageId}")?.resolved;
if (string.IsNullOrWhiteSpace(packageUrl))
return null;
// Package is not installed, install it
var packageFolder = Path.GetFullPath(Path.Combine(basepath, "node_modules", packageId));
if (!Directory.Exists(packageFolder))
return InstallNpmPackage(packageUrl, packageFolder);
// The installed package is missing version, reinstall
var installedPackageJson = Path.Combine(packageFolder, "package.json");
if (!File.Exists(installedPackageJson))
return InstallNpmPackage(packageUrl, packageFolder);
// The installed package is not the correct version, reinstall
var installedJson = JsonSerializer.Deserialize<InstalledPackageJson>(File.ReadAllText(installedPackageJson));
if (installedJson == null || string.IsNullOrWhiteSpace(installedJson.version) || installedJson.version != version)
return InstallNpmPackage(packageUrl, packageFolder);
// Package is installed and correct version
return new SpaConfig(new FileInfo(Path.Combine(packageFolder, "index.html")), packageFolder);
}
catch (Exception ex)
{
Library.Logging.Log.WriteErrorMessage(LOGTAG, "SpaDebugHelperFail", ex, "NPM SPA package installation failed");
}
return null;
}
}
#endif