Files
ShrinkDataSaver/Runtime/LocalStorageProvider.cs

202 lines
6.5 KiB
C#

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
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 ?? ShrinkDataSaverPlatform.Paths?.PersistentDataPath
#if UNITY_5_3_OR_NEWER
?? UnityEngine.Application.persistentDataPath
#endif
?? throw new InvalidOperationException("ShrinkDataSaver persistent path is not configured.");
}
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 Task.Run(() => Directory.CreateDirectory(dir), 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 Task.Run(() =>
{
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);
}
}, ct);
}
catch
{
await Task.Run(() =>
{
if (File.Exists(tempPath))
{
File.Delete(tempPath);
}
}, 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) =>
Task.Run(() => File.Exists(Resolve(path)), ct).AsUniTask();
public async UniTask DeleteAsync(string path, CancellationToken ct = default)
{
var fullPath = Resolve(path);
var pathLock = GetPathLock(fullPath);
await pathLock.WaitAsync(ct);
try
{
await Task.Run(() =>
{
if (File.Exists(fullPath))
{
File.Delete(fullPath);
}
}, ct);
}
finally
{
pathLock.Release();
}
}
public async UniTask<string[]> ListAsync(string prefix = "", CancellationToken ct = default)
{
return await Task.Run(() =>
{
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();
}, 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;
}
}
}