/* ========================================================================
* 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.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Opc.Ua.WotCon.Client
{
///
/// Extension methods on the generated
/// proxy that implement the OPC 10100-1 §6.3.10 upload-and-update
/// flow (Open(Write|EraseExisting) → Write* → CloseAndUpdate).
///
public static class WoTAssetFileTypeClientExtensions
{
///
/// Spec §6.3.10 only allows Read (1) and Write|EraseExisting (6).
///
private const byte WriteEraseMode = 6;
///
/// Uploads to the WoT
/// asset file and then triggers CloseAndUpdate so the
/// server materialises the new asset shape.
///
/// The asset file proxy.
/// The TD payload as UTF-8 JSON bytes.
/// Maximum per-write chunk size.
/// Cancellation token.
/// is null.
/// is less than or equal to zero.
public static async ValueTask UploadAndUpdateAsync(
this WoTAssetFileTypeClient file,
ReadOnlyMemory thingDescriptionJson,
int chunkSize = FileTypeClientExtensions.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(WriteEraseMode, ct).ConfigureAwait(false);
try
{
int offset = 0;
while (offset < thingDescriptionJson.Length)
{
int take = Math.Min(chunkSize, thingDescriptionJson.Length - offset);
var chunk = ByteString.From(
thingDescriptionJson.Slice(offset, take).ToArray());
await file.WriteAsync(handle, chunk, ct).ConfigureAwait(false);
offset += take;
}
await file.CloseAndUpdateAsync(handle, ct).ConfigureAwait(false);
}
catch
{
try
{
await file.CloseAsync(handle, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// Best-effort cleanup.
}
throw;
}
}
///
/// Uploads the contents of
/// to the WoT asset file in chunks and then triggers
/// CloseAndUpdate so the server materialises the new
/// asset shape. 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 asset file proxy.
/// A readable stream producing
/// the TD payload as UTF-8 JSON bytes.
/// 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 UploadAndUpdateAsync(
this WoTAssetFileTypeClient file,
Stream thingDescriptionJson,
int chunkSize = FileTypeClientExtensions.DefaultChunkSize,
CancellationToken ct = default)
{
if (file is null)
{
throw new ArgumentNullException(nameof(file));
}
if (thingDescriptionJson is null)
{
throw new ArgumentNullException(nameof(thingDescriptionJson));
}
if (!thingDescriptionJson.CanRead)
{
throw new ArgumentException(
"Stream must be readable.", nameof(thingDescriptionJson));
}
if (chunkSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(chunkSize), "Chunk size must be positive.");
}
uint handle = await file.OpenAsync(WriteEraseMode, ct).ConfigureAwait(false);
try
{
await thingDescriptionJson.CopyStreamInChunksAsync(
chunkSize,
(chunk, token) => file.WriteAsync(handle, ByteString.From(chunk), token),
ct).ConfigureAwait(false);
await file.CloseAndUpdateAsync(handle, ct).ConfigureAwait(false);
}
catch
{
try
{
await file.CloseAsync(handle, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// Best-effort cleanup.
}
throw;
}
}
}
}