using System.Collections.Concurrent; using System.Threading; using static LarpixServer.Utils.Utils; namespace LarpixServer.Filesystem; public class Fs { public static ConcurrentDictionary fileCache = new ConcurrentDictionary(); public static ConcurrentDictionary existCache = new ConcurrentDictionary(); public static ConcurrentDictionary dirCache = new ConcurrentDictionary(); 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(ConcurrentDictionary 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 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 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(); } directoryData = Directory.GetFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).ToArray(); dirCache[path] = directoryData; return directoryData; } finally { fileLock.Release(); } } public async static Task 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(); } } }