Check lock against base archive in multipart scenario
Explicitly attempt to close Reader when using ArchiveFactory
This commit is contained in:
@@ -22,7 +22,6 @@ namespace TinfoilVibeServer.Services
|
||||
private readonly ZipArchive? _zipArchive;
|
||||
private readonly IArchive? _archive;
|
||||
private readonly Stream? _archiveStream; // the stream actually handed to SharpCompress
|
||||
private readonly ICollection<Stream>? _partStreams;
|
||||
|
||||
public RomArchiveReader(string path)
|
||||
{
|
||||
@@ -55,7 +54,15 @@ namespace TinfoilVibeServer.Services
|
||||
stream.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
_archiveStream = ms;
|
||||
stream.Dispose(); // original non‑seekable stream no longer needed
|
||||
if (stream is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
var disposeAsync = asyncDisposable.DisposeAsync();
|
||||
disposeAsync.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.Dispose(); // original non‑seekable stream no longer needed
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -73,28 +80,29 @@ namespace TinfoilVibeServer.Services
|
||||
// Detect whether the file is a multi‑part RAR and wrap it if necessary
|
||||
private static IArchive DetectAndWrap(string path)
|
||||
{
|
||||
string ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
|
||||
if (ext == ".rar" || ext == ".r00" || ext == ".r01" || ext == ".r02")
|
||||
if (ext is ".rar" or ".r00" or ".r01" or ".r02")
|
||||
{
|
||||
var dir = Path.GetDirectoryName(path)!;
|
||||
var dir = Path.GetDirectoryName(path)!;
|
||||
var fileName = Path.GetFileName(path);
|
||||
|
||||
// ----- 1️⃣ Determine the base name (everything before the first ".rar" or ".partNN") -----
|
||||
string baseName = MultiPartRarHelper.GetBaseNameForRarVolume(fileName);
|
||||
var baseName = MultiPartRarHelper.GetBaseNameForRarVolume(fileName);
|
||||
|
||||
// Any file that ends with .rar or .rNN could be the start of a multi‑part set
|
||||
// Let MultiPartRarStream decide which parts belong together.
|
||||
var volumes = MultiPartRarHelper.DiscoverVolumes(dir, baseName);
|
||||
if (volumes.Count is 0 or 1)
|
||||
{
|
||||
return ArchiveFactory.Open(path);
|
||||
return ArchiveFactory.Open(path, new ReaderOptions { LeaveStreamOpen = false });
|
||||
}
|
||||
|
||||
var streams = new List<Stream>(volumes.Count);
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
streams.Add(new FileStream(volume, FileMode.Open, FileAccess.Read, FileShare.Read));
|
||||
// todo: check all streams for read validity? The rar may be available but the parts are not all downloaded yet
|
||||
streams.Add(new FileStream(volume, FileMode.Open, FileAccess.Read, FileShare.Read, 10*1024*1024, FileOptions.Asynchronous));
|
||||
}
|
||||
|
||||
return ArchiveFactory.Open(streams, new ReaderOptions { LeaveStreamOpen = false });
|
||||
@@ -102,7 +110,7 @@ namespace TinfoilVibeServer.Services
|
||||
|
||||
// Normal single‑file archive (zip, 7z, single‑rar, etc.)
|
||||
using var archiveStream = File.OpenRead(path);
|
||||
return ArchiveFactory.Open(archiveStream);
|
||||
return ArchiveFactory.Open(archiveStream, new ReaderOptions { LeaveStreamOpen = false });
|
||||
}
|
||||
|
||||
private static Stream? GetPart(int arg)
|
||||
@@ -170,26 +178,15 @@ namespace TinfoilVibeServer.Services
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// Dispose ZipArchive (no async support – just dispose synchronously)
|
||||
if (_zipArchive is IAsyncDisposable asyncZip)
|
||||
await asyncZip.DisposeAsync().ConfigureAwait(false);
|
||||
else
|
||||
_zipArchive?.Dispose();
|
||||
// Dispose SharpCompress IArchive (no async support)
|
||||
if (_archive is IAsyncDisposable asyncArc)
|
||||
await asyncArc.DisposeAsync().ConfigureAwait(false);
|
||||
else
|
||||
_archive?.Dispose();
|
||||
// Dispose the underlying stream (may support async)
|
||||
_zipArchive?.Dispose();
|
||||
_archive?.Dispose();
|
||||
|
||||
// Dispose of the underlying stream (may support async)
|
||||
if (_archiveStream is IAsyncDisposable asyncStream)
|
||||
await asyncStream.DisposeAsync().ConfigureAwait(false);
|
||||
else
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
_archiveStream?.Dispose();
|
||||
// Avoid double‑dispose if the caller disposes again.
|
||||
// (not strictly required but prevents accidental misuse)
|
||||
// _zipArchive = null; // not possible – fields are readonly
|
||||
// _archive = null;
|
||||
// _archiveStream = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -319,6 +319,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
|
||||
var lastModified = File.GetLastWriteTimeUtc(entry.Path.Contains(ArchivePathSeparator) ? entry.Path.Split(ArchivePathSeparator)[0] : entry.Path);
|
||||
|
||||
var cacheUpdated = _cache.ContainsKey(entry.Path);
|
||||
_cache[entry.Path] = new SnapshotEntry(entry.Path, entry.Hash, entry.Size, lastModified, entry.Titles);
|
||||
_hashCache[entry.Hash] = entry.Path;
|
||||
_sizeLookup[entry.Hash] = entry.Size;
|
||||
@@ -339,12 +340,13 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
|
||||
_hashCache[ncaMetadataWithHash.Hash] = entry.Path;
|
||||
_sizeLookup[ncaMetadataWithHash.Hash] = entry.Size;
|
||||
//_logger.LogInformation("Added entry {titleId} to snapshot (hash={hash})", ncaMetadataWithHash.TitleId, ncaMetadataWithHash.Hash);
|
||||
}
|
||||
|
||||
// Persist snapshot to disk
|
||||
var titleIds = entry.Titles.Aggregate("", (current, titles) => $"{current} ,{titles.TitleId}");
|
||||
_logger.LogInformation("Added {Path} to snapshot, titleIds=[{TitleIds}]", entry.Path, titleIds);
|
||||
// If entry.Titles is null, treat it as an empty collection
|
||||
var titleIds = string.Join(",", entry.Titles.Select(t => t.TitleId.ToString()));
|
||||
_logger.LogInformation(cacheUpdated ? "Updated snapshot for {Path}, titleIds=[{TitleIds}]" : "Added {Path} to snapshot, titleIds=[{TitleIds}]", entry.Path, titleIds);
|
||||
|
||||
await PersistSnapshotAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -406,7 +408,7 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
}
|
||||
}
|
||||
|
||||
var currentHash = ComputeSnapshotHash(entries);
|
||||
//var currentHash = ComputeSnapshotHash(entries);
|
||||
if (entries.Count > 0 || fileInfo.Exists && index.Count == 0)
|
||||
SnapshotRebuilt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
@@ -596,6 +598,8 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
|
||||
private Task PersistSnapshotAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_debouncerCache.TryGetValue(_jsonPath, out _))
|
||||
{
|
||||
_logger.LogDebug("Sliding debounce in progress, skipping snapshot persistence");
|
||||
@@ -623,26 +627,25 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
if (reason is not (EvictionReason.Expired or EvictionReason.TokenExpired))
|
||||
return;
|
||||
var filePath = (string)key;
|
||||
if (_snapshotFileSemaphore.Wait(SnapshotFileLockTimeout))
|
||||
if (!_snapshotFileSemaphore.Wait(SnapshotFileLockTimeout)) return;
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
if (FileLockHelper.IsFileLocked(filePath))
|
||||
{
|
||||
if (FileLockHelper.IsFileLocked(filePath))
|
||||
{
|
||||
_logger.LogInformation("File {FilePath} is locked, skipping snapshot persistence", filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.WriteAllText(filePath, JsonSerializer.Serialize(value, _jsonSerializerOptions));
|
||||
_logger.LogInformation("Persisted snapshot to {FilePath}", filePath);
|
||||
SnapshotRebuilt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
_logger.LogInformation("File {FilePath} is locked, skipping snapshot persistence", filePath);
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
_snapshotFileSemaphore.Release();
|
||||
File.WriteAllText(filePath, JsonSerializer.Serialize(value, _jsonSerializerOptions));
|
||||
_logger.LogInformation("Persisted snapshot to {FilePath}", filePath);
|
||||
SnapshotRebuilt?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_snapshotFileSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -854,13 +857,19 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
{
|
||||
try
|
||||
{
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
|
||||
var multiPartBasePathWithExtension = $"{MultiPartRarHelper.GetBaseNameForRarVolume(filePath)}{ext}";
|
||||
if (string.CompareOrdinal(multiPartBasePathWithExtension, filePath) != 0)
|
||||
{
|
||||
filePath = multiPartBasePathWithExtension;
|
||||
}
|
||||
if (FileLockHelper.IsFileLocked(filePath))
|
||||
{
|
||||
throw new IOException("File is locked");
|
||||
}
|
||||
|
||||
// Only treat NSP/XCI/XCZ as “first‑stream” files
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
if (ext is not ".nsp" and not ".xci" and not ".xcz")
|
||||
{
|
||||
// Open the NSP/XCI with LibHac and read the first stream.
|
||||
@@ -868,12 +877,11 @@ public sealed class SnapshotService : IDisposable, ISnapshotService, IHostedServ
|
||||
try
|
||||
{
|
||||
await using var reader = new RomArchiveReader(filePath);
|
||||
|
||||
var first = reader.GetEntries().FirstOrDefault();
|
||||
if (first == null) return ComputeFullHash(filePath);
|
||||
|
||||
//using var seekableWrapper = new SeekableBufferedStream(first.Stream, first.Stream.Length, 10*1024*1024, true);
|
||||
await using var rewindableWrapper = new RewindableStream(first.Stream, () => { return reader.GetEntries().FirstOrDefault().Stream; }, 10 * 1024 * 1024, first.Stream.Length);
|
||||
await using var rewindableWrapper = new RewindableStream(first.Stream, () => first.Stream, 10 * 1024 * 1024, first.Stream.Length);
|
||||
var hash = _nspExtractor.ExtractHashFromStream(rewindableWrapper);
|
||||
return hash;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user