[feat] Abyssctl Basic functions

This commit is contained in:
acite
2025-10-05 16:48:54 +08:00
parent 50eae5e275
commit a0273e3334
22 changed files with 428 additions and 238 deletions

View File

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

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

View File

@@ -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">{
&quot;associatedIndex&quot;: 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>

View File

@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<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>

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) =>
{

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

View File

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

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

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

View File

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