LarpixServer/LarpixServer/Filesystem/Fs.cs
olcxja cd581593bb
All checks were successful
Server Build / publish (push) Successful in 29s
Voice Build / publish (push) Successful in 26s
improve locks
2026-05-29 12:01:13 +02:00

428 lines
No EOL
12 KiB
C#

using System.Collections.Concurrent;
using System.Threading;
using static LarpixServer.Utils.Utils;
namespace LarpixServer.Filesystem;
public class Fs
{
public static ConcurrentDictionary<string, FileData> fileCache = new ConcurrentDictionary<string, FileData>();
public static ConcurrentDictionary<string, bool> existCache = new ConcurrentDictionary<string, bool>();
public static ConcurrentDictionary<string, string[]> dirCache = new ConcurrentDictionary<string, string[]>();
private static SemaphoreSlim[]? _fileLocksArray = null;
private static void InvalidateDirCacheFor(string path)
{
var parentDir = Path.GetDirectoryName(path)?.Replace("\\", "/").TrimEnd('/');
if (!string.IsNullOrEmpty(parentDir))
{
dirCache.TryRemove(parentDir, out _);
}
}
private static SemaphoreSlim GetFileLock(string path)
{
if (_fileLocksArray == null)
{
int size = LOCK_SIZE > 0 ? LOCK_SIZE : 65536;
var newArray = Enumerable.Range(0, size).Select(_ => new SemaphoreSlim(1, 1)).ToArray();
Interlocked.CompareExchange(ref _fileLocksArray, newArray, null);
}
int hash = path.GetHashCode();
if (hash < 0) hash = -hash; // Or use Math.Abs, but hash < 0 logic avoids OverflowException on int.MinValue
return _fileLocksArray[hash % _fileLocksArray.Length];
}
public static void ProcessCacheSpace()
{
while (fileCache.Count >= FILE_CACHE_SIZE)
{
var firstKey = fileCache.Keys.FirstOrDefault();
if (firstKey != null) fileCache.TryRemove(firstKey, out _);
}
while (existCache.Count >= EXIST_CACHE_SIZE)
{
var firstKey = existCache.Keys.FirstOrDefault();
if (firstKey != null) existCache.TryRemove(firstKey, out _);
}
while (dirCache.Count >= DIR_CACHE_SIZE)
{
var firstKey = dirCache.Keys.FirstOrDefault();
if (firstKey != null) dirCache.TryRemove(firstKey, out _);
}
}
public static ulong ClearCache(string pattern)
{
var comparison = StringComparison.OrdinalIgnoreCase;
ulong removed = 0;
removed += ClearDict(fileCache, pattern, comparison);
removed += ClearDict(existCache, pattern, comparison);
removed += ClearDict(dirCache, pattern, comparison);
return removed;
}
static ulong ClearDict<TValue>(ConcurrentDictionary<string, TValue> dict, string pattern,
StringComparison comparison)
{
ulong count = 0;
Parallel.ForEach(
dict.Where(x => x.Key.Contains(pattern, comparison))
.Select(x => x.Key)
.ToList(),
key =>
{
if (dict.TryRemove(key, out _))
Interlocked.Increment(ref count);
});
return count;
}
public static async Task<Stream> ReadFileStream(string path)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
await fileLock.WaitAsync();
try
{
if (fileCache.TryGetValue(path, out var cachedData))
{
return new MemoryStream(cachedData.content);
}
var fileInfo = new FileInfo(path);
if (!fileInfo.Exists) throw new FileNotFoundException();
if (fileInfo.Length < CACHE_ENTRY_SIZE)
{
var bytes = await File.ReadAllBytesAsync(path);
fileCache[path] = new FileData(bytes, false, fileInfo.Length);
return new MemoryStream(bytes);
}
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 8192, true);
}
finally
{
fileLock.Release();
}
}
public static async Task<byte[]> ReadFile(string path)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
await fileLock.WaitAsync();
try
{
if (fileCache.TryGetValue(path, out var fileData))
{
return fileData.content;
}
var fileInfo = new FileInfo(path);
if (fileInfo.Length < CACHE_ENTRY_SIZE)
{
byte[] content = await File.ReadAllBytesAsync(path);
fileCache[path] = new FileData(content, false, fileInfo.Length);
existCache[path] = true;
return content;
}
byte[] bytes = await File.ReadAllBytesAsync(path);
existCache[path] = true;
return bytes;
}
finally
{
fileLock.Release();
}
}
public static async Task WriteFile(string path, byte[] content)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
await fileLock.WaitAsync();
try
{
var dir = Path.GetDirectoryName(path)?.Replace("\\", "/");
if (!string.IsNullOrEmpty(dir))
{
existCache[dir] = true;
if (!Directory.Exists(dir))
{
CreateDirectory(dir);
}
}
await File.WriteAllBytesAsync(path, content);
existCache[path] = true;
if (content.Length < CACHE_ENTRY_SIZE)
{
fileCache[path] = new FileData(content, false, content.Length);
}
else
{
fileCache.TryRemove(path, out _);
}
InvalidateDirCacheFor(path);
}
finally
{
fileLock.Release();
}
}
public static void MoveFile(string path, string newPath)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
newPath = newPath.Replace("\\", "/").TrimEnd('/');
var lock1Path = string.Compare(path, newPath, StringComparison.OrdinalIgnoreCase) < 0 ? path : newPath;
var lock2Path = string.Compare(path, newPath, StringComparison.OrdinalIgnoreCase) < 0 ? newPath : path;
var sem1 = GetFileLock(lock1Path);
var sem2 = GetFileLock(lock2Path);
sem1.Wait();
try
{
sem2.Wait();
try
{
File.Move(path, newPath);
existCache[path] = false;
existCache[newPath] = true;
fileCache.TryRemove(path, out _);
InvalidateDirCacheFor(path);
InvalidateDirCacheFor(newPath);
}
finally
{
sem2.Release();
}
}
finally
{
sem1.Release();
}
}
public static void DeleteFile(string path)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
fileLock.Wait();
try
{
if (File.Exists(path))
{
File.Delete(path);
}
existCache[path] = false;
if (fileCache.ContainsKey(path))
{
fileCache.TryRemove(path, out _);
}
InvalidateDirCacheFor(path);
}
finally
{
fileLock.Release();
}
}
public static void DeleteDirectory(string path)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
fileLock.Wait();
try
{
if (Directory.Exists(path))
{
Directory.Delete(path, true);
}
var keysToRemove = existCache.Keys.Where(k => k.StartsWith(path + "/")).ToList();
foreach (var key in keysToRemove)
{
existCache[key] = false;
fileCache.TryRemove(key, out _);
dirCache.TryRemove(key, out _);
}
existCache[path] = false;
dirCache.TryRemove(path, out _);
InvalidateDirCacheFor(path);
}
finally
{
fileLock.Release();
}
}
public static void CreateDirectory(string path)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
fileLock.Wait();
try
{
Directory.CreateDirectory(path);
var current = path;
while (!string.IsNullOrEmpty(current))
{
existCache[current] = true;
InvalidateDirCacheFor(current);
var parent = Path.GetDirectoryName(current)?.Replace("\\", "/").TrimEnd('/');
if (string.IsNullOrEmpty(parent) || parent == current) break;
current = parent;
}
}
finally
{
fileLock.Release();
}
}
public static bool Exists(string path)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
fileLock.Wait();
try
{
if (existCache.TryGetValue(path, out bool exists))
{
return exists;
}
if (File.Exists(path) || Directory.Exists(path))
{
FileAttributes attr = File.GetAttributes(path);
bool isDir = attr.HasFlag(FileAttributes.Directory);
if (isDir)
{
fileCache[path] = new FileData(null, true, 0);
}
existCache[path] = true;
return true;
}
else
{
existCache[path] = false;
return false;
}
}
finally
{
fileLock.Release();
}
}
public static string[] ReadDirectory(string path)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
fileLock.Wait();
try
{
if (dirCache.TryGetValue(path, out var directoryData))
{
return directoryData;
}
if (!Directory.Exists(path))
{
return Array.Empty<string>();
}
directoryData = Directory.GetFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).ToArray();
dirCache[path] = directoryData;
return directoryData;
}
finally
{
fileLock.Release();
}
}
public async static Task<bool> isDirectory(string path)
{
ProcessCacheSpace();
path = path.Replace("\\", "/").TrimEnd('/');
var fileLock = GetFileLock(path);
await fileLock.WaitAsync();
try
{
if (fileCache.TryGetValue(path, out var fileData))
{
return fileData.isDir;
}
if (!File.Exists(path) && !Directory.Exists(path))
{
return false;
}
var fileInfo = new FileInfo(path);
if (fileInfo.Attributes.HasFlag(FileAttributes.Directory))
{
fileCache[path] = new FileData(null, true, 0);
return true;
}
if (fileInfo.Length < CACHE_ENTRY_SIZE)
{
byte[] content = await File.ReadAllBytesAsync(path);
fileCache[path] = new FileData(content, false, fileInfo.Length);
existCache[path] = true;
}
return false;
}
finally
{
fileLock.Release();
}
}
}