feat(mosstank): add VTank-style automation PoC
This commit is contained in:
parent
f6fe0f2a4f
commit
4e6e9bc9d9
212 changed files with 49462 additions and 416 deletions
86
src/AcDream.App/Plugins/FilePluginStorage.cs
Normal file
86
src/AcDream.App/Plugins/FilePluginStorage.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using System.Text;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.App.Plugins;
|
||||
|
||||
/// <summary>Crash-safe filesystem implementation behind scoped plugin keys.</summary>
|
||||
internal sealed class FilePluginStorage : IPluginStorage
|
||||
{
|
||||
private readonly string _root;
|
||||
|
||||
internal FilePluginStorage(string root)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(root);
|
||||
_root = Path.GetFullPath(root);
|
||||
}
|
||||
|
||||
public bool IsAvailable => true;
|
||||
|
||||
public string? ReadText(string key)
|
||||
{
|
||||
string path = Resolve(key);
|
||||
return File.Exists(path)
|
||||
? File.ReadAllText(path, Encoding.UTF8)
|
||||
: null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> List(string prefix)
|
||||
{
|
||||
string directory = Resolve(prefix);
|
||||
if (!Directory.Exists(directory))
|
||||
return Array.Empty<string>();
|
||||
return Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)
|
||||
.Select(path => Path.GetRelativePath(_root, path)
|
||||
.Replace(Path.DirectorySeparatorChar, '/'))
|
||||
.OrderBy(static key => key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public void WriteText(string key, string content)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
string path = Resolve(key);
|
||||
string directory = Path.GetDirectoryName(path)!;
|
||||
Directory.CreateDirectory(directory);
|
||||
string temporary = Path.Combine(
|
||||
directory,
|
||||
$".{Path.GetFileName(path)}.{Guid.NewGuid():N}.tmp");
|
||||
try
|
||||
{
|
||||
File.WriteAllText(temporary, content, new UTF8Encoding(false));
|
||||
File.Move(temporary, path, overwrite: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporary))
|
||||
File.Delete(temporary);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Delete(string key)
|
||||
{
|
||||
string path = Resolve(key);
|
||||
if (!File.Exists(path))
|
||||
return false;
|
||||
File.Delete(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
private string Resolve(string key)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||
if (Path.IsPathRooted(key))
|
||||
throw new ArgumentException("Plugin storage keys must be relative.", nameof(key));
|
||||
string path = Path.GetFullPath(Path.Combine(_root, key));
|
||||
string relative = Path.GetRelativePath(_root, path);
|
||||
if (Path.IsPathRooted(relative)
|
||||
|| relative.Equals("..", StringComparison.Ordinal)
|
||||
|| relative.StartsWith(
|
||||
".." + Path.DirectorySeparatorChar,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("Plugin storage key escapes its root.", nameof(key));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue