初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class HashCache
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
|
||||
public HashCache(string dbPath)
|
||||
{
|
||||
_dbPath = dbPath ?? throw new ArgumentNullException(nameof(dbPath));
|
||||
var dir = Path.GetDirectoryName(Path.GetFullPath(_dbPath));
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
_Initialize();
|
||||
}
|
||||
|
||||
private void _Initialize()
|
||||
{
|
||||
using var connection = _CreateConnection();
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
CREATE TABLE IF NOT EXISTS HashCache (
|
||||
FilePath TEXT NOT NULL PRIMARY KEY,
|
||||
FileSize INTEGER NOT NULL,
|
||||
LastWriteTime TEXT NOT NULL,
|
||||
MD5 TEXT NULL,
|
||||
SHA1 TEXT NULL,
|
||||
SHA256 TEXT NULL,
|
||||
SHA512 TEXT NULL,
|
||||
MurmurHash2 TEXT NULL
|
||||
)
|
||||
""";
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
using var setCmd = connection.CreateCommand();
|
||||
setCmd.CommandText = "PRAGMA journal_mode=WAL";
|
||||
setCmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private SqliteConnection _CreateConnection()
|
||||
{
|
||||
var connection = new SqliteConnection($"Data Source={_dbPath};Pooling=True");
|
||||
connection.Open();
|
||||
return connection;
|
||||
}
|
||||
|
||||
public Task<string> GetMD5Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, MD5Provider.Instance, "MD5");
|
||||
|
||||
public Task<string> GetSHA1Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, SHA1Provider.Instance, "SHA1");
|
||||
|
||||
public Task<string> GetSHA256Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, SHA256Provider.Instance, "SHA256");
|
||||
|
||||
public Task<string> GetSHA512Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, SHA512Provider.Instance, "SHA512");
|
||||
|
||||
public Task<string> GetMurmurHash2Async(string filePath) =>
|
||||
_GetHashWithPending(filePath, MurmurHash2Provider.Instance, "MurmurHash2");
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Task<string>> _FileHashComputePending = new();
|
||||
|
||||
public async Task<string> GetHashAsync(string filePath, IHashProvider provider)
|
||||
{
|
||||
var algoName = provider switch
|
||||
{
|
||||
MD5Provider => "MD5",
|
||||
SHA1Provider => "SHA1",
|
||||
SHA256Provider => "SHA256",
|
||||
SHA512Provider => "SHA512",
|
||||
MurmurHash2Provider => "MurmurHash2",
|
||||
_ => throw new ArgumentException($"不支持的哈希算法: {provider.GetType().Name}")
|
||||
};
|
||||
var computeKey = $"{filePath}:{algoName}";
|
||||
return await _GetHashWithPending(filePath, provider, algoName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task<string> _GetHashWithPending(string filePath, IHashProvider provider, string algoName)
|
||||
{
|
||||
var computeKey = $"{filePath}:{algoName}";
|
||||
return _FileHashComputePending.GetOrAdd(computeKey, key =>
|
||||
{
|
||||
var computeTask = _GetHashAsync(filePath, provider, algoName);
|
||||
_ = computeTask.ContinueWith(t =>
|
||||
{
|
||||
_FileHashComputePending.TryRemove(computeKey, out _);
|
||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||
return computeTask;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<string> _GetHashAsync(string filePath, IHashProvider provider, string algoName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
|
||||
var fullPath = Path.GetFullPath(filePath);
|
||||
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
var fileSize = fileInfo.Length;
|
||||
var lastWrite = fileInfo.LastWriteTimeUtc.ToString("O");
|
||||
|
||||
var cached = await _FindCacheEntryAsync(fullPath).ConfigureAwait(false);
|
||||
|
||||
if (cached != null)
|
||||
{
|
||||
if (cached.FileSize == fileSize && cached.LastWriteTime == lastWrite)
|
||||
{
|
||||
var hash = _GetHashFromEntry(cached, algoName);
|
||||
if (hash != null)
|
||||
return hash;
|
||||
|
||||
var computedHash = await _ComputeHashAsync(fullPath, provider).ConfigureAwait(false);
|
||||
await _InsertOrUpdateHashAsync(fullPath, fileSize, lastWrite, algoName, computedHash).ConfigureAwait(false);
|
||||
return computedHash;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _DeleteCacheEntryAsync(fullPath).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var computed = await _ComputeHashAsync(fullPath, provider).ConfigureAwait(false);
|
||||
await _InsertOrUpdateHashAsync(fullPath, fileSize, lastWrite, algoName, computed).ConfigureAwait(false);
|
||||
return computed;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
await _DeleteCacheEntryAsync(fullPath).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> _ComputeHashAsync(string fullPath, IHashProvider provider)
|
||||
{
|
||||
using FileStream fs = new(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return (await provider.ComputeHashAsync(fs).ConfigureAwait(false)).ToHexString();
|
||||
}
|
||||
|
||||
private async Task<CacheEntry?> _FindCacheEntryAsync(string fullPath)
|
||||
{
|
||||
using var conn = _CreateConnection();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT FilePath, FileSize, LastWriteTime, MD5, SHA1, SHA256, SHA512, MurmurHash2 FROM HashCache WHERE FilePath = @FilePath";
|
||||
cmd.Parameters.AddWithValue("@FilePath", fullPath);
|
||||
|
||||
using var reader = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync().ConfigureAwait(false))
|
||||
return null;
|
||||
|
||||
return new CacheEntry
|
||||
{
|
||||
FilePath = reader.GetString(0),
|
||||
FileSize = reader.GetInt64(1),
|
||||
LastWriteTime = reader.GetString(2),
|
||||
MD5 = reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||
SHA1 = reader.IsDBNull(4) ? null : reader.GetString(4),
|
||||
SHA256 = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||
SHA512 = reader.IsDBNull(6) ? null : reader.GetString(6),
|
||||
MurmurHash2 = reader.IsDBNull(7) ? null : reader.GetString(7)
|
||||
};
|
||||
}
|
||||
|
||||
private static string? _GetHashFromEntry(CacheEntry entry, string algoName) => algoName switch
|
||||
{
|
||||
"MD5" => entry.MD5,
|
||||
"SHA1" => entry.SHA1,
|
||||
"SHA256" => entry.SHA256,
|
||||
"SHA512" => entry.SHA512,
|
||||
"MurmurHash2" => entry.MurmurHash2,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private async Task _InsertOrUpdateHashAsync(string fullPath, long fileSize, string lastWrite, string algoName, string hash)
|
||||
{
|
||||
if (hash.IsNullOrWhiteSpace()) return;
|
||||
using var conn = _CreateConnection();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
INSERT INTO HashCache (FilePath, FileSize, LastWriteTime, MD5, SHA1, SHA256, SHA512, MurmurHash2)
|
||||
VALUES (@FilePath, @FileSize, @LastWriteTime, @MD5, @SHA1, @SHA256, @SHA512, @MurmurHash2)
|
||||
ON CONFLICT(FilePath) DO UPDATE SET
|
||||
FileSize = excluded.FileSize,
|
||||
LastWriteTime = excluded.LastWriteTime,
|
||||
MD5 = COALESCE(excluded.MD5, HashCache.MD5),
|
||||
SHA1 = COALESCE(excluded.SHA1, HashCache.SHA1),
|
||||
SHA256 = COALESCE(excluded.SHA256, HashCache.SHA256),
|
||||
SHA512 = COALESCE(excluded.SHA512, HashCache.SHA512),
|
||||
MurmurHash2 = COALESCE(excluded.MurmurHash2, HashCache.MurmurHash2)
|
||||
""";
|
||||
cmd.Parameters.AddWithValue("@FilePath", fullPath);
|
||||
cmd.Parameters.AddWithValue("@FileSize", fileSize);
|
||||
cmd.Parameters.AddWithValue("@LastWriteTime", lastWrite);
|
||||
cmd.Parameters.AddWithValue("@MD5", algoName == "MD5" ? hash : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@SHA1", algoName == "SHA1" ? hash : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@SHA256", algoName == "SHA256" ? hash : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@SHA512", algoName == "SHA512" ? hash : DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("@MurmurHash2", algoName == "MurmurHash2" ? hash : DBNull.Value);
|
||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task _DeleteCacheEntryAsync(string fullPath)
|
||||
{
|
||||
using var conn = _CreateConnection();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM HashCache WHERE FilePath = @FilePath";
|
||||
cmd.Parameters.AddWithValue("@FilePath", fullPath);
|
||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private sealed class CacheEntry
|
||||
{
|
||||
public string FilePath { get; init; } = "";
|
||||
public long FileSize { get; init; }
|
||||
public string LastWriteTime { get; init; } = "";
|
||||
public string? MD5 { get; init; }
|
||||
public string? SHA1 { get; init; }
|
||||
public string? SHA256 { get; init; }
|
||||
public string? SHA512 { get; init; }
|
||||
public string? MurmurHash2 { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public interface IHashProvider
|
||||
{
|
||||
ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default);
|
||||
byte[] ComputeHash(Stream input);
|
||||
byte[] ComputeHash(ReadOnlySpan<byte> input);
|
||||
byte[] ComputeHash(string input, Encoding? en = null);
|
||||
int Length { get; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class MD5Provider : IHashProvider {
|
||||
public static MD5Provider Instance { get; } = new();
|
||||
public int Length => 32;
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> input)
|
||||
{
|
||||
return MD5.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? en = null)
|
||||
{
|
||||
return ComputeHash(en is null ? Encoding.UTF8.GetBytes(input) : en.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
return MD5.HashData(input);
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await MD5.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class MurmurHash2Provider : IHashProvider
|
||||
{
|
||||
public static MurmurHash2Provider Instance { get; } = new();
|
||||
public int Length => 8;
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var filtered = new List<byte>(data.Length);
|
||||
foreach (var b in data)
|
||||
if (b != 9 && b != 10 && b != 13 && b != 32)
|
||||
filtered.Add(b);
|
||||
return _ComputeHash(CollectionsMarshal.AsSpan(filtered));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
return ComputeHash(encoding.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
input.CopyTo(ms);
|
||||
return ComputeHash(ms.GetBuffer().AsSpan(0, (int)ms.Length));
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await input.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||
return ComputeHash(ms.GetBuffer().AsSpan(0, (int)ms.Length));
|
||||
}
|
||||
|
||||
private static byte[] _ComputeHash(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var h = (uint)(1 ^ data.Length);
|
||||
var i = 0;
|
||||
var loopTo = data.Length - 4;
|
||||
|
||||
for (; i <= loopTo; i += 4)
|
||||
{
|
||||
var k = data[i]
|
||||
| ((uint)data[i + 1] << 8)
|
||||
| ((uint)data[i + 2] << 16)
|
||||
| ((uint)data[i + 3] << 24);
|
||||
k *= 0x5BD1E995;
|
||||
k ^= k >> 24;
|
||||
k *= 0x5BD1E995;
|
||||
h *= 0x5BD1E995;
|
||||
h ^= k;
|
||||
}
|
||||
|
||||
switch (data.Length - i)
|
||||
{
|
||||
case 3:
|
||||
h ^= (uint)(data[i] | ((uint)data[i + 1] << 8));
|
||||
h ^= (uint)data[i + 2] << 16;
|
||||
h *= 0x5BD1E995;
|
||||
break;
|
||||
case 2:
|
||||
h ^= (uint)(data[i] | ((uint)data[i + 1] << 8));
|
||||
h *= 0x5BD1E995;
|
||||
break;
|
||||
case 1:
|
||||
h ^= data[i];
|
||||
h *= 0x5BD1E995;
|
||||
break;
|
||||
}
|
||||
|
||||
h ^= h >> 13;
|
||||
h *= 0x5BD1E995;
|
||||
h ^= h >> 15;
|
||||
|
||||
return BitConverter.GetBytes(h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class SHA1Provider : IHashProvider
|
||||
{
|
||||
public static SHA1Provider Instance { get; } = new();
|
||||
public int Length => 40;
|
||||
|
||||
public byte[] ComputeHash(byte[] input)
|
||||
{
|
||||
return SHA1.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> input)
|
||||
{
|
||||
return SHA1.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
return ComputeHash(encoding.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
return SHA1.HashData(input);
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await SHA1.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class SHA256Provider : IHashProvider
|
||||
{
|
||||
public static SHA256Provider Instance { get; } = new();
|
||||
public int Length => 64;
|
||||
|
||||
public byte[] ComputeHash(byte[] input)
|
||||
{
|
||||
return SHA256.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> input)
|
||||
{
|
||||
return SHA256.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
return ComputeHash(encoding.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
return SHA256.HashData(input);
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await SHA256.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Utils.Hash;
|
||||
|
||||
public class SHA512Provider : IHashProvider
|
||||
{
|
||||
public static SHA512Provider Instance { get; } = new();
|
||||
public int Length => 128;
|
||||
|
||||
public byte[] ComputeHash(byte[] input)
|
||||
{
|
||||
return SHA512.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> input)
|
||||
{
|
||||
return SHA512.HashData(input);
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(string input, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
return ComputeHash(encoding.GetBytes(input));
|
||||
}
|
||||
|
||||
public byte[] ComputeHash(Stream input)
|
||||
{
|
||||
return SHA512.HashData(input);
|
||||
}
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream input, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await SHA512.HashDataAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user