2021-09-27 12:42:46 +00:00
|
|
|
using System.Data.SQLite;
|
|
|
|
using System.IO;
|
|
|
|
using System.Threading;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using Wabbajack.Hashing.xxHash64;
|
|
|
|
using Wabbajack.Paths;
|
|
|
|
using Wabbajack.Paths.IO;
|
2021-10-12 12:20:34 +00:00
|
|
|
using Wabbajack.RateLimiter;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
namespace Wabbajack.VFS;
|
|
|
|
|
|
|
|
public class FileHashCache
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
private readonly SQLiteConnection _conn;
|
|
|
|
private readonly string _connectionString;
|
|
|
|
private readonly IResource<FileHashCache> _limiter;
|
|
|
|
private readonly AbsolutePath _location;
|
|
|
|
|
|
|
|
public FileHashCache(AbsolutePath location, IResource<FileHashCache> limiter)
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
_limiter = limiter;
|
|
|
|
_location = location;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
if (!_location.Parent.DirectoryExists())
|
|
|
|
_location.Parent.CreateDirectory();
|
|
|
|
|
|
|
|
_connectionString =
|
|
|
|
string.Intern($"URI=file:{_location};Pooling=True;Max Pool Size=100; Journal Mode=Memory;");
|
|
|
|
_conn = new SQLiteConnection(_connectionString);
|
|
|
|
_conn.Open();
|
|
|
|
|
|
|
|
|
|
|
|
using var cmd = new SQLiteCommand(_conn);
|
|
|
|
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS HashCache (
|
2021-09-27 12:42:46 +00:00
|
|
|
Path TEXT PRIMARY KEY,
|
|
|
|
LastModified BIGINT,
|
|
|
|
Hash BIGINT)
|
|
|
|
WITHOUT ROWID";
|
2021-10-23 16:51:17 +00:00
|
|
|
cmd.ExecuteNonQuery();
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
private async Task<(AbsolutePath Path, long LastModified, Hash Hash)> Get(AbsolutePath path)
|
2021-10-23 16:51:17 +00:00
|
|
|
{
|
|
|
|
using var cmd = new SQLiteCommand(_conn);
|
|
|
|
cmd.CommandText = "SELECT LastModified, Hash FROM HashCache WHERE Path = @path";
|
2022-08-22 15:34:19 +00:00
|
|
|
cmd.Parameters.AddWithValue("@path", path.ToString().ToLowerInvariant());
|
2022-10-01 05:21:58 +00:00
|
|
|
await cmd.PrepareAsync();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
await using var reader = await cmd.ExecuteReaderAsync();
|
|
|
|
while (await reader.ReadAsync()) return (path, reader.GetInt64(0), Hash.FromLong(reader.GetInt64(1)));
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
return default;
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public void Purge(AbsolutePath path)
|
|
|
|
{
|
|
|
|
using var cmd = new SQLiteCommand(_conn);
|
|
|
|
cmd.CommandText = "DELETE FROM HashCache WHERE Path = @path";
|
2022-08-22 15:34:19 +00:00
|
|
|
cmd.Parameters.AddWithValue("@path", path.ToString().ToLowerInvariant());
|
2021-10-23 16:51:17 +00:00
|
|
|
cmd.PrepareAsync();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
cmd.ExecuteNonQuery();
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
private async Task Upsert(AbsolutePath path, long lastModified, Hash hash)
|
2021-10-23 16:51:17 +00:00
|
|
|
{
|
2022-10-01 05:21:58 +00:00
|
|
|
await using var cmd = new SQLiteCommand(_conn);
|
2021-10-23 16:51:17 +00:00
|
|
|
cmd.CommandText = @"INSERT INTO HashCache (Path, LastModified, Hash) VALUES (@path, @lastModified, @hash)
|
2021-09-27 12:42:46 +00:00
|
|
|
ON CONFLICT(Path) DO UPDATE SET LastModified = @lastModified, Hash = @hash";
|
2022-08-22 15:34:19 +00:00
|
|
|
cmd.Parameters.AddWithValue("@path", path.ToString().ToLowerInvariant());
|
2021-10-23 16:51:17 +00:00
|
|
|
cmd.Parameters.AddWithValue("@lastModified", lastModified);
|
|
|
|
cmd.Parameters.AddWithValue("@hash", (long) hash);
|
2022-10-01 05:21:58 +00:00
|
|
|
await cmd.PrepareAsync();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
await cmd.ExecuteNonQueryAsync();
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
public void VacuumDatabase()
|
|
|
|
{
|
|
|
|
using var cmd = new SQLiteCommand(_conn);
|
|
|
|
cmd.CommandText = @"VACUUM";
|
|
|
|
cmd.PrepareAsync();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
cmd.ExecuteNonQuery();
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
public async Task<Hash> TryGetHashCache(AbsolutePath file)
|
2021-10-23 16:51:17 +00:00
|
|
|
{
|
2022-10-01 05:21:58 +00:00
|
|
|
if (!file.FileExists()) return default;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
var result = await Get(file);
|
2022-06-08 03:48:13 +00:00
|
|
|
if (result == default || result.Hash == default)
|
2022-10-01 05:21:58 +00:00
|
|
|
return default;
|
2022-10-30 13:16:12 +00:00
|
|
|
|
|
|
|
// Fix for strange issue where dates are messed up on some systems
|
|
|
|
if (file.LastModifiedUtc() < file.CreatedUtc())
|
|
|
|
file.Touch();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
if (result.LastModified == file.LastModifiedUtc().ToFileTimeUtc())
|
2021-09-27 12:42:46 +00:00
|
|
|
{
|
2022-10-01 05:21:58 +00:00
|
|
|
return result.Hash;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
Purge(file);
|
2022-10-01 05:21:58 +00:00
|
|
|
return default;
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
private async Task WriteHashCache(AbsolutePath file, Hash hash)
|
2021-10-23 16:51:17 +00:00
|
|
|
{
|
|
|
|
if (!file.FileExists()) return;
|
2022-10-01 05:21:58 +00:00
|
|
|
await Upsert(file, file.LastModifiedUtc().ToFileTimeUtc(), hash);
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
public async Task FileHashWriteCache(AbsolutePath file, Hash hash)
|
2021-10-23 16:51:17 +00:00
|
|
|
{
|
2022-10-01 05:21:58 +00:00
|
|
|
await WriteHashCache(file, hash);
|
2021-10-23 16:51:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public async Task<Hash> FileHashCachedAsync(AbsolutePath file, CancellationToken token)
|
|
|
|
{
|
2022-10-01 05:21:58 +00:00
|
|
|
var hash = await TryGetHashCache(file);
|
|
|
|
if (hash != default) return hash;
|
2021-10-23 16:51:17 +00:00
|
|
|
|
2022-01-06 04:12:40 +00:00
|
|
|
using var job = await _limiter.Begin($"Hashing {file.FileName}", file.Size(), token);
|
2021-10-23 16:51:17 +00:00
|
|
|
await using var fs = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
|
|
|
2022-10-01 05:21:58 +00:00
|
|
|
hash = await fs.HashingCopy(Stream.Null, token, job);
|
2021-10-23 16:51:17 +00:00
|
|
|
if (hash != default)
|
2022-10-01 05:21:58 +00:00
|
|
|
await WriteHashCache(file, hash);
|
2021-10-23 16:51:17 +00:00
|
|
|
return hash;
|
2021-09-27 12:42:46 +00:00
|
|
|
}
|
|
|
|
}
|