- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档 - 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理 - 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言 - 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则 - 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护 - 新增 Network 语义扫描、模板保护和 App 组件生命周期测试 - 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试 - 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
198 lines
6.5 KiB
C#
198 lines
6.5 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using Cysharp.Threading.Tasks;
|
|
using UnityEngine;
|
|
|
|
namespace ShrinkDataSaver
|
|
{
|
|
public class LocalStorageProvider : IStorageProvider
|
|
{
|
|
private const string BackupPrimarySuffix = ".bak1";
|
|
private const string BackupSecondarySuffix = ".bak2";
|
|
private const string TempWriteSuffix = ".tmp";
|
|
|
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> PathLocks =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private readonly string _rootPath;
|
|
|
|
public LocalStorageProvider(string rootPath = null)
|
|
{
|
|
_rootPath = rootPath ?? Application.persistentDataPath;
|
|
}
|
|
|
|
private string Resolve(string path) =>
|
|
Path.IsPathRooted(path) ? path : Path.Combine(_rootPath, path);
|
|
|
|
public async UniTask WriteAsync(string path, byte[] data, CancellationToken ct = default)
|
|
{
|
|
var fullPath = Resolve(path);
|
|
var dir = Path.GetDirectoryName(fullPath);
|
|
if (!string.IsNullOrEmpty(dir))
|
|
{
|
|
await UniTask.RunOnThreadPool(() => Directory.CreateDirectory(dir), cancellationToken: ct);
|
|
}
|
|
|
|
var tempPath = fullPath + TempWriteSuffix;
|
|
var backupPath = fullPath + BackupPrimarySuffix;
|
|
var secondaryBackupPath = fullPath + BackupSecondarySuffix;
|
|
var pathLock = GetPathLock(fullPath);
|
|
|
|
await pathLock.WaitAsync(ct);
|
|
try
|
|
{
|
|
await WriteFileBytesAsync(tempPath, data, ct);
|
|
await UniTask.RunOnThreadPool(() =>
|
|
{
|
|
if (File.Exists(fullPath))
|
|
{
|
|
if (File.Exists(secondaryBackupPath))
|
|
{
|
|
File.Delete(secondaryBackupPath);
|
|
}
|
|
|
|
if (File.Exists(backupPath))
|
|
{
|
|
File.Move(backupPath, secondaryBackupPath);
|
|
}
|
|
|
|
File.Replace(tempPath, fullPath, backupPath, true);
|
|
}
|
|
else
|
|
{
|
|
File.Move(tempPath, fullPath);
|
|
}
|
|
}, cancellationToken: ct);
|
|
}
|
|
catch
|
|
{
|
|
await UniTask.RunOnThreadPool(() =>
|
|
{
|
|
if (File.Exists(tempPath))
|
|
{
|
|
File.Delete(tempPath);
|
|
}
|
|
}, cancellationToken: CancellationToken.None);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
pathLock.Release();
|
|
}
|
|
}
|
|
|
|
public async UniTask<byte[]> ReadAsync(string path, CancellationToken ct = default)
|
|
{
|
|
var fullPath = Resolve(path);
|
|
var pathLock = GetPathLock(fullPath);
|
|
await pathLock.WaitAsync(ct);
|
|
try
|
|
{
|
|
if (!File.Exists(fullPath))
|
|
{
|
|
throw new FileNotFoundException($"ShrinkDataSaver: file not found: {fullPath}");
|
|
}
|
|
|
|
return await ReadFileBytesAsync(fullPath, ct);
|
|
}
|
|
finally
|
|
{
|
|
pathLock.Release();
|
|
}
|
|
}
|
|
|
|
public UniTask<bool> ExistsAsync(string path, CancellationToken ct = default) =>
|
|
UniTask.RunOnThreadPool(() => File.Exists(Resolve(path)), cancellationToken: ct);
|
|
|
|
public async UniTask DeleteAsync(string path, CancellationToken ct = default)
|
|
{
|
|
var fullPath = Resolve(path);
|
|
var pathLock = GetPathLock(fullPath);
|
|
await pathLock.WaitAsync(ct);
|
|
try
|
|
{
|
|
await UniTask.RunOnThreadPool(() =>
|
|
{
|
|
if (File.Exists(fullPath))
|
|
{
|
|
File.Delete(fullPath);
|
|
}
|
|
}, cancellationToken: ct);
|
|
}
|
|
finally
|
|
{
|
|
pathLock.Release();
|
|
}
|
|
}
|
|
|
|
public async UniTask<string[]> ListAsync(string prefix = "", CancellationToken ct = default)
|
|
{
|
|
return await UniTask.RunOnThreadPool(() =>
|
|
{
|
|
var dir = string.IsNullOrEmpty(prefix) ? _rootPath : Path.Combine(_rootPath, prefix);
|
|
if (!Directory.Exists(dir))
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
return Directory.GetFiles(dir)
|
|
.Select(f => Path.GetRelativePath(_rootPath, f))
|
|
.ToArray();
|
|
}, cancellationToken: ct);
|
|
}
|
|
|
|
private static SemaphoreSlim GetPathLock(string fullPath)
|
|
{
|
|
return PathLocks.GetOrAdd(fullPath, _ => new SemaphoreSlim(1, 1));
|
|
}
|
|
|
|
private static async UniTask WriteFileBytesAsync(string fullPath, byte[] data, CancellationToken ct)
|
|
{
|
|
await using var stream = new FileStream(
|
|
fullPath,
|
|
FileMode.Create,
|
|
FileAccess.Write,
|
|
FileShare.None,
|
|
4096,
|
|
FileOptions.Asynchronous);
|
|
await stream.WriteAsync(data, 0, data.Length, ct);
|
|
stream.Flush(true);
|
|
}
|
|
|
|
private static async UniTask<byte[]> ReadFileBytesAsync(string fullPath, CancellationToken ct)
|
|
{
|
|
await using var stream = new FileStream(
|
|
fullPath,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.Read,
|
|
4096,
|
|
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
|
|
|
var length = stream.Length;
|
|
if (length > int.MaxValue)
|
|
{
|
|
throw new IOException($"ShrinkDataSaver: file too large to read into memory: {fullPath}");
|
|
}
|
|
|
|
var buffer = new byte[length];
|
|
var offset = 0;
|
|
while (offset < buffer.Length)
|
|
{
|
|
var read = await stream.ReadAsync(buffer, offset, buffer.Length - offset, ct);
|
|
if (read <= 0)
|
|
{
|
|
throw new EndOfStreamException($"ShrinkDataSaver: unexpected EOF while reading {fullPath}");
|
|
}
|
|
|
|
offset += read;
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
}
|
|
}
|