From a0273e33345cfa365b95a8e1eae20cf91d96943b Mon Sep 17 00:00:00 2001
From: acite <1498045907@qq.com>
Date: Sun, 5 Oct 2025 16:48:54 +0800
Subject: [PATCH] [feat] Abyssctl Basic functions
---
.../bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml | 48 +++-
.../.idea.Abyss/.idea/data_source_mapping.xml | 6 +
.idea/.idea.Abyss/.idea/workspace.xml | 32 ++-
Abyss.sln.DotSettings.user | 1 +
.../{Module.cs => ModuleAttribute.cs} | 4 +-
Abyss/Components/Services/Admin/CtlService.cs | 18 +-
.../Services/Admin/Modules/InitModule.cs | 58 ++++
.../Services/Admin/Modules/UserAddModule.cs | 50 ++++
.../Services/Media/ResourceDatabaseService.cs | 20 +-
.../Services/Media/ResourceService.cs | 249 +++++++++---------
.../Components/Services/Media/VideoService.cs | 3 +-
.../Services/Security/UserService.cs | 48 +---
Abyss/Model/Media/ResourceAttribute.cs | 2 +-
Abyss/Model/Security/User.cs | 2 +-
Abyss/Program.cs | 2 +-
Abyss/Properties/launchSettings.json | 2 +-
abyssctl/App/App.cs | 23 +-
abyssctl/App/Attributes/ModuleAttribute.cs | 28 ++
abyssctl/App/Modules/HelloOptions.cs | 22 +-
abyssctl/App/Modules/InitOptions.cs | 20 ++
abyssctl/App/Modules/UserAddOptions.cs | 26 ++
abyssctl/App/Modules/VersionOptions.cs | 2 +
22 files changed, 428 insertions(+), 238 deletions(-)
create mode 100644 .idea/.idea.Abyss/.idea/data_source_mapping.xml
rename Abyss/Components/Services/Admin/Attributes/{Module.cs => ModuleAttribute.cs} (89%)
create mode 100644 Abyss/Components/Services/Admin/Modules/InitModule.cs
create mode 100644 Abyss/Components/Services/Admin/Modules/UserAddModule.cs
create mode 100644 abyssctl/App/Attributes/ModuleAttribute.cs
create mode 100644 abyssctl/App/Modules/InitOptions.cs
create mode 100644 abyssctl/App/Modules/UserAddOptions.cs
diff --git a/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml b/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml
index c859b9f..c26919f 100644
--- a/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml
+++ b/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml
@@ -499,7 +499,7 @@
1
- 2025-08-23.10:03:56
+ 2025-10-05.08:15:22
R
@@ -1590,45 +1590,67 @@
1
-
+
-
+
+
+ 1
+ 1
1
- varchar|0s
+ integer|0s
-
+
+ 1
2
varchar|0s
-
+
+ 1
3
- varchar|0s
+ integer|0s
-
+
+ 1
4
varchar|0s
-
+
+ Uid
+ 1
+
+
+ Id
+ 1
+
+
1
TEXT|0s
-
+
2
TEXT|0s
-
+
3
TEXT|0s
-
+
4
INT|0s
-
+
5
TEXT|0s
+
+ 1
+
+
+ 2
+
\ No newline at end of file
diff --git a/.idea/.idea.Abyss/.idea/data_source_mapping.xml b/.idea/.idea.Abyss/.idea/data_source_mapping.xml
new file mode 100644
index 0000000..9464031
--- /dev/null
+++ b/.idea/.idea.Abyss/.idea/data_source_mapping.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Abyss/.idea/workspace.xml b/.idea/.idea.Abyss/.idea/workspace.xml
index 5196781..3f75433 100644
--- a/.idea/.idea.Abyss/.idea/workspace.xml
+++ b/.idea/.idea.Abyss/.idea/workspace.xml
@@ -11,9 +11,24 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -31,6 +46,7 @@
+
@@ -49,13 +65,14 @@
-
-
+
+
+
@@ -88,12 +105,14 @@
+
+
-
+
{
"associatedIndex": 3
@@ -128,7 +147,7 @@
"vue.rearranger.settings.migration": "true"
}
}]]>
-
+
@@ -275,7 +294,8 @@
-
+
+
diff --git a/Abyss.sln.DotSettings.user b/Abyss.sln.DotSettings.user
index f5d1b68..bdeabf4 100644
--- a/Abyss.sln.DotSettings.user
+++ b/Abyss.sln.DotSettings.user
@@ -1,4 +1,5 @@
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
diff --git a/Abyss/Components/Services/Admin/Attributes/Module.cs b/Abyss/Components/Services/Admin/Attributes/ModuleAttribute.cs
similarity index 89%
rename from Abyss/Components/Services/Admin/Attributes/Module.cs
rename to Abyss/Components/Services/Admin/Attributes/ModuleAttribute.cs
index c3e1a4a..5b77900 100644
--- a/Abyss/Components/Services/Admin/Attributes/Module.cs
+++ b/Abyss/Components/Services/Admin/Attributes/ModuleAttribute.cs
@@ -4,7 +4,7 @@ using Abyss.Components.Services.Admin.Interfaces;
namespace Abyss.Components.Services.Admin.Attributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
-public class Module(int head) : Attribute
+public class ModuleAttribute(int head) : Attribute
{
public int Head { get; } = head;
@@ -13,7 +13,7 @@ public class Module(int head) : Attribute
get
{
Assembly assembly = Assembly.GetExecutingAssembly();
- Type attributeType = typeof(Module);
+ Type attributeType = typeof(ModuleAttribute);
const string targetNamespace = "Abyss.Components.Services.Admin.Modules";
var moduleTypes = assembly.GetTypes()
diff --git a/Abyss/Components/Services/Admin/CtlService.cs b/Abyss/Components/Services/Admin/CtlService.cs
index 0073d0e..1da672e 100644
--- a/Abyss/Components/Services/Admin/CtlService.cs
+++ b/Abyss/Components/Services/Admin/CtlService.cs
@@ -6,14 +6,14 @@ using Abyss.Model.Admin;
using Newtonsoft.Json;
using System.Reflection;
+using Abyss.Components.Services.Admin.Attributes;
using Abyss.Components.Services.Admin.Interfaces;
-using Module = Abyss.Components.Services.Admin.Attributes.Module;
namespace Abyss.Components.Services.Admin;
public class CtlService(ILogger logger, IServiceProvider serviceProvider) : IHostedService
{
- private readonly string _socketPath = "ctl.sock";
+ private static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "abyss-ctl.sock");
private Task? _executingTask;
private CancellationTokenSource? _cts;
@@ -21,10 +21,10 @@ public class CtlService(ILogger logger, IServiceProvider serviceProv
public Task StartAsync(CancellationToken cancellationToken)
{
- var t = Module.Modules;
+ var t = ModuleAttribute.Modules;
foreach (var module in t)
{
- var attr = module.GetCustomAttribute();
+ var attr = module.GetCustomAttribute();
if (attr != null)
{
_handlers[attr.Head] = module;
@@ -54,12 +54,12 @@ public class CtlService(ILogger logger, IServiceProvider serviceProv
private async Task ExecuteAsync(CancellationToken stoppingToken)
{
- if (File.Exists(_socketPath))
+ if (File.Exists(SocketPath))
{
- File.Delete(_socketPath);
+ File.Delete(SocketPath);
}
-
- var endPoint = new UnixDomainSocketEndPoint(_socketPath);
+
+ var endPoint = new UnixDomainSocketEndPoint(SocketPath);
using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
socket.Bind(endPoint);
@@ -77,6 +77,8 @@ public class CtlService(ILogger logger, IServiceProvider serviceProv
break;
}
}
+
+ File.Delete(SocketPath);
}
private async Task HandleClientAsync(Socket clientSocket, CancellationToken stoppingToken)
diff --git a/Abyss/Components/Services/Admin/Modules/InitModule.cs b/Abyss/Components/Services/Admin/Modules/InitModule.cs
new file mode 100644
index 0000000..6b359f7
--- /dev/null
+++ b/Abyss/Components/Services/Admin/Modules/InitModule.cs
@@ -0,0 +1,58 @@
+using Abyss.Components.Services.Admin.Attributes;
+using Abyss.Components.Services.Admin.Interfaces;
+using Abyss.Components.Services.Media;
+using Abyss.Components.Services.Misc;
+using Abyss.Components.Services.Security;
+using Abyss.Model.Admin;
+using Abyss.Model.Security;
+using NSec.Cryptography;
+
+namespace Abyss.Components.Services.Admin.Modules;
+
+[Module(103)]
+public class InitModule(ILogger logger, UserService userService, ConfigureService configureService, ResourceDatabaseService resourceDatabaseService): IModule
+{
+ public async Task ExecuteAsync(Ctl request, CancellationToken ct)
+ {
+ bool empty = await userService.IsEmptyUser();
+ if (!empty)
+ return new Ctl
+ {
+ Head = 403,
+ Params = ["Access Denied: User list is not empty."]
+ };
+
+ var key = UserService.GenerateKeyPair();
+ string privateKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPrivateKey));
+ string publicKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPublicKey));
+
+ await userService.AddUserAsync(new User
+ {
+ Uuid = 1,
+ Username = "root",
+ ParentId = 1,
+ PublicKey = publicKeyBase64,
+ Privilege = 1145141919,
+ });
+
+ var paths = new string[] { "Tasks", "Live", "Videos", "Images" }
+ .Select(x => Path.Combine(configureService.MediaRoot, x));
+ foreach (var path in paths)
+ {
+ if(!Directory.Exists(path))
+ Directory.CreateDirectory(path);
+
+ var i = await resourceDatabaseService.InsertRaRow(path, 1, "rw,r-,r-", true);
+ if (!i)
+ {
+ logger.LogError("Could not create resource database");
+ }
+ }
+
+ return new Ctl
+ {
+ Head = 200,
+ Params = [privateKeyBase64]
+ };
+ }
+}
\ No newline at end of file
diff --git a/Abyss/Components/Services/Admin/Modules/UserAddModule.cs b/Abyss/Components/Services/Admin/Modules/UserAddModule.cs
new file mode 100644
index 0000000..75c4aee
--- /dev/null
+++ b/Abyss/Components/Services/Admin/Modules/UserAddModule.cs
@@ -0,0 +1,50 @@
+using Abyss.Components.Services.Admin.Attributes;
+using Abyss.Components.Services.Admin.Interfaces;
+using Abyss.Components.Services.Security;
+using Abyss.Model.Admin;
+using Abyss.Model.Security;
+using NSec.Cryptography;
+
+namespace Abyss.Components.Services.Admin.Modules;
+
+[Module(104)]
+public class UserAddModule(UserService userService): IModule
+{
+ public async Task ExecuteAsync(Ctl request, CancellationToken ct)
+ {
+ // request.Params[0] -> Username
+ // request.Params[1] -> Privilege
+
+ if (request.Params.Length != 2 || !UserService.IsAlphanumeric(request.Params[0]) || !int.TryParse(request.Params[1], out var privilege))
+ return new Ctl
+ {
+ Head = 400,
+ Params = ["Bad Request"]
+ };
+
+ if (await userService.QueryUser(request.Params[0]) != null)
+ return new Ctl
+ {
+ Head = 403,
+ Params = ["User exists"]
+ };
+
+ var key = UserService.GenerateKeyPair();
+ string privateKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPrivateKey));
+ string publicKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPublicKey));
+
+ await userService.AddUserAsync(new User
+ {
+ Username = request.Params[0],
+ ParentId = 1,
+ PublicKey = publicKeyBase64,
+ Privilege = privilege,
+ });
+
+ return new Ctl
+ {
+ Head = 200,
+ Params = [privateKeyBase64]
+ };
+ }
+}
\ No newline at end of file
diff --git a/Abyss/Components/Services/Media/ResourceDatabaseService.cs b/Abyss/Components/Services/Media/ResourceDatabaseService.cs
index 2842d70..b0ae288 100644
--- a/Abyss/Components/Services/Media/ResourceDatabaseService.cs
+++ b/Abyss/Components/Services/Media/ResourceDatabaseService.cs
@@ -22,22 +22,9 @@ public class ResourceDatabaseService
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)
+ public async Task InsertRaRow(string fullPath, int owner, string permission, bool update = false)
{
if (!PermissionRegex.IsMatch(permission))
{
@@ -46,11 +33,12 @@ public class ResourceDatabaseService
}
var path = Path.GetRelativePath(_config.MediaRoot, fullPath);
+ var uid = Uid(path);
if (update)
return await ResourceDatabase.InsertOrReplaceAsync(new ResourceAttribute()
{
- Uid = Uid(path),
+ Uid = uid,
Owner = owner,
Permission = permission,
}) == 1;
@@ -58,7 +46,7 @@ public class ResourceDatabaseService
{
return await ResourceDatabase.InsertAsync(new ResourceAttribute()
{
- Uid = Uid(path),
+ Uid = uid,
Owner = owner,
Permission = permission,
}) == 1;
diff --git a/Abyss/Components/Services/Media/ResourceService.cs b/Abyss/Components/Services/Media/ResourceService.cs
index 5cc7e32..89785c9 100644
--- a/Abyss/Components/Services/Media/ResourceService.cs
+++ b/Abyss/Components/Services/Media/ResourceService.cs
@@ -16,21 +16,12 @@ public enum OperationType
Security // Chown, Chmod
}
-public class ResourceService
+public class ResourceService(
+ ILogger logger,
+ ConfigureService config,
+ UserService user,
+ ResourceDatabaseService db)
{
- private readonly ILogger _logger;
- private readonly ConfigureService _config;
- private readonly UserService _user;
- private readonly ResourceDatabaseService _db;
-
- public ResourceService(ILogger logger, ConfigureService config, UserService user, ResourceDatabaseService db)
- {
- _logger = logger;
- _config = config;
- _user = user;
- _db = db;
- }
-
// Create UID only for resources, without considering advanced hash security such as adding salt
private async Task> ValidAny(string[] paths, string token, OperationType type, string ip)
{
@@ -40,7 +31,7 @@ public class ResourceService
return result; // empty input -> empty result
// Normalize media root
- var mediaRootFull = Path.GetFullPath(_config.MediaRoot);
+ var mediaRootFull = Path.GetFullPath(config.MediaRoot);
// Prepare normalized full paths and early-check outside-media-root
var fullPaths = new List(paths.Length);
@@ -52,7 +43,7 @@ public class ResourceService
// record normalized path as key
if (!full.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase))
{
- _logger.LogError($"Path outside media root or null: {p}");
+ logger.LogError($"Path outside media root or null: {p}");
result[full] = false;
}
else
@@ -65,7 +56,7 @@ public class ResourceService
catch (Exception ex)
{
// malformed path -> mark false and continue
- _logger.LogError(ex, $"Invalid path encountered in ValidAny: {p}");
+ logger.LogError(ex, $"Invalid path encountered in ValidAny: {p}");
try
{
result[Path.GetFullPath(p)] = false;
@@ -81,18 +72,18 @@ public class ResourceService
return result;
// Validate token and user once
- int uuid = _user.Validate(token, ip);
+ int uuid = user.Validate(token, ip);
if (uuid == -1)
{
- _logger.LogError($"Invalid token: {token}");
+ logger.LogError($"Invalid token: {token}");
// all previously-initialized keys remain false
return result;
}
- User? user = await _user.QueryUser(uuid);
- if (user == null || user.Uuid != uuid)
+ User? user1 = await user.QueryUser(uuid);
+ if (user1 == null || user1.Uuid != uuid)
{
- _logger.LogError($"Verification failed: {token}");
+ logger.LogError($"Verification failed: {token}");
return result;
}
@@ -107,7 +98,7 @@ public class ResourceService
try
{
// rel path relative to media root for Uid calculation
- var rel = Path.GetRelativePath(_config.MediaRoot, full);
+ var rel = Path.GetRelativePath(config.MediaRoot, full);
var parts = rel
.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
@@ -152,7 +143,7 @@ public class ResourceService
}
catch (Exception ex)
{
- _logger.LogError(ex, $"Error building requirements for path '{full}' in ValidAny.");
+ logger.LogError(ex, $"Error building requirements for path '{full}' in ValidAny.");
// leave result[full] as false
}
}
@@ -162,7 +153,7 @@ public class ResourceService
var rasList = new List();
if (uidsNeeded.Count > 0)
{
- rasList = await _db.GetResourceAttributesByUidsAsync(uidsNeeded);
+ rasList = await db.GetResourceAttributesByUidsAsync(uidsNeeded);
}
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
@@ -181,7 +172,7 @@ public class ResourceService
{
permCache[(uid, op)] = false;
var examplePath = uidToExampleRelPath.GetValueOrDefault(uid, uid);
- _logger.LogDebug($"ValidAny: missing ResourceAttribute for Uid={uid}, example='{examplePath}'");
+ logger.LogDebug($"ValidAny: missing ResourceAttribute for Uid={uid}, example='{examplePath}'");
}
continue;
@@ -192,7 +183,7 @@ public class ResourceService
var key = (uid, op);
if (!permCache.TryGetValue(key, out var ok))
{
- ok = await CheckPermission(user, ra, op);
+ ok = await CheckPermission(user1, ra, op);
permCache[key] = ok;
}
}
@@ -224,11 +215,11 @@ public class ResourceService
{
if (paths.Length == 0)
{
- _logger.LogError("ValidAll called with empty path set");
+ logger.LogError("ValidAll called with empty path set");
return false;
}
- var mediaRootFull = Path.GetFullPath(_config.MediaRoot);
+ var mediaRootFull = Path.GetFullPath(config.MediaRoot);
// 1. basic path checks & normalize to relative
var relPaths = new List(paths.Length);
@@ -236,25 +227,25 @@ public class ResourceService
{
if (!p.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase))
{
- _logger.LogError($"Path outside media root or null: {p}");
+ logger.LogError($"Path outside media root or null: {p}");
return false;
}
- relPaths.Add(Path.GetRelativePath(_config.MediaRoot, Path.GetFullPath(p)));
+ relPaths.Add(Path.GetRelativePath(config.MediaRoot, Path.GetFullPath(p)));
}
// 2. validate token and user once
- int uuid = _user.Validate(token, ip);
+ int uuid = user.Validate(token, ip);
if (uuid == -1)
{
- _logger.LogError($"Invalid token: {token}");
+ logger.LogError($"Invalid token: {token}");
return false;
}
- User? user = await _user.QueryUser(uuid);
- if (user == null || user.Uuid != uuid)
+ User? user1 = await user.QueryUser(uuid);
+ if (user1 == null || user1.Uuid != uuid)
{
- _logger.LogError($"Verification failed: {token}");
+ logger.LogError($"Verification failed: {token}");
return false;
}
@@ -303,7 +294,7 @@ public class ResourceService
var rasList = new List();
if (uidsNeeded.Count > 0)
{
- rasList = await _db.GetResourceAttributesByUidsAsync(uidsNeeded);
+ rasList = await db.GetResourceAttributesByUidsAsync(uidsNeeded);
}
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
@@ -317,7 +308,7 @@ public class ResourceService
if (!raDict.TryGetValue(uid, out var ra))
{
var examplePath = uidToExampleRelPath.GetValueOrDefault(uid, uid);
- _logger.LogError(
+ logger.LogError(
$"Permission check failed (missing resource attribute): User: {uuid}, Resource: {examplePath}, Uid: {uid}");
return false;
}
@@ -327,14 +318,14 @@ public class ResourceService
var key = (uid, op);
if (!permCache.TryGetValue(key, out var ok))
{
- ok = await CheckPermission(user, ra, op);
+ ok = await CheckPermission(user1, ra, op);
permCache[key] = ok;
}
if (!ok)
{
var examplePath = uidToExampleRelPath.TryGetValue(uid, out var p) ? p : uid;
- _logger.LogError(
+ logger.LogError(
$"Permission check failed: User: {uuid}, Resource: {examplePath}, Uid: {uid}, Type: {op}");
return false;
}
@@ -347,23 +338,23 @@ public class ResourceService
private async Task Valid(string path, string token, OperationType type, string ip)
{
// 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))
return false;
- path = Path.GetRelativePath(_config.MediaRoot, path);
+ path = Path.GetRelativePath(config.MediaRoot, path);
- int uuid = _user.Validate(token, ip);
+ int uuid = user.Validate(token, ip);
if (uuid == -1)
{
// No permission granted for invalid tokens
- _logger.LogError($"Invalid token: {token}");
+ logger.LogError($"Invalid token: {token}");
return false;
}
- User? user = await _user.QueryUser(uuid);
- if (user == null || user.Uuid != uuid)
+ User? user1 = await user.QueryUser(uuid);
+ if (user1 == null || user1.Uuid != uuid)
{
- _logger.LogError($"Verification failed: {token}");
+ logger.LogError($"Verification failed: {token}");
return false; // Two-factor authentication
}
@@ -374,51 +365,51 @@ public class ResourceService
{
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
var uidDir = ResourceDatabaseService.Uid(subPath);
- var raDir = await _db.GetResourceAttributeByUidAsync(uidDir);
+ var raDir = await db.GetResourceAttributeByUidAsync(uidDir);
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}");
return false;
}
- if (!await CheckPermission(user, raDir, OperationType.Read))
+ if (!await CheckPermission(user1, raDir, OperationType.Read))
{
- _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}");
return false;
}
}
var uid = ResourceDatabaseService.Uid(path);
- ResourceAttribute? ra = await _db.GetResourceAttributeByUidAsync(uid);
+ ResourceAttribute? ra = await db.GetResourceAttributeByUidAsync(uid);
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()} ");
return false;
}
- var l = await CheckPermission(user, ra, type);
+ var l = await CheckPermission(user1, ra, type);
if (!l)
{
- _logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} ");
+ logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} ");
}
return l;
}
- private async Task CheckPermission(User? user, ResourceAttribute? ra, OperationType type)
+ private async Task CheckPermission(User? user1, ResourceAttribute? ra, OperationType type)
{
- if (user == null || ra == null) return false;
+ if (user1 == null || ra == null) return false;
if (!ResourceDatabaseService.PermissionRegex.IsMatch(ra.Permission)) return false;
var perms = ra.Permission.Split(',');
if (perms.Length != 3) return false;
- var owner = await _user.QueryUser(ra.Owner);
+ var owner = await user.QueryUser(ra.Owner);
if (owner == null) return false;
- bool isOwner = ra.Owner == user.Uuid;
- bool isPeer = !isOwner && user.Privilege == owner.Privilege;
+ bool isOwner = ra.Owner == user1.Uuid;
+ bool isPeer = !isOwner && user1.Privilege == owner.Privilege;
bool isOther = !isOwner && !isPeer;
string currentPerm;
@@ -430,11 +421,11 @@ public class ResourceService
switch (type)
{
case OperationType.Read:
- return currentPerm.Contains('r') || (user.Privilege > owner.Privilege);
+ return currentPerm.Contains('r') || (user1.Privilege > owner.Privilege);
case OperationType.Write:
- return currentPerm.Contains('w') || (user.Privilege > owner.Privilege);
+ return currentPerm.Contains('w') || (user1.Privilege > owner.Privilege);
case OperationType.Security:
- return (isOwner && currentPerm.Contains('w')) || user.Uuid == 1;
+ return (isOwner && currentPerm.Contains('w')) || user1.Uuid == 1;
default:
return false;
}
@@ -470,13 +461,13 @@ public class ResourceService
}
else
{
- _logger.LogDebug(
+ logger.LogDebug(
$"Query: access denied or not managed for '{entry}' (user token: {token}) - item skipped.");
}
}
catch (Exception exEntry)
{
- _logger.LogError(exEntry, $"Error processing entry '{entry}' in Query.");
+ logger.LogError(exEntry, $"Error processing entry '{entry}' in Query.");
}
}
@@ -484,7 +475,7 @@ public class ResourceService
}
catch (Exception ex)
{
- _logger.LogError(ex, $"Error while listing directory '{path}' in Query.");
+ logger.LogError(ex, $"Error while listing directory '{path}' in Query.");
return null;
}
}
@@ -539,22 +530,22 @@ public class ResourceService
public async Task Initialize(string path, string token, string owner, string ip)
{
- var u = await _user.QueryUser(owner);
+ var u = await user.QueryUser(owner);
if (u == null || u.Uuid == -1) return false;
- return await Initialize(path, token, u.Uuid, ip);
+ return await Initialize(path, token, u.Uuid!.Value, ip);
}
public async Task Initialize(string path, string token, int owner, string ip)
{
// TODO: Use a more elegant Debug mode
- if (_config.DebugMode == "Debug")
+ if (config.DebugMode == "Debug")
goto debug;
// 1. Authorization: Verify the operation is performed by 'root'
- var requester = _user.Validate(token, ip);
+ var requester = user.Validate(token, ip);
if (requester != 1)
{
- _logger.LogWarning(
+ logger.LogWarning(
$"Permission denied: Non-root user '{requester}' attempted to initialize resources.");
return false;
}
@@ -563,14 +554,14 @@ public class ResourceService
// 2. Validation: Ensure the target path and owner are valid
if (!Directory.Exists(path))
{
- _logger.LogError($"Initialization failed: Path '{path}' does not exist or is not a directory.");
+ logger.LogError($"Initialization failed: Path '{path}' does not exist or is not a directory.");
return false;
}
- var ownerUser = await _user.QueryUser(owner);
+ var ownerUser = await user.QueryUser(owner);
if (ownerUser == null)
{
- _logger.LogError($"Initialization failed: Owner user '{owner}' does not exist.");
+ logger.LogError($"Initialization failed: Owner user '{owner}' does not exist.");
return false;
}
@@ -583,9 +574,9 @@ public class ResourceService
var newResources = new List();
foreach (var p in allPaths)
{
- var currentPath = Path.GetRelativePath(_config.MediaRoot, p);
+ var currentPath = Path.GetRelativePath(config.MediaRoot, p);
var uid = ResourceDatabaseService.Uid(currentPath);
- var existing = await _db.GetResourceAttributeByUidAsync(uid);
+ var existing = await db.GetResourceAttributeByUidAsync(uid);
// If it's not in the database, add it to our list for batch insertion
if (existing == null)
@@ -602,13 +593,13 @@ public class ResourceService
// 5. Database Insertion: Add all new resources in a single, efficient transaction
if (newResources.Any())
{
- await _db.InsertResourceAttributesAsync(newResources);
- _logger.LogInformation(
+ await db.InsertResourceAttributesAsync(newResources);
+ logger.LogInformation(
$"Successfully initialized {newResources.Count} new resources under '{path}' for user '{owner}'.");
}
else
{
- _logger.LogInformation(
+ logger.LogInformation(
$"No new resources to initialize under '{path}'. All items already exist in the database.");
}
@@ -616,84 +607,84 @@ public class ResourceService
}
catch (Exception ex)
{
- _logger.LogError(ex, $"An error occurred during resource initialization for path '{path}'.");
+ logger.LogError(ex, $"An error occurred during resource initialization for path '{path}'.");
return false;
}
}
public async Task Exclude(string path, string token, string ip)
{
- var requester = _user.Validate(token, ip);
+ var requester = user.Validate(token, ip);
if (requester != 1)
{
- _logger.LogWarning(
+ logger.LogWarning(
$"Permission denied: Non-root user '{requester}' attempted to exclude resource '{path}'.");
return false;
}
try
{
- var relPath = Path.GetRelativePath(_config.MediaRoot, path);
+ var relPath = Path.GetRelativePath(config.MediaRoot, path);
var uid = ResourceDatabaseService.Uid(relPath);
- var resource = await _db.GetResourceAttributeByUidAsync(uid);
+ var resource = await db.GetResourceAttributeByUidAsync(uid);
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;
}
- var deleted = await _db.DeleteByUidAsync(uid);
+ var deleted = await db.DeleteByUidAsync(uid);
if (deleted > 0)
{
- _logger.LogInformation($"Successfully excluded resource '{relPath}' from management.");
+ logger.LogInformation($"Successfully excluded resource '{relPath}' from management.");
return true;
}
else
{
- _logger.LogError($"Failed to exclude resource '{relPath}' from database.");
+ logger.LogError($"Failed to exclude resource '{relPath}' from database.");
return false;
}
}
catch (Exception ex)
{
- _logger.LogError(ex, $"Error excluding resource '{path}'.");
+ logger.LogError(ex, $"Error excluding resource '{path}'.");
return false;
}
}
public async Task Include(string path, string token, string ip, int owner, string permission)
{
- var requester = _user.Validate(token, ip);
+ var requester = user.Validate(token, ip);
if (requester != 1)
{
- _logger.LogWarning(
+ logger.LogWarning(
$"Permission denied: Non-root user '{requester}' attempted to include resource '{path}'.");
return false;
}
if (!ResourceDatabaseService.PermissionRegex.IsMatch(permission))
{
- _logger.LogError($"Invalid permission format: {permission}");
+ logger.LogError($"Invalid permission format: {permission}");
return false;
}
- var ownerUser = await _user.QueryUser(owner);
+ var ownerUser = await user.QueryUser(owner);
if (ownerUser == null)
{
- _logger.LogError($"Include failed: Owner user '{owner}' does not exist.");
+ logger.LogError($"Include failed: Owner user '{owner}' does not exist.");
return false;
}
try
{
- var relPath = Path.GetRelativePath(_config.MediaRoot, path);
+ var relPath = Path.GetRelativePath(config.MediaRoot, path);
var uid = ResourceDatabaseService.Uid(relPath);
- var existing = await _db.GetResourceAttributeByUidAsync(uid);
+ var existing = await db.GetResourceAttributeByUidAsync(uid);
if (existing != null)
{
- _logger.LogError($"Include failed: Resource '{relPath}' already exists in database.");
+ logger.LogError($"Include failed: Resource '{relPath}' already exists in database.");
return false;
}
@@ -704,22 +695,22 @@ public class ResourceService
Permission = permission
};
- var inserted = await _db.InsertResourceAttributeAsync(newResource);
+ var inserted = await db.InsertResourceAttributeAsync(newResource);
if (inserted > 0)
{
- _logger.LogInformation(
+ logger.LogInformation(
$"Successfully included '{relPath}' into resource management (Owner={owner}, Permission={permission}).");
return true;
}
else
{
- _logger.LogError($"Failed to include resource '{relPath}' into database.");
+ logger.LogError($"Failed to include resource '{relPath}' into database.");
return false;
}
}
catch (Exception ex)
{
- _logger.LogError(ex, $"Error including resource '{path}'.");
+ logger.LogError(ex, $"Error including resource '{path}'.");
return false;
}
}
@@ -728,14 +719,14 @@ public class ResourceService
{
try
{
- var relPath = Path.GetRelativePath(_config.MediaRoot, path);
+ var relPath = Path.GetRelativePath(config.MediaRoot, path);
var uid = ResourceDatabaseService.Uid(relPath);
- return await _db.ExistsUidAsync(uid);
+ return await db.ExistsUidAsync(uid);
}
catch (Exception ex)
{
- _logger.LogError(ex, $"Error checking existence of resource '{path}'.");
+ logger.LogError(ex, $"Error checking existence of resource '{path}'.");
return false;
}
}
@@ -745,7 +736,7 @@ public class ResourceService
// Validate permission format first
if (!ResourceDatabaseService.PermissionRegex.IsMatch(permission))
{
- _logger.LogError($"Invalid permission format: {permission}");
+ logger.LogError($"Invalid permission format: {permission}");
return false;
}
@@ -758,7 +749,7 @@ public class ResourceService
{
if (recursive && Directory.Exists(path))
{
- _logger.LogInformation($"Recursive directory '{path}'.");
+ logger.LogInformation($"Recursive directory '{path}'.");
targets.Add(path);
foreach (var entry in Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories))
{
@@ -767,17 +758,17 @@ public class ResourceService
if (!await ValidAll(targets.ToArray(), token, OperationType.Security, ip))
{
- _logger.LogWarning($"Permission denied for recursive chmod on '{path}'");
+ logger.LogWarning($"Permission denied for recursive chmod on '{path}'");
return false;
}
- _logger.LogInformation($"Successfully validated chmod on '{path}'.");
+ logger.LogInformation($"Successfully validated chmod on '{path}'.");
}
else
{
if (!await Valid(path, token, OperationType.Security, ip))
{
- _logger.LogWarning($"Permission denied for chmod on '{path}'");
+ logger.LogWarning($"Permission denied for chmod on '{path}'");
return false;
}
@@ -786,35 +777,35 @@ public class ResourceService
// Build distinct UIDs
var relUids = targets
- .Select(t => Path.GetRelativePath(_config.MediaRoot, t))
+ .Select(t => Path.GetRelativePath(config.MediaRoot, t))
.Select(rel => ResourceDatabaseService.Uid(rel))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (relUids.Count == 0)
{
- _logger.LogWarning($"No targets resolved for chmod on '{path}'");
+ logger.LogWarning($"No targets resolved for chmod on '{path}'");
return false;
}
// Use DatabaseService to perform chunked updates
- var updatedCount = await _db.UpdatePermissionsByUidsAsync(relUids, permission);
+ var updatedCount = await db.UpdatePermissionsByUidsAsync(relUids, permission);
if (updatedCount > 0)
{
- _logger.LogInformation(
+ logger.LogInformation(
$"Chmod: updated permissions for {updatedCount} resource(s) (root='{path}', recursive={recursive})");
return true;
}
else
{
- _logger.LogWarning($"Chmod: no resources updated for '{path}' (recursive={recursive})");
+ logger.LogWarning($"Chmod: no resources updated for '{path}' (recursive={recursive})");
return false;
}
}
catch (Exception ex)
{
- _logger.LogError(ex, $"Error changing permissions for: {path}");
+ logger.LogError(ex, $"Error changing permissions for: {path}");
return false;
}
}
@@ -822,10 +813,10 @@ public class ResourceService
public async Task Chown(string path, string token, int owner, string ip, bool recursive = false)
{
// Validate new owner exists
- var newOwner = await _user.QueryUser(owner);
+ var newOwner = await user.QueryUser(owner);
if (newOwner == null)
{
- _logger.LogError($"New owner '{owner}' does not exist");
+ logger.LogError($"New owner '{owner}' does not exist");
return false;
}
@@ -846,7 +837,7 @@ public class ResourceService
if (!await ValidAll(targets.ToArray(), token, OperationType.Security, ip))
{
- _logger.LogWarning($"Permission denied for recursive chown on '{path}'");
+ logger.LogWarning($"Permission denied for recursive chown on '{path}'");
return false;
}
}
@@ -854,7 +845,7 @@ public class ResourceService
{
if (!await Valid(path, token, OperationType.Security, ip))
{
- _logger.LogWarning($"Permission denied for chown on '{path}'");
+ logger.LogWarning($"Permission denied for chown on '{path}'");
return false;
}
@@ -863,35 +854,35 @@ public class ResourceService
// Build distinct UIDs
var relUids = targets
- .Select(t => Path.GetRelativePath(_config.MediaRoot, t))
+ .Select(t => Path.GetRelativePath(config.MediaRoot, t))
.Select(rel => ResourceDatabaseService.Uid(rel))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (relUids.Count == 0)
{
- _logger.LogWarning($"No targets resolved for chown on '{path}'");
+ logger.LogWarning($"No targets resolved for chown on '{path}'");
return false;
}
// Use DatabaseService to perform chunked owner updates
- var updatedCount = await _db.UpdateOwnerByUidsAsync(relUids, owner);
+ var updatedCount = await db.UpdateOwnerByUidsAsync(relUids, owner);
if (updatedCount > 0)
{
- _logger.LogInformation(
+ logger.LogInformation(
$"Chown: changed owner for {updatedCount} resource(s) (root='{path}', recursive={recursive})");
return true;
}
else
{
- _logger.LogWarning($"Chown: no resources updated for '{path}' (recursive={recursive})");
+ logger.LogWarning($"Chown: no resources updated for '{path}' (recursive={recursive})");
return false;
}
}
catch (Exception ex)
{
- _logger.LogError(ex, $"Error changing ownership for: {path}");
+ logger.LogError(ex, $"Error changing ownership for: {path}");
return false;
}
}
@@ -904,20 +895,20 @@ public class ResourceService
var full = Path.GetFullPath(path);
// ensure it's under media root
- var mediaRootFull = Path.GetFullPath(_config.MediaRoot);
+ var mediaRootFull = Path.GetFullPath(config.MediaRoot);
if (!full.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase))
return null;
- var rel = Path.GetRelativePath(_config.MediaRoot, full);
+ var rel = Path.GetRelativePath(config.MediaRoot, full);
var uid = ResourceDatabaseService.Uid(rel);
- var ra = await _db.GetResourceAttributeByUidAsync(uid);
+ var ra = await db.GetResourceAttributeByUidAsync(uid);
return ra;
}
catch (Exception ex)
{
- _logger.LogError(ex, $"GetAttribute failed for path '{path}'");
+ logger.LogError(ex, $"GetAttribute failed for path '{path}'");
return null;
}
}
diff --git a/Abyss/Components/Services/Media/VideoService.cs b/Abyss/Components/Services/Media/VideoService.cs
index 5f3036d..7a89a83 100644
--- a/Abyss/Components/Services/Media/VideoService.cs
+++ b/Abyss/Components/Services/Media/VideoService.cs
@@ -11,7 +11,8 @@ public class VideoService(ResourceService rs, ConfigureService config)
{
public readonly string VideoFolder = Path.Combine(config.MediaRoot, "Videos");
- public async Task Init(string token, string owner, string ip) => await rs.Initialize(VideoFolder, token, owner, ip);
+ public async Task Init(string token, string owner, string ip)
+ => await rs.Initialize(VideoFolder, token, owner, ip);
public async Task GetClasses(string token, string ip)
=> (await rs.Query(VideoFolder, token, ip))?.SortLikeWindows();
diff --git a/Abyss/Components/Services/Security/UserService.cs b/Abyss/Components/Services/Security/UserService.cs
index d956c84..0bc74f7 100644
--- a/Abyss/Components/Services/Security/UserService.cs
+++ b/Abyss/Components/Services/Security/UserService.cs
@@ -25,39 +25,16 @@ public class UserService
_database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
_database.CreateTableAsync().Wait();
- var rootUser = _database.Table().Where(x => x.Uuid == 1).FirstOrDefaultAsync().Result;
if (config.DebugMode == "Debug")
_cache.Set("abyss", $"1@127.0.0.1", DateTimeOffset.Now.AddHours(1));
// Test token, can only be used locally. Will be destroyed in one hour.
-
- if (rootUser == null)
- {
- var key = GenerateKeyPair();
- string privateKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPrivateKey));
- string publicKeyBase64 = Convert.ToBase64String(key.Export(KeyBlobFormat.RawPublicKey));
-
- var s = GenerateRandomAsciiString(8);
- Console.WriteLine($"Enter the following string to create a root user: '{s}'");
-
- if (Console.ReadLine() != s)
- {
- throw (new Exception("Invalid Input"));
- }
- Console.WriteLine($"Created root user. Please keep the key safe.");
- Console.WriteLine("key: '" + privateKeyBase64 + "'");
- _database.InsertAsync(new User()
- {
- Uuid = 1,
- Username = "root",
- ParentId = 1,
- PublicKey = publicKeyBase64,
- Privilege = 1145141919,
- }).Wait();
-
- Console.ReadKey();
- }
+ }
+
+ public async Task IsEmptyUser()
+ {
+ return await _database.Table().CountAsync() == 0;
}
public async Task OpenUserAsync(string user, string token, string? bindIp, string ip)
@@ -76,7 +53,7 @@ public class UserService
var ipToBind = string.IsNullOrWhiteSpace(bindIp) ? ip : bindIp;
- var t = CreateToken(target.Uuid, ipToBind, TimeSpan.FromHours(1));
+ var t = CreateToken(target.Uuid!.Value, ipToBind, TimeSpan.FromHours(1));
_logger.LogInformation("Root created 1h token for {User}, bound to {BindIp}, request from {ReqIp}", user,
ipToBind, ip);
@@ -104,10 +81,10 @@ public class UserService
if (creating.Privilege > ou?.Privilege || ou == null)
return false;
- await CreateUser(new User
+ await AddUserAsync(new User
{
Username = creating.Name,
- ParentId = ou.Uuid,
+ ParentId = ou.Uuid!.Value,
Privilege = creating.Privilege,
PublicKey = creating.PublicKey,
});
@@ -122,7 +99,7 @@ public class UserService
if (u == null) // Error: User not exists
return null;
- if (_cache.TryGetValue(u.Uuid, out _)) // The previous challenge has not yet expired
+ if (_cache.TryGetValue(u.Uuid!.Value, out _)) // The previous challenge has not yet expired
_cache.Remove(u.Uuid);
var c = Convert.ToBase64String(Encoding.UTF8.GetBytes(GenerateRandomAsciiString(32)));
@@ -140,7 +117,7 @@ public class UserService
return null;
}
- if (_cache.TryGetValue(u.Uuid, out string? challenge))
+ if (_cache.TryGetValue(u.Uuid!.Value, out string? challenge))
{
bool isVerified = VerifySignature(
PublicKey.Import(
@@ -208,13 +185,13 @@ public class UserService
return u;
}
- public async Task CreateUser(User user)
+ public async Task AddUserAsync(User user)
{
await _database.InsertAsync(user);
_logger.LogInformation($"Created user: {user.Username}, Uid: {user.Uuid}, Parent: {user.ParentId}, Privilege: {user.Privilege}");
}
- static Key GenerateKeyPair()
+ public static Key GenerateKeyPair()
{
var algorithm = SignatureAlgorithm.Ed25519;
var creationParameters = new KeyCreationParameters
@@ -283,7 +260,6 @@ public class UserService
return token;
}
-
public static bool IsAlphanumeric(string input)
{
if (string.IsNullOrEmpty(input))
diff --git a/Abyss/Model/Media/ResourceAttribute.cs b/Abyss/Model/Media/ResourceAttribute.cs
index 3499d32..33ad26a 100644
--- a/Abyss/Model/Media/ResourceAttribute.cs
+++ b/Abyss/Model/Media/ResourceAttribute.cs
@@ -6,7 +6,7 @@ namespace Abyss.Model.Media;
public class ResourceAttribute
{
[PrimaryKey, AutoIncrement]
- public int Id { get; set; }
+ public int? Id { get; set; }
[Unique, NotNull]
public string Uid { get; init; } = "@";
diff --git a/Abyss/Model/Security/User.cs b/Abyss/Model/Security/User.cs
index 721e16b..7b682a8 100644
--- a/Abyss/Model/Security/User.cs
+++ b/Abyss/Model/Security/User.cs
@@ -6,7 +6,7 @@ namespace Abyss.Model.Security;
public class User
{
[PrimaryKey, AutoIncrement]
- public int Uuid { get; set; }
+ public int? Uuid { get; set; }
[Unique, NotNull]
public string Username { get; set; } = "";
[NotNull]
diff --git a/Abyss/Program.cs b/Abyss/Program.cs
index 579abb5..c988421 100644
--- a/Abyss/Program.cs
+++ b/Abyss/Program.cs
@@ -34,7 +34,7 @@ public class Program
builder.Services.AddHostedService();
builder.Services.AddHostedService();
- foreach (var t in Module.Modules)
+ foreach (var t in ModuleAttribute.Modules)
{
builder.Services.AddTransient(t);
}
diff --git a/Abyss/Properties/launchSettings.json b/Abyss/Properties/launchSettings.json
index e918606..df8a95a 100644
--- a/Abyss/Properties/launchSettings.json
+++ b/Abyss/Properties/launchSettings.json
@@ -8,7 +8,7 @@
"applicationUrl": "http://localhost:3000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
- "MEDIA_ROOT" : "/storage",
+ "MEDIA_ROOT" : "/opt/abyss",
"ALLOWED_PORTS" : "3000",
"DEBUG_MODE": "Debug"
}
diff --git a/abyssctl/App/App.cs b/abyssctl/App/App.cs
index d7f19da..565cd92 100644
--- a/abyssctl/App/App.cs
+++ b/abyssctl/App/App.cs
@@ -2,8 +2,8 @@
using System.Net.Sockets;
using System.Reflection;
using System.Text;
+using abyssctl.App.Attributes;
using abyssctl.App.Interfaces;
-using abyssctl.App.Modules;
using abyssctl.Model;
using abyssctl.Static;
using CommandLine;
@@ -13,16 +13,18 @@ namespace abyssctl.App;
public class App
{
- private static readonly string SocketPath = "ctl.sock";
+ private static readonly string SocketPath = Path.Combine(Path.GetTempPath(), "abyss-ctl.sock");
- public static async Task CtlWriteRead(Ctl ctl)
+ public static async Task CtlWriteRead(string[] param)
{
+ var attr = typeof(T).GetCustomAttribute()!;
+
var endPoint = new UnixDomainSocketEndPoint(SocketPath);
using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
try
{
await socket.ConnectAsync(endPoint);
- await socket.WriteBase64Async(Ctl.MakeBase64(ctl.Head, ctl.Params));
+ await socket.WriteBase64Async(Ctl.MakeBase64(attr.Head, param));
var s = Encoding.UTF8.GetString(
Convert.FromBase64String(await socket.ReadBase64Async()));
return JsonConvert.DeserializeObject(s)!;
@@ -41,18 +43,7 @@ public class App
{
return await Task.Run(() =>
{
- Assembly assembly = Assembly.GetExecutingAssembly();
- Type attributeType = typeof(VerbAttribute);
- const string targetNamespace = "abyssctl.App.Modules";
-
- var moduleTypes = assembly.GetTypes()
- .Where(t => t is { IsClass: true, IsAbstract: false, IsInterface: false })
- .Where(t => t.Namespace == targetNamespace)
- .Where(t => typeof(IOptions).IsAssignableFrom(t))
- .Where(t => t.IsDefined(attributeType, inherit: true))
- .ToArray();
-
- return Parser.Default.ParseArguments(args, moduleTypes)
+ return Parser.Default.ParseArguments(args, ModuleAttribute.Modules)
.MapResult(
(object obj) =>
{
diff --git a/abyssctl/App/Attributes/ModuleAttribute.cs b/abyssctl/App/Attributes/ModuleAttribute.cs
new file mode 100644
index 0000000..85910b2
--- /dev/null
+++ b/abyssctl/App/Attributes/ModuleAttribute.cs
@@ -0,0 +1,28 @@
+using System.Reflection;
+using abyssctl.App.Interfaces;
+using CommandLine;
+
+namespace abyssctl.App.Attributes;
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public class ModuleAttribute(int head) : Attribute
+{
+ public int Head { get; } = head;
+
+ public static Type[] Modules
+ {
+ get
+ {
+ Assembly assembly = Assembly.GetExecutingAssembly();
+ const string targetNamespace = "abyssctl.App.Modules";
+
+ return assembly.GetTypes()
+ .Where(t => t is { IsClass: true, IsAbstract: false, IsInterface: false })
+ .Where(t => t.Namespace == targetNamespace)
+ .Where(t => typeof(IOptions).IsAssignableFrom(t))
+ .Where(t => t.IsDefined(typeof(VerbAttribute), inherit: true))
+ .Where(t => t.IsDefined(typeof(ModuleAttribute), inherit: false))
+ .ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/abyssctl/App/Modules/HelloOptions.cs b/abyssctl/App/Modules/HelloOptions.cs
index b765f84..b54b125 100644
--- a/abyssctl/App/Modules/HelloOptions.cs
+++ b/abyssctl/App/Modules/HelloOptions.cs
@@ -1,22 +1,30 @@
+using abyssctl.App.Attributes;
using abyssctl.App.Interfaces;
using abyssctl.Model;
using CommandLine;
namespace abyssctl.App.Modules;
+[Module(100)]
[Verb("hello", HelpText = "Say hello to abyss server")]
public class HelloOptions: IOptions
{
+ [Option('r', "raw", Default = false, HelpText = "Show raw response.")]
+ public bool Raw { get; set; }
+
public async Task Run()
{
- var r = await App.CtlWriteRead(new Ctl
+ var r = await App.CtlWriteRead([]);
+
+ if (Raw)
{
- Head = 100,
- Params = []
- });
-
- Console.WriteLine($"Response Code: {r.Head}");
- Console.WriteLine($"Params: {string.Join(",", r.Params)}");
+ Console.WriteLine($"Response Code: {r.Head}");
+ Console.WriteLine($"Params: {string.Join(",", r.Params)}");
+ }
+ else
+ {
+ Console.WriteLine($"Server: {string.Join(",", r.Params)}");
+ }
return 0;
}
}
\ No newline at end of file
diff --git a/abyssctl/App/Modules/InitOptions.cs b/abyssctl/App/Modules/InitOptions.cs
new file mode 100644
index 0000000..9033f67
--- /dev/null
+++ b/abyssctl/App/Modules/InitOptions.cs
@@ -0,0 +1,20 @@
+using abyssctl.App.Attributes;
+using abyssctl.App.Interfaces;
+using CommandLine;
+
+namespace abyssctl.App.Modules;
+
+
+[Module(103)]
+[Verb("init", HelpText = "Initialize abyss server")]
+public class InitOptions: IOptions
+{
+ public async Task Run()
+ {
+ var r = await App.CtlWriteRead([]);
+ Console.WriteLine($"Response Code: {r.Head}");
+ Console.WriteLine($"Params: {string.Join(",", r.Params)}");
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/abyssctl/App/Modules/UserAddOptions.cs b/abyssctl/App/Modules/UserAddOptions.cs
new file mode 100644
index 0000000..46eb472
--- /dev/null
+++ b/abyssctl/App/Modules/UserAddOptions.cs
@@ -0,0 +1,26 @@
+using abyssctl.App.Attributes;
+using abyssctl.App.Interfaces;
+using CommandLine;
+
+namespace abyssctl.App.Modules;
+
+
+[Module(104)]
+[Verb("useradd", HelpText = "Add user")]
+public class UserAddOptions: IOptions
+{
+ [Option('u', "username", Required = true, HelpText = "Username for new user.")]
+ public string Username { get; set; } = "";
+
+ [Option('p', "privilege", Required = true, HelpText = "User privilege.")]
+ public int Privilege { get; set; }
+ public async Task Run()
+ {
+ var r = await App.CtlWriteRead([Username, Privilege.ToString()]);
+
+ Console.WriteLine($"Response Code: {r.Head}");
+ Console.WriteLine($"Params: {string.Join(",", r.Params)}");
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/abyssctl/App/Modules/VersionOptions.cs b/abyssctl/App/Modules/VersionOptions.cs
index 3615a87..f4f83a8 100644
--- a/abyssctl/App/Modules/VersionOptions.cs
+++ b/abyssctl/App/Modules/VersionOptions.cs
@@ -1,8 +1,10 @@
+using abyssctl.App.Attributes;
using abyssctl.App.Interfaces;
using CommandLine;
namespace abyssctl.App.Modules;
+[Module(101)]
[Verb("ver", HelpText = "Get server version")]
public class VersionOptions: IOptions
{