diff --git a/src/TelegramPanel.Web/Components/Pages/Settings.razor b/src/TelegramPanel.Web/Components/Pages/Settings.razor index b1472f6..5719bf7 100644 --- a/src/TelegramPanel.Web/Components/Pages/Settings.razor +++ b/src/TelegramPanel.Web/Components/Pages/Settings.razor @@ -10,6 +10,7 @@ @inject ISnackbar Snackbar @inject IDialogService DialogService @inject IWebHostEnvironment Environment +@using TelegramPanel.Web.Services 系统设置 - Telegram Panel @@ -150,7 +151,7 @@ private int logRetentionDays = 30; private bool syncing = false; - private string LocalConfigPath => System.IO.Path.Combine(Environment.ContentRootPath, "appsettings.local.json"); + private string LocalConfigPath => LocalConfigFile.ResolvePath(Configuration, Environment); private bool LocalConfigExists => System.IO.File.Exists(LocalConfigPath); protected override void OnInitialized() @@ -188,8 +189,18 @@ // 配置变更后清理客户端缓存,确保后续操作使用新 ApiId/ApiHash await TelegramClientPool.RemoveAllClientsAsync(); - var fileInfo = new System.IO.FileInfo(localConfigPath); - Snackbar.Add($"API 配置已保存:{localConfigPath}({fileInfo.Length} bytes)。已清理 Telegram 客户端缓存,后续操作将使用新配置。", Severity.Success); + string sizeHint; + try + { + var fileInfo = new System.IO.FileInfo(localConfigPath); + sizeHint = $"({fileInfo.Length} bytes)"; + } + catch + { + sizeHint = ""; + } + + Snackbar.Add($"API 配置已保存:{localConfigPath}{sizeHint}。已清理 Telegram 客户端缓存,后续操作将使用新配置。", Severity.Success); StateHasChanged(); } catch (Exception ex) @@ -240,11 +251,9 @@ private async Task SaveLocalConfigAsync(System.Text.Json.Nodes.JsonObject root) { var json = root.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); - await System.IO.File.WriteAllTextAsync( - LocalConfigPath, - json, - new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false) - ); + var path = LocalConfigPath; + await LocalConfigFile.EnsureExistsAsync(path); + await LocalConfigFile.WriteJsonAtomicallyAsync(path, json); } private static System.Text.Json.Nodes.JsonObject EnsureObject(System.Text.Json.Nodes.JsonObject root, string key) diff --git a/src/TelegramPanel.Web/Program.cs b/src/TelegramPanel.Web/Program.cs index a904e8a..affa17f 100644 --- a/src/TelegramPanel.Web/Program.cs +++ b/src/TelegramPanel.Web/Program.cs @@ -18,7 +18,7 @@ var builder = WebApplication.CreateBuilder(args); // 直接加载会抛 FileNotFoundException 导致容器不断重启,因此这里先确保文件存在一个空 JSON。 try { - var localOverridePath = Path.Combine(builder.Environment.ContentRootPath, "appsettings.local.json"); + var localOverridePath = LocalConfigFile.ResolvePath(builder.Configuration, builder.Environment); if (!File.Exists(localOverridePath)) File.WriteAllText(localOverridePath, "{}", new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } @@ -31,7 +31,8 @@ try { // 注意:如果该路径是“悬空符号链接”,配置系统可能会认为文件存在并尝试打开,从而抛 FileNotFoundException。 // 本地覆盖配置不应该阻塞启动,因此这里兜底吞掉 FileNotFoundException。 - builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true); + var localOverridePath = LocalConfigFile.ResolvePath(builder.Configuration, builder.Environment); + builder.Configuration.AddJsonFile(localOverridePath, optional: true, reloadOnChange: true); } catch (FileNotFoundException ex) { diff --git a/src/TelegramPanel.Web/Services/LocalConfigFile.cs b/src/TelegramPanel.Web/Services/LocalConfigFile.cs new file mode 100644 index 0000000..412f153 --- /dev/null +++ b/src/TelegramPanel.Web/Services/LocalConfigFile.cs @@ -0,0 +1,46 @@ +using System.Text; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +namespace TelegramPanel.Web.Services; + +public static class LocalConfigFile +{ + public static string ResolvePath(IConfiguration configuration, IWebHostEnvironment environment) + { + var configured = (configuration["LocalConfig:Path"] ?? "").Trim(); + if (!string.IsNullOrWhiteSpace(configured)) + return configured; + + // Docker 部署默认持久化目录为 /data(docker-compose 挂载 ./docker-data:/data) + // 即便没有显式配置,也优先写到 /data,避免写入镜像层 /app 导致丢失或权限问题。 + if (Directory.Exists("/data")) + return "/data/appsettings.local.json"; + + return Path.Combine(environment.ContentRootPath, "appsettings.local.json"); + } + + public static async Task EnsureExistsAsync(string path, CancellationToken cancellationToken = default) + { + var dir = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(dir)) + Directory.CreateDirectory(dir); + + if (File.Exists(path)) + return; + + await File.WriteAllTextAsync(path, "{}", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), cancellationToken); + } + + public static async Task WriteJsonAtomicallyAsync(string path, string json, CancellationToken cancellationToken = default) + { + var dir = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(dir)) + Directory.CreateDirectory(dir); + + var tmp = $"{path}.tmp"; + await File.WriteAllTextAsync(tmp, json, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), cancellationToken); + File.Move(tmp, path, overwrite: true); + } +} +