using System.Globalization; using System.Text; using System.Text.Json; using MoShiro.Backend.Application; using MoShiro.Protocol; namespace MoShiro.Backend.Storage.Sqlite; /// Backend 到宿主机的文件信箱。状态卷只读挂载,业务进程改写不了验签和安装结果。 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 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 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 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; // 更新器正做原子替换,下一次读取再试 } } }