[feat] Index Service

This commit is contained in:
acite
2025-09-25 15:14:02 +08:00
parent 46ff611706
commit 76bdba1755
11 changed files with 559 additions and 201 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();

View 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
{
}

View File

@@ -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" });

View File

@@ -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";
} }

View 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;
}
}

View 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;
}
}
}

View File

@@ -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
View 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; } = "";
}

View File

@@ -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();