[feat] Index Service
This commit is contained in:
12
.idea/.idea.Abyss/.idea/workspace.xml
generated
12
.idea/.idea.Abyss/.idea/workspace.xml
generated
@@ -9,7 +9,9 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="" />
|
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
|
||||||
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
@@ -28,16 +30,13 @@
|
|||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/af/aac0eaa5/ExceptionDispatchInfo.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/af/aac0eaa5/ExceptionDispatchInfo.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/d0/3b166e9e/String.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/d0/3b166e9e/String.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/5df2accb46d040ccbbbe8331bf4d24b61daa00/df/93debd37/ControllerBase.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/5df2accb46d040ccbbbe8331bf4d24b61daa00/df/93debd37/ControllerBase.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/61fe11e9d86b4d2a9bd2b806929b7d381a400/e9/67f4a40e/SQLiteAsyncConnection.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/7598e47d5cdf4107ba88f8220720fdc89000/a6/79d67871/xxHash128.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/7598e47d5cdf4107ba88f8220720fdc89000/a6/79d67871/xxHash128.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/f09ccaeb94c34c2299acd3efee0facee1a400/81/137b58b4/Key.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/f09ccaeb94c34c2299acd3efee0facee1a400/81/137b58b4/Key.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/AbyssController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/AbyssController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Controllers/Media/LiveController.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Controllers/Media/LiveController.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Middleware/BadRequestExceptionMiddleware.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Middleware/BadRequestExceptionMiddleware.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/RootController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/RootController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Controllers/Security/RootController.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Controllers/Security/RootController.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Task/TaskController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Task/TaskController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/AbyssService.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/AbyssService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
@@ -60,7 +59,6 @@
|
|||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/User.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/User.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/UserCreating.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/UserCreating.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/Video.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Video.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/AbyssCli/Program.cs" root0="FORCE_HIGHLIGHTING" />
|
|
||||||
<setting file="file:///usr/lib/dotnet/sdk/9.0.109/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.targets" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file:///usr/lib/dotnet/sdk/9.0.109/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.targets" root0="FORCE_HIGHLIGHTING" />
|
||||||
</component>
|
</component>
|
||||||
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
||||||
@@ -229,6 +227,8 @@
|
|||||||
<workItem from="1758349710909" duration="16000" />
|
<workItem from="1758349710909" duration="16000" />
|
||||||
<workItem from="1758350096355" duration="452000" />
|
<workItem from="1758350096355" duration="452000" />
|
||||||
<workItem from="1758350848039" duration="946000" />
|
<workItem from="1758350848039" duration="946000" />
|
||||||
|
<workItem from="1758352441563" duration="281000" />
|
||||||
|
<workItem from="1758599755722" duration="14000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncTableQuery_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F61fe11e9d86b4d2a9bd2b806929b7d381a400_003Fa1_003F62750ee4_003FAsyncTableQuery_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfiguredValueTaskAwaitable_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003F25_003F817def70_003FConfiguredValueTaskAwaitable_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AConfiguredValueTaskAwaitable_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003F25_003F817def70_003FConfiguredValueTaskAwaitable_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5df2accb46d040ccbbbe8331bf4d24b61daa00_003Fdf_003F93debd37_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5df2accb46d040ccbbbe8331bf4d24b61daa00_003Fdf_003F93debd37_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003Faf_003Faac0eaa5_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003Faf_003Faac0eaa5_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
|||||||
@@ -47,12 +47,10 @@ public class ImageController(ResourceService rs, ConfigureService config) : Base
|
|||||||
|
|
||||||
return Ok(await System.IO.File.ReadAllTextAsync(d));
|
return Ok(await System.IO.File.ReadAllTextAsync(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("bulkquery")]
|
[HttpPost("bulkquery")]
|
||||||
public async Task<IActionResult> QueryBulk([FromQuery] string token, [FromBody] string[] id)
|
public async Task<IActionResult> QueryBulk([FromQuery] string token, [FromBody] string[] id)
|
||||||
{
|
{
|
||||||
List<string> result = new List<string>();
|
|
||||||
|
|
||||||
var db = id.Select(x => Helpers.SafePathCombine(ImageFolder, [x, "summary.json"])).ToArray();
|
var db = id.Select(x => Helpers.SafePathCombine(ImageFolder, [x, "summary.json"])).ToArray();
|
||||||
if (db.Any(x => x == null))
|
if (db.Any(x => x == null))
|
||||||
return BadRequest();
|
return BadRequest();
|
||||||
|
|||||||
11
Abyss/Components/Controllers/Media/IndexController.cs
Normal file
11
Abyss/Components/Controllers/Media/IndexController.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Abyss.Components.Static;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Abyss.Components.Controllers.Media;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class IndexController: BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ public class VideoController(ILogger<VideoController> logger, ResourceService rs
|
|||||||
{
|
{
|
||||||
var d = Helpers.SafePathCombine(VideoFolder, klass);
|
var d = Helpers.SafePathCombine(VideoFolder, klass);
|
||||||
if (d == null) return StatusCode(403, new { message = "403 Denied" });
|
if (d == null) return StatusCode(403, new { message = "403 Denied" });
|
||||||
|
|
||||||
var r = await rs.Query(d, token, Ip);
|
var r = await rs.Query(d, token, Ip);
|
||||||
if (r == null) return StatusCode(401, new { message = "Unauthorized" });
|
if (r == null) return StatusCode(401, new { message = "Unauthorized" });
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ public class ConfigureService
|
|||||||
public string Version { get; } = "Alpha v0.1";
|
public string Version { get; } = "Alpha v0.1";
|
||||||
public string UserDatabase { get; set; } = "user.db";
|
public string UserDatabase { get; set; } = "user.db";
|
||||||
public string RaDatabase { get; set; } = "ra.db";
|
public string RaDatabase { get; set; } = "ra.db";
|
||||||
|
public string IndexDatabase { get; set; } = "index.db";
|
||||||
}
|
}
|
||||||
230
Abyss/Components/Services/IndexService.cs
Normal file
230
Abyss/Components/Services/IndexService.cs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
using SQLite;
|
||||||
|
using Index = Abyss.Model.Index;
|
||||||
|
namespace Abyss.Components.Services;
|
||||||
|
|
||||||
|
public class IndexService: IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly SQLiteAsyncConnection _db;
|
||||||
|
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
|
||||||
|
private IndexService(string dbPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dbPath)) throw new ArgumentNullException(nameof(dbPath));
|
||||||
|
_db = new SQLiteAsyncConnection(dbPath);
|
||||||
|
|
||||||
|
_db.CreateTableAsync<Index>().Wait();
|
||||||
|
EnsureRootExistsAsync().Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureRootExistsAsync()
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure there is a root node with Id = 1. If it already exists, this does nothing.
|
||||||
|
// Using INSERT OR IGNORE so that explicit Id insertion will be ignored if existing.
|
||||||
|
await _db.ExecuteAsync("INSERT OR IGNORE INTO \"Index\" (Id, Type, Reference, Children) VALUES (?, ?, ?, ?)",
|
||||||
|
1, 0, string.Empty, string.Empty).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Index?> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _db.FindAsync<Index>(id).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Index> InsertNodeAsChildAsync(int parentId, int type, string reference = "")
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parent = await _db.FindAsync<Index>(parentId).ConfigureAwait(false);
|
||||||
|
if (parent == null) throw new InvalidOperationException($"Parent node {parentId} not found");
|
||||||
|
|
||||||
|
var node = new Index
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Reference = reference,
|
||||||
|
Children = string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
await _db.InsertAsync(node).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Update parent's children
|
||||||
|
var children = ParseChildren(parent.Children);
|
||||||
|
if (!children.Contains(node.Id))
|
||||||
|
{
|
||||||
|
children.Add(node.Id);
|
||||||
|
parent.Children = SerializeChildren(children);
|
||||||
|
await _db.UpdateAsync(parent).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteNodeAsync(int id)
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var node = await _db.FindAsync<Index>(id).ConfigureAwait(false);
|
||||||
|
if (node == null) return false;
|
||||||
|
|
||||||
|
await _db.DeleteAsync(node).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Remove references from all parents
|
||||||
|
var all = await _db.Table<Index>().ToListAsync().ConfigureAwait(false);
|
||||||
|
foreach (var parent in all)
|
||||||
|
{
|
||||||
|
var children = ParseChildren(parent.Children);
|
||||||
|
if (children.Remove(id))
|
||||||
|
{
|
||||||
|
parent.Children = SerializeChildren(children);
|
||||||
|
await _db.UpdateAsync(parent).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateTypeOrReferenceAsync(int id, int? type = null, string? reference = null)
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var node = await _db.FindAsync<Index>(id).ConfigureAwait(false) ?? throw new InvalidOperationException($"Node {id} not found");
|
||||||
|
var changed = false;
|
||||||
|
if (type.HasValue && node.Type != type.Value)
|
||||||
|
{
|
||||||
|
node.Type = type.Value;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (reference != null && node.Reference != reference)
|
||||||
|
{
|
||||||
|
node.Reference = reference;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) await _db.UpdateAsync(node).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddEdgeAsync(int fromId, int toId)
|
||||||
|
{
|
||||||
|
if (fromId == toId) throw new InvalidOperationException("Self-loop not allowed");
|
||||||
|
|
||||||
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var from = await _db.FindAsync<Index>(fromId).ConfigureAwait(false) ?? throw new InvalidOperationException($"From node {fromId} not found");
|
||||||
|
_ = await _db.FindAsync<Index>(toId).ConfigureAwait(false) ?? throw new InvalidOperationException($"To node {toId} not found");
|
||||||
|
|
||||||
|
var children = ParseChildren(from.Children);
|
||||||
|
if (!children.Contains(toId))
|
||||||
|
{
|
||||||
|
children.Add(toId);
|
||||||
|
from.Children = SerializeChildren(children);
|
||||||
|
await _db.UpdateAsync(from).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RemoveEdgeAsync(int fromId, int toId)
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var from = await _db.FindAsync<Index>(fromId).ConfigureAwait(false);
|
||||||
|
if (from == null) return false;
|
||||||
|
|
||||||
|
var children = ParseChildren(from.Children);
|
||||||
|
var removed = children.Remove(toId);
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
from.Children = SerializeChildren(children);
|
||||||
|
await _db.UpdateAsync(from).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<int>> GetChildrenIdsAsync(int id)
|
||||||
|
{
|
||||||
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var node = await _db.FindAsync<Index>(id).ConfigureAwait(false);
|
||||||
|
if (node == null) return new List<int>();
|
||||||
|
return ParseChildren(node.Children);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_lock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static List<int> ParseChildren(string children)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(children)) return new List<int>();
|
||||||
|
return children.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(s => s.Trim())
|
||||||
|
.Where(s => int.TryParse(s, out _))
|
||||||
|
.Select(int.Parse)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
private static string SerializeChildren(List<int> children)
|
||||||
|
{
|
||||||
|
if (children.Count == 0) return string.Empty;
|
||||||
|
var seen = new HashSet<int>();
|
||||||
|
var ordered = new List<int>();
|
||||||
|
foreach (var c in children)
|
||||||
|
{
|
||||||
|
if (seen.Add(c)) ordered.Add(c);
|
||||||
|
}
|
||||||
|
return string.Join(",", ordered);
|
||||||
|
}
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
_lock.Dispose();
|
||||||
|
// SQLiteAsyncConnection does not expose a Close method; rely on finalizer if any.
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
241
Abyss/Components/Services/ResourceDatabaseService.cs
Normal file
241
Abyss/Components/Services/ResourceDatabaseService.cs
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
using System.IO.Hashing;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Abyss.Components.Static;
|
||||||
|
using Abyss.Model;
|
||||||
|
using SQLite;
|
||||||
|
|
||||||
|
namespace Abyss.Components.Services;
|
||||||
|
|
||||||
|
public class ResourceDatabaseService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ResourceDatabaseService> _logger;
|
||||||
|
private readonly ConfigureService _config;
|
||||||
|
public readonly SQLiteAsyncConnection ResourceDatabase;
|
||||||
|
public static readonly Regex PermissionRegex = new("^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public ResourceDatabaseService(ConfigureService config, ILogger<ResourceDatabaseService> logger)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
ResourceDatabase = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
|
||||||
|
ResourceDatabase.CreateTableAsync<ResourceAttribute>().Wait();
|
||||||
|
|
||||||
|
|
||||||
|
var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks");
|
||||||
|
if (tasksPath != null)
|
||||||
|
{
|
||||||
|
InsertRaRow(tasksPath, 1, "rw,r-,r-", true).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
var livePath = Helpers.SafePathCombine(_config.MediaRoot, "Live");
|
||||||
|
if (livePath != null)
|
||||||
|
{
|
||||||
|
InsertRaRow(livePath, 1, "rw,r-,r-", true).Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> InsertRaRow(string fullPath, int owner, string permission, bool update = false)
|
||||||
|
{
|
||||||
|
if (!PermissionRegex.IsMatch(permission))
|
||||||
|
{
|
||||||
|
_logger.LogError($"Invalid permission format: {permission}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = Path.GetRelativePath(_config.MediaRoot, fullPath);
|
||||||
|
|
||||||
|
if (update)
|
||||||
|
return await ResourceDatabase.InsertOrReplaceAsync(new ResourceAttribute()
|
||||||
|
{
|
||||||
|
Uid = Uid(path),
|
||||||
|
Owner = owner,
|
||||||
|
Permission = permission,
|
||||||
|
}) == 1;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return await ResourceDatabase.InsertAsync(new ResourceAttribute()
|
||||||
|
{
|
||||||
|
Uid = Uid(path),
|
||||||
|
Owner = owner,
|
||||||
|
Permission = permission,
|
||||||
|
}) == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Uid(string path)
|
||||||
|
{
|
||||||
|
var b = Encoding.UTF8.GetBytes(path);
|
||||||
|
var r = XxHash128.Hash(b, 0x11451419);
|
||||||
|
return Convert.ToBase64String(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> ExecuteAsync(string sql, params object[] args)
|
||||||
|
=> ResourceDatabase.ExecuteAsync(sql, args);
|
||||||
|
|
||||||
|
public Task<List<T>> QueryAsync<T>(string sql, params object[] args) where T : new()
|
||||||
|
=> ResourceDatabase.QueryAsync<T>(sql, args);
|
||||||
|
|
||||||
|
public async Task<ResourceAttribute?> GetResourceAttributeByUidAsync(string uid)
|
||||||
|
{
|
||||||
|
return await ResourceDatabase.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<ResourceAttribute>> GetResourceAttributesByUidsAsync(IEnumerable<string> uidsEnumerable)
|
||||||
|
{
|
||||||
|
var uids = uidsEnumerable.Where(s => !string.IsNullOrEmpty(s)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
var result = new List<ResourceAttribute>();
|
||||||
|
if (uids.Count == 0) return result;
|
||||||
|
|
||||||
|
const int sqliteMaxVariableNumber = 900;
|
||||||
|
for (int i = 0; i < uids.Count; i += sqliteMaxVariableNumber)
|
||||||
|
{
|
||||||
|
var chunk = uids.Skip(i).Take(sqliteMaxVariableNumber).ToList();
|
||||||
|
var placeholders = string.Join(",", chunk.Select(_ => "?"));
|
||||||
|
var sql = $"SELECT * FROM ResourceAttributes WHERE Uid IN ({placeholders})";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var chunkResult = await ResourceDatabase.QueryAsync<ResourceAttribute>(sql, chunk.Cast<object>().ToArray());
|
||||||
|
if (chunkResult != null && chunkResult.Any())
|
||||||
|
result.AddRange(chunkResult);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error querying ResourceAttributes chunk (size {chunk.Count}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> InsertResourceAttributeAsync(ResourceAttribute ra)
|
||||||
|
{
|
||||||
|
if (ra == null) throw new ArgumentNullException(nameof(ra));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await ResourceDatabase.InsertAsync(ra);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "InsertResourceAttributeAsync failed.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> InsertResourceAttributesAsync(IEnumerable<ResourceAttribute> ras)
|
||||||
|
{
|
||||||
|
var list = ras.ToList();
|
||||||
|
if (!list.Any()) return 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await ResourceDatabase.InsertAllAsync(list);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "InsertResourceAttributesAsync failed.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> InsertOrReplaceResourceAttributeAsync(ResourceAttribute ra)
|
||||||
|
{
|
||||||
|
if (ra == null) throw new ArgumentNullException(nameof(ra));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await ResourceDatabase.InsertOrReplaceAsync(ra);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "InsertOrReplaceResourceAttributeAsync failed.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> DeleteByUidAsync(string uid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(uid)) return 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = "DELETE FROM ResourceAttributes WHERE Uid = ?";
|
||||||
|
return await ResourceDatabase.ExecuteAsync(sql, uid);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"DeleteByUidAsync failed for uid={uid}");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> UpdatePermissionsByUidsAsync(IEnumerable<string> uids, string permission)
|
||||||
|
{
|
||||||
|
var list = uids.Where(s => !string.IsNullOrEmpty(s)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
if (!list.Any()) return 0;
|
||||||
|
|
||||||
|
int updatedCount = 0;
|
||||||
|
const int sqliteMaxVariableNumber = 900;
|
||||||
|
for (int i = 0; i < list.Count; i += sqliteMaxVariableNumber)
|
||||||
|
{
|
||||||
|
var chunk = list.Skip(i).Take(sqliteMaxVariableNumber).ToList();
|
||||||
|
var placeholders = string.Join(",", chunk.Select(_ => "?"));
|
||||||
|
var args = new List<object> { permission };
|
||||||
|
args.AddRange(chunk);
|
||||||
|
var sql = $"UPDATE ResourceAttributes SET Permission = ? WHERE Uid IN ({placeholders})";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rows = await ResourceDatabase.ExecuteAsync(sql, args.ToArray());
|
||||||
|
updatedCount += rows;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"UpdatePermissionsByUidsAsync chunk failed (size {chunk.Count}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> UpdateOwnerByUidsAsync(IEnumerable<string> uids, int owner)
|
||||||
|
{
|
||||||
|
var list = uids.Where(s => !string.IsNullOrEmpty(s)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
if (!list.Any()) return 0;
|
||||||
|
|
||||||
|
int updatedCount = 0;
|
||||||
|
const int sqliteMaxVariableNumber = 900;
|
||||||
|
for (int i = 0; i < list.Count; i += sqliteMaxVariableNumber)
|
||||||
|
{
|
||||||
|
var chunk = list.Skip(i).Take(sqliteMaxVariableNumber).ToList();
|
||||||
|
var placeholders = string.Join(",", chunk.Select(_ => "?"));
|
||||||
|
var args = new List<object> { owner };
|
||||||
|
args.AddRange(chunk);
|
||||||
|
var sql = $"UPDATE ResourceAttributes SET Owner = ? WHERE Uid IN ({placeholders})";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rows = await ResourceDatabase.ExecuteAsync(sql, args.ToArray());
|
||||||
|
updatedCount += rows;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"UpdateOwnerByUidsAsync chunk failed (size {chunk.Count}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsUidAsync(string uid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(uid)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ra = await GetResourceAttributeByUidAsync(uid);
|
||||||
|
return ra != null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"ExistsUidAsync failed for uid={uid}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
// ResourceService.cs
|
// ResourceService.cs
|
||||||
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Abyss.Components.Static;
|
using Abyss.Components.Static;
|
||||||
using Abyss.Model;
|
using Abyss.Model;
|
||||||
using SQLite;
|
|
||||||
using System.IO.Hashing;
|
|
||||||
|
|
||||||
namespace Abyss.Components.Services;
|
namespace Abyss.Components.Services;
|
||||||
|
|
||||||
@@ -21,40 +17,17 @@ public class ResourceService
|
|||||||
private readonly ILogger<ResourceService> _logger;
|
private readonly ILogger<ResourceService> _logger;
|
||||||
private readonly ConfigureService _config;
|
private readonly ConfigureService _config;
|
||||||
private readonly UserService _user;
|
private readonly UserService _user;
|
||||||
private readonly SQLiteAsyncConnection _database;
|
private readonly ResourceDatabaseService _db;
|
||||||
|
|
||||||
private static readonly Regex PermissionRegex = new("^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
|
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, UserService user, ResourceDatabaseService db)
|
||||||
|
|
||||||
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, UserService user)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config;
|
_config = config;
|
||||||
_user = user;
|
_user = user;
|
||||||
|
_db = db;
|
||||||
_database = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
|
|
||||||
_database.CreateTableAsync<ResourceAttribute>().Wait();
|
|
||||||
|
|
||||||
var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks");
|
|
||||||
if (tasksPath != null)
|
|
||||||
{
|
|
||||||
InsertRaRow(tasksPath, 1, "rw,r-,r-", true).Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
var livePath = Helpers.SafePathCombine(_config.MediaRoot, "Live");
|
|
||||||
if (livePath != null)
|
|
||||||
{
|
|
||||||
InsertRaRow(livePath, 1, "rw,r-,r-", true).Wait();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create UID only for resources, without considering advanced hash security such as adding salt
|
// Create UID only for resources, without considering advanced hash security such as adding salt
|
||||||
private static string Uid(string path)
|
|
||||||
{
|
|
||||||
var b = Encoding.UTF8.GetBytes(path);
|
|
||||||
var r = XxHash128.Hash(b, 0x11451419);
|
|
||||||
return Convert.ToBase64String(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dictionary<string, bool>> ValidAny(string[] paths, string token, OperationType type, string ip)
|
public async Task<Dictionary<string, bool>> ValidAny(string[] paths, string token, OperationType type, string ip)
|
||||||
{
|
{
|
||||||
var result = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
var result = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -144,7 +117,7 @@ public class ResourceService
|
|||||||
for (int i = 0; i < parts.Length - 1; i++)
|
for (int i = 0; i < parts.Length - 1; i++)
|
||||||
{
|
{
|
||||||
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
||||||
var uidDir = Uid(subPath);
|
var uidDir = ResourceDatabaseService.Uid(subPath);
|
||||||
reqs.Add((uidDir, OperationType.Read));
|
reqs.Add((uidDir, OperationType.Read));
|
||||||
|
|
||||||
if (!uidToOps.TryGetValue(uidDir, out var ops))
|
if (!uidToOps.TryGetValue(uidDir, out var ops))
|
||||||
@@ -159,7 +132,7 @@ public class ResourceService
|
|||||||
|
|
||||||
// resource itself requires requested 'type'
|
// resource itself requires requested 'type'
|
||||||
var resourcePath = (parts.Length == 0) ? string.Empty : Path.Combine(parts);
|
var resourcePath = (parts.Length == 0) ? string.Empty : Path.Combine(parts);
|
||||||
var uidRes = Uid(resourcePath);
|
var uidRes = ResourceDatabaseService.Uid(resourcePath);
|
||||||
reqs.Add((uidRes, type));
|
reqs.Add((uidRes, type));
|
||||||
|
|
||||||
if (!uidToOps.TryGetValue(uidRes, out var resOps))
|
if (!uidToOps.TryGetValue(uidRes, out var resOps))
|
||||||
@@ -180,22 +153,12 @@ public class ResourceService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch query DB for all UIDs (chunked)
|
// Batch query DB for all UIDs (via DatabaseService)
|
||||||
var uidsNeeded = uidToOps.Keys.ToList();
|
var uidsNeeded = uidToOps.Keys.ToList();
|
||||||
var rasList = new List<ResourceAttribute>();
|
var rasList = new List<ResourceAttribute>();
|
||||||
|
|
||||||
const int sqliteMaxVariableNumber = 900;
|
|
||||||
if (uidsNeeded.Count > 0)
|
if (uidsNeeded.Count > 0)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < uidsNeeded.Count; i += sqliteMaxVariableNumber)
|
rasList = await _db.GetResourceAttributesByUidsAsync(uidsNeeded);
|
||||||
{
|
|
||||||
var chunk = uidsNeeded.Skip(i).Take(sqliteMaxVariableNumber).ToList();
|
|
||||||
var placeholders = string.Join(",", chunk.Select(_ => "?"));
|
|
||||||
var queryArgs = chunk.Cast<object>().ToArray();
|
|
||||||
var sql = $"SELECT * FROM ResourceAttributes WHERE Uid IN ({placeholders})";
|
|
||||||
var chunkResult = await _database.QueryAsync<ResourceAttribute>(sql, queryArgs);
|
|
||||||
rasList.AddRange(chunkResult);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
|
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -253,7 +216,7 @@ public class ResourceService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> ValidAll(string[] paths, string token, OperationType type, string ip)
|
private async Task<bool> ValidAll(string[] paths, string token, OperationType type, string ip)
|
||||||
{
|
{
|
||||||
if (paths.Length == 0)
|
if (paths.Length == 0)
|
||||||
{
|
{
|
||||||
@@ -307,7 +270,7 @@ public class ResourceService
|
|||||||
for (int i = 0; i < parts.Length - 1; i++)
|
for (int i = 0; i < parts.Length - 1; i++)
|
||||||
{
|
{
|
||||||
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
||||||
var uidDir = Uid(subPath);
|
var uidDir = ResourceDatabaseService.Uid(subPath);
|
||||||
if (!uidToOps.TryGetValue(uidDir, out var ops))
|
if (!uidToOps.TryGetValue(uidDir, out var ops))
|
||||||
{
|
{
|
||||||
ops = new HashSet<OperationType>();
|
ops = new HashSet<OperationType>();
|
||||||
@@ -320,7 +283,7 @@ public class ResourceService
|
|||||||
|
|
||||||
// resource itself requires requested 'type'
|
// resource itself requires requested 'type'
|
||||||
var resourcePath = (parts.Length == 0) ? string.Empty : Path.Combine(parts);
|
var resourcePath = (parts.Length == 0) ? string.Empty : Path.Combine(parts);
|
||||||
var uidRes = Uid(resourcePath);
|
var uidRes = ResourceDatabaseService.Uid(resourcePath);
|
||||||
if (!uidToOps.TryGetValue(uidRes, out var resOps))
|
if (!uidToOps.TryGetValue(uidRes, out var resOps))
|
||||||
{
|
{
|
||||||
resOps = new HashSet<OperationType>();
|
resOps = new HashSet<OperationType>();
|
||||||
@@ -331,33 +294,12 @@ public class ResourceService
|
|||||||
resOps.Add(type);
|
resOps.Add(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. batch query DB for all UIDs using parameterized IN (...) and chunking to respect SQLite param limits
|
// 4. batch query DB for all UIDs using DatabaseService
|
||||||
var uidsNeeded = uidToOps.Keys.ToList();
|
var uidsNeeded = uidToOps.Keys.ToList();
|
||||||
var rasList = new List<ResourceAttribute>();
|
var rasList = new List<ResourceAttribute>();
|
||||||
|
|
||||||
const int sqliteMaxVariableNumber = 900; // keep below default 999 for safety
|
|
||||||
if (uidsNeeded.Count > 0)
|
if (uidsNeeded.Count > 0)
|
||||||
{
|
{
|
||||||
if (uidsNeeded.Count <= sqliteMaxVariableNumber)
|
rasList = await _db.GetResourceAttributesByUidsAsync(uidsNeeded);
|
||||||
{
|
|
||||||
var placeholders = string.Join(",", uidsNeeded.Select(_ => "?"));
|
|
||||||
var queryArgs = uidsNeeded.Cast<object>().ToArray();
|
|
||||||
var sql = $"SELECT * FROM ResourceAttributes WHERE Uid IN ({placeholders})";
|
|
||||||
var chunkResult = await _database.QueryAsync<ResourceAttribute>(sql, queryArgs);
|
|
||||||
rasList.AddRange(chunkResult);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (int i = 0; i < uidsNeeded.Count; i += sqliteMaxVariableNumber)
|
|
||||||
{
|
|
||||||
var chunk = uidsNeeded.Skip(i).Take(sqliteMaxVariableNumber).ToList();
|
|
||||||
var placeholders = string.Join(",", chunk.Select(_ => "?"));
|
|
||||||
var queryArgs = chunk.Cast<object>().ToArray();
|
|
||||||
var sql = $"SELECT * FROM ResourceAttributes WHERE Uid IN ({placeholders})";
|
|
||||||
var chunkResult = await _database.QueryAsync<ResourceAttribute>(sql, queryArgs);
|
|
||||||
rasList.AddRange(chunkResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
|
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -397,9 +339,8 @@ public class ResourceService
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Valid(string path, string token, OperationType type, string ip)
|
||||||
public async Task<bool> Valid(string path, string token, OperationType type, string ip)
|
|
||||||
{
|
{
|
||||||
// Path is abs path here, due to Helpers.SafePathCombine
|
// Path is abs path here, due to Helpers.SafePathCombine
|
||||||
if (!path.StartsWith(Path.GetFullPath(_config.MediaRoot), StringComparison.OrdinalIgnoreCase))
|
if (!path.StartsWith(Path.GetFullPath(_config.MediaRoot), StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -428,11 +369,8 @@ public class ResourceService
|
|||||||
for (int i = 0; i < parts.Length - 1; i++)
|
for (int i = 0; i < parts.Length - 1; i++)
|
||||||
{
|
{
|
||||||
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
||||||
var uidDir = Uid(subPath);
|
var uidDir = ResourceDatabaseService.Uid(subPath);
|
||||||
var raDir = await _database
|
var raDir = await _db.GetResourceAttributeByUidAsync(uidDir);
|
||||||
.Table<ResourceAttribute>()
|
|
||||||
.Where(r => r.Uid == uidDir)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (raDir == null)
|
if (raDir == null)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Permission denied: {uuid} has no read access to parent directory {subPath}");
|
_logger.LogError($"Permission denied: {uuid} has no read access to parent directory {subPath}");
|
||||||
@@ -446,11 +384,8 @@ public class ResourceService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var uid = Uid(path);
|
var uid = ResourceDatabaseService.Uid(path);
|
||||||
ResourceAttribute? ra = await _database
|
ResourceAttribute? ra = await _db.GetResourceAttributeByUidAsync(uid);
|
||||||
.Table<ResourceAttribute>()
|
|
||||||
.Where(r => r.Uid == uid)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (ra == null)
|
if (ra == null)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} ");
|
_logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} ");
|
||||||
@@ -470,7 +405,7 @@ public class ResourceService
|
|||||||
{
|
{
|
||||||
if (user == null || ra == null) return false;
|
if (user == null || ra == null) return false;
|
||||||
|
|
||||||
if (!PermissionRegex.IsMatch(ra.Permission)) return false;
|
if (!ResourceDatabaseService.PermissionRegex.IsMatch(ra.Permission)) return false;
|
||||||
|
|
||||||
var perms = ra.Permission.Split(',');
|
var perms = ra.Permission.Split(',');
|
||||||
if (perms.Length != 3) return false;
|
if (perms.Length != 3) return false;
|
||||||
@@ -612,9 +547,8 @@ public class ResourceService
|
|||||||
foreach (var p in allPaths)
|
foreach (var p in allPaths)
|
||||||
{
|
{
|
||||||
var currentPath = Path.GetRelativePath(_config.MediaRoot, p);
|
var currentPath = Path.GetRelativePath(_config.MediaRoot, p);
|
||||||
var uid = Uid(currentPath);
|
var uid = ResourceDatabaseService.Uid(currentPath);
|
||||||
var existing = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid)
|
var existing = await _db.GetResourceAttributeByUidAsync(uid);
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
// If it's not in the database, add it to our list for batch insertion
|
// If it's not in the database, add it to our list for batch insertion
|
||||||
if (existing == null)
|
if (existing == null)
|
||||||
@@ -631,7 +565,7 @@ public class ResourceService
|
|||||||
// 5. Database Insertion: Add all new resources in a single, efficient transaction
|
// 5. Database Insertion: Add all new resources in a single, efficient transaction
|
||||||
if (newResources.Any())
|
if (newResources.Any())
|
||||||
{
|
{
|
||||||
await _database.InsertAllAsync(newResources);
|
await _db.InsertResourceAttributesAsync(newResources);
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
$"Successfully initialized {newResources.Count} new resources under '{path}' for user '{owner}'.");
|
$"Successfully initialized {newResources.Count} new resources under '{path}' for user '{owner}'.");
|
||||||
}
|
}
|
||||||
@@ -650,16 +584,6 @@ public class ResourceService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async Task<bool> Put(string path, string token, string ip)
|
|
||||||
// {
|
|
||||||
// throw new NotImplementedException();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public async Task<bool> Delete(string path, string token, string ip)
|
|
||||||
// {
|
|
||||||
// throw new NotImplementedException();
|
|
||||||
// }
|
|
||||||
|
|
||||||
public async Task<bool> Exclude(string path, string token, string ip)
|
public async Task<bool> Exclude(string path, string token, string ip)
|
||||||
{
|
{
|
||||||
var requester = _user.Validate(token, ip);
|
var requester = _user.Validate(token, ip);
|
||||||
@@ -673,16 +597,16 @@ public class ResourceService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
|
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
|
||||||
var uid = Uid(relPath);
|
var uid = ResourceDatabaseService.Uid(relPath);
|
||||||
|
|
||||||
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
|
var resource = await _db.GetResourceAttributeByUidAsync(uid);
|
||||||
if (resource == null)
|
if (resource == null)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Exclude failed: Resource '{relPath}' not found in database.");
|
_logger.LogError($"Exclude failed: Resource '{relPath}' not found in database.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deleted = await _database.DeleteAsync(resource);
|
var deleted = await _db.DeleteByUidAsync(uid);
|
||||||
if (deleted > 0)
|
if (deleted > 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"Successfully excluded resource '{relPath}' from management.");
|
_logger.LogInformation($"Successfully excluded resource '{relPath}' from management.");
|
||||||
@@ -711,7 +635,7 @@ public class ResourceService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PermissionRegex.IsMatch(permission))
|
if (!ResourceDatabaseService.PermissionRegex.IsMatch(permission))
|
||||||
{
|
{
|
||||||
_logger.LogError($"Invalid permission format: {permission}");
|
_logger.LogError($"Invalid permission format: {permission}");
|
||||||
return false;
|
return false;
|
||||||
@@ -727,9 +651,9 @@ public class ResourceService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
|
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
|
||||||
var uid = Uid(relPath);
|
var uid = ResourceDatabaseService.Uid(relPath);
|
||||||
|
|
||||||
var existing = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
|
var existing = await _db.GetResourceAttributeByUidAsync(uid);
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
_logger.LogError($"Include failed: Resource '{relPath}' already exists in database.");
|
_logger.LogError($"Include failed: Resource '{relPath}' already exists in database.");
|
||||||
@@ -743,7 +667,7 @@ public class ResourceService
|
|||||||
Permission = permission
|
Permission = permission
|
||||||
};
|
};
|
||||||
|
|
||||||
var inserted = await _database.InsertAsync(newResource);
|
var inserted = await _db.InsertResourceAttributeAsync(newResource);
|
||||||
if (inserted > 0)
|
if (inserted > 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
@@ -768,10 +692,9 @@ public class ResourceService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
|
var relPath = Path.GetRelativePath(_config.MediaRoot, path);
|
||||||
var uid = Uid(relPath);
|
var uid = ResourceDatabaseService.Uid(relPath);
|
||||||
|
|
||||||
var resource = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid).FirstOrDefaultAsync();
|
return await _db.ExistsUidAsync(uid);
|
||||||
return resource != null;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -783,7 +706,7 @@ public class ResourceService
|
|||||||
public async Task<bool> Chmod(string path, string token, string permission, string ip, bool recursive = false)
|
public async Task<bool> Chmod(string path, string token, string permission, string ip, bool recursive = false)
|
||||||
{
|
{
|
||||||
// Validate permission format first
|
// Validate permission format first
|
||||||
if (!PermissionRegex.IsMatch(permission))
|
if (!ResourceDatabaseService.PermissionRegex.IsMatch(permission))
|
||||||
{
|
{
|
||||||
_logger.LogError($"Invalid permission format: {permission}");
|
_logger.LogError($"Invalid permission format: {permission}");
|
||||||
return false;
|
return false;
|
||||||
@@ -827,7 +750,7 @@ public class ResourceService
|
|||||||
// Build distinct UIDs
|
// Build distinct UIDs
|
||||||
var relUids = targets
|
var relUids = targets
|
||||||
.Select(t => Path.GetRelativePath(_config.MediaRoot, t))
|
.Select(t => Path.GetRelativePath(_config.MediaRoot, t))
|
||||||
.Select(rel => Uid(rel))
|
.Select(rel => ResourceDatabaseService.Uid(rel))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -837,30 +760,8 @@ public class ResourceService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunked bulk UPDATE using SQL "UPDATE ... WHERE Uid IN (...)"
|
// Use DatabaseService to perform chunked updates
|
||||||
int updatedCount = 0;
|
var updatedCount = await _db.UpdatePermissionsByUidsAsync(relUids, permission);
|
||||||
const int sqliteMaxVariableNumber = 900; // leave some headroom for other params
|
|
||||||
for (int i = 0; i < relUids.Count; i += sqliteMaxVariableNumber)
|
|
||||||
{
|
|
||||||
var chunk = relUids.Skip(i).Take(sqliteMaxVariableNumber).ToList();
|
|
||||||
var placeholders = string.Join(",", chunk.Select(_ => "?"));
|
|
||||||
// First param is permission, rest are Uid values
|
|
||||||
var args = new List<object> { permission };
|
|
||||||
args.AddRange(chunk);
|
|
||||||
|
|
||||||
var sql = $"UPDATE ResourceAttributes SET Permission = ? WHERE Uid IN ({placeholders})";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var rowsAffected = await _database.ExecuteAsync(sql, args.ToArray());
|
|
||||||
updatedCount += rowsAffected;
|
|
||||||
_logger.LogInformation($"Chmod chunk updated {rowsAffected} rows (chunk size {chunk.Count}).");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Error executing chmod update chunk for path '{path}'.");
|
|
||||||
// continue with other chunks; do not abort whole operation on one chunk error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedCount > 0)
|
if (updatedCount > 0)
|
||||||
{
|
{
|
||||||
@@ -926,7 +827,7 @@ public class ResourceService
|
|||||||
// Build distinct UIDs
|
// Build distinct UIDs
|
||||||
var relUids = targets
|
var relUids = targets
|
||||||
.Select(t => Path.GetRelativePath(_config.MediaRoot, t))
|
.Select(t => Path.GetRelativePath(_config.MediaRoot, t))
|
||||||
.Select(rel => Uid(rel))
|
.Select(rel => ResourceDatabaseService.Uid(rel))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -936,29 +837,8 @@ public class ResourceService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunked bulk UPDATE: SET Owner = ? WHERE Uid IN (...)
|
// Use DatabaseService to perform chunked owner updates
|
||||||
int updatedCount = 0;
|
var updatedCount = await _db.UpdateOwnerByUidsAsync(relUids, owner);
|
||||||
const int sqliteMaxVariableNumber = 900;
|
|
||||||
for (int i = 0; i < relUids.Count; i += sqliteMaxVariableNumber)
|
|
||||||
{
|
|
||||||
var chunk = relUids.Skip(i).Take(sqliteMaxVariableNumber).ToList();
|
|
||||||
var placeholders = string.Join(",", chunk.Select(_ => "?"));
|
|
||||||
var args = new List<object> { owner };
|
|
||||||
args.AddRange(chunk);
|
|
||||||
|
|
||||||
var sql = $"UPDATE ResourceAttributes SET Owner = ? WHERE Uid IN ({placeholders})";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var rowsAffected = await _database.ExecuteAsync(sql, args.ToArray());
|
|
||||||
updatedCount += rowsAffected;
|
|
||||||
_logger.LogInformation($"Chown chunk updated {rowsAffected} rows (chunk size {chunk.Count}).");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Error executing chown update chunk for path '{path}'.");
|
|
||||||
// continue with remaining chunks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedCount > 0)
|
if (updatedCount > 0)
|
||||||
{
|
{
|
||||||
@@ -979,35 +859,6 @@ public class ResourceService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<bool> InsertRaRow(string fullPath, int owner, string permission, bool update = false)
|
|
||||||
{
|
|
||||||
if (!PermissionRegex.IsMatch(permission))
|
|
||||||
{
|
|
||||||
_logger.LogError($"Invalid permission format: {permission}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = Path.GetRelativePath(_config.MediaRoot, fullPath);
|
|
||||||
|
|
||||||
if (update)
|
|
||||||
return await _database.InsertOrReplaceAsync(new ResourceAttribute()
|
|
||||||
{
|
|
||||||
Uid = Uid(path),
|
|
||||||
Owner = owner,
|
|
||||||
Permission = permission,
|
|
||||||
}) == 1;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return await _database.InsertAsync(new ResourceAttribute()
|
|
||||||
{
|
|
||||||
Uid = Uid(path),
|
|
||||||
Owner = owner,
|
|
||||||
Permission = permission,
|
|
||||||
}) == 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ResourceAttribute?> GetAttribute(string path)
|
public async Task<ResourceAttribute?> GetAttribute(string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -1021,11 +872,9 @@ public class ResourceService
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var rel = Path.GetRelativePath(_config.MediaRoot, full);
|
var rel = Path.GetRelativePath(_config.MediaRoot, full);
|
||||||
var uid = Uid(rel);
|
var uid = ResourceDatabaseService.Uid(rel);
|
||||||
|
|
||||||
var ra = await _database.Table<ResourceAttribute>()
|
var ra = await _db.GetResourceAttributeByUidAsync(uid);
|
||||||
.Where(r => r.Uid == uid)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
return ra;
|
return ra;
|
||||||
}
|
}
|
||||||
|
|||||||
24
Abyss/Model/Index.cs
Normal file
24
Abyss/Model/Index.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using SQLite;
|
||||||
|
|
||||||
|
namespace Abyss.Model;
|
||||||
|
|
||||||
|
[Table("Index")]
|
||||||
|
public class Index
|
||||||
|
{
|
||||||
|
[PrimaryKey, AutoIncrement]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
// 0: folder, 1: video, 2: comic
|
||||||
|
public int Type { get; set; }
|
||||||
|
|
||||||
|
// The resources referenced by the index node, the format is "Video, Class, ID", "Comic, ID"
|
||||||
|
// eg: "Video,Animation,12"
|
||||||
|
// eg: "Comic,9"
|
||||||
|
// eg: "Video,Movie,45"
|
||||||
|
// When a directory node references an actual resource, the resource is treated as the cover page of the directory
|
||||||
|
public string Reference { get; set; } = "";
|
||||||
|
|
||||||
|
// The direct successor node of this node
|
||||||
|
// eg: "1,2,3,4"
|
||||||
|
public string Children { get; set; } = "";
|
||||||
|
}
|
||||||
@@ -15,11 +15,13 @@ public class Program
|
|||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddSingleton<ResourceDatabaseService>();
|
||||||
builder.Services.AddSingleton<ConfigureService>();
|
builder.Services.AddSingleton<ConfigureService>();
|
||||||
builder.Services.AddSingleton<UserService>();
|
builder.Services.AddSingleton<UserService>();
|
||||||
builder.Services.AddSingleton<ResourceService>();
|
builder.Services.AddSingleton<ResourceService>();
|
||||||
builder.Services.AddSingleton<TaskController>();
|
builder.Services.AddSingleton<TaskController>();
|
||||||
builder.Services.AddSingleton<TaskService>();
|
builder.Services.AddSingleton<TaskService>();
|
||||||
|
builder.Services.AddSingleton<IndexService>();
|
||||||
builder.Services.AddHostedService<AbyssService>();
|
builder.Services.AddHostedService<AbyssService>();
|
||||||
|
|
||||||
builder.Services.AddRateLimiter(options =>
|
builder.Services.AddRateLimiter(options =>
|
||||||
@@ -38,7 +40,7 @@ public class Program
|
|||||||
await context.HttpContext.Response.WriteAsync("Too many requests. Please try later.", token);
|
await context.HttpContext.Response.WriteAsync("Too many requests. Please try later.", token);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// app.UseHttpsRedirection();
|
// app.UseHttpsRedirection();
|
||||||
|
|||||||
Reference in New Issue
Block a user