mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 00:31:32 +08:00
c4e0721a29
## Summary
### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.
### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.
### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.
## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.
Review and validation artifacts are maintained outside the repository.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
420 lines
20 KiB
Markdown
420 lines
20 KiB
Markdown
# FileSystemClient — a `System.IO`-style async client for OPC UA file systems
|
|
|
|
`FileSystemClient` (in `src/Opc.Ua.Client/FileSystem/`, namespace
|
|
`Opc.Ua.Client.FileSystem`) is an ergonomic, async-only wrapper around the
|
|
OPC UA file-system primitives defined in **Part 5 §C** and **Part 20 §4** —
|
|
the `FileType`, `FileDirectoryType`, and `TemporaryFileTransferType`
|
|
ObjectTypes. It is layered on top of the source-generated proxies emitted
|
|
into `Opc.Ua.Core` (`FileTypeClient`, `FileDirectoryTypeClient`,
|
|
`TemporaryFileTransferTypeClient`) and exposes a surface that closely
|
|
mirrors `System.IO.{File, Directory, FileInfo, DirectoryInfo, FileStream}`
|
|
to make remote file-system navigation feel like working with a local disk.
|
|
|
|
## Quick reference
|
|
|
|
| Type | Mirrors | Purpose |
|
|
|---|---|---|
|
|
| `FileSystemClient` | `System.IO.Directory` + `System.IO.File` | Top-level entry point. Exposes path-based operations (`GetFileAsync`, `CreateDirectoryAsync`, `DeleteAsync`, `MoveAsync`, `CopyAsync`, `ReadAllBytesAsync`, …). Rooted at any `FileDirectoryType` instance. |
|
|
| `UaFileSystemInfo` | `System.IO.FileSystemInfo` | Abstract base for `UaFileInfo` and `UaDirectoryInfo`. Carries the resolved `NodeId`, parent reference, browse name, and canonical path. |
|
|
| `UaFileInfo` | `System.IO.FileInfo` | A single file. Lazy-loaded `Size` / `Writable` / `MimeType` / etc. metadata; `OpenAsync` / `OpenReadAsync` / `OpenWriteAsync` returning a `UaFileStream`; bulk `ReadAllBytes` / `WriteAllText` shortcuts. |
|
|
| `UaDirectoryInfo` | `System.IO.DirectoryInfo` | A single directory. `EnumerateAsync` / `EnumerateFilesAsync` / `EnumerateDirectoriesAsync` (`IAsyncEnumerable<>`); `CreateSubdirectoryAsync`, `CreateFileAsync`, `DeleteAsync(recursive)`. |
|
|
| `UaFileStream` | `System.IO.FileStream` | A `Stream`-derived wrapper around an open server file handle. Async members hit the wire via `FileTypeClient`; sync members forward via `GetAwaiter().GetResult()`. |
|
|
| `UaPath` | `System.IO.Path` | Helpers for parsing, formatting, combining, and splitting OPC UA file paths. |
|
|
| `FileSystemClientOptions` | n/a | Tuning knobs: `ChunkSize`, `MaxBufferedReadSize`, `PathCacheSize`, type-subtype filters. |
|
|
| `TemporaryFileTransferClient` + `UaTemporaryWriteFile` | n/a | Separate surface for Part 5 §C.5 atomic temp-file transfers (`GenerateFileForRead/Write` + `CloseAndCommit`). |
|
|
|
|
## Contents
|
|
|
|
- [Quick reference](#quick-reference)
|
|
- [Getting started](#getting-started)
|
|
- [Open the standard server file system](#open-the-standard-server-file-system)
|
|
- [Open any `FileDirectoryType` instance](#open-any-filedirectorytype-instance)
|
|
- [Read a file](#read-a-file)
|
|
- [Stream a large file](#stream-a-large-file)
|
|
- [Create a directory tree and write a file](#create-a-directory-tree-and-write-a-file)
|
|
- [Move and delete](#move-and-delete)
|
|
- [Path syntax](#path-syntax)
|
|
- [Path resolution and caching](#path-resolution-and-caching)
|
|
- [Type classification](#type-classification-filetype-vs-filedirectorytype)
|
|
- [File metadata](#file-metadata)
|
|
- [Streams](#streams)
|
|
- [Error mapping](#error-mapping)
|
|
- [Move, copy, and delete semantics](#move-copy-and-delete-semantics)
|
|
- [Temporary file transfer](#temporary-file-transfer-part-5-c5)
|
|
- [What lives where](#what-lives-where)
|
|
- [Server-side file bindings](#server-side-file-bindings)
|
|
- [See also](#see-also)
|
|
|
|
## Getting started
|
|
|
|
### Open the standard server file system
|
|
|
|
Many OPC UA servers expose the standard `Server.FileSystem` object
|
|
(`NodeId i=16314`):
|
|
|
|
```csharp
|
|
using Opc.Ua.Client.FileSystem;
|
|
|
|
ISession session = /* an active OPC UA client session */;
|
|
|
|
var fs = FileSystemClient.OpenServerFileSystem(session);
|
|
|
|
await foreach (UaFileSystemInfo entry in fs.EnumerateAsync("/"))
|
|
{
|
|
Console.WriteLine($"{(entry.IsDirectory ? "DIR " : "FILE")} {entry.FullPath}");
|
|
}
|
|
```
|
|
|
|
### Open any `FileDirectoryType` instance
|
|
|
|
```csharp
|
|
NodeId vendorRoot = /* result of a Browse / TranslateBrowsePathsToNodeIds */;
|
|
var fs = new FileSystemClient(session, vendorRoot);
|
|
```
|
|
|
|
### Read a file
|
|
|
|
```csharp
|
|
string content = await fs.ReadAllTextAsync("/Reports/2024/summary.json");
|
|
```
|
|
|
|
### Stream a large file
|
|
|
|
```csharp
|
|
await using UaFileStream stream = await fs.OpenReadAsync("/Logs/server.log");
|
|
using var reader = new System.IO.StreamReader(stream);
|
|
string firstLine = await reader.ReadLineAsync();
|
|
```
|
|
|
|
### Create a directory tree and write a file
|
|
|
|
```csharp
|
|
UaDirectoryInfo dir = await fs.CreateDirectoryAsync("/Uploads/2024-05/test", createIntermediate: true);
|
|
await fs.WriteAllBytesAsync($"{dir.FullPath}/payload.bin", payload);
|
|
```
|
|
|
|
### Move and delete
|
|
|
|
```csharp
|
|
UaFileInfo file = await fs.GetFileAsync("/Drafts/v1.txt");
|
|
await file.MoveToAsync("/Published/v1.txt");
|
|
|
|
await fs.DeleteAsync("/Drafts", recursive: true);
|
|
```
|
|
|
|
## Path syntax
|
|
|
|
Paths use the forward slash `'/'` as the segment separator. Each segment
|
|
is parsed by `QualifiedName.Parse(...)`, so the standard
|
|
`"<ns>:<name>"` form is supported. Examples:
|
|
|
|
| Path | Meaning |
|
|
|---|---|
|
|
| `""` or `"/"` | The root directory |
|
|
| `"foo"` | The child `foo` (namespace 0) of the root |
|
|
| `"/foo/bar"` | Same as `foo/bar`; leading slash is optional |
|
|
| `"1:Reports/1:2024/data.csv"` | Qualified path using namespace index 1 for the first two segments and namespace 0 for `data.csv` |
|
|
|
|
`UaPath.Combine`, `UaPath.GetDirectoryName`, `UaPath.GetFileName`, and
|
|
`UaPath.Normalize` mirror the equivalents on `System.IO.Path`. Canonical
|
|
paths returned from `UaFileSystemInfo.FullPath` always include the
|
|
namespace prefix when it is non-zero, so siblings with the same `Name`
|
|
in different namespaces are never collapsed.
|
|
|
|
## Path resolution and caching
|
|
|
|
Path → NodeId resolution uses
|
|
`TranslateBrowsePathsToNodeIdsAsync` segment-by-segment, with each step
|
|
following `ReferenceTypeIds.HierarchicalReferences` (with subtypes).
|
|
`(parent NodeId, browse name) → child NodeId` mappings are cached in a
|
|
small LRU controlled by `FileSystemClientOptions.PathCacheSize` (default
|
|
`1024`; set to zero to disable). The cache is **best-effort**: when a
|
|
resolved entry stops working (`BadNodeIdUnknown` from the server), the
|
|
entry is evicted and the path is re-resolved before the error propagates.
|
|
After any successful create/delete/move/copy, the path-cache entries
|
|
rooted at the affected parent are invalidated to avoid serving stale
|
|
NodeIds.
|
|
|
|
## Type classification (FileType vs FileDirectoryType)
|
|
|
|
Each child returned by `EnumerateAsync` is classified by its
|
|
`HasTypeDefinition` reference. Subtype-aware classification uses
|
|
`Session.TypeTree.IsTypeOf(...)` — by default subtypes count, so
|
|
`TrustListType`, `AddressSpaceFileType`, `ConfigurationFileType` etc.
|
|
appear as files. To restrict enumeration to the exact `FileType` /
|
|
`FileDirectoryType` set:
|
|
|
|
```csharp
|
|
var fs = new FileSystemClient(session, ObjectIds.FileSystem,
|
|
new FileSystemClientOptions
|
|
{
|
|
IncludeFileTypeSubtypes = false,
|
|
IncludeFileDirectoryTypeSubtypes = false,
|
|
});
|
|
```
|
|
|
|
## File metadata
|
|
|
|
`UaFileInfo` exposes the seven well-known `FileType` properties (`Size`,
|
|
`Writable`, `UserWritable`, `OpenCount`, `MimeType`,
|
|
`MaxByteStringLength`, `LastModifiedTime`). They are populated lazily by
|
|
`UaFileInfo.RefreshAsync` (a single batched `Read` against the
|
|
properties resolved via `TranslateBrowsePathsToNodeIds`). The optional
|
|
properties (`MimeType`, `MaxByteStringLength`, `LastModifiedTime`) are
|
|
returned as `null` when the server does not expose them
|
|
(`BadNoMatch`, `BadNodeIdUnknown`, or empty target lists are tolerated).
|
|
|
|
In the stack's `FileSystemNodeManager`, metadata reads and initial
|
|
monitored-item values use the asynchronous provider binding. These reads do
|
|
not allocate retained file-handle state or open a stream. The first `Size`
|
|
notification contains the file's current byte count and status, not a stored
|
|
null placeholder.
|
|
|
|
When the manager reattaches an item or re-enables a disabled item, it also
|
|
reads fresh metadata. Initial reads do not add periodic polling for external
|
|
file changes. Later notifications still require the node's normal change
|
|
notification mechanism.
|
|
|
|
`Writable` / `UserWritable` are advisory: callers should not pre-check
|
|
them before opening a file. Rely on the server's `Open` response for the
|
|
authoritative answer.
|
|
|
|
## Streams
|
|
|
|
`UaFileStream` derives from `System.IO.Stream` and supports both async
|
|
(`ReadAsync`, `WriteAsync`, `DisposeAsync`) and sync (`Read`, `Write`,
|
|
`Dispose`) members. The sync members forward to the async ones via
|
|
`GetAwaiter().GetResult()` — this means they can deadlock on
|
|
single-threaded synchronization contexts (e.g. WPF UI thread). Prefer
|
|
the async overrides.
|
|
|
|
Reads and writes are chunked at `FileSystemClientOptions.ChunkSize`,
|
|
clamped down to `FileType.MaxByteStringLength` when the server advertises
|
|
a smaller maximum. Empty `ByteString` returns are interpreted as EOF;
|
|
zero-length reads/writes never hit the wire.
|
|
Path-based `WriteAllBytesAsync` and `WriteAllTextAsync` refresh metadata
|
|
for existing and newly created files before opening them, so these helpers
|
|
honor the same per-file chunk limit as `OpenWriteAsync`.
|
|
|
|
`Position` is tracked locally; the server is informed via
|
|
`SetPosition` only when the local cursor diverges from the last
|
|
successfully transmitted position.
|
|
After a failed read or write, the server cursor is treated as unknown:
|
|
the next operation sends `SetPosition` to the last locally confirmed
|
|
position before transferring more bytes. This prevents a lost response
|
|
from causing a retry to skip or duplicate a chunk. Successfully completed
|
|
earlier chunks remain reflected in `Position`.
|
|
|
|
`Length` is tracked locally too —
|
|
opened from `FileType.Size` at construction and bumped whenever a write
|
|
extends past it. Callers that mutate the underlying file through other
|
|
handles (or other clients) should call `UaFileInfo.RefreshAsync()`
|
|
before relying on `Length`.
|
|
|
|
`UaFileStream` is not thread-safe in the sense that multiple threads can
|
|
share a single instance: concurrent calls are serialised by an internal
|
|
`SemaphoreSlim`, matching `FileStream`'s "not thread-safe but doesn't
|
|
corrupt" contract. `DisposeAsync` issues `FileType.Close` exactly once
|
|
even when called concurrently.
|
|
|
|
Server-issued file handles are opaque and bound to the Session that
|
|
opened them. They cannot be used by another Session, and the server
|
|
automatically closes all remaining handles when their owning Session
|
|
closes.
|
|
|
|
## Error mapping
|
|
|
|
OPC UA Bad status codes returned by the server are translated into the
|
|
familiar `System.IO` exception family at the public boundary:
|
|
|
|
| StatusCode | Mapped exception |
|
|
|---|---|
|
|
| `BadNoMatch`, `BadNodeIdUnknown`, `BadNotFound` | `FileNotFoundException` (files) / `DirectoryNotFoundException` (directories) |
|
|
| `BadBrowseNameDuplicated` | `IOException("already exists: …")` |
|
|
| `BadUserAccessDenied`, `BadNotWritable`, `BadWriteNotSupported`, `BadSecurityChecksFailed` | `UnauthorizedAccessException` |
|
|
| `BadInvalidArgument`, `BadOutOfRange`, `BadInvalidState`, `BadResourceUnavailable`, `BadOutOfMemory` | `IOException` |
|
|
| Other `Bad…` codes | `ServiceResultException` (preserved unchanged) |
|
|
|
|
Mapped exceptions wrap the original `ServiceResultException` as
|
|
`InnerException`, so callers that need the raw OPC UA status code can
|
|
still retrieve it.
|
|
|
|
`TranslateBrowsePathsToNodeIds` returning more than one target throws
|
|
`IOException("ambiguous path: …")`; the client never silently picks the
|
|
first match.
|
|
|
|
## Move, copy, and delete semantics
|
|
|
|
These three operations are routed through the **source's parent**
|
|
directory (per Part 20 §4.3) — the server's `MoveOrCopy` and `Delete`
|
|
methods take a NodeId and operate from the directory's perspective.
|
|
|
|
`DeleteAsync(recursive: false)` on a directory first enumerates the
|
|
directory and throws `IOException("directory not empty: …")` if any
|
|
child exists. `DeleteAsync(recursive: true)` invokes the server's
|
|
`Delete` exactly once and lets the server perform recursive removal
|
|
(per spec). The client never walks the tree itself for a recursive
|
|
delete — that would weaken the server's atomicity / locking guarantees
|
|
and double-traverse.
|
|
|
|
`CreateDirectoryAsync` and `CreateFileAsync` accept only a plain
|
|
**string** for the leaf name; they reject leaf segments with a namespace
|
|
prefix (the server picks the BrowseName namespace). After the server
|
|
returns the new NodeId, the client reads its actual `BrowseName` and
|
|
uses that for the canonical path / cache entry.
|
|
|
|
`CreateFileAsync` always passes `requestFileOpen: false` to the server
|
|
— we never leak a server-allocated handle out of the create call.
|
|
Callers that want an immediate stream should call `UaFileInfo.OpenAsync`
|
|
afterwards.
|
|
|
|
## Temporary file transfer (Part 5 §C.5)
|
|
|
|
The temporary-file-transfer pattern is exposed on a separate surface
|
|
because its lifecycle does not fit the `System.IO` abstraction. The
|
|
server allocates a transient file, the client streams data through it,
|
|
and a final commit (or rollback) tells the server to either publish or
|
|
discard the result.
|
|
|
|
```csharp
|
|
using Opc.Ua.Client.FileSystem;
|
|
|
|
NodeId transferObject = /* TemporaryFileTransferType instance */;
|
|
var temp = new TemporaryFileTransferClient(session, transferObject);
|
|
|
|
// Read flow
|
|
await using UaFileStream readStream = await temp
|
|
.GenerateFileForReadAsync(generateOptions: default);
|
|
byte[] payload = ReadAll(readStream);
|
|
|
|
// Write flow
|
|
await using UaTemporaryWriteFile write = await temp.GenerateFileForWriteAsync();
|
|
await write.Stream.WriteAsync(payload, 0, payload.Length);
|
|
NodeId completion = await write.CommitAsync(); // CloseAndCommit
|
|
```
|
|
|
|
`UaTemporaryWriteFile` owns the close lifecycle: exactly one terminal
|
|
call — `CommitAsync` (CloseAndCommit) or `DisposeAsync` (Close,
|
|
implicit server rollback) — is sent to the server. The wrapped
|
|
`Stream` cannot accidentally close the server handle; its `Dispose`
|
|
is a no-op.
|
|
|
|
## What lives where
|
|
|
|
```
|
|
src/Opc.Ua.Client/FileSystem/
|
|
├── FileSystemClient.cs # entry point
|
|
├── FileSystemClientOptions.cs # tuning knobs
|
|
├── FileMetadata.cs # internal property snapshot
|
|
├── FileSystemErrors.cs # status code → IO exception mapper
|
|
├── PathCache.cs # internal LRU
|
|
├── UaPath.cs # System.IO.Path mirror
|
|
├── UaFileMode.cs # OpenFileMode flags wrapper
|
|
├── UaFileSystemInfo.cs # abstract base
|
|
├── UaFileInfo.cs # System.IO.FileInfo mirror
|
|
├── UaDirectoryInfo.cs # System.IO.DirectoryInfo mirror
|
|
├── UaFileStream.cs # System.IO.Stream mirror
|
|
├── TemporaryFileTransferClient.cs # Part 5 §C.5 surface
|
|
└── UaTemporaryWriteFile.cs # commit/rollback wrapper
|
|
```
|
|
|
|
Tests live under `tests/Opc.Ua.Client.Tests/FileSystem/`.
|
|
|
|
## Server-side file bindings
|
|
|
|
Both lazy `FileSystemNodeManager` nodes and materialized
|
|
`FileDirectoryBinder` nodes protect their mounted root from deletion,
|
|
move, and copy. A NodeId from another namespace, an invalid encoded type,
|
|
or a component identifier cannot alias a file-system object.
|
|
|
|
Virtual directory browsing uses one provider cursor and preserves provider
|
|
order across Browse/BrowseNext pages. It retains only the current entry and a
|
|
continuation's lookahead reference. It does not snapshot the directory or
|
|
apply the materialized binder's `MaxEntries` limit. Named-child lookup scans
|
|
without retaining unrelated entries and stops when it finds a match.
|
|
|
|
The cursor is disposed exactly once on exhaustion, cancellation, or
|
|
continuation release. Cancellation affects only the active page; cancelling
|
|
an already completed page does not cancel later pages. Synchronous
|
|
continuation release starts and observes asynchronous cursor cleanup without
|
|
blocking its caller. The system surfaces provider failures instead of
|
|
returning an empty directory.
|
|
|
|
Before Delete or MoveOrCopy reaches an arbitrary provider, the lazy host
|
|
validates the decoded provider path. Separator-only root aliases resolve to
|
|
the protected empty root. Nonroot paths use forward slashes and contain no
|
|
empty, `.` or `..` segments. On Windows, backslashes and drive/stream
|
|
qualifiers are rejected; on Unix, colon and backslash are ordinary name
|
|
characters and remain addressable. Valid relative paths are passed unchanged.
|
|
The root remains a valid move/copy **destination** for a child, but never
|
|
the object being deleted, moved, or copied.
|
|
|
|
`FileType.Read` requires a positive length and may return fewer bytes than
|
|
requested. The server advertises `MaxByteStringLength` and bounds each
|
|
read by the file's limit, the server byte-string limit, and the available
|
|
single-result binary response budget. If server limits are disabled,
|
|
the default encoding byte-string limit still bounds individual read
|
|
allocations. At EOF, a valid positive-length read returns an empty byte
|
|
string. Service calls use the asynchronous read handler; legacy direct
|
|
synchronous delegates remain available.
|
|
|
|
File opens reserve compatible access before invoking the provider. This
|
|
prevents a rejected erase request from truncating a file held by another
|
|
opener. Failed or cancelled opens release their reservations. If a provider
|
|
returns a stream after session closure or disposal, the system closes it
|
|
instead of publishing it.
|
|
`Read` and `Write` enforce the requested open mode even if the provider's
|
|
stream supports both operations. `SetPosition` clamps positions beyond
|
|
EOF to the current file length. `Writable` describes the provider/file
|
|
capability and does not become false merely because a handle is open.
|
|
|
|
Providers with multiple spellings for one file can implement
|
|
`IFileSystemPathIdentityProvider`. The physical provider uses Windows
|
|
case-insensitive identity and Win32 trailing-dot/space normalization, so
|
|
alternate NodeIds share `OpenCount` and writer exclusion. Metadata reads
|
|
query the provider asynchronously without allocating retained handle state.
|
|
Closed or failed opens release idle handle bags; pending opens remain
|
|
protected by their reservations. Extended Windows roots (`\\?\...`) retain
|
|
literal trailing-dot names rather than conflating distinct directory entries.
|
|
|
|
Materialized directory bindings serialize capacity admission, provider
|
|
mutations, refresh, and teardown. Creates and cross-directory moves/copies
|
|
are rejected before touching the provider when the target directory is
|
|
already at `MaxEntries`; a rename within the same directory needs no extra
|
|
slot. Lookup snapshots are published only after node registration completes.
|
|
Teardown retires lookups and handles before provider cleanup and drains
|
|
queued operations before disposing its synchronization resources.
|
|
|
|
`IFileSystemProvider.CreateDirectoryAsync` remains idempotent, including
|
|
package-upload retries. The OPC UA `CreateDirectory` method checks the
|
|
provider for an existing entry and returns `BadBrowseNameDuplicated`
|
|
without creating or replacing it, independently of the provider.
|
|
|
|
`FileDirectoryBinder.BindAsync` registers the complete pre-existing tree
|
|
before returning, including files on read-only mounts. Supply the
|
|
`registerNode` and `deregisterNode` callbacks directly on its single
|
|
signature. Disposal deregisters materialized nodes from leaves to roots,
|
|
closes handles, and detaches file and directory callbacks. The supplied
|
|
root remains owned by its original node manager.
|
|
|
|
If a provider mutation commits but the subsequent refresh fails, the
|
|
mutation remains successful and an error identifies the unsynchronized
|
|
binding. `RefreshAsync` retries reconciliation, as does the next mutation
|
|
before it changes the provider again. A recoverable pre-mutation refresh
|
|
failure is also logged, without blocking a corrective mutation such as
|
|
deleting an entry after external changes exceeded `MaxEntries`. The error
|
|
records whether the current provider mutation has committed. Capacity
|
|
admission and provider errors still apply; cancellation before commit
|
|
still stops the operation. Reconciliation does not truncate over-limit
|
|
directories, and an explicit `RefreshAsync` still reports its failure to
|
|
the caller. Failed registration callbacks are retried rather than treating
|
|
an unregistered node as complete.
|
|
|
|
## See also
|
|
|
|
- [SourceGeneratedDataTypes.md](SourceGeneratedDataTypes.md) — for an
|
|
overview of the generated `*TypeClient` proxies the FileSystem client
|
|
builds on.
|
|
- OPC UA Part 5 Annex C (FileType, TemporaryFileTransferType).
|
|
- OPC UA Part 20 §4 (FileSystem object model).
|