[feat] Abyssctl Basic functions 2

This commit is contained in:
acite
2025-10-05 21:43:20 +08:00
parent a0273e3334
commit 2433175757
10 changed files with 400 additions and 28 deletions

View File

@@ -11,27 +11,16 @@
</component>
<component name="ChangeListManager">
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
<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 afterPath="$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/ChmodModule.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/IncludeModule.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Abyss/Components/Services/Admin/Modules/ListModule.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/abyssctl/App/Modules/ChmodOptions.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/abyssctl/App/Modules/IncludeOptions.cs" afterDir="false" />
<change afterPath="$PROJECT_DIR$/abyssctl/App/Modules/ListOptions.cs" 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" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Security/RootController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Security/RootController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Toolkits/update-video.py" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Toolkits/update-video.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/abyssctl/App/Modules/UserAddOptions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/abyssctl/App/Modules/UserAddOptions.cs" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -70,8 +59,11 @@
<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/ChmodModule.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/IncludeModule.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/ListModule.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" />
@@ -104,8 +96,11 @@
<setting file="file://$PROJECT_DIR$/Abyss/Model/Security/UserCreating.cs" root0="FORCE_HIGHLIGHTING" />
<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/ChmodOptions.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/IncludeOptions.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/ListOptions.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" />
@@ -124,7 +119,7 @@
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
".NET Launch Settings Profile.Abyss: http.executor": "Debug",
".NET Launch Settings Profile.Abyss: http.executor": "Run",
".NET Launch Settings Profile.Abyss: https.executor": "Debug",
".NET Project.AbyssCli.executor": "Run",
".NET Project.abyssctl.executor": "Debug",
@@ -132,6 +127,8 @@
"ModuleVcsDetector.initialDetectionPerformed": "true",
"Publish to folder.Publish Abyss to folder x86.executor": "Run",
"Publish to folder.Publish Abyss to folder.executor": "Run",
"Publish to folder.p1.executor": "Run",
"Publish to folder.p2.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
@@ -148,7 +145,7 @@
}
}]]></component>
<component name="RunManager" selected=".NET Launch Settings Profile.Abyss: http">
<configuration name="Publish Abyss to folder" type="DotNetFolderPublish" factoryName="Publish to folder" singleton="false">
<configuration name="p1" 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>
<item value="linux-arm64" />
@@ -156,6 +153,14 @@
</riderPublish>
<method v="2" />
</configuration>
<configuration name="p2" type="DotNetFolderPublish" factoryName="Publish to folder">
<riderPublish configuration="Release" platform="Any CPU" produce_single_file="true" self_contained="true" target_folder="$PROJECT_DIR$/publish" target_framework="net9.0" uuid_high="-657823440020091444" uuid_low="-8550226025966742844">
<runtimes>
<item value="linux-arm64" />
</runtimes>
</riderPublish>
<method v="2" />
</configuration>
<configuration name="abyssctl" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/build/net9.0/abyssctl" />
<option name="PROGRAM_PARAMETERS" value="hello" />
@@ -198,7 +203,8 @@
<list>
<item itemvalue=".NET Launch Settings Profile.Abyss: http" />
<item itemvalue=".NET Project.abyssctl" />
<item itemvalue="Publish to folder.Publish Abyss to folder" />
<item itemvalue="Publish to folder.p1" />
<item itemvalue="Publish to folder.p2" />
</list>
</component>
<component name="TaskManager">
@@ -295,7 +301,7 @@
<workItem from="1759561043616" duration="201000" />
<workItem from="1759591584659" duration="8123000" />
<workItem from="1759634209525" duration="1767000" />
<workItem from="1759639928617" duration="9029000" />
<workItem from="1759639928617" duration="18716000" />
</task>
<servers />
</component>

View File

@@ -69,7 +69,7 @@ public class RootController(ILogger<RootController> 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<RootController> 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))

View File

@@ -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<ChmodModule> logger,
ConfigureService configureService,
ResourceDatabaseService resourceDatabaseService
) : IModule
{
public async Task<Ctl> 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<string> targets = new List<string>();
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]
};
}
}
}

View File

@@ -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<IncludeModule> logger,
UserService userService,
ConfigureService configureService,
ResourceDatabaseService resourceDatabaseService) : IModule
{
public async Task<Ctl> 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<ResourceAttribute>();
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"]
};
}
}

View File

@@ -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<ListModule> logger,
ConfigureService configureService,
ResourceService resourceService
) : IModule
{
public async Task<Ctl> 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]
};
}
}
}

View File

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

View File

@@ -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<int> Run()
{
var r = await App.CtlWriteRead<ChmodOptions>([Path, Permission, Recursive.ToString()]);
Console.WriteLine($"Response Code: {r.Head}");
Console.WriteLine($"Params: {string.Join(",", r.Params)}");
return 0;
}
}

View File

@@ -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<int> Run()
{
var r = await App.CtlWriteRead<IncludeOptions>([Path, Id.ToString(), Recursive.ToString()]);
Console.WriteLine($"Response Code: {r.Head}");
Console.WriteLine($"Params: {string.Join(",", r.Params)}");
return 0;
}
}

View File

@@ -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<int> Run()
{
var r = await App.CtlWriteRead<ListOptions>([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;
}
}

View File

@@ -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<int> Run()
{
var r = await App.CtlWriteRead<UserAddOptions>([Username, Privilege.ToString()]);