From 02ca63cd13779dbff9971e10a7afd62d2634337b Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 26 May 2026 22:37:17 +0000 Subject: [PATCH] Moved IsFileIdenticalAsync & IsStreamIdenticalAsync to StreamExtensions. --- .../Savers/BaseNfoSaver.cs | 64 +------------ src/Jellyfin.Extensions/StreamExtensions.cs | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 63 deletions(-) diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 64daac68e3..78907a5e68 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -208,7 +208,7 @@ namespace MediaBrowser.XbmcMetadata.Savers Directory.CreateDirectory(directory); // Compare byte-for-byte before proceeding. - if (File.Exists(path) && await IsFileIdenticalAsync(stream, path, cancellationToken).ConfigureAwait(false)) + if (File.Exists(path) && await stream.IsFileIdenticalAsync(path, cancellationToken).ConfigureAwait(false)) { return; // Don't save since .nfo is unchanged. } @@ -239,68 +239,6 @@ namespace MediaBrowser.XbmcMetadata.Savers } } - private static async Task IsFileIdenticalAsync(Stream stream, string path, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(stream); - ArgumentException.ThrowIfNullOrEmpty(path); - - if (!stream.CanSeek) - { - return false; - } - - const int BufferSize = 81920; - var originalPosition = stream.Position; - - try - { - stream.Position = 0; - - using var existingFileStream = new FileStream( - path, - FileMode.Open, - FileAccess.Read, - FileShare.Read, - bufferSize: BufferSize, - FileOptions.Asynchronous); - - if (existingFileStream.Length != stream.Length) - { - return false; - } - - var streamBuffer = new byte[BufferSize]; - var existingBuffer = new byte[BufferSize]; - - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - - var streamBytesRead = await stream.ReadAsync(streamBuffer.AsMemory(), cancellationToken).ConfigureAwait(false); - var existingBytesRead = await existingFileStream.ReadAsync(existingBuffer.AsMemory(), cancellationToken).ConfigureAwait(false); - - if (streamBytesRead != existingBytesRead) - { - return false; - } - - if (streamBytesRead == 0) - { - return true; - } - - if (!streamBuffer.AsSpan(0, streamBytesRead).SequenceEqual(existingBuffer.AsSpan(0, existingBytesRead))) - { - return false; - } - } - } - finally - { - stream.Position = originalPosition; - } - } - private void SetHidden(string path, bool hidden) { try diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs index 0cfac384e3..fa019b0059 100644 --- a/src/Jellyfin.Extensions/StreamExtensions.cs +++ b/src/Jellyfin.Extensions/StreamExtensions.cs @@ -1,9 +1,12 @@ +using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace Jellyfin.Extensions { @@ -12,6 +15,8 @@ namespace Jellyfin.Extensions /// public static class StreamExtensions { + private const int StreamComparisonBufferSize = 65536; + /// /// Reads all lines in the . /// @@ -60,5 +65,96 @@ namespace Jellyfin.Extensions yield return line; } } + + /// + /// Determines whether a stream is identical to a file on disk. + /// + /// The stream to compare. + /// The file path to compare against. + /// The token to monitor for cancellation requests. + /// True if the stream and file are identical; otherwise false. + public static async Task IsFileIdenticalAsync(this Stream stream, string path, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentException.ThrowIfNullOrEmpty(path); + + if (!stream.CanSeek) + { + return false; + } + + var originalPosition = stream.Position; + try + { + stream.Position = 0; + + var existingFileStream = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: StreamComparisonBufferSize, + FileOptions.Asynchronous | FileOptions.SequentialScan); + await using (existingFileStream.ConfigureAwait(false)) + { + return await stream.IsStreamIdenticalAsync(existingFileStream, cancellationToken).ConfigureAwait(false); + } + } + finally + { + stream.Position = originalPosition; + } + } + + /// + /// Determines whether two streams are identical. + /// + /// The first stream to compare. + /// The second stream to compare. + /// The token to monitor for cancellation requests. + /// True if the streams are identical; otherwise false. + public static async Task IsStreamIdenticalAsync(this Stream a, Stream b, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(a); + ArgumentNullException.ThrowIfNull(b); + + if (b.Length != a.Length) + { + return false; + } + + var bufferA = ArrayPool.Shared.Rent(StreamComparisonBufferSize); + var bufferB = ArrayPool.Shared.Rent(StreamComparisonBufferSize); + try + { + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + var bytesReadA = await a.ReadAsync(bufferA.AsMemory(), cancellationToken).ConfigureAwait(false); + var bytesReadB = await b.ReadAsync(bufferB.AsMemory(), cancellationToken).ConfigureAwait(false); + + if (bytesReadA != bytesReadB) + { + return false; + } + + if (bytesReadA == 0) + { + return true; + } + + if (!bufferA.AsSpan(0, bytesReadA).SequenceEqual(bufferB.AsSpan(0, bytesReadB))) + { + return false; + } + } + } + finally + { + ArrayPool.Shared.Return(bufferA); + ArrayPool.Shared.Return(bufferB); + } + } } }