From 02ca63cd13779dbff9971e10a7afd62d2634337b Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 26 May 2026 22:37:17 +0000 Subject: Moved IsFileIdenticalAsync & IsStreamIdenticalAsync to StreamExtensions. --- src/Jellyfin.Extensions/StreamExtensions.cs | 96 +++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) (limited to 'src') 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); + } + } } } -- cgit v1.2.3