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 @@ - - - - - - + + + + + + - - - - - - - - - - - - - - + + + - + @@ -156,6 +153,14 @@ + + + + + + + + @@ -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()]);