Moved IsFileIdenticalAsync & IsStreamIdenticalAsync to StreamExtensions.

This commit is contained in:
Marc Brooks
2026-05-26 22:37:17 +00:00
parent 961c6d3d54
commit 02ca63cd13
2 changed files with 97 additions and 63 deletions

View File

@@ -208,7 +208,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
// Compare byte-for-byte before proceeding. // 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. return; // Don't save since .nfo is unchanged.
} }
@@ -239,68 +239,6 @@ namespace MediaBrowser.XbmcMetadata.Savers
} }
} }
private static async Task<bool> 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) private void SetHidden(string path, bool hidden)
{ {
try try

View File

@@ -1,9 +1,12 @@
using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin.Extensions namespace Jellyfin.Extensions
{ {
@@ -12,6 +15,8 @@ namespace Jellyfin.Extensions
/// </summary> /// </summary>
public static class StreamExtensions public static class StreamExtensions
{ {
private const int StreamComparisonBufferSize = 65536;
/// <summary> /// <summary>
/// Reads all lines in the <see cref="Stream" />. /// Reads all lines in the <see cref="Stream" />.
/// </summary> /// </summary>
@@ -60,5 +65,96 @@ namespace Jellyfin.Extensions
yield return line; yield return line;
} }
} }
/// <summary>
/// Determines whether a stream is identical to a file on disk.
/// </summary>
/// <param name="stream">The stream to compare.</param>
/// <param name="path">The file path to compare against.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>True if the stream and file are identical; otherwise false.</returns>
public static async Task<bool> 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;
}
}
/// <summary>
/// Determines whether two streams are identical.
/// </summary>
/// <param name="a">The first stream to compare.</param>
/// <param name="b">The second stream to compare.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>True if the streams are identical; otherwise false.</returns>
public static async Task<bool> IsStreamIdenticalAsync(this Stream a, Stream b, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(a);
ArgumentNullException.ThrowIfNull(b);
if (b.Length != a.Length)
{
return false;
}
var bufferA = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
var bufferB = ArrayPool<byte>.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<byte>.Shared.Return(bufferA);
ArrayPool<byte>.Shared.Return(bufferB);
}
}
} }
} }