If filename can extract to a NcaMetadata entry, don't use nspextractor to pull information
Scan directories sequentially to reduce memory footprint
This commit is contained in:
@@ -79,7 +79,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
FileSystemExtensions.EnsureDirectoryExists(Path.GetFullPath(Path.GetDirectoryName(_snapshotPath) ?? throw new InvalidOperationException()));
|
||||
|
||||
// 1️⃣ Register for *property* changes
|
||||
options.OnChange(snapshotOptions =>
|
||||
options.OnChange((snapshotOptions, arg) =>
|
||||
{
|
||||
_options.RootDirectories = snapshotOptions.RootDirectories;
|
||||
});
|
||||
@@ -256,61 +256,40 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
var index = LoadSnapshotIndex();
|
||||
var latestModifiedUtcParallel = FileSystemExtensions.GetLatestModifiedUtcParallel(_options.RootDirectories);
|
||||
var fileInfo = new FileInfo(_snapshotPath);
|
||||
bool snapshotVerified = true;
|
||||
bool snapshotVerified = fileInfo.Exists;
|
||||
if (latestModifiedUtcParallel.HasValue && latestModifiedUtcParallel.Value < fileInfo.LastWriteTimeUtc)
|
||||
{
|
||||
if (index.Count != 0)
|
||||
{
|
||||
// directory may have been added with older roms, verify that the snapshot is still up to date
|
||||
foreach (var dir in _options.RootDirectories)
|
||||
{
|
||||
// check first entry is in index
|
||||
var entry = BuildSnapshot(dir).FirstOrDefault();
|
||||
if (entry != null)
|
||||
var firstEntry = BuildSnapshot(dir).FirstOrDefault();
|
||||
if (firstEntry != null && !index.TryGetValue(firstEntry.Path, out _))
|
||||
{
|
||||
if (!index.TryGetValue(entry.Path, out var cached))
|
||||
{
|
||||
snapshotVerified = false;
|
||||
_logger.LogInformation("Snapshot does not contain first entry in directory {Directory}", dir);
|
||||
}
|
||||
snapshotVerified = false;
|
||||
_logger.LogInformation("Snapshot does not contain first entry in directory {Directory}", dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshotVerified)
|
||||
if (!snapshotVerified)
|
||||
{
|
||||
_logger.LogInformation("Rebuilding snapshot (root dirs: {Count})", _options.RootDirectories.Count);
|
||||
var entries = new List<FileEntry>();
|
||||
foreach (var dir in _options.RootDirectories)
|
||||
{
|
||||
foreach (var entry in BuildSnapshot(dir))
|
||||
{
|
||||
_logger.LogInformation("Snapshot is up to date");
|
||||
return Task.CompletedTask;
|
||||
if (entry != null) entries.Add(entry);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Snapshot is up to date but index is empty");
|
||||
}
|
||||
}
|
||||
_logger.LogInformation("Rebuilding snapshot (root dirs: {Count})", _options.RootDirectories.Count);
|
||||
var entries = new List<FileEntry>();
|
||||
|
||||
var snapshotChanged = false;
|
||||
foreach (var dir in _options.RootDirectories)
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
_logger.LogInformation("Rebuilding directory {Directory}", dir);
|
||||
var buildSnapshot = BuildSnapshot(dir);
|
||||
var fileEntries = buildSnapshot.ToList();
|
||||
snapshotChanged = snapshotChanged || fileEntries.Count != 0;
|
||||
entries.AddRange(fileEntries.Where(entry => entry != null)!);
|
||||
});
|
||||
var currentHash = ComputeSnapshotHash(entries);
|
||||
if (entries.Count > 0 || fileInfo.Exists && index.Count == 0)
|
||||
SnapshotRebuilt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
var snapshotEmptied = fileInfo.Exists && index.Count == 0 && _options.RootDirectories.Count == 0;
|
||||
// Replace the entire snapshot
|
||||
var currentSnapshotHash = ComputeSnapshotHash(entries);
|
||||
if (snapshotChanged || snapshotEmptied)
|
||||
{
|
||||
_logger.LogInformation("Snapshot rebuilt");
|
||||
SnapshotRebuilt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
PersistSnapshotAsync();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -341,6 +320,15 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
var titles = new List<(string, long, NcaMetadataWithHash)>();
|
||||
if (_options.RomExtensions.Contains(ext))
|
||||
{
|
||||
var fileInfo = new FileInfo(file);
|
||||
var ncaMetadataWithHash = fileInfo.GetNcaMetadataWithHash();
|
||||
if (ncaMetadataWithHash != null)
|
||||
{
|
||||
//var titleInfo = _titleDatabaseService.GetAsync(ncaMetadataWithHash.TitleId).Result;
|
||||
var fileEntryFromFileName = new FileEntry(file, fileInfo.Length, ncaMetadataWithHash.Hash, [ncaMetadataWithHash]);
|
||||
AddToSnapshotAsync(fileEntryFromFileName);
|
||||
yield return fileEntryFromFileName;
|
||||
}
|
||||
using var nspStream = File.OpenRead(file);
|
||||
hash = ComputeFirstStreamHash(nspStream);
|
||||
|
||||
@@ -416,74 +404,71 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private string ComputeFirstStreamHash(Stream nspStream)
|
||||
{
|
||||
return _nspExtractor.ExtractHashFromStream(nspStream);
|
||||
}
|
||||
private string ComputeFirstStreamHash(Stream nspStream) => _nspExtractor.ExtractHashFromStream(nspStream);
|
||||
|
||||
private void UpdateSnapshot() => BuildSnapshotAsync();
|
||||
|
||||
IEnumerable<FileEntry> GetEntries()
|
||||
private IEnumerable<FileEntry> GetEntries()
|
||||
{
|
||||
foreach (var snapshotEntry in _cache)
|
||||
{
|
||||
_sizeLookup.TryGetValue(snapshotEntry.Value.Hash, out var size);
|
||||
var fileEntry = new FileEntry(snapshotEntry.Key, snapshotEntry.Value.Size, snapshotEntry.Value.Hash, snapshotEntry.Value.NcaMetadataWithHash);
|
||||
yield return fileEntry;
|
||||
}
|
||||
foreach (var kv in _cache)
|
||||
yield return new FileEntry(kv.Key, kv.Value.Size, kv.Value.Hash, kv.Value.NcaMetadataWithHash);
|
||||
}
|
||||
|
||||
private Task PersistSnapshotAsync()
|
||||
{
|
||||
if (_debouncerCache.TryGetValue(_jsonPath, out var value))
|
||||
if (_debouncerCache.TryGetValue(_jsonPath, out _))
|
||||
{
|
||||
_logger.LogInformation("Sliding debounce in progress, skipping snapshot persistence");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
var entries = GetEntries().ToList();
|
||||
var newHash = ComputeSnapshotHash(entries);
|
||||
var snapshot = GetSnapshot();
|
||||
var entries = GetEntries();
|
||||
var fileEntries = entries.ToList();
|
||||
var newHash = ComputeSnapshotHash(fileEntries);
|
||||
if (snapshot.Hash == newHash) return Task.CompletedTask;
|
||||
|
||||
CancellationTokenSource cts = new();
|
||||
_logger.LogInformation("Snapshot hash changed – persisting new snapshot");
|
||||
using var debouncedPersistence = _debouncerCache.CreateEntry(_jsonPath);
|
||||
debouncedPersistence.AddExpirationToken(new CancellationChangeToken(cts.Token));
|
||||
//debouncedPersistence.AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(DebounceMs);
|
||||
debouncedPersistence.Value = fileEntries;
|
||||
debouncedPersistence.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration
|
||||
{
|
||||
EvictionCallback = (key, entriesCallback, reason, state) =>
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
using var cacheEntry = _debouncerCache.CreateEntry(_jsonPath)
|
||||
.AddExpirationToken(new CancellationChangeToken(cancellationTokenSource.Token))
|
||||
.SetValue(entries)
|
||||
.SetOptions(new MemoryCacheEntryOptions
|
||||
{
|
||||
if (entriesCallback is IEnumerable<FileEntry> entriesToPersist && key is string filePath)
|
||||
PostEvictionCallbacks =
|
||||
{
|
||||
if (_snapshotFileSemaphore.Wait(SnapshotFileLockTimeout))
|
||||
new PostEvictionCallbackRegistration
|
||||
{
|
||||
if (IsFileLocked(filePath))
|
||||
EvictionCallback = (key, value, reason, state) =>
|
||||
{
|
||||
_logger.LogInformation("File {FilePath} is locked, skipping snapshot persistence", filePath);
|
||||
if (!(reason == EvictionReason.Expired || reason == EvictionReason.TokenExpired))
|
||||
return;
|
||||
var filePath = (string)key;
|
||||
if (_snapshotFileSemaphore.Wait(SnapshotFileLockTimeout))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsFileLocked(filePath))
|
||||
{
|
||||
_logger.LogInformation("File {FilePath} is locked, skipping snapshot persistence", filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.WriteAllText(filePath, JsonSerializer.Serialize(value, _jsonSerializerOptions));
|
||||
SnapshotRebuilt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_snapshotFileSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
File.WriteAllText(filePath,
|
||||
JsonSerializer.Serialize(entriesToPersist, _jsonSerializerOptions));
|
||||
_snapshotFileSemaphore.Release();
|
||||
_logger.LogInformation("Persisted snapshot");
|
||||
SnapshotRebuilt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Failed to persist file {FilePath} due to timeout", filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
cts.CancelAfter(TimeSpan.FromMilliseconds(DebounceMs));
|
||||
});
|
||||
cancellationTokenSource.CancelAfter(TimeSpan.FromMilliseconds(DebounceMs));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
private static string ComputeHash(string filePath)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
|
||||
Reference in New Issue
Block a user