90 lines
2.9 KiB
C#
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; // 更新器正做原子替换,下一次读取再试
|
|
}
|
|
}
|
|
}
|