202 lines
6.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|