/* ========================================================================
* 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.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Opc.Ua.Client.FileSystem
{
///
/// Strongly-typed handle to a single OPC UA FileType
/// instance. Mirrors .
///
///
/// File metadata properties (,
/// , , …) are populated
/// lazily by ; before the first call they
/// return their default values. The property
/// / are advisory
/// only (TOCTOU-prone): rely on the server's
/// response
/// to determine whether write access is actually granted.
///
public sealed class UaFileInfo : UaFileSystemInfo
{
internal UaFileInfo(
FileSystemClient owner,
UaDirectoryInfo? parent,
NodeId nodeId,
QualifiedName browseName,
IReadOnlyList segments)
: base(owner, parent, nodeId, browseName, segments)
{
Proxy = new FileTypeClient(
owner.Session,
nodeId,
owner.Session.MessageContext.Telemetry);
}
///
public override bool IsDirectory => false;
/// The cached Size property in bytes.
public ulong Size { get; private set; }
/// The cached Writable property (advisory).
public bool Writable { get; private set; }
/// The cached UserWritable property (advisory).
public bool UserWritable { get; private set; }
/// The cached OpenCount property.
public ushort OpenCount { get; private set; }
/// The cached MimeType property; null
/// when the server does not expose it.
public string? MimeType { get; private set; }
/// The cached MaxByteStringLength property;
/// null when the server does not expose it.
public uint? MaxByteStringLength { get; private set; }
/// The cached LastModifiedTime property;
/// null when the server does not expose it.
public DateTime? LastModifiedTime { get; private set; }
///
/// The underlying proxy.
/// Exposed so advanced callers can reach the raw method
/// wrappers when the high-level API does not suffice.
///
public FileTypeClient Proxy { get; }
///
public override async ValueTask RefreshAsync(CancellationToken ct = default)
{
FileMetadata metadata = await Owner
.ReadFileMetadataAsync(NodeId, FullPath, ct)
.ConfigureAwait(false);
Size = metadata.Size ?? 0UL;
Writable = metadata.Writable ?? false;
UserWritable = metadata.UserWritable ?? false;
OpenCount = metadata.OpenCount ?? 0;
MimeType = metadata.MimeType;
MaxByteStringLength = metadata.MaxByteStringLength;
LastModifiedTime = metadata.LastModifiedTime;
}
///
/// Opens this file with the supplied .
///
/// The combined open-mode flags.
/// Cancellation token.
/// A wrapping the server
/// handle.
///
public async ValueTask OpenAsync(
UaFileMode mode,
CancellationToken ct = default)
{
if (mode == 0)
{
throw new ArgumentException(
"UaFileMode must include at least one of Read or Write.",
nameof(mode));
}
uint handle;
try
{
handle = await Proxy.OpenAsync((byte)mode, ct).ConfigureAwait(false);
}
catch (ServiceResultException ex)
{
throw FileSystemErrors.Translate(ex, FullPath, targetIsDirectory: false);
}
long initialPosition = 0;
long initialLength = (long)Size;
try
{
if ((mode & UaFileMode.Append) != 0)
{
// Spec leaves this server-defined; query for accuracy.
ulong serverPos = await Proxy.GetPositionAsync(handle, ct)
.ConfigureAwait(false);
initialPosition = checked((long)serverPos);
if (initialPosition > initialLength)
{
initialLength = initialPosition;
}
}
else if ((mode & UaFileMode.EraseExisting) != 0)
{
initialLength = 0;
}
}
catch
{
// If GetPosition fails after a successful Open we still
// own the handle and must release it.
try
{
await Proxy.CloseAsync(handle, CancellationToken.None)
.ConfigureAwait(false);
}
catch
{
// Best-effort cleanup.
}
throw;
}
int chunk = Owner.Options.ChunkSize;
if (MaxByteStringLength is uint cap && cap > 0 && cap < int.MaxValue)
{
chunk = Math.Min(chunk, (int)cap);
}
return new UaFileStream(
Proxy,
handle,
mode,
initialLength,
initialPosition,
chunk);
}
///
/// Equivalent to with
/// .
///
public ValueTask OpenReadAsync(CancellationToken ct = default)
{
return OpenAsync(UaFileMode.Read, ct);
}
///
/// Equivalent to with
/// |
/// (truncates).
///
public ValueTask OpenWriteAsync(CancellationToken ct = default)
{
return OpenAsync(UaFileMode.Write | UaFileMode.EraseExisting, ct);
}
///
/// Equivalent to with
/// | .
///
public ValueTask OpenAppendAsync(CancellationToken ct = default)
{
return OpenAsync(UaFileMode.Write | UaFileMode.Append, ct);
}
///
/// Reads the entire file contents into memory. Throws when the
/// file size exceeds
/// .
///
///
public async ValueTask ReadAllBytesAsync(CancellationToken ct = default)
{
UaFileStream stream = await OpenReadAsync(ct).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
long length = stream.Length;
if (length > Owner.Options.MaxBufferedReadSize)
{
throw ServiceResultException.Create(
StatusCodes.BadEncodingLimitsExceeded,
"File '{0}' size ({1}) exceeds MaxBufferedReadSize ({2}).",
FullPath,
length,
Owner.Options.MaxBufferedReadSize);
}
using var ms = new MemoryStream(length > 0 ? (int)length : 0);
int rentSize = length > 0
? (int)Math.Min(length, Owner.Options.ChunkSize)
: Owner.Options.ChunkSize;
byte[] rented = ArrayPool.Shared.Rent(rentSize);
try
{
while (true)
{
#if NETSTANDARD2_1_OR_GREATER || NET
int read = await stream.ReadAsync(rented.AsMemory(), ct)
.ConfigureAwait(false);
#else
int read = await stream.ReadAsync(rented, 0, rented.Length, ct)
.ConfigureAwait(false);
#endif
if (read == 0)
{
break;
}
ms.Write(rented, 0, read);
if (ms.Length > Owner.Options.MaxBufferedReadSize)
{
throw ServiceResultException.Create(
StatusCodes.BadEncodingLimitsExceeded,
"File '{0}' size exceeds MaxBufferedReadSize ({1}).",
FullPath,
Owner.Options.MaxBufferedReadSize);
}
}
}
finally
{
ArrayPool.Shared.Return(rented);
}
return ms.ToArray();
}
}
///
/// Reads the entire file contents and decodes it as text using
/// the supplied (UTF-8 by default).
///
public async ValueTask ReadAllTextAsync(
Encoding? encoding = null,
CancellationToken ct = default)
{
byte[] bytes = await ReadAllBytesAsync(ct).ConfigureAwait(false);
return (encoding ?? Encoding.UTF8).GetString(bytes);
}
///
/// Truncates the file and writes as
/// the new contents.
///
public async ValueTask WriteAllBytesAsync(
ReadOnlyMemory bytes,
CancellationToken ct = default)
{
UaFileStream stream = await OpenWriteAsync(ct).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
if (!bytes.IsEmpty)
{
#if NETSTANDARD2_1_OR_GREATER || NET
await stream.WriteAsync(bytes, ct).ConfigureAwait(false);
#else
byte[] buffer = bytes.ToArray();
await stream.WriteAsync(buffer, 0, buffer.Length, ct)
.ConfigureAwait(false);
#endif
}
}
}
///
/// Truncates the file and writes
/// using the supplied (UTF-8 by
/// default).
///
/// is null.
public ValueTask WriteAllTextAsync(
string contents,
Encoding? encoding = null,
CancellationToken ct = default)
{
if (contents == null)
{
throw new ArgumentNullException(nameof(contents));
}
byte[] bytes = (encoding ?? Encoding.UTF8).GetBytes(contents);
return WriteAllBytesAsync(bytes, ct);
}
}
}