Framework_YooAsset_HybridCLR/Assets/Plugins/TouchSocket/TouchSocket.Http/Common/FileCachePool.cs

439 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//------------------------------------------------------------------------------
// 此代码版权除特别声明或在XREF结尾的命名空间的代码归作者本人若汝棋茗所有
// 源代码使用协议遵循本仓库的开源协议及附加协议若本仓库没有设置则按MIT开源协议授权
// CSDN博客https://blog.csdn.net/qq_40374647
// 哔哩哔哩视频https://space.bilibili.com/94253567
// Gitee源代码仓库https://gitee.com/RRQM_Home
// Github源代码仓库https://github.com/RRQM
// API首页https://touchsocket.net/
// 交流QQ群234762506
// 感谢您的下载和使用
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Web;
using Codice.Utils;
using TouchSocket.Core;
namespace TouchSocket.Http
{
/// <summary>
/// 静态文件缓存池
/// </summary>
public class FileCachePool : DisposableObject
{
/// <summary>
/// 添加委托
/// </summary>
/// <param name="cache"></param>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="millisecondsTimeout"></param>
/// <returns></returns>
public delegate bool InsertHandler(FileCachePool cache, string key, byte[] value, TimeSpan millisecondsTimeout);
#region Cache items access
/// <summary>
/// Is the file cache empty?
/// </summary>
public bool Empty => this.entriesByKey.Count == 0;
/// <summary>
/// Get the file cache size
/// </summary>
public int Size => this.entriesByKey.Count;
/// <summary>
/// Add a new cache value with the given millisecondsTimeout into the file cache
/// </summary>
/// <param name="key">Key to add</param>
/// <param name="value">Value to add</param>
/// <param name="millisecondsTimeout">Cache millisecondsTimeout (default is 0 - no millisecondsTimeout)</param>
/// <returns>'true' if the cache value was added, 'false' if the given key was not added</returns>
public bool Add(string key, byte[] value, TimeSpan millisecondsTimeout = new TimeSpan())
{
using (new WriteLock(this.lockEx))
{
// Try to find and remove the previous key
this.entriesByKey.Remove(key);
// Update the cache entry
this.entriesByKey.Add(key, new MemCacheEntry(value, millisecondsTimeout));
return true;
}
}
/// <summary>
/// Try to find the cache value by the given key
/// </summary>
/// <param name="key">Key to find</param>
/// <param name="data"></param>
/// <returns>'true' and cache value if the cache value was found, 'false' if the given key was not found</returns>
public bool Find(string key, out byte[] data)
{
using (new ReadLock(this.lockEx))
{
// Try to find the given key
if (!this.entriesByKey.TryGetValue(key, out var cacheValue))
{
data = null;
return false;
}
data = cacheValue.Value;
return true;
}
}
/// <summary>
/// Remove the cache value with the given key from the file cache
/// </summary>
/// <param name="key">Key to remove</param>
/// <returns>'true' if the cache value was removed, 'false' if the given key was not found</returns>
public bool Remove(string key)
{
using (new WriteLock(this.lockEx))
{
return this.entriesByKey.Remove(key);
}
}
#endregion Cache items access
#region Cache management methods
/// <summary>
/// Insert a new cache path with the given millisecondsTimeout into the file cache
/// </summary>
/// <param name="path">Path to insert</param>
/// <param name="prefix">Cache prefix (default is "/")</param>
/// <param name="filter">Cache filter (default is "*.*")</param>
/// <param name="millisecondsTimeout">Cache millisecondsTimeout (default is 0 - no millisecondsTimeout)</param>
/// <param name="handler">Cache insert handler (default is 'return cache.Add(key, value, millisecondsTimeout)')</param>
/// <returns>'true' if the cache path was setup, 'false' if failed to setup the cache path</returns>
public bool InsertPath(string path, string prefix = "/", string filter = "*.*", TimeSpan millisecondsTimeout = new TimeSpan(), InsertHandler handler = null)
{
handler ??= (FileCachePool cache, string key, byte[] value, TimeSpan timespan) => cache.Add(key, value, timespan);
// Try to find and remove the previous path
this.RemovePathInternal(path);
using (new WriteLock(this.lockEx))
{
// Add the given path to the cache
this.pathsByKey.Add(path, new FileCacheEntry(this, prefix, path, filter, handler, millisecondsTimeout));
// Create entries by path map
this.entriesByPath[path] = new HashSet<string>();
}
// Insert the cache path
return this.InsertPathInternal(path, path, prefix, millisecondsTimeout, handler);
}
/// <summary>
/// Try to find the cache path
/// </summary>
/// <param name="path">Path to find</param>
/// <returns>'true' if the cache path was found, 'false' if the given path was not found</returns>
public bool FindPath(string path)
{
using (new ReadLock(this.lockEx))
{
// Try to find the given key
return this.pathsByKey.ContainsKey(path);
}
}
/// <summary>
/// Remove the cache path from the file cache
/// </summary>
/// <param name="path">Path to remove</param>
/// <returns>'true' if the cache path was removed, 'false' if the given path was not found</returns>
public bool RemovePath(string path)
{
return this.RemovePathInternal(path);
}
/// <summary>
/// Clear the memory cache
/// </summary>
public void Clear()
{
using (new WriteLock(this.lockEx))
{
// Stop all file system watchers
foreach (var fileCacheEntry in this.pathsByKey)
fileCacheEntry.Value.StopWatcher();
// Clear all cache entries
this.entriesByKey.Clear();
this.entriesByPath.Clear();
this.pathsByKey.Clear();
}
}
#endregion Cache management methods
#region Cache implementation
private readonly ReaderWriterLockSlim lockEx = new ReaderWriterLockSlim();
private readonly Dictionary<string, MemCacheEntry> entriesByKey = new Dictionary<string, MemCacheEntry>();
private readonly Dictionary<string, HashSet<string>> entriesByPath = new Dictionary<string, HashSet<string>>();
private readonly Dictionary<string, FileCacheEntry> pathsByKey = new Dictionary<string, FileCacheEntry>();
private class MemCacheEntry
{
public byte[] Value { get; }
public TimeSpan Timespan { get; private set; }
public MemCacheEntry(byte[] value, TimeSpan timespan = new TimeSpan())
{
this.Value = value;
this.Timespan = timespan;
}
public MemCacheEntry(string value, TimeSpan timespan = new TimeSpan())
{
this.Value = Encoding.UTF8.GetBytes(value);
this.Timespan = timespan;
}
};
private class FileCacheEntry
{
private readonly string _prefix;
private readonly string _path;
private readonly InsertHandler _handler;
private readonly TimeSpan _timespan;
private readonly FileSystemWatcher _watcher;
public FileCacheEntry(FileCachePool cache, string prefix, string path, string filter, InsertHandler handler, TimeSpan timespan)
{
this._prefix = prefix;
this._path = path;
this._handler = handler;
this._timespan = timespan;
this._watcher = new FileSystemWatcher();
// Start the filesystem watcher
this.StartWatcher(cache, path, filter);
}
private void StartWatcher(FileCachePool cache, string path, string filter)
{
var entry = this;
// Initialize a new filesystem watcher
this._watcher.Created += (sender, e) => OnCreated(sender, e, cache, entry);
this._watcher.Changed += (sender, e) => OnChanged(sender, e, cache, entry);
this._watcher.Deleted += (sender, e) => OnDeleted(sender, e, cache, entry);
this._watcher.Renamed += (sender, e) => OnRenamed(sender, e, cache, entry);
this._watcher.Path = path;
this._watcher.IncludeSubdirectories = true;
this._watcher.Filter = filter;
this._watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
this._watcher.EnableRaisingEvents = true;
}
public void StopWatcher()
{
this._watcher.Dispose();
}
private static bool IsDirectory(string path)
{
try
{
// Skip directory updates
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
return true;
}
catch (Exception) { }
return false;
}
private static void OnCreated(object sender, FileSystemEventArgs e, FileCachePool cache, FileCacheEntry entry)
{
var key = e.FullPath.Replace(entry._path, entry._prefix);
var file = e.FullPath;
// Skip missing files
if (!File.Exists(file))
return;
// Skip directory updates
if (IsDirectory(file))
return;
cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler);
}
private static void OnChanged(object sender, FileSystemEventArgs e, FileCachePool cache, FileCacheEntry entry)
{
if (e.ChangeType != WatcherChangeTypes.Changed)
return;
var key = e.FullPath.Replace(entry._path, entry._prefix);
var file = e.FullPath;
// Skip missing files
if (!File.Exists(file))
return;
// Skip directory updates
if (IsDirectory(file))
return;
cache.InsertFileInternal(entry._path, file, key, entry._timespan, entry._handler);
}
private static void OnDeleted(object sender, FileSystemEventArgs e, FileCachePool cache, FileCacheEntry entry)
{
var key = e.FullPath.Replace(entry._path, entry._prefix);
var file = e.FullPath;
cache.RemoveFileInternal(entry._path, key);
}
private static void OnRenamed(object sender, RenamedEventArgs e, FileCachePool cache, FileCacheEntry entry)
{
var oldKey = e.OldFullPath.Replace(entry._path, entry._prefix);
var oldFile = e.OldFullPath;
var newKey = e.FullPath.Replace(entry._path, entry._prefix);
var newFile = e.FullPath;
// Skip missing files
if (!File.Exists(newFile))
return;
// Skip directory updates
if (IsDirectory(newFile))
return;
cache.RemoveFileInternal(entry._path, oldKey);
cache.InsertFileInternal(entry._path, newFile, newKey, entry._timespan, entry._handler);
}
};
private bool InsertFileInternal(string path, string file, string key, TimeSpan millisecondsTimeout, InsertHandler handler)
{
try
{
key = key.Replace('\\', '/');
file = file.Replace('\\', '/');
// Load the cache file content
var content = File.ReadAllBytes(file);
if (!handler(this, key, content, millisecondsTimeout))
return false;
using (new WriteLock(this.lockEx))
{
// Update entries by path map
this.entriesByPath[path].Add(key);
}
return true;
}
catch (Exception) { return false; }
}
private bool RemoveFileInternal(string path, string key)
{
try
{
key = key.Replace('\\', '/');
using (new WriteLock(this.lockEx))
{
// Update entries by path map
this.entriesByPath[path].Remove(key);
}
return this.Remove(key);
}
catch (Exception) { return false; }
}
private bool InsertPathInternal(string root, string path, string prefix, TimeSpan millisecondsTimeout, InsertHandler handler)
{
try
{
var keyPrefix = (string.IsNullOrEmpty(prefix) || (prefix == "/")) ? "/" : (prefix + "/");
// Iterate through all directory entries
foreach (var item in Directory.GetDirectories(path))
{
var key = keyPrefix + HttpUtility.UrlDecode(Path.GetFileName(item));
// Recursively insert sub-directory
if (!this.InsertPathInternal(root, item, key, millisecondsTimeout, handler))
return false;
}
foreach (var item in Directory.GetFiles(path))
{
var key = keyPrefix + HttpUtility.UrlDecode(Path.GetFileName(item));
// Insert file into the cache
if (!this.InsertFileInternal(root, item, key, millisecondsTimeout, handler))
return false;
}
return true;
}
catch (Exception) { return false; }
}
private bool RemovePathInternal(string path)
{
using (new WriteLock(this.lockEx))
{
// Try to find the given path
if (!this.pathsByKey.TryGetValue(path, out var cacheValue))
return false;
// Stop the file system watcher
cacheValue.StopWatcher();
// Remove path entries
foreach (var entryKey in this.entriesByPath[path])
this.entriesByKey.Remove(entryKey);
this.entriesByPath.Remove(path);
// Remove cache path
this.pathsByKey.Remove(path);
return true;
}
}
#endregion Cache implementation
#region IDisposable implementation
/// <summary>
/// 释放
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
this.Clear();
base.Dispose(disposing);
}
/// <summary>
/// 析构函数
/// </summary>
~FileCachePool()
{
// Simply call Dispose(false).
this.Dispose(false);
}
#endregion IDisposable implementation
}
}