duplicati/Duplicati/WebserverCore/Middlewares/StaticFilesMiddleware.Debug.cs
Marcelo C. 614f047dbb Sets infinite timeout for HTTP clients
Ensures that long-running HTTP requests, such as downloads and uploads, do not timeout prematurely.

This change configures specific HttpClient instances with an infinite timeout to accommodate operations that may take an extended period. The default 100s timeout is retained for some operations like registration, where a long delay may indicate a problem.
2025-06-12 10:33:13 -03:00

197 lines
No EOL
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 = HttpClientHelper.CreateClient())
{
// Explicitly leaving the 100s default timeout
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