Исправление: том ./mods:/lampac/mods в compose и копии 10 модулей с enable:true; конфиг без SkipModules
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Shared;
|
||||
using Shared.Attributes;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AdminPanel;
|
||||
|
||||
[Authorization(redirectUri: "/adminpanel/auth")]
|
||||
public class AdminPanelController : BaseController
|
||||
{
|
||||
const string InitFile = "init.conf";
|
||||
const string CurrentFile = "current.conf";
|
||||
const string UsersFile = "users.json";
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
[Route("/adminpanel/auth")]
|
||||
public ActionResult Auth()
|
||||
{
|
||||
var path = Path.Combine(ModInit.modpath, "auth.html");
|
||||
var html = System.IO.File.ReadAllText(path, Encoding.UTF8);
|
||||
return Content(html, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/adminpanel")]
|
||||
public ActionResult Index()
|
||||
{
|
||||
var path = Path.Combine(ModInit.modpath, "index.html");
|
||||
var html = System.IO.File.ReadAllText(path, Encoding.UTF8);
|
||||
return Content(html, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/adminpanel/api/groups")]
|
||||
public ActionResult Groups()
|
||||
{
|
||||
var current = LoadCurrentRoot();
|
||||
var built = ConfigSectionGroups.Build(current);
|
||||
var json = JsonConvert.SerializeObject(built, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
return Content(json, "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/adminpanel/api/groups/catalog")]
|
||||
public ActionResult GroupsCatalog()
|
||||
{
|
||||
var built = ConfigSectionGroups.BuildCatalog();
|
||||
var current = LoadCurrentRoot();
|
||||
var inCatalog = ConfigSectionGroups.CatalogRootKeys;
|
||||
var orphans = current.Properties()
|
||||
.Select(p => p.Name)
|
||||
.Where(k => !inCatalog.Contains(k))
|
||||
.OrderBy(k => k, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (orphans.Length > 0)
|
||||
built.Add(new GroupDto("other", "Прочее", "Ключи из current, не из каталога групп.", orphans));
|
||||
|
||||
var json = JsonConvert.SerializeObject(built, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
return Content(json, "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
static JObject LoadCurrentRoot()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.IO.File.Exists(CurrentFile))
|
||||
return JObject.Parse(System.IO.File.ReadAllText(CurrentFile, Encoding.UTF8));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
if (CoreInit.CurrentConf != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (JObject)CoreInit.CurrentConf.DeepClone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return new JObject();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/adminpanel/api/init")]
|
||||
public ActionResult GetInit()
|
||||
{
|
||||
if (!System.IO.File.Exists(InitFile))
|
||||
return Content("{}", "application/json; charset=utf-8");
|
||||
|
||||
var text = System.IO.File.ReadAllText(InitFile, Encoding.UTF8);
|
||||
return Content(NormalizeJsonText(text), "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/adminpanel/api/current")]
|
||||
public ActionResult GetCurrent()
|
||||
{
|
||||
if (!System.IO.File.Exists(CurrentFile))
|
||||
{
|
||||
if (CoreInit.CurrentConf != null)
|
||||
return Content(CoreInit.CurrentConf.ToString(Formatting.Indented), "application/json; charset=utf-8");
|
||||
return Content("{}", "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
var text = System.IO.File.ReadAllText(CurrentFile, Encoding.UTF8);
|
||||
return Content(NormalizeJsonText(text), "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/adminpanel/api/init")]
|
||||
public async Task<IActionResult> SaveInit()
|
||||
{
|
||||
string body;
|
||||
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true))
|
||||
body = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return AdminJsonError(400, "empty body");
|
||||
|
||||
try
|
||||
{
|
||||
var parsed = JToken.Parse(body);
|
||||
if (parsed.Type != JTokenType.Object)
|
||||
return AdminJsonError(400, "root must be a JSON object");
|
||||
|
||||
var formatted = ((JObject)parsed).ToString(Formatting.Indented);
|
||||
await WriteInitAtomicAsync(formatted).ConfigureAwait(false);
|
||||
return AdminJsonOk();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return AdminJsonError(400, "invalid json", ex.Message);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
return AdminJsonError(500, "failed to write init.conf", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/adminpanel/api/init/section/{key}")]
|
||||
public async Task<IActionResult> SaveInitSection(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || key.Contains('/') || key.Contains('\\'))
|
||||
return AdminJsonError(400, "invalid section key");
|
||||
|
||||
string body;
|
||||
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true))
|
||||
body = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return AdminJsonError(400, "empty body");
|
||||
|
||||
JToken sectionToken;
|
||||
try
|
||||
{
|
||||
sectionToken = JToken.Parse(body);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return AdminJsonError(400, "invalid json", ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
JObject root;
|
||||
if (System.IO.File.Exists(InitFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
root = JObject.Parse(System.IO.File.ReadAllText(InitFile, Encoding.UTF8));
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
root = new JObject();
|
||||
}
|
||||
}
|
||||
else
|
||||
root = new JObject();
|
||||
|
||||
root[key] = sectionToken.DeepClone();
|
||||
var formatted = root.ToString(Formatting.Indented);
|
||||
await WriteInitAtomicAsync(formatted).ConfigureAwait(false);
|
||||
return AdminJsonOk();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
return AdminJsonError(500, "failed to write init.conf", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// tmp+Move — атомарно; на bind mount Move(..., overwrite) часто EBUSY → пишем в init.conf напрямую.
|
||||
static async Task WriteInitAtomicAsync(string formatted)
|
||||
{
|
||||
var tmp = InitFile + ".tmp";
|
||||
await System.IO.File.WriteAllTextAsync(tmp, formatted, Encoding.UTF8).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.File.Move(tmp, InitFile, overwrite: true);
|
||||
}
|
||||
catch (IOException ex) when (IsReplaceTargetBusy(ex))
|
||||
{
|
||||
await System.IO.File.WriteAllTextAsync(InitFile, formatted, Encoding.UTF8).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.IO.File.Exists(tmp))
|
||||
System.IO.File.Delete(tmp);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsReplaceTargetBusy(IOException ex)
|
||||
{
|
||||
for (Exception e = ex; e != null; e = e.InnerException)
|
||||
{
|
||||
if (e.Message != null && e.Message.Contains("busy", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static ContentResult AdminJsonOk()
|
||||
{
|
||||
return new ContentResult
|
||||
{
|
||||
Content = "{\"ok\":true}",
|
||||
ContentType = "application/json; charset=utf-8",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
static ContentResult AdminJsonError(int status, string error, string detail = null)
|
||||
{
|
||||
var o = new JObject { ["error"] = error };
|
||||
if (!string.IsNullOrEmpty(detail))
|
||||
o["detail"] = detail;
|
||||
return new ContentResult
|
||||
{
|
||||
Content = o.ToString(Formatting.None),
|
||||
ContentType = "application/json; charset=utf-8",
|
||||
StatusCode = status
|
||||
};
|
||||
}
|
||||
|
||||
static string NormalizeJsonText(string raw)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JToken.Parse(raw).ToString(Formatting.Indented);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/adminpanel/api/users-json")]
|
||||
public ActionResult GetUsersJson()
|
||||
{
|
||||
if (!System.IO.File.Exists(UsersFile))
|
||||
return Content("[]", "application/json; charset=utf-8");
|
||||
|
||||
var text = System.IO.File.ReadAllText(UsersFile, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return Content("[]", "application/json; charset=utf-8");
|
||||
|
||||
return Content(NormalizeJsonText(text), "application/json; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/adminpanel/api/users-json")]
|
||||
public async Task<IActionResult> SaveUsersJson()
|
||||
{
|
||||
string body;
|
||||
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true))
|
||||
body = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
return AdminJsonError(400, "empty body");
|
||||
|
||||
try
|
||||
{
|
||||
var parsed = JToken.Parse(body);
|
||||
if (parsed.Type != JTokenType.Array)
|
||||
return AdminJsonError(400, "root must be a JSON array", "users.json must be a list of AccsUser objects");
|
||||
|
||||
foreach (var item in (JArray)parsed)
|
||||
{
|
||||
if (item.Type != JTokenType.Object)
|
||||
return AdminJsonError(400, "invalid array item", "each element must be a JSON object");
|
||||
}
|
||||
|
||||
var formatted = ((JArray)parsed).ToString(Formatting.Indented);
|
||||
await WriteUsersAtomicAsync(formatted).ConfigureAwait(false);
|
||||
return AdminJsonOk();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return AdminJsonError(400, "invalid json", ex.Message);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
return AdminJsonError(500, "failed to write users.json", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task WriteUsersAtomicAsync(string formatted)
|
||||
{
|
||||
var tmp = UsersFile + ".tmp";
|
||||
await System.IO.File.WriteAllTextAsync(tmp, formatted, Encoding.UTF8).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.File.Move(tmp, UsersFile, overwrite: true);
|
||||
}
|
||||
catch (IOException ex) when (IsReplaceTargetBusy(ex))
|
||||
{
|
||||
await System.IO.File.WriteAllTextAsync(UsersFile, formatted, Encoding.UTF8).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.IO.File.Exists(tmp))
|
||||
System.IO.File.Delete(tmp);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user