mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 15:22:15 +08:00
a193a0a278
This change introduces two independent additions to the .NET Standard
stack:
## `Libraries/Opc.Ua.Server.FileSystem` (Part 5 §C / Part 20 §4)
Server-side implementation of the OPC UA `FileType` /
`FileDirectoryType` companion model, mirroring the existing client
namespace `Libraries/Opc.Ua.Client.FileSystem`.
### Provider abstraction
- `IFileSystemProvider` — async storage abstraction
(`GetEntryAsync` / `EnumerateAsync` / `OpenRead/WriteAsync` /
`Create{Directory,File}Async` / `DeleteAsync` / `MoveAsync` /
`CopyAsync`). All paths are forward-slash provider-relative; the
empty string is the root.
- `FileSystemEntry` record + `FileWriteMode` enum.
### Default `PhysicalFileSystemProvider`
`System.IO`-based implementation that mounts a **single physical
directory** as the FileSystem root. Rejects path-traversal attempts
(`..` escape) via `Path.GetFullPath` + root-prefix check. Supports
an optional `isWritable: false` mode for read-only mounts.
### Node manager
`FileSystemNodeManager` drives every Browse / Read / Method call
through the configured provider. Each mounted provider is anchored
as a `FileDirectoryType` instance under the standard
`Server.FileSystem` object (`ObjectIds.FileSystem`, `i=16314`)
via `HasComponent`, which is what
`FileSystemClient.OpenServerFileSystem` expects to find. Mount
multiple providers by registering multiple
`FileSystemNodeManager` instances. Companion files
(`DirectoryBrowser`, `DirectoryObjectState`, `FileObjectState`,
`FileHandle`, `FileSystemNodeId`, `FileSystemNodeManagerFactory`)
are all provider-driven.
### Client-side fix
`Libraries/Opc.Ua.Client.FileSystem.FileSystemClient.EnumerateChildrenAsync`
now stops the BrowseNext loop on `continuation.Length == 0` in
addition to `continuation.IsNull`. Without this fix, an
empty-but-non-null ByteString continuation returned by the SDK after
wire round-trip would cause a spurious BrowseNext and fail with
`BadContinuationPointInvalid`.
### Reference-server hook
`Quickstarts.ReferenceServer.ReferenceServer` gains two opt-in
properties (default off):
- `EnableFileSystemNodeManager` — toggles whether the FS node
manager is registered.
- `FileSystemProvider` — caller-supplied
`IFileSystemProvider`. When `null` and the flag is on, falls
back to a `PhysicalFileSystemProvider` rooted at
`%TEMP%/OpcUaReferenceServerFs` (mount name `Temp`).
### Tests
`Tests/Opc.Ua.Client.Tests/FileSystem/FileSystemClientIntegrationTests.cs`
— 12 end-to-end tests over a real
`ServerFixture<ReferenceServer>` + `ClientFixture` (opc.tcp,
SecurityNone, temp-dir provider): round-trip write/read,
enumeration, nested `CreateDirectory` / `CreateSubdirectory`,
delete, move, copy, large-file chunked read, mount BrowseName,
missing path returns null, multi-level enumeration, text round-trip,
read-only provider rejects write.
## `Libraries/Opc.Ua.Lds.Server`
A new Local Discovery Server (LDS) library implementing OPC UA Part
12 §7:
- `LdsServer` — main server class.
- `MulticastDiscovery` — DNS-SD / mDNS bootstrap via
`Makaretu.Dns.Multicast` (version 0.27.0 added to
`Directory.Packages.props`).
- `RegisteredServerStore` — in-memory store with TTL.
- `RegistrationEntry` + `ServerOnNetworkRecord` — value types.
Comes with a sample host `Applications/ConsoleLdsServer`.
## Build / test
- `dotnet build UA.slnx -c Release` -> 0 errors on all target
frameworks (net472, net48, netstandard2.1, net8.0, net9.0, net10.0).
- `Opc.Ua.Server.Tests` -> 964 pass, 5 skip, 0 fail.
- `FileSystemClientIntegrationTests` -> 12/12 pass.
181 lines
6.1 KiB
C#
181 lines
6.1 KiB
C#
/* ========================================================================
|
|
* 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.Collections.Generic;
|
|
using System.Threading;
|
|
|
|
namespace Opc.Ua.Server.FileSystem
|
|
{
|
|
/// <summary>
|
|
/// Enumerates the immediate children (files + directories) of a
|
|
/// <see cref="DirectoryObjectState"/> via the underlying
|
|
/// <see cref="IFileSystemProvider"/>.
|
|
/// </summary>
|
|
internal sealed class DirectoryBrowser : NodeBrowser
|
|
{
|
|
public DirectoryBrowser(
|
|
ISystemContext context, ViewDescription? view,
|
|
NodeId referenceType, bool includeSubtypes, BrowseDirection browseDirection,
|
|
QualifiedName browseName, IEnumerable<IReference>? additionalReferences,
|
|
bool internalOnly,
|
|
FileSystemNodeManager manager,
|
|
DirectoryObjectState source)
|
|
: base(context, view, referenceType, includeSubtypes, browseDirection,
|
|
browseName, additionalReferences, internalOnly)
|
|
{
|
|
m_manager = manager;
|
|
m_source = source;
|
|
m_stage = Stage.Begin;
|
|
}
|
|
|
|
public override IReference? Next()
|
|
{
|
|
lock (DataLock)
|
|
{
|
|
IReference? reference = base.Next();
|
|
if (reference != null)
|
|
{
|
|
return reference;
|
|
}
|
|
|
|
if (InternalOnly || m_manager == null)
|
|
{
|
|
return null;
|
|
}
|
|
if (!IsRequired(ReferenceTypeIds.HasComponent, false))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (m_stage == Stage.Begin)
|
|
{
|
|
m_pending = LoadEntries();
|
|
m_stage = Stage.Children;
|
|
}
|
|
|
|
if (m_stage == Stage.Children)
|
|
{
|
|
reference = NextChild();
|
|
if (reference != null)
|
|
{
|
|
return reference;
|
|
}
|
|
m_stage = Stage.Done;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private List<FileSystemEntry> LoadEntries()
|
|
{
|
|
var list = new List<FileSystemEntry>();
|
|
try
|
|
{
|
|
IAsyncEnumerator<FileSystemEntry> enumerator = m_manager.Provider
|
|
.EnumerateAsync(m_source.ProviderPath, CancellationToken.None)
|
|
.GetAsyncEnumerator(CancellationToken.None);
|
|
try
|
|
{
|
|
while (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult())
|
|
{
|
|
list.Add(enumerator.Current);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
enumerator.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Browse continues without children when the provider
|
|
// can't enumerate (e.g. permission denied).
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private NodeStateReference? NextChild()
|
|
{
|
|
if (m_pending == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Named child requested — scan once and stop.
|
|
if (!BrowseName.IsNull)
|
|
{
|
|
if (m_source.BrowseName.NamespaceIndex != BrowseName.NamespaceIndex)
|
|
{
|
|
m_pending = null;
|
|
return null;
|
|
}
|
|
foreach (FileSystemEntry entry in m_pending)
|
|
{
|
|
if (entry.Name == BrowseName.Name)
|
|
{
|
|
m_pending = null;
|
|
return CreateReference(entry);
|
|
}
|
|
}
|
|
m_pending = null;
|
|
return null;
|
|
}
|
|
|
|
if (m_pending.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
FileSystemEntry head = m_pending[0];
|
|
m_pending.RemoveAt(0);
|
|
return CreateReference(head);
|
|
}
|
|
|
|
private NodeStateReference CreateReference(FileSystemEntry entry)
|
|
{
|
|
NodeId targetId = entry.IsDirectory
|
|
? FileSystemNodeId.BuildDirectory(entry.Path, m_manager.NamespaceIndex)
|
|
: FileSystemNodeId.BuildFile(entry.Path, m_manager.NamespaceIndex);
|
|
return new NodeStateReference(ReferenceTypeIds.HasComponent, false, targetId);
|
|
}
|
|
|
|
private enum Stage
|
|
{
|
|
Begin,
|
|
Children,
|
|
Done
|
|
}
|
|
|
|
private readonly FileSystemNodeManager m_manager;
|
|
private readonly DirectoryObjectState m_source;
|
|
private List<FileSystemEntry>? m_pending;
|
|
private Stage m_stage;
|
|
}
|
|
}
|