From 76bdba1755f24e78487df424d5c48c1202300105 Mon Sep 17 00:00:00 2001
From: acite <1498045907@qq.com>
Date: Thu, 25 Sep 2025 15:14:02 +0800
Subject: [PATCH] [feat] Index Service
---
.idea/.idea.Abyss/.idea/workspace.xml | 12 +-
Abyss.sln.DotSettings.user | 1 +
.../Controllers/Media/ImageController.cs | 4 +-
.../Controllers/Media/IndexController.cs | 11 +
.../Controllers/Media/VideoController.cs | 1 +
Abyss/Components/Services/ConfigureService.cs | 1 +
Abyss/Components/Services/IndexService.cs | 230 +++++++++++++++++
.../Services/ResourceDatabaseService.cs | 241 ++++++++++++++++++
Abyss/Components/Services/ResourceService.cs | 231 +++--------------
Abyss/Model/Index.cs | 24 ++
Abyss/Program.cs | 4 +-
11 files changed, 559 insertions(+), 201 deletions(-)
create mode 100644 Abyss/Components/Controllers/Media/IndexController.cs
create mode 100644 Abyss/Components/Services/IndexService.cs
create mode 100644 Abyss/Components/Services/ResourceDatabaseService.cs
create mode 100644 Abyss/Model/Index.cs
diff --git a/.idea/.idea.Abyss/.idea/workspace.xml b/.idea/.idea.Abyss/.idea/workspace.xml
index 4abe013..cd407b8 100644
--- a/.idea/.idea.Abyss/.idea/workspace.xml
+++ b/.idea/.idea.Abyss/.idea/workspace.xml
@@ -9,7 +9,9 @@
-
+
+
+
@@ -28,16 +30,13 @@
+
-
-
-
-
@@ -60,7 +59,6 @@
-
@@ -229,6 +227,8 @@
+
+
diff --git a/Abyss.sln.DotSettings.user b/Abyss.sln.DotSettings.user
index dcf236a..e57d8eb 100644
--- a/Abyss.sln.DotSettings.user
+++ b/Abyss.sln.DotSettings.user
@@ -1,4 +1,5 @@
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
diff --git a/Abyss/Components/Controllers/Media/ImageController.cs b/Abyss/Components/Controllers/Media/ImageController.cs
index 97a21f9..6e388ad 100644
--- a/Abyss/Components/Controllers/Media/ImageController.cs
+++ b/Abyss/Components/Controllers/Media/ImageController.cs
@@ -47,12 +47,10 @@ public class ImageController(ResourceService rs, ConfigureService config) : Base
return Ok(await System.IO.File.ReadAllTextAsync(d));
}
-
+
[HttpPost("bulkquery")]
public async Task QueryBulk([FromQuery] string token, [FromBody] string[] id)
{
- List result = new List();
-
var db = id.Select(x => Helpers.SafePathCombine(ImageFolder, [x, "summary.json"])).ToArray();
if (db.Any(x => x == null))
return BadRequest();
diff --git a/Abyss/Components/Controllers/Media/IndexController.cs b/Abyss/Components/Controllers/Media/IndexController.cs
new file mode 100644
index 0000000..d7343cc
--- /dev/null
+++ b/Abyss/Components/Controllers/Media/IndexController.cs
@@ -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
+{
+
+}
\ No newline at end of file
diff --git a/Abyss/Components/Controllers/Media/VideoController.cs b/Abyss/Components/Controllers/Media/VideoController.cs
index c991a14..fd80746 100644
--- a/Abyss/Components/Controllers/Media/VideoController.cs
+++ b/Abyss/Components/Controllers/Media/VideoController.cs
@@ -42,6 +42,7 @@ public class VideoController(ILogger logger, ResourceService rs
{
var d = Helpers.SafePathCombine(VideoFolder, klass);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
+
var r = await rs.Query(d, token, Ip);
if (r == null) return StatusCode(401, new { message = "Unauthorized" });
diff --git a/Abyss/Components/Services/ConfigureService.cs b/Abyss/Components/Services/ConfigureService.cs
index a14c0be..9d5bf5f 100644
--- a/Abyss/Components/Services/ConfigureService.cs
+++ b/Abyss/Components/Services/ConfigureService.cs
@@ -8,4 +8,5 @@ public class ConfigureService
public string Version { get; } = "Alpha v0.1";
public string UserDatabase { get; set; } = "user.db";
public string RaDatabase { get; set; } = "ra.db";
+ public string IndexDatabase { get; set; } = "index.db";
}
\ No newline at end of file
diff --git a/Abyss/Components/Services/IndexService.cs b/Abyss/Components/Services/IndexService.cs
new file mode 100644
index 0000000..e19a929
--- /dev/null
+++ b/Abyss/Components/Services/IndexService.cs
@@ -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().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 GetByIdAsync(int id)
+ {
+ await _lock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ return await _db.FindAsync(id).ConfigureAwait(false);
+ }
+ finally
+ {
+ _lock.Release();
+ }
+ }
+
+ public async Task InsertNodeAsChildAsync(int parentId, int type, string reference = "")
+ {
+ await _lock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ var parent = await _db.FindAsync(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 DeleteNodeAsync(int id)
+ {
+ await _lock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ var node = await _db.FindAsync(id).ConfigureAwait(false);
+ if (node == null) return false;
+
+ await _db.DeleteAsync(node).ConfigureAwait(false);
+
+ // Remove references from all parents
+ var all = await _db.Table().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(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(fromId).ConfigureAwait(false) ?? throw new InvalidOperationException($"From node {fromId} not found");
+ _ = await _db.FindAsync(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 RemoveEdgeAsync(int fromId, int toId)
+ {
+ await _lock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ var from = await _db.FindAsync(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> GetChildrenIdsAsync(int id)
+ {
+ await _lock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ var node = await _db.FindAsync(id).ConfigureAwait(false);
+ if (node == null) return new List();
+ return ParseChildren(node.Children);
+ }
+ finally
+ {
+ _lock.Release();
+ }
+ }
+ private static List ParseChildren(string children)
+ {
+ if (string.IsNullOrWhiteSpace(children)) return new List();
+ 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 children)
+ {
+ if (children.Count == 0) return string.Empty;
+ var seen = new HashSet();
+ var ordered = new List();
+ 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;
+ }
+
+
+}
diff --git a/Abyss/Components/Services/ResourceDatabaseService.cs b/Abyss/Components/Services/ResourceDatabaseService.cs
new file mode 100644
index 0000000..a9aa816
--- /dev/null
+++ b/Abyss/Components/Services/ResourceDatabaseService.cs
@@ -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 _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 logger)
+ {
+ _config = config;
+ _logger = logger;
+
+ ResourceDatabase = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
+ ResourceDatabase.CreateTableAsync().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 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 ExecuteAsync(string sql, params object[] args)
+ => ResourceDatabase.ExecuteAsync(sql, args);
+
+ public Task> QueryAsync(string sql, params object[] args) where T : new()
+ => ResourceDatabase.QueryAsync(sql, args);
+
+ public async Task GetResourceAttributeByUidAsync(string uid)
+ {
+ return await ResourceDatabase.Table().Where(r => r.Uid == uid).FirstOrDefaultAsync();
+ }
+
+ public async Task> GetResourceAttributesByUidsAsync(IEnumerable uidsEnumerable)
+ {
+ var uids = uidsEnumerable.Where(s => !string.IsNullOrEmpty(s)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
+
+ var result = new List();
+ 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(sql, chunk.Cast