From 24331757571c354abb724109e0a80dc546a0e160 Mon Sep 17 00:00:00 2001
From: acite <1498045907@qq.com>
Date: Sun, 5 Oct 2025 21:43:20 +0800
Subject: [PATCH] [feat] Abyssctl Basic functions 2
---
.idea/.idea.Abyss/.idea/workspace.xml | 54 +++++----
.../Controllers/Security/RootController.cs | 4 +-
.../Services/Admin/Modules/ChmodModule.cs | 108 ++++++++++++++++++
.../Services/Admin/Modules/IncludeModule.cs | 88 ++++++++++++++
.../Services/Admin/Modules/ListModule.cs | 80 +++++++++++++
Abyss/Toolkits/update-video.py | 2 +
abyssctl/App/Modules/ChmodOptions.cs | 28 +++++
abyssctl/App/Modules/IncludeOptions.cs | 29 +++++
abyssctl/App/Modules/ListOptions.cs | 30 +++++
abyssctl/App/Modules/UserAddOptions.cs | 5 +-
10 files changed, 400 insertions(+), 28 deletions(-)
create mode 100644 Abyss/Components/Services/Admin/Modules/ChmodModule.cs
create mode 100644 Abyss/Components/Services/Admin/Modules/IncludeModule.cs
create mode 100644 Abyss/Components/Services/Admin/Modules/ListModule.cs
create mode 100644 abyssctl/App/Modules/ChmodOptions.cs
create mode 100644 abyssctl/App/Modules/IncludeOptions.cs
create mode 100644 abyssctl/App/Modules/ListOptions.cs
diff --git a/.idea/.idea.Abyss/.idea/workspace.xml b/.idea/.idea.Abyss/.idea/workspace.xml
index 3f75433..caadd27 100644
--- a/.idea/.idea.Abyss/.idea/workspace.xml
+++ b/.idea/.idea.Abyss/.idea/workspace.xml
@@ -11,27 +11,16 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -70,8 +59,11 @@
+
+
+
@@ -104,8 +96,11 @@
+
+
+
@@ -124,7 +119,7 @@
-
+
@@ -156,6 +153,14 @@
+
+
+
+
+
+
+
+
@@ -198,7 +203,8 @@
-
+
+
@@ -295,7 +301,7 @@
-
+
diff --git a/Abyss/Components/Controllers/Security/RootController.cs b/Abyss/Components/Controllers/Security/RootController.cs
index 0df6a7d..6dfd9b7 100644
--- a/Abyss/Components/Controllers/Security/RootController.cs
+++ b/Abyss/Components/Controllers/Security/RootController.cs
@@ -69,7 +69,7 @@ public class RootController(ILogger logger, UserService userServ
if (!Directory.Exists(fullPath))
{
logger.LogInformation("Directory does not exist: {FullPath}", fullPath);
- return _400;
+ return _404;
}
var entries = Directory.EnumerateFileSystemEntries(fullPath, "*", SearchOption.TopDirectoryOnly).ToArray();
@@ -125,7 +125,7 @@ public class RootController(ILogger logger, UserService userServ
return _403;
}
- private static string ConvertToLsPerms(string permRaw, bool isDirectory)
+ public static string ConvertToLsPerms(string permRaw, bool isDirectory)
{
// expects format like "rw,r-,r-"
if (string.IsNullOrEmpty(permRaw))
diff --git a/Abyss/Components/Services/Admin/Modules/ChmodModule.cs b/Abyss/Components/Services/Admin/Modules/ChmodModule.cs
new file mode 100644
index 0000000..231670c
--- /dev/null
+++ b/Abyss/Components/Services/Admin/Modules/ChmodModule.cs
@@ -0,0 +1,108 @@
+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.Static;
+using Abyss.Model.Admin;
+
+namespace Abyss.Components.Services.Admin.Modules;
+
+[Module(106)]
+public class ChmodModule(
+ ILogger logger,
+ ConfigureService configureService,
+ ResourceDatabaseService resourceDatabaseService
+ ) : IModule
+{
+ public async Task ExecuteAsync(Ctl request, CancellationToken ct)
+ {
+ // request.Params[0] -> Relative Path
+ // request.Params[1] -> Permission
+ // request.Params[2] -> recursive?
+
+ var path = Helpers.SafePathCombine(configureService.MediaRoot, [request.Params[0]]);
+
+ if (request.Params.Length != 3 || !ResourceDatabaseService.PermissionRegex.IsMatch(request.Params[1]))
+ return new Ctl
+ {
+ Head = 400,
+ Params = ["Bad Request"]
+ };
+
+ if (!Directory.Exists(path))
+ return new Ctl
+ {
+ Head = 404,
+ Params = ["Directory not found"]
+ };
+
+ var recursive = request.Params[2] == "True";
+
+ List targets = new List();
+ try
+ {
+ if (recursive)
+ {
+ logger.LogInformation($"Recursive directory '{path}'.");
+ targets.Add(path);
+ foreach (var entry in Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories))
+ {
+ targets.Add(entry);
+ }
+ }
+ else
+ {
+ targets.Add(path);
+ }
+
+ // Build distinct UIDs
+ var relUids = targets
+ .Select(t => Path.GetRelativePath(configureService.MediaRoot, t))
+ .Select(rel => ResourceDatabaseService.Uid(rel))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ if (relUids.Count == 0)
+ {
+ logger.LogWarning($"No targets resolved for chmod on '{path}'");
+ return new Ctl
+ {
+ Head = 304,
+ Params = ["No targets, Not Modified."]
+ };
+ }
+
+ // Use DatabaseService to perform chunked updates
+ var updatedCount = await resourceDatabaseService.UpdatePermissionsByUidsAsync(relUids, request.Params[1] );
+
+ if (updatedCount > 0)
+ {
+ logger.LogInformation(
+ $"Chmod: updated permissions for {updatedCount} resource(s) (root='{path}', recursive={recursive})");
+ return new Ctl
+ {
+ Head = 200,
+ Params = ["Ok", updatedCount.ToString()]
+ };
+ }
+ else
+ {
+ logger.LogWarning($"Chmod: no resources updated for '{path}' (recursive={recursive})");
+ return new Ctl
+ {
+ Head = 304,
+ Params = ["Not Modified."]
+ };
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, $"Error changing permissions for: {path}");
+ return new Ctl
+ {
+ Head = 500,
+ Params = ["Error", ex.Message]
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Abyss/Components/Services/Admin/Modules/IncludeModule.cs b/Abyss/Components/Services/Admin/Modules/IncludeModule.cs
new file mode 100644
index 0000000..ed4d988
--- /dev/null
+++ b/Abyss/Components/Services/Admin/Modules/IncludeModule.cs
@@ -0,0 +1,88 @@
+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.Components.Static;
+using Abyss.Model.Admin;
+using Abyss.Model.Media;
+
+namespace Abyss.Components.Services.Admin.Modules;
+
+[Module(105)]
+public class IncludeModule(
+ ILogger logger,
+ UserService userService,
+ ConfigureService configureService,
+ ResourceDatabaseService resourceDatabaseService) : IModule
+{
+ public async Task ExecuteAsync(Ctl request, CancellationToken ct)
+ {
+ // request.Params[0] -> Relative Path
+ // request.Params[1] -> Owner Id
+ // request.Params[2] -> recursive?
+
+ var path = Helpers.SafePathCombine(configureService.MediaRoot, [request.Params[0]]);
+
+ if (request.Params.Length != 3 || !int.TryParse(request.Params[1], out var id))
+ return new Ctl
+ {
+ Head = 400,
+ Params = ["Bad Request"]
+ };
+
+ if (await userService.QueryUser(id) == null)
+ return new Ctl
+ {
+ Head = 404,
+ Params = ["User not found"]
+ };
+
+ if (!Directory.Exists(path))
+ return new Ctl
+ {
+ Head = 404,
+ Params = ["Directory not found"]
+ };
+
+ var allPaths = request.Params[2] == "True" ?
+ Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories).Prepend(path)
+ : [path];
+ var newResources = new List();
+ int c = 0;
+ foreach (var p in allPaths)
+ {
+ var currentPath = Path.GetRelativePath(configureService.MediaRoot, p);
+ var uid = ResourceDatabaseService.Uid(currentPath);
+ var existing = await resourceDatabaseService.GetResourceAttributeByUidAsync(uid);
+
+ if (existing == null)
+ {
+ newResources.Add(new ResourceAttribute
+ {
+ Uid = uid,
+ Owner = id,
+ Permission = "rw,--,--"
+ });
+ }
+ }
+
+ if (newResources.Any())
+ {
+ c = await resourceDatabaseService.InsertResourceAttributesAsync(newResources);
+ logger.LogInformation(
+ $"Successfully initialized {c} new resources under '{path}' for user '{id}'.");
+ }
+ else
+ {
+ logger.LogInformation(
+ $"No new resources to initialize under '{path}'. All items already exist in the database.");
+ }
+
+ return new Ctl
+ {
+ Head = 200,
+ Params = [c.ToString(), "resource(s) add to system"]
+ };
+ }
+}
\ No newline at end of file
diff --git a/Abyss/Components/Services/Admin/Modules/ListModule.cs b/Abyss/Components/Services/Admin/Modules/ListModule.cs
new file mode 100644
index 0000000..9512b19
--- /dev/null
+++ b/Abyss/Components/Services/Admin/Modules/ListModule.cs
@@ -0,0 +1,80 @@
+using System.Text;
+using Abyss.Components.Controllers.Security;
+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.Static;
+using Abyss.Model.Admin;
+
+namespace Abyss.Components.Services.Admin.Modules;
+
+[Module(107)]
+public class ListModule(
+ ILogger logger,
+ ConfigureService configureService,
+ ResourceService resourceService
+ ) : IModule
+{
+ public async Task ExecuteAsync(Ctl request, CancellationToken ct)
+ {
+ // request.Params[0] -> Relative Path
+ try
+ {
+ var path = Helpers.SafePathCombine(configureService.MediaRoot, [request.Params[0]]);
+
+ if (!Directory.Exists(path))
+ {
+ logger.LogInformation("Directory does not exist: {FullPath}", path);
+ return new Ctl
+ {
+ Head = 404,
+ Params = ["Not found"]
+ };
+ }
+
+ var entries = Directory.EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly).ToArray();
+ var sb = new StringBuilder();
+
+ foreach (var entry in entries)
+ {
+ try
+ {
+ var filename = Path.GetFileName(entry);
+ var isDir = Directory.Exists(entry);
+
+ var ra = await resourceService.GetAttribute(entry);
+
+ var ownerId = ra?.Owner ?? -1;
+ var uid = ra?.Uid ?? string.Empty;
+ var permRaw = ra?.Permission ?? "--,--,--";
+
+ var permStr = RootController.ConvertToLsPerms(permRaw, isDir);
+
+ sb.AppendLine($"{permStr} {ownerId,5} {uid} {filename}");
+ }
+ catch (Exception ex)
+ {
+ logger.LogInformation("Error processing entry {Entry}: {ErrorMessage}", entry, ex.Message);
+ // ignored
+ }
+ }
+
+ logger.LogInformation("Ls operation completed successfully");
+ return new Ctl
+ {
+ Head = 200,
+ Params = [sb.ToString()]
+ };
+ }
+ catch (Exception ex)
+ {
+ logger.LogInformation("Ls operation failed with error: {ErrorMessage}", ex.Message);
+ return new Ctl
+ {
+ Head = 500,
+ Params = ["Server Exception", ex.Message]
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Abyss/Toolkits/update-video.py b/Abyss/Toolkits/update-video.py
index cf99d18..8bb23bf 100644
--- a/Abyss/Toolkits/update-video.py
+++ b/Abyss/Toolkits/update-video.py
@@ -340,6 +340,7 @@ def main():
shutil.copy(video_source_path, video_dest_path)
print(f"Video copied to {video_dest_path}")
+ # === 新增:如果源视频同目录存在同名 .vtt 字幕,复制为 subtitle.vtt 到新项目目录 ===
subtitle_copied = False
candidate_vtt = video_source_path.with_suffix('.vtt')
candidate_vtt_upper = video_source_path.with_suffix('.VTT')
@@ -354,6 +355,7 @@ def main():
break
if not subtitle_copied:
print("No matching .vtt subtitle found next to source video; skipping subtitle copy.")
+ # === 新增结束 ===
# Auto-generate thumbnails
create_thumbnails(video_dest_path, gallery_path)
diff --git a/abyssctl/App/Modules/ChmodOptions.cs b/abyssctl/App/Modules/ChmodOptions.cs
new file mode 100644
index 0000000..a7c3938
--- /dev/null
+++ b/abyssctl/App/Modules/ChmodOptions.cs
@@ -0,0 +1,28 @@
+using abyssctl.App.Attributes;
+using abyssctl.App.Interfaces;
+using CommandLine;
+
+namespace abyssctl.App.Modules;
+
+[Module(106)]
+[Verb("chmod", HelpText = "Change resources permissions")]
+public class ChmodOptions: IOptions
+{
+ [Value(0, MetaName = "path", Required = true, HelpText = "Relative path to resources.")]
+ public string Path { get; set; } = "";
+
+ [Value(1, MetaName = "permission", Required = true, HelpText = "Permission mask.")]
+ public string Permission { get; set; } = "";
+
+ [Option('r', "recursive", Default = false, HelpText = "Recursive change resources.")]
+ public bool Recursive { get; set; }
+ public async Task Run()
+ {
+ var r = await App.CtlWriteRead([Path, Permission, Recursive.ToString()]);
+
+ Console.WriteLine($"Response Code: {r.Head}");
+ Console.WriteLine($"Params: {string.Join(",", r.Params)}");
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/abyssctl/App/Modules/IncludeOptions.cs b/abyssctl/App/Modules/IncludeOptions.cs
new file mode 100644
index 0000000..9448ecd
--- /dev/null
+++ b/abyssctl/App/Modules/IncludeOptions.cs
@@ -0,0 +1,29 @@
+using abyssctl.App.Attributes;
+using abyssctl.App.Interfaces;
+using CommandLine;
+
+namespace abyssctl.App.Modules;
+
+[Module(105)]
+[Verb("include", HelpText = "include resources to system")]
+public class IncludeOptions: IOptions
+{
+ [Value(0, MetaName = "path", Required = true, HelpText = "Relative path to resources.")]
+ public string Path { get; set; } = "";
+
+ [Value(1, MetaName = "owner", Required = true, HelpText = "Owner id.")]
+ public int Id { get; set; }
+
+ [Option('r', "recursive", Default = false, HelpText = "Recursive include resources.")]
+ public bool Recursive { get; set; }
+
+ public async Task Run()
+ {
+ var r = await App.CtlWriteRead([Path, Id.ToString(), Recursive.ToString()]);
+
+ Console.WriteLine($"Response Code: {r.Head}");
+ Console.WriteLine($"Params: {string.Join(",", r.Params)}");
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/abyssctl/App/Modules/ListOptions.cs b/abyssctl/App/Modules/ListOptions.cs
new file mode 100644
index 0000000..d86cb66
--- /dev/null
+++ b/abyssctl/App/Modules/ListOptions.cs
@@ -0,0 +1,30 @@
+using abyssctl.App.Attributes;
+using abyssctl.App.Interfaces;
+using CommandLine;
+
+namespace abyssctl.App.Modules;
+
+[Module(107)]
+[Verb("list", HelpText = "List items")]
+public class ListOptions: IOptions
+{
+ [Value(0, MetaName = "path", Required = true, HelpText = "Relative path to resources.")]
+ public string Path { get; set; } = "";
+
+ public async Task Run()
+ {
+ var r = await App.CtlWriteRead([Path]);
+
+ if (r.Head != 200)
+ {
+ Console.WriteLine($"Response Code: {r.Head}");
+ Console.WriteLine($"Params: {string.Join(",", r.Params)}");
+ }
+ else
+ {
+ Console.WriteLine(r.Params[0]);
+ }
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/abyssctl/App/Modules/UserAddOptions.cs b/abyssctl/App/Modules/UserAddOptions.cs
index 46eb472..7128ff6 100644
--- a/abyssctl/App/Modules/UserAddOptions.cs
+++ b/abyssctl/App/Modules/UserAddOptions.cs
@@ -9,11 +9,12 @@ namespace abyssctl.App.Modules;
[Verb("useradd", HelpText = "Add user")]
public class UserAddOptions: IOptions
{
- [Option('u', "username", Required = true, HelpText = "Username for new user.")]
+ [Value(0, MetaName = "username", Required = true, HelpText = "Username for new user.")]
public string Username { get; set; } = "";
- [Option('p', "privilege", Required = true, HelpText = "User privilege.")]
+ [Value(1, MetaName = "privilege", Required = true, HelpText = "User privilege.")]
public int Privilege { get; set; }
+
public async Task Run()
{
var r = await App.CtlWriteRead([Username, Privilege.ToString()]);