feat(caddy): support renaming configuration files
This commit is contained in:
@@ -41,6 +41,14 @@ public interface ICaddyService
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
CaddyOperationResponse SaveCaddyGlobalConfiguration(string content);
|
CaddyOperationResponse SaveCaddyGlobalConfiguration(string content);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Method to rename an existing Caddy configuration file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="oldFileName"></param>
|
||||||
|
/// <param name="newFileName"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
CaddyOperationResponse RenameCaddyConfiguration(string oldFileName, string newFileName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Method to delete the given Caddy configurations by name
|
/// Method to delete the given Caddy configurations by name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -106,6 +106,91 @@ public class CaddyService(
|
|||||||
Content = content
|
Content = content
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public CaddyOperationResponse RenameCaddyConfiguration(string oldFileName, string newFileName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(newFileName))
|
||||||
|
{
|
||||||
|
return Failure("The configuration file name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldFileName == CaddyGlobalConfigName || newFileName == CaddyGlobalConfigName)
|
||||||
|
{
|
||||||
|
return Failure("The global Caddyfile cannot be renamed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldFileName == newFileName)
|
||||||
|
{
|
||||||
|
return new CaddyOperationResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Configuration file renamed successfully"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInvalidFileName(newFileName))
|
||||||
|
{
|
||||||
|
return Failure("The configuration file name contains invalid characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldPath = Path.Combine(Configurations.ConfigDir, $"{oldFileName}.caddy");
|
||||||
|
var newPath = Path.Combine(Configurations.ConfigDir, $"{newFileName}.caddy");
|
||||||
|
|
||||||
|
if (!File.Exists(oldPath))
|
||||||
|
{
|
||||||
|
return Failure("The configuration file to rename does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
// On case insensitive file systems the target resolves to the source for a case only rename,
|
||||||
|
// so the collision check has to be skipped there
|
||||||
|
var caseOnlyRename = string.Equals(oldFileName, newFileName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!caseOnlyRename && File.Exists(newPath))
|
||||||
|
{
|
||||||
|
return Failure("The configuration file already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (caseOnlyRename)
|
||||||
|
{
|
||||||
|
// A direct move would be rejected as an existing destination on a case insensitive
|
||||||
|
// file system, so go through an intermediate name that cannot collide
|
||||||
|
var tempPath = Path.Combine(Configurations.ConfigDir, $"{newFileName}.caddy.renaming");
|
||||||
|
File.Move(oldPath, tempPath);
|
||||||
|
File.Move(tempPath, newPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(oldPath, newPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CaddyOperationResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Configuration file renamed successfully"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Failure(e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CaddyOperationResponse Failure(string message) => new()
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guards the rename against path traversal and characters the file system would reject
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsInvalidFileName(string fileName) =>
|
||||||
|
fileName.Contains("..") || fileName.IndexOfAny(InvalidFileNameChars) >= 0;
|
||||||
|
|
||||||
|
private static readonly char[] InvalidFileNameChars =
|
||||||
|
[.. Path.GetInvalidFileNameChars().Union(['/', '\\'])];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CaddyDeleteOperationResponse DeleteCaddyConfigurations(List<string> configurationNames)
|
public CaddyDeleteOperationResponse DeleteCaddyConfigurations(List<string> configurationNames)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -466,6 +466,187 @@ public class CaddyServiceTests : IDisposable
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region RenameCaddyConfiguration Tests
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service renames an existing configuration file while preserving its content.
|
||||||
|
/// Setup: Creates a configuration file with known content, then requests a rename to an unused name.
|
||||||
|
/// Expectation: The service should move the file to the new name, remove the old one and keep the content intact, so a user can correct a configuration's name without losing its reverse proxy definition.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RenameCaddyConfiguration_WithExistingFile_RenamesSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string content = "example.com {\n reverse_proxy localhost:8080\n}";
|
||||||
|
File.WriteAllText(Path.Combine(_tempConfigDir, "old-name.caddy"), content);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.RenameCaddyConfiguration("old-name", "new-name");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
result.Message.Should().Be("Configuration file renamed successfully");
|
||||||
|
File.Exists(Path.Combine(_tempConfigDir, "old-name.caddy")).Should().BeFalse();
|
||||||
|
File.ReadAllText(Path.Combine(_tempConfigDir, "new-name.caddy")).Should().Be(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service refuses to rename a configuration onto a name that is already taken.
|
||||||
|
/// Setup: Creates two configuration files with distinct content, then attempts to rename the first onto the second.
|
||||||
|
/// Expectation: The service should fail without touching either file, preventing a rename from silently overwriting an unrelated reverse proxy configuration.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RenameCaddyConfiguration_WithExistingTarget_ReturnsFailureAndLeavesFilesUntouched()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sourcePath = Path.Combine(_tempConfigDir, "source.caddy");
|
||||||
|
var targetPath = Path.Combine(_tempConfigDir, "target.caddy");
|
||||||
|
File.WriteAllText(sourcePath, "source content");
|
||||||
|
File.WriteAllText(targetPath, "target content");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.RenameCaddyConfiguration("source", "target");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeFalse();
|
||||||
|
result.Message.Should().Be("The configuration file already exists");
|
||||||
|
File.ReadAllText(sourcePath).Should().Be("source content");
|
||||||
|
File.ReadAllText(targetPath).Should().Be("target content");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service reports a failure when the configuration to rename is not present.
|
||||||
|
/// Setup: Requests a rename for a configuration name that has no file in the configuration directory.
|
||||||
|
/// Expectation: The service should return a descriptive failure rather than creating anything, so a stale UI listing cannot produce an empty configuration file.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RenameCaddyConfiguration_WithMissingSource_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = _service.RenameCaddyConfiguration("does-not-exist", "new-name");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeFalse();
|
||||||
|
result.Message.Should().Be("The configuration file to rename does not exist");
|
||||||
|
File.Exists(Path.Combine(_tempConfigDir, "new-name.caddy")).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service rejects an empty new name for a rename.
|
||||||
|
/// Setup: Creates a configuration file, then attempts to rename it to whitespace.
|
||||||
|
/// Expectation: The service should fail and leave the original file in place, since an unnamed configuration file cannot be addressed or loaded by Caddy.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RenameCaddyConfiguration_WithEmptyNewName_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sourcePath = Path.Combine(_tempConfigDir, "source.caddy");
|
||||||
|
File.WriteAllText(sourcePath, "content");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.RenameCaddyConfiguration("source", " ");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeFalse();
|
||||||
|
result.Message.Should().Be("The configuration file name is required");
|
||||||
|
File.Exists(sourcePath).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service protects the global Caddyfile from being renamed in either direction.
|
||||||
|
/// Setup: Creates the global Caddyfile plus a regular configuration, then attempts a rename using "Caddyfile" as the source and as the target.
|
||||||
|
/// Expectation: Both attempts should fail and leave the files untouched, because Caddy loads the global configuration by that exact name and renaming it would break the whole proxy.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RenameCaddyConfiguration_WithGlobalCaddyfile_ReturnsFailure()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var globalPath = Path.Combine(_tempConfigDir, "Caddyfile");
|
||||||
|
var regularPath = Path.Combine(_tempConfigDir, "regular.caddy");
|
||||||
|
File.WriteAllText(globalPath, "global content");
|
||||||
|
File.WriteAllText(regularPath, "regular content");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var renameGlobalAway = _service.RenameCaddyConfiguration("Caddyfile", "something-else");
|
||||||
|
var renameOntoGlobal = _service.RenameCaddyConfiguration("regular", "Caddyfile");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
renameGlobalAway.Success.Should().BeFalse();
|
||||||
|
renameGlobalAway.Message.Should().Be("The global Caddyfile cannot be renamed");
|
||||||
|
renameOntoGlobal.Success.Should().BeFalse();
|
||||||
|
renameOntoGlobal.Message.Should().Be("The global Caddyfile cannot be renamed");
|
||||||
|
File.ReadAllText(globalPath).Should().Be("global content");
|
||||||
|
File.ReadAllText(regularPath).Should().Be("regular content");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service treats a rename to the unchanged name as a successful no-op.
|
||||||
|
/// Setup: Creates a configuration file and requests a rename to the exact same name.
|
||||||
|
/// Expectation: The service should succeed and leave the file as is, letting the editor call rename unconditionally on save without special-casing an untouched name field.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RenameCaddyConfiguration_WithUnchangedName_SucceedsAsNoOp()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var filePath = Path.Combine(_tempConfigDir, "same-name.caddy");
|
||||||
|
File.WriteAllText(filePath, "content");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.RenameCaddyConfiguration("same-name", "same-name");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
File.ReadAllText(filePath).Should().Be("content");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service allows a rename that only changes letter casing.
|
||||||
|
/// Setup: Creates a lowercase configuration file, then renames it to the same name with different casing.
|
||||||
|
/// Expectation: The service should succeed and the content should be reachable under the new casing, since on case insensitive file systems a naive collision check would otherwise see the source file as an existing target and reject a legitimate rename.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RenameCaddyConfiguration_WithCaseOnlyChange_RenamesSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
File.WriteAllText(Path.Combine(_tempConfigDir, "myapp.caddy"), "content");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.RenameCaddyConfiguration("myapp", "MyApp");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
File.ReadAllText(Path.Combine(_tempConfigDir, "MyApp.caddy")).Should().Be("content");
|
||||||
|
_service.GetExistingCaddyConfigurations().Select(c => c.FileName).Should().BeEquivalentTo(["MyApp"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service rejects rename targets that would escape the configuration directory or use characters the file system disallows.
|
||||||
|
/// Setup: Creates a configuration file, then attempts renames using a path traversal segment, a directory separator and an invalid file name character.
|
||||||
|
/// Expectation: Every attempt should fail with an invalid characters message and write nothing outside the configuration directory, since the new name arrives from user input and is used directly to build a file path.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("../escaped")]
|
||||||
|
[InlineData("nested/name")]
|
||||||
|
[InlineData("invalid\0name")]
|
||||||
|
public void RenameCaddyConfiguration_WithUnsafeNewName_ReturnsFailure(string newFileName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sourcePath = Path.Combine(_tempConfigDir, "source.caddy");
|
||||||
|
File.WriteAllText(sourcePath, "content");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.RenameCaddyConfiguration("source", newFileName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeFalse();
|
||||||
|
result.Message.Should().Be("The configuration file name contains invalid characters");
|
||||||
|
File.ReadAllText(sourcePath).Should().Be("content");
|
||||||
|
Directory.GetFiles(_tempConfigDir).Should().HaveCount(1);
|
||||||
|
File.Exists(Path.Combine(_tempConfigDir, "..", "escaped.caddy")).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region DeleteCaddyConfigurations Tests
|
#region DeleteCaddyConfigurations Tests
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ public partial class CaddyReverseProxiesPage : ComponentBase
|
|||||||
private void Refresh()
|
private void Refresh()
|
||||||
{
|
{
|
||||||
var notSearching = string.IsNullOrWhiteSpace(_debouncedText);
|
var notSearching = string.IsNullOrWhiteSpace(_debouncedText);
|
||||||
var configurations = CaddyService.GetExistingCaddyConfigurations()
|
var allConfigurations = CaddyService.GetExistingCaddyConfigurations();
|
||||||
|
var configurations = allConfigurations
|
||||||
.Where(conf => notSearching || conf.FileName.Contains(_debouncedText, StringComparison.OrdinalIgnoreCase) || conf.ReverseProxyHostname.Contains(_debouncedText, StringComparison.OrdinalIgnoreCase) || conf.Tags.Any(tag => tag.Contains(_debouncedText, StringComparison.OrdinalIgnoreCase)))
|
.Where(conf => notSearching || conf.FileName.Contains(_debouncedText, StringComparison.OrdinalIgnoreCase) || conf.ReverseProxyHostname.Contains(_debouncedText, StringComparison.OrdinalIgnoreCase) || conf.Tags.Any(tag => tag.Contains(_debouncedText, StringComparison.OrdinalIgnoreCase)))
|
||||||
.OrderBy(conf => conf.FileName)
|
.OrderBy(conf => conf.FileName)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -112,6 +113,9 @@ public partial class CaddyReverseProxiesPage : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
_availableCaddyConfigurations = [..configurations];
|
_availableCaddyConfigurations = [..configurations];
|
||||||
|
// Drop selections that no longer exist, e.g. after a rename, so a later delete does not target a stale name.
|
||||||
|
// Checked against every configuration on disk so an active search filter does not clear the selection.
|
||||||
|
_selectedCaddyConfigurations = [.. _selectedCaddyConfigurations.Where(allConfigurations.Contains)];
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
<MudFocusTrap>
|
<MudFocusTrap>
|
||||||
<MudTextField @bind-Value="FileName" Label="File name" Variant="Variant.Outlined"
|
<MudTextField @bind-Value="FileName" Label="File name" Variant="Variant.Outlined"
|
||||||
Style="margin-bottom: 8px;"
|
Style="margin-bottom: 8px;"
|
||||||
ShrinkLabel="true"
|
ShrinkLabel="true"></MudTextField>
|
||||||
ReadOnly="@(!IsNew)"></MudTextField>
|
|
||||||
</MudFocusTrap>
|
</MudFocusTrap>
|
||||||
<MudText Typo="Typo.caption" class="pl-4">File content</MudText>
|
<MudText Typo="Typo.caption" class="pl-4">File content</MudText>
|
||||||
<StandaloneCodeEditor @ref="_codeEditor"
|
<StandaloneCodeEditor @ref="_codeEditor"
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ public partial class CaddyfileEditor : ComponentBase
|
|||||||
private string _caddyConfigurationContent = string.Empty;
|
private string _caddyConfigurationContent = string.Empty;
|
||||||
private StandaloneCodeEditor _codeEditor = null!;
|
private StandaloneCodeEditor _codeEditor = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The file name the dialog was opened with, used to detect a rename since FileName follows the text field
|
||||||
|
/// </summary>
|
||||||
|
private string _originalFileName = string.Empty;
|
||||||
|
|
||||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -38,6 +43,7 @@ public partial class CaddyfileEditor : ComponentBase
|
|||||||
protected override Task OnInitializedAsync()
|
protected override Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
IsNew = string.IsNullOrWhiteSpace(FileName);
|
IsNew = string.IsNullOrWhiteSpace(FileName);
|
||||||
|
_originalFileName = FileName;
|
||||||
|
|
||||||
if (!IsNew)
|
if (!IsNew)
|
||||||
{
|
{
|
||||||
@@ -74,10 +80,33 @@ public partial class CaddyfileEditor : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves the Caddy configuration file
|
/// Renames the Caddy configuration file when needed, then saves its content
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task Submit()
|
/// <returns>True when the configuration was persisted</returns>
|
||||||
|
private async Task<bool> Save()
|
||||||
{
|
{
|
||||||
|
// Rename first so a failed rename never leaves the content written under a stale name
|
||||||
|
if (!IsNew && !string.Equals(_originalFileName, FileName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var renameResponse = CaddyService.RenameCaddyConfiguration(_originalFileName, FileName);
|
||||||
|
|
||||||
|
if (!renameResponse.Success)
|
||||||
|
{
|
||||||
|
Snackbar.Add(renameResponse.Message, Severity.Error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.Add($"Renamed {_originalFileName} to {FileName}", Severity.Info);
|
||||||
|
|
||||||
|
if (CaddyService.GetCaddyGlobalConfigurationContent().Contains(_originalFileName))
|
||||||
|
{
|
||||||
|
Snackbar.Add($"The global Caddyfile still references {_originalFileName}, update its import",
|
||||||
|
Severity.Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
_originalFileName = FileName;
|
||||||
|
}
|
||||||
|
|
||||||
var response = CaddyService.SaveCaddyConfiguration(new CaddySaveConfigurationRequest
|
var response = CaddyService.SaveCaddyConfiguration(new CaddySaveConfigurationRequest
|
||||||
{
|
{
|
||||||
IsNew = IsNew,
|
IsNew = IsNew,
|
||||||
@@ -85,15 +114,24 @@ public partial class CaddyfileEditor : ComponentBase
|
|||||||
Content = await _codeEditor.GetValue(),
|
Content = await _codeEditor.GetValue(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.Success)
|
if (!response.Success)
|
||||||
{
|
|
||||||
Snackbar.Add($"{FileName} Caddy configuration saved successfully", Severity.Success);
|
|
||||||
MudDialog.Close(DialogResult.Ok(false)); // Indicate successful save but no restart
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
Snackbar.Add(response.Message, Severity.Error);
|
Snackbar.Add(response.Message, Severity.Error);
|
||||||
// MudDialog.Close(DialogResult.Ok(false)); // Indicate failed save
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snackbar.Add($"{FileName} Caddy configuration saved successfully", Severity.Success);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the Caddy configuration file
|
||||||
|
/// </summary>
|
||||||
|
private async Task Submit()
|
||||||
|
{
|
||||||
|
if (await Save())
|
||||||
|
{
|
||||||
|
MudDialog.Close(DialogResult.Ok(false)); // Indicate successful save but no restart
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,25 +148,11 @@ public partial class CaddyfileEditor : ComponentBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SaveAndRestart()
|
private async Task SaveAndRestart()
|
||||||
{
|
{
|
||||||
var submitResponse = CaddyService.SaveCaddyConfiguration(new CaddySaveConfigurationRequest
|
if (await Save())
|
||||||
{
|
{
|
||||||
IsNew = IsNew,
|
|
||||||
FileName = FileName,
|
|
||||||
Content = await _codeEditor.GetValue(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (submitResponse.Success)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"{FileName} Caddy configuration saved successfully", Severity.Success);
|
|
||||||
// Indicate successful save and that a restart is required by the calling component
|
// Indicate successful save and that a restart is required by the calling component
|
||||||
MudDialog.Close(DialogResult.Ok(true));
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
Snackbar.Add(submitResponse.Message, Severity.Error);
|
|
||||||
// Indicate failed save, no restart needed
|
|
||||||
// MudDialog.Close(DialogResult.Ok(false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
22
scripts/dotnet-docker.sh
Executable file
22
scripts/dotnet-docker.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Runs any dotnet command inside the official .NET SDK container, for machines
|
||||||
|
# without a local SDK installed. The repository is mounted at /src.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/dotnet-docker.sh test
|
||||||
|
# ./scripts/dotnet-docker.sh build CaddyManager.sln
|
||||||
|
# ./scripts/dotnet-docker.sh test --filter FullyQualifiedName~RenameCaddyConfiguration
|
||||||
|
#
|
||||||
|
# NuGet packages are cached in a named volume so only the first run downloads them.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
exec docker run --rm \
|
||||||
|
--volume "$repo_root":/src \
|
||||||
|
--workdir /src \
|
||||||
|
--volume caddymanager-nuget:/root/.nuget/packages \
|
||||||
|
mcr.microsoft.com/dotnet/sdk:9.0 \
|
||||||
|
dotnet "$@"
|
||||||
Reference in New Issue
Block a user