/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Opc.Ua.WotCon.Client { /// /// Extension methods that add System.IO-style upload / download /// helpers on top of the generated /// proxy. These keep the WoT Connectivity client free of any /// inheritance over the existing Opc.Ua.Client.FileSystem /// types — every wire interaction goes through the auto-generated /// Open/Read/Write/Close method /// wrappers. /// public static class FileTypeClientExtensions { /// /// Default per-call chunk size used by upload / download when /// the server does not advertise a smaller /// MaxByteStringLength. /// public const int DefaultChunkSize = 4096; /// /// Uploads to the file behind /// in chunks. The file is opened with /// Write | EraseExisting (the only write mode supported /// by some servers, including WoT Connectivity), written in /// pieces of at most bytes, and /// then closed. /// /// The FileType proxy. /// The bytes to upload. /// The Open mode to use; defaults to /// Write | EraseExisting. /// Maximum per-write chunk size. /// Cancellation token. /// is null. /// is less than or equal to zero. public static async ValueTask UploadAsync( this FileTypeClient file, ReadOnlyMemory content, byte mode = 6, int chunkSize = DefaultChunkSize, CancellationToken ct = default) { if (file is null) { throw new ArgumentNullException(nameof(file)); } if (chunkSize <= 0) { throw new ArgumentOutOfRangeException(nameof(chunkSize), "Chunk size must be positive."); } uint handle = await file.OpenAsync(mode, ct).ConfigureAwait(false); try { int offset = 0; while (offset < content.Length) { int take = Math.Min(chunkSize, content.Length - offset); var chunk = ByteString.From(content.Slice(offset, take).ToArray()); await file.WriteAsync(handle, chunk, ct).ConfigureAwait(false); offset += take; } } finally { await file.CloseAsync(handle, CancellationToken.None).ConfigureAwait(false); } } /// /// Uploads the contents of to the /// file behind in chunks. The stream is /// read sequentially until end-of-stream; non-seekable streams /// (e.g. , /// ) are /// supported. The caller retains ownership of /// and is responsible for disposing /// it. /// /// The FileType proxy. /// A readable stream producing the bytes /// to upload. /// The Open mode to use; defaults to /// Write | EraseExisting. /// Maximum per-write chunk size and /// size of the rented intermediate buffer. /// Cancellation token. /// is null. /// is null. /// is not readable. /// is less than or equal to zero. public static async ValueTask UploadAsync( this FileTypeClient file, Stream content, byte mode = 6, int chunkSize = DefaultChunkSize, CancellationToken ct = default) { if (file is null) { throw new ArgumentNullException(nameof(file)); } if (content is null) { throw new ArgumentNullException(nameof(content)); } if (!content.CanRead) { throw new ArgumentException("Stream must be readable.", nameof(content)); } if (chunkSize <= 0) { throw new ArgumentOutOfRangeException(nameof(chunkSize), "Chunk size must be positive."); } uint handle = await file.OpenAsync(mode, ct).ConfigureAwait(false); try { await CopyStreamInChunksAsync( content, chunkSize, (chunk, token) => file.WriteAsync(handle, ByteString.From(chunk), token), ct).ConfigureAwait(false); } finally { await file.CloseAsync(handle, CancellationToken.None).ConfigureAwait(false); } } /// /// Reads the entire content of the file behind /// into memory using chunked Read calls. /// /// is null. /// is less than or equal to zero. public static async ValueTask DownloadAllAsync( this FileTypeClient file, int chunkSize = DefaultChunkSize, CancellationToken ct = default) { if (file is null) { throw new ArgumentNullException(nameof(file)); } if (chunkSize <= 0) { throw new ArgumentOutOfRangeException(nameof(chunkSize), "Chunk size must be positive."); } const byte readMode = 1; uint handle = await file.OpenAsync(readMode, ct).ConfigureAwait(false); try { using MemoryStream buffer = new(); while (true) { ByteString chunk = await file.ReadAsync(handle, chunkSize, ct).ConfigureAwait(false); if (chunk.IsNull || chunk.Span.Length == 0) { break; } byte[] copy = chunk.Span.ToArray(); buffer.Write(copy, 0, copy.Length); if (chunk.Span.Length < chunkSize) { break; } } return buffer.ToArray(); } finally { await file.CloseAsync(handle, CancellationToken.None).ConfigureAwait(false); } } /// /// Reads the entire content of the file behind /// using chunked Read calls and /// writes it sequentially into . /// The caller retains ownership of /// and is responsible for /// disposing it. /// /// The FileType proxy. /// A writable stream that receives /// the file contents. /// Maximum per-read chunk size. /// Cancellation token. /// is null. /// is null. /// is not writable. /// is less than or equal to zero. public static async ValueTask DownloadToAsync( this FileTypeClient file, Stream destination, int chunkSize = DefaultChunkSize, CancellationToken ct = default) { if (file is null) { throw new ArgumentNullException(nameof(file)); } if (destination is null) { throw new ArgumentNullException(nameof(destination)); } if (!destination.CanWrite) { throw new ArgumentException("Stream must be writable.", nameof(destination)); } if (chunkSize <= 0) { throw new ArgumentOutOfRangeException(nameof(chunkSize), "Chunk size must be positive."); } const byte readMode = 1; uint handle = await file.OpenAsync(readMode, ct).ConfigureAwait(false); try { await CopyChunksToStreamAsync( destination, chunkSize, async (size, token) => { ByteString chunk = await file.ReadAsync(handle, size, token).ConfigureAwait(false); if (chunk.IsNull) { return ReadOnlyMemory.Empty; } return chunk.Span.ToArray(); }, ct).ConfigureAwait(false); } finally { await file.CloseAsync(handle, CancellationToken.None).ConfigureAwait(false); } } /// /// Reads from in chunks of up to /// bytes and invokes /// for each chunk that contained /// data. The loop terminates when /// /// returns 0 (end-of-stream). The buffer is rented from /// . /// internal static async ValueTask CopyStreamInChunksAsync( this Stream source, int chunkSize, Func, CancellationToken, ValueTask> writeChunk, CancellationToken ct) { byte[] buffer = ArrayPool.Shared.Rent(chunkSize); try { while (true) { ct.ThrowIfCancellationRequested(); #if NETSTANDARD2_1_OR_GREATER || NET int read = await source.ReadAsync(buffer.AsMemory(0, chunkSize), ct) .ConfigureAwait(false); #else int read = await source.ReadAsync(buffer, 0, chunkSize, ct) .ConfigureAwait(false); #endif if (read <= 0) { break; } await writeChunk(buffer.AsMemory(0, read), ct).ConfigureAwait(false); } } finally { ArrayPool.Shared.Return(buffer); } } /// /// Repeatedly invokes until it /// returns an empty chunk (end-of-file) or a partial chunk /// shorter than (which signals the /// last chunk for a chunk-aligned server protocol like /// FileType.Read) and writes each chunk sequentially /// into . /// internal static async ValueTask CopyChunksToStreamAsync( this Stream destination, int chunkSize, Func>> readChunk, CancellationToken ct) { while (true) { ct.ThrowIfCancellationRequested(); ReadOnlyMemory chunk = await readChunk(chunkSize, ct).ConfigureAwait(false); if (chunk.IsEmpty) { break; } #if NETSTANDARD2_1_OR_GREATER || NET await destination.WriteAsync(chunk, ct).ConfigureAwait(false); #else byte[] copy = chunk.ToArray(); await destination.WriteAsync(copy, 0, copy.Length, ct).ConfigureAwait(false); #endif if (chunk.Length < chunkSize) { break; } } } } }