This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user