[feat] Abyssctl Basic functions
This commit is contained in:
@@ -499,7 +499,7 @@
|
||||
</routine>
|
||||
<schema id="191" parent="1" name="main">
|
||||
<Current>1</Current>
|
||||
<LastIntrospectionLocalTimestamp>2025-08-23.10:03:56</LastIntrospectionLocalTimestamp>
|
||||
<LastIntrospectionLocalTimestamp>2025-10-05.08:15:22</LastIntrospectionLocalTimestamp>
|
||||
</schema>
|
||||
<argument id="192" parent="16">
|
||||
<ArgumentDirection>R</ArgumentDirection>
|
||||
@@ -1590,45 +1590,67 @@
|
||||
<argument id="554" parent="190">
|
||||
<Position>1</Position>
|
||||
</argument>
|
||||
<table id="555" parent="191" name="ResourceAttribute"/>
|
||||
<table id="555" parent="191" name="ResourceAttributes"/>
|
||||
<table id="556" parent="191" name="sqlite_master">
|
||||
<System>1</System>
|
||||
</table>
|
||||
<column id="557" parent="555" name="Uid">
|
||||
<table id="557" parent="191" name="sqlite_sequence">
|
||||
<System>1</System>
|
||||
</table>
|
||||
<column id="558" parent="555" name="Id">
|
||||
<AutoIncrement>1</AutoIncrement>
|
||||
<NotNull>1</NotNull>
|
||||
<Position>1</Position>
|
||||
<StoredType>varchar|0s</StoredType>
|
||||
<StoredType>integer|0s</StoredType>
|
||||
</column>
|
||||
<column id="558" parent="555" name="Name">
|
||||
<column id="559" parent="555" name="Uid">
|
||||
<NotNull>1</NotNull>
|
||||
<Position>2</Position>
|
||||
<StoredType>varchar|0s</StoredType>
|
||||
</column>
|
||||
<column id="559" parent="555" name="Owner">
|
||||
<column id="560" parent="555" name="Owner">
|
||||
<NotNull>1</NotNull>
|
||||
<Position>3</Position>
|
||||
<StoredType>varchar|0s</StoredType>
|
||||
<StoredType>integer|0s</StoredType>
|
||||
</column>
|
||||
<column id="560" parent="555" name="Permission">
|
||||
<column id="561" parent="555" name="Permission">
|
||||
<NotNull>1</NotNull>
|
||||
<Position>4</Position>
|
||||
<StoredType>varchar|0s</StoredType>
|
||||
</column>
|
||||
<column id="561" parent="556" name="type">
|
||||
<index id="562" parent="555" name="ResourceAttributes_Uid">
|
||||
<ColNames>Uid</ColNames>
|
||||
<Unique>1</Unique>
|
||||
</index>
|
||||
<key id="563" parent="555">
|
||||
<ColNames>Id</ColNames>
|
||||
<Primary>1</Primary>
|
||||
</key>
|
||||
<column id="564" parent="556" name="type">
|
||||
<Position>1</Position>
|
||||
<StoredType>TEXT|0s</StoredType>
|
||||
</column>
|
||||
<column id="562" parent="556" name="name">
|
||||
<column id="565" parent="556" name="name">
|
||||
<Position>2</Position>
|
||||
<StoredType>TEXT|0s</StoredType>
|
||||
</column>
|
||||
<column id="563" parent="556" name="tbl_name">
|
||||
<column id="566" parent="556" name="tbl_name">
|
||||
<Position>3</Position>
|
||||
<StoredType>TEXT|0s</StoredType>
|
||||
</column>
|
||||
<column id="564" parent="556" name="rootpage">
|
||||
<column id="567" parent="556" name="rootpage">
|
||||
<Position>4</Position>
|
||||
<StoredType>INT|0s</StoredType>
|
||||
</column>
|
||||
<column id="565" parent="556" name="sql">
|
||||
<column id="568" parent="556" name="sql">
|
||||
<Position>5</Position>
|
||||
<StoredType>TEXT|0s</StoredType>
|
||||
</column>
|
||||
<column id="569" parent="557" name="name">
|
||||
<Position>1</Position>
|
||||
</column>
|
||||
<column id="570" parent="557" name="seq">
|
||||
<Position>2</Position>
|
||||
</column>
|
||||
</database-model>
|
||||
</dataSource>
|
||||
6
.idea/.idea.Abyss/.idea/data_source_mapping.xml
generated
Normal file
6
.idea/.idea.Abyss/.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0/console.sql" value="bf32ff15-97d4-4301-bb5e-c1d57c7be5c0" />
|
||||
</component>
|
||||
</project>
|
||||
32
.idea/.idea.Abyss/.idea/workspace.xml
generated
32
.idea/.idea.Abyss/.idea/workspace.xml
generated
@@ -11,9 +11,24 @@
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/abyssctl/App/Interfaces/IOptions.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/InitModule.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/UserAddModule.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/abyssctl/App/Attributes/ModuleAttribute.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/abyssctl/App/Modules/InitOptions.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/abyssctl/App/Modules/UserAddOptions.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/dataSources/bf32ff15-97d4-4301-bb5e-c1d57c7be5c0.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss.sln.DotSettings.user" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss.sln.DotSettings.user" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/Admin/Attributes/Module.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/Admin/Attributes/ModuleAttribute.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/Admin/CtlService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/Admin/CtlService.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/Media/ResourceDatabaseService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/Media/ResourceDatabaseService.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/Media/ResourceService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/Media/ResourceService.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/Media/VideoService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/Media/VideoService.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/Security/UserService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/Security/UserService.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Model/Media/ResourceAttribute.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/Media/ResourceAttribute.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Model/Security/User.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/Security/User.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Program.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Properties/launchSettings.json" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Properties/launchSettings.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/abyssctl/App/App.cs" beforeDir="false" afterPath="$PROJECT_DIR$/abyssctl/App/App.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/abyssctl/App/Modules/HelloOptions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/abyssctl/App/Modules/HelloOptions.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/abyssctl/App/Modules/VersionOptions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/abyssctl/App/Modules/VersionOptions.cs" afterDir="false" />
|
||||
@@ -31,6 +46,7 @@
|
||||
</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/011a191356a243438f987de3ec3d6c6230800/04/8419ff35/ServiceProvider.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/18f2eb258dcf45748fa1903c530f5f07d1a000/f2/f5e8fb60/Array.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/25/817def70/ConfiguredValueTaskAwaitable`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/4c/4b962087/Monitor.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/af/aac0eaa5/ExceptionDispatchInfo.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
@@ -49,13 +65,14 @@
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Media/IndexController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.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/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/Services/Admin/Attributes/Module.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Admin/Attributes/ModuleAttribute.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Admin/CtlService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Admin/Interfaces/IModule.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/HelloModule.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/InitModule.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/UserAddModule.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/VersionModule.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Media/ComicService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/Media/IndexService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
@@ -88,12 +105,14 @@
|
||||
<setting file="file://$PROJECT_DIR$/abyssctl/App/App.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/abyssctl/App/Interfaces/IOptions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/abyssctl/App/Modules/HelloOptions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/abyssctl/App/Modules/InitOptions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/abyssctl/App/Modules/UserAddOptions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/abyssctl/App/Modules/VersionOptions.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/abyssctl/Program.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="CurrentFile" />
|
||||
<option name="selectedTabId" value="SWEA" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 3
|
||||
@@ -128,7 +147,7 @@
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunManager" selected=".NET Project.abyssctl">
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.Abyss: http">
|
||||
<configuration name="Publish Abyss to folder" type="DotNetFolderPublish" factoryName="Publish to folder" singleton="false">
|
||||
<riderPublish configuration="Release" platform="Any CPU" produce_single_file="true" self_contained="true" target_folder="$PROJECT_DIR$/publish" target_framework="net9.0" uuid_high="3690631506471504162" uuid_low="-4858628519588143325">
|
||||
<runtimes>
|
||||
@@ -275,7 +294,8 @@
|
||||
<workItem from="1759551752441" duration="5836000" />
|
||||
<workItem from="1759561043616" duration="201000" />
|
||||
<workItem from="1759591584659" duration="8123000" />
|
||||
<workItem from="1759634209525" duration="1338000" />
|
||||
<workItem from="1759634209525" duration="1767000" />
|
||||
<workItem from="1759639928617" duration="9029000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArray_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F18f2eb258dcf45748fa1903c530f5f07d1a000_003Ff2_003Ff5e8fb60_003FArray_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<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_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5df2accb46d040ccbbbe8331bf4d24b61daa00_003Fdf_003F93debd37_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
||||
@@ -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()
|
||||
@@ -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<CtlService> 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<CtlService> 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<Module>();
|
||||
var attr = module.GetCustomAttribute<ModuleAttribute>();
|
||||
if (attr != null)
|
||||
{
|
||||
_handlers[attr.Head] = module;
|
||||
@@ -54,12 +54,12 @@ public class CtlService(ILogger<CtlService> 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<CtlService> logger, IServiceProvider serviceProv
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
File.Delete(SocketPath);
|
||||
}
|
||||
|
||||
private async Task HandleClientAsync(Socket clientSocket, CancellationToken stoppingToken)
|
||||
|
||||
58
Abyss/Components/Services/Admin/Modules/InitModule.cs
Normal file
58
Abyss/Components/Services/Admin/Modules/InitModule.cs
Normal file
@@ -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<InitModule> logger, UserService userService, ConfigureService configureService, ResourceDatabaseService resourceDatabaseService): IModule
|
||||
{
|
||||
public async Task<Ctl> 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]
|
||||
};
|
||||
}
|
||||
}
|
||||
50
Abyss/Components/Services/Admin/Modules/UserAddModule.cs
Normal file
50
Abyss/Components/Services/Admin/Modules/UserAddModule.cs
Normal file
@@ -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<Ctl> 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]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -22,22 +22,9 @@ public class ResourceDatabaseService
|
||||
|
||||
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)
|
||||
public async Task<bool> 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;
|
||||
|
||||
@@ -16,21 +16,12 @@ public enum OperationType
|
||||
Security // Chown, Chmod
|
||||
}
|
||||
|
||||
public class ResourceService
|
||||
public class ResourceService(
|
||||
ILogger<ResourceService> logger,
|
||||
ConfigureService config,
|
||||
UserService user,
|
||||
ResourceDatabaseService db)
|
||||
{
|
||||
private readonly ILogger<ResourceService> _logger;
|
||||
private readonly ConfigureService _config;
|
||||
private readonly UserService _user;
|
||||
private readonly ResourceDatabaseService _db;
|
||||
|
||||
public ResourceService(ILogger<ResourceService> 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<Dictionary<string, bool>> 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<string>(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<ResourceAttribute>();
|
||||
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<string>(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<ResourceAttribute>();
|
||||
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<bool> 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<bool> CheckPermission(User? user, ResourceAttribute? ra, OperationType type)
|
||||
private async Task<bool> 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<bool> 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<bool> 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<ResourceAttribute>();
|
||||
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<bool> 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<bool> 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<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ public class VideoService(ResourceService rs, ConfigureService config)
|
||||
{
|
||||
public readonly string VideoFolder = Path.Combine(config.MediaRoot, "Videos");
|
||||
|
||||
public async Task<bool> Init(string token, string owner, string ip) => await rs.Initialize(VideoFolder, token, owner, ip);
|
||||
public async Task<bool> Init(string token, string owner, string ip)
|
||||
=> await rs.Initialize(VideoFolder, token, owner, ip);
|
||||
|
||||
public async Task<string[]?> GetClasses(string token, string ip)
|
||||
=> (await rs.Query(VideoFolder, token, ip))?.SortLikeWindows();
|
||||
|
||||
@@ -25,39 +25,16 @@ public class UserService
|
||||
|
||||
_database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
|
||||
_database.CreateTableAsync<User>().Wait();
|
||||
var rootUser = _database.Table<User>().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<bool> IsEmptyUser()
|
||||
{
|
||||
return await _database.Table<User>().CountAsync() == 0;
|
||||
}
|
||||
|
||||
public async Task<string?> 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))
|
||||
|
||||
@@ -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; } = "@";
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -34,7 +34,7 @@ public class Program
|
||||
builder.Services.AddHostedService<AbyssService>();
|
||||
builder.Services.AddHostedService<CtlService>();
|
||||
|
||||
foreach (var t in Module.Modules)
|
||||
foreach (var t in ModuleAttribute.Modules)
|
||||
{
|
||||
builder.Services.AddTransient(t);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<Ctl> CtlWriteRead(Ctl ctl)
|
||||
public static async Task<Ctl> CtlWriteRead<T>(string[] param)
|
||||
{
|
||||
var attr = typeof(T).GetCustomAttribute<ModuleAttribute>()!;
|
||||
|
||||
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<Ctl>(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) =>
|
||||
{
|
||||
|
||||
28
abyssctl/App/Attributes/ModuleAttribute.cs
Normal file
28
abyssctl/App/Attributes/ModuleAttribute.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<int> Run()
|
||||
{
|
||||
var r = await App.CtlWriteRead(new Ctl
|
||||
var r = await App.CtlWriteRead<HelloOptions>([]);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
20
abyssctl/App/Modules/InitOptions.cs
Normal file
20
abyssctl/App/Modules/InitOptions.cs
Normal file
@@ -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<int> Run()
|
||||
{
|
||||
var r = await App.CtlWriteRead<InitOptions>([]);
|
||||
Console.WriteLine($"Response Code: {r.Head}");
|
||||
Console.WriteLine($"Params: {string.Join(",", r.Params)}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
26
abyssctl/App/Modules/UserAddOptions.cs
Normal file
26
abyssctl/App/Modules/UserAddOptions.cs
Normal file
@@ -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<int> Run()
|
||||
{
|
||||
var r = await App.CtlWriteRead<UserAddOptions>([Username, Privilege.ToString()]);
|
||||
|
||||
Console.WriteLine($"Response Code: {r.Head}");
|
||||
Console.WriteLine($"Params: {string.Join(",", r.Params)}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user