428 lines
No EOL
12 KiB
C#
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();
|
|
}
|
|
}
|
|
} |