mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
be50f02700
# Description Expose existing stack APIs through **337 additional domain MCP tools**: 114 industrial-companion tools, 35 WoT/xRegistry/document tools, and 188 higher-level stack-workflow tools. The fully enabled catalogue contains **492 tools**; profile selection is not an execution grant. The packages now follow the reviewed structure: **`Opc.Ua.Mcp.Workflows`** contains roles/users, files, alarms and history; **`Opc.Ua.Mcp.WotCon`** contains management/documents and independently registered live WoT execution. GDS and schema tools remain separate packages. The executable composes **18 libraries**. | Workflow | Added domain tools | Profiles | | --- | ---: | --- | | GDS directory, certificates, reference-only credentials/tokens and Part 21 onboarding | 46 | `gds`, `certificates`, `credentials`, `onboarding` | | Roles/users, regular and temporary files, alarms/conditions and historical access | 74 | `roles`, `users`, `files`, `alarms`, `history` | | Bounded runtime types, XSD/BSD/UA JSON, PubSub and REST OpenAPI artifacts | 6 | `schemas` | | Named PubSub transports, remote configuration, declarative adapters/bridges and SKS | 52 | `pubsub-runtime`, `pubsub-configuration`, `pubsub-bridge`, `pubsub-security` | | Live HTTP(S), MQTT, Modbus TCP and OPC UA WoT forms | 10 | `wot-runtime` | - Reuse typed clients, generated proxies, SDK transports, schema providers and injected services rather than introducing another session manager or protocol engine. - Require host enablement plus exact caller/session/secret/network grants. Protected generic Call/Write/node-management/history/file paths enforce the same policy. - Bind classifications, dispatches, file handles and historical cursors to actual UA generations. Recovery cannot retarget an old handle or authorized mutation. Preserve acknowledged results after late cancellation; do not imply atomicity or retry business commands. - Use opaque secret-store references, explicit writable-store registration, issuer/resource/caller binding and reserved capacity before issuance. Report remote success/local persistence failure as partial completion; redact provider-controlled diagnostic text. - Bound new named runtimes to eight per host, two per caller, five-minute idle expiry and a thirty-minute absolute lease by default. Pending/closing resources consume quota; failed cleanup remains visible. Credential invalidation and session recovery close dependent runtimes. - Keep document planning separate from execution, artifacts root-confined and real JSON inputs typed. Preserve old profile ordinals while widening the set to 64 bits. CI grades fully enabled privileged catalogues rather than connection-only views. **Limitations:** live Kafka remains explicitly unavailable because the backend cannot enforce policy on broker-discovered connections; configuration/schema/capability inspection stays available. The latest public Dekaf v1.23.5 was also checked and its DNS injection remains internal. No private-API workaround or bootstrap-only security claim was added. Full legacy Core registration retains its documented JSON-reflection limitation: native tests isolate its four connection descriptors and validate the expanded production registrations, not whole-host NativeAOT support. ## Related Issues - Fixes #4730. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received.
476 lines
23 KiB
Markdown
476 lines
23 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`). |
|
|
| `SessionFileHandle` | n/a | Opaque, generation-bound handle for bounded transfers that must not follow a replacement session. |
|
|
|
|
## 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)
|
|
- [Generation-bound transfers](#generation-bound-transfers)
|
|
- [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.
|
|
|
|
## Generation-bound transfers
|
|
|
|
Use [`SessionRequestContext`](Sessions.md#generation-bound-client-requests) when
|
|
retaining a remote file handle across calls. If the owning managed session
|
|
recovers, the transfer fails rather than sending the old handle to its
|
|
replacement.
|
|
|
|
```csharp
|
|
using Opc.Ua.Client;
|
|
using Opc.Ua.Client.FileSystem;
|
|
|
|
await using SessionRequestContext context =
|
|
await SessionRequestContext.CaptureAsync(managedSession, ct);
|
|
await using SessionFileHandle file = await context.OpenFileAsync(fileId, mode: 6, ct);
|
|
long bytes = await file.WriteFromAsync(source, maxBytes: 64 * 1024 * 1024, ct: ct);
|
|
CallMethodResult close = await file.CloseAsync(ct);
|
|
```
|
|
|
|
`SessionFileHandle` has no raw-handle constructor or import method and cannot be
|
|
attached to another context. Use it sequentially like a stream and dispose it
|
|
before the context. Disposal attempts Close only on the original session;
|
|
cleanup failures are logged without replacing the transfer failure.
|
|
|
|
For whole transfers, `context.UploadFromWithResultAsync` and
|
|
`context.DownloadToStreamWithResultAsync` return the byte count and unmodified
|
|
Close result, including a refused Close. They enforce chunk and total-byte
|
|
bounds, continue after short nonempty reads, and leave the caller's stream open.
|
|
|
|
Construct `TemporaryFileTransferClient(context.Session, transferId)` for
|
|
generation-bound temporary transfers. Its context-taking generation overloads
|
|
return `SessionFileHandle`; bounded `UploadFromAsync` and `DownloadToAsync`
|
|
preserve the final method result. Upload returns the file ID, the
|
|
completion-state-machine ID and the actual `CloseAndCommit` result.
|
|
`IsCommitted` remains true if a successful commit has malformed output, so
|
|
disposal does not close the consumed handle again.
|
|
|
|
Ordinary Close is not an atomic commit or rollback. A known committed result
|
|
is not replaced by late cancellation. Existing raw-handle helpers remain
|
|
compatible, but keep Open/Generate, every chunk and Close/CloseAndUpdate or
|
|
CloseAndCommit on the same captured session; do not recreate a proxy from the
|
|
newly current connection.
|
|
|
|
## 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
|
|
├── SessionFileHandle.cs # opaque generation-bound transfer handle
|
|
├── 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
|
|
|
|
`IFileTypeBinder` / `FileTypeBinder` binds an already-created `FileState` to
|
|
bounded in-memory content and an optional asynchronous commit callback, without
|
|
a separate file-system node manager. It shares `FileHandle` ownership/mode rules
|
|
and the existing response-size calculation. Bindings allow at most 64 open
|
|
handles, one writer, and independent reader snapshots. Temporary writers commit
|
|
through `CommitAsync`; `commitOnClose` selects the ordinary FileType close lifecycle.
|
|
Application commit callbacks run without holding the binding's operation gate, so
|
|
publication can refresh other file bindings. Disposal cancels and drains an active
|
|
commit before releasing its handles. [Machine Vision](Vision.md) uses this
|
|
surface for its specialized transfers and recipe files.
|
|
|
|
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).
|