// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.Horde.Storage.Nodes;
using EpicGames.Horde.Utilities;
namespace EpicGames.Horde.Storage
{
///
/// Base class for implementations of a content defined chunker
///
public abstract class ContentChunker
{
///
/// Source channel.
///
public abstract ChannelWriter SourceWriter { get; }
///
/// Output channel. The order of items written to the source writer will be preserved in the output reader. Each
/// output item should be read completely (through calls to )
/// before reading the next.
///
public abstract ChannelReader OutputReader { get; }
}
///
/// Base class for input to a . Allows the chunker to read data into a buffer as required.
///
public abstract class ChunkerSource
{
///
/// Length of the input data
///
public abstract long Length { get; }
///
/// Optional user specified data to be propagated to the output
///
public virtual object? UserData { get; } = null;
///
/// Starts a task to read the next chunk of data. Note that this task may be called again before the task completes.
///
/// Buffer to store the data
/// Cancellation token for the operation
public abstract Task StartReadAsync(Memory memory, CancellationToken cancellationToken = default);
}
///
/// Implementation of for files on disk
///
public class FileChunkerSource : ChunkerSource
{
readonly FileInfo _fileInfo;
readonly object? _userData;
long _offset;
///
public override object? UserData
=> _userData;
///
/// Constructor
///
public FileChunkerSource(FileInfo fileInfo, object? userData)
{
_fileInfo = fileInfo;
_userData = userData;
}
///
public override long Length
=> _fileInfo.Length;
///
public override Task StartReadAsync(Memory memory, CancellationToken cancellationToken = default)
{
long offset = _offset;
_offset += memory.Length;
return Task.FromResult(HandleReadAsync(offset, memory, cancellationToken));
}
///
async Task HandleReadAsync(long offset, Memory memory, CancellationToken cancellationToken)
{
using PlatformFileLock? platformFileLock = await PlatformFileLock.CreateAsync(cancellationToken);
FileStreamOptions options = new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read, Options = FileOptions.Asynchronous };
using FileStream stream = _fileInfo.Open(options);
stream.Seek(offset, SeekOrigin.Begin);
await stream.ReadExactlyAsync(memory, cancellationToken);
}
}
///
/// Implementation of for data in memory
///
class MemoryChunkerSource : ChunkerSource
{
readonly ReadOnlyMemory _data;
int _offset;
///
public override long Length => _data.Length;
///
/// Constructor
///
public MemoryChunkerSource(Memory data)
=> _data = data;
///
public override Task StartReadAsync(Memory memory, CancellationToken cancellationToken = default)
{
_data.Slice(_offset, memory.Length).CopyTo(memory);
_offset += memory.Length;
return Task.FromResult(Task.CompletedTask);
}
}
///
/// Enumerates chunks from an input file
///
public abstract class ChunkerOutput
{
///
/// Rolling hash for this chunk
///
public abstract uint RollingHash { get; }
///
/// Accessor for the chunk's data
///
public abstract ReadOnlyMemory Data { get; }
///
/// User specified data from the
///
public abstract object? UserData { get; }
///
/// Moves to the next output chunk
///
/// Cancellation token for the operation
/// True if there was another output chunk
public abstract ValueTask MoveNextAsync(CancellationToken cancellationToken = default);
}
///
/// Simple serial implementation of content chunking
///
public class SerialBuzHashChunker : ContentChunker
{
class Output : ChunkerOutput
{
readonly SerialBuzHashChunker _pipeline;
public readonly ChunkerSource Input;
public bool _firstOutput = true;
public uint _rollingHash;
public long _inputOffset;
public long _outputOffset;
public ReadOnlyMemory _data;
public override uint RollingHash => _rollingHash;
public override ReadOnlyMemory Data => _data;
public override object? UserData => Input.UserData;
public Output(SerialBuzHashChunker pipeline, ChunkerSource input)
{
_pipeline = pipeline;
Input = input;
}
///
public override ValueTask MoveNextAsync(CancellationToken cancellationToken = default)
=> _pipeline.MoveNextAsync(Input, this, cancellationToken);
}
readonly Memory _buffer;
readonly Channel _sourceChannel;
readonly Channel _outputChannel;
readonly LeafChunkedDataNodeOptions _options;
readonly Queue