Files
MoShiro/backend/src/MoShiro.Backend.Storage.Sqlite/BackendUpdateFiles.cs
T
justme 84f9d211bc
ci/woodpecker/pr/verify Pipeline failed
ci/woodpecker/pr/web-build unknown status
backend: require confirmation for signed channel updates [CI]
2026-10-07 12:57:20 +08:00

90 lines
2.9 KiB
C#

using System.Globalization;
using System.Text;
using System.Text.Json;
using MoShiro.Backend.Application;
using MoShiro.Protocol;
namespace MoShiro.Backend.Storage.Sqlite;
/// <summary>Backend 到宿主机的文件信箱。状态卷只读挂载,业务进程改写不了验签和安装结果。</summary>
public sealed class BackendUpdateFiles : IBackendUpdateFiles
{
private readonly string _requests;
private readonly string _status;
public BackendUpdateFiles(BackendOptions options)
{
_requests = Path.GetFullPath(options.BackendUpdateDirectory ?? Path.Combine(options.DataDirectory, "update"));
_status = Path.GetFullPath(options.BackendUpdateStatusDirectory ?? Path.Combine(options.DataDirectory, "update-status"));
}
public async ValueTask WriteOfferAsync(string manifest, string signature, CancellationToken ct)
{
await WriteAsync("offer.manifest", manifest, ct);
await WriteAsync("offer.sig", signature, ct);
}
public async ValueTask<UpdateStatus?> ReadStatusAsync(CancellationToken ct)
{
string? json = await ReadAsync(Path.Combine(_status, "status.json"), 4096, ct);
if (json is null)
{
return null;
}
try
{
return JsonSerializer.Deserialize(json, MoShiroJsonContext.Default.UpdateStatus);
}
catch (JsonException)
{
return null;
}
}
public async ValueTask<long?> ReadRequestAsync(CancellationToken ct)
{
string? request = await ReadAsync(Path.Combine(_requests, "apply.request"), 64, ct);
return long.TryParse(request, NumberStyles.None, CultureInfo.InvariantCulture, out long seq) ? seq : null;
}
public ValueTask RequestAsync(long seq, CancellationToken ct) => WriteAsync("apply.request", seq.ToString(CultureInfo.InvariantCulture), ct);
private async ValueTask WriteAsync(string name, string value, CancellationToken ct)
{
Directory.CreateDirectory(_requests);
string path = Path.Combine(_requests, name);
string temporary = Path.Combine(_requests, $".{name}.{Guid.NewGuid():N}");
try
{
await File.WriteAllTextAsync(temporary, value, new UTF8Encoding(false), ct);
File.Move(temporary, path, overwrite: true);
}
finally
{
if (File.Exists(temporary))
{
File.Delete(temporary);
}
}
}
private static async ValueTask<string?> ReadAsync(string path, int limit, CancellationToken ct)
{
try
{
var file = new FileInfo(path);
if (!file.Exists || file.LinkTarget is not null || file.Length > limit)
{
return null;
}
return await File.ReadAllTextAsync(path, ct);
}
catch (IOException)
{
return null; // 更新器正做原子替换,下一次读取再试
}
}
}