mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-24 19:13:02 +08:00
# Description Remove stock phrasing and correct misleading or inconsistent technical documentation. The initial two passes covered 367 Markdown files, followed by an interactive review of 169 editorial flags. The U5 interpretation originally deferred in that review has since received the upstream Mantis answer recorded by #4843; remaining server/spec follow-ups stay explicit in the CTT log. - Correct package responsibilities, migration steps, API names, ownership, protocol descriptions and links. Keep bundled migration guides synchronized. Preserve historical conclusions as historical records with separate re-check notes. - Provide at least two rendered TOC levels throughout `docs/`. Convert appropriate architecture figures to Mermaid without changing code examples, transcripts or historical quotations. - Address the October 19 feedback with concrete setup/migration instructions, corrected certificate paths and PubSub names, shorter repeated caveats, consistent XML comments, and a full review of the Concepts guide. Retain #4839's API corrections. - Integrate #4841's runtime/performance guidance and review incoming/recent prose. Preserve dated measurements rather than claiming new benchmark runs; clarify allocation ratios, startup-only processor settings, JIT warm-up and the benchmark commit's scope. Missing raw measurement artifacts and repeat ranges remain tracked by #4842. - Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log conflict using the updated Mantis states and OPC answers, retaining historical framing and re-checks for C28/C32/C43. - Address the latest naming/index feedback in `adb5bf693`: rename the guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the provider and DataType-declaration contracts into the conversion guide and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and `samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di, FluentApi, ISA95, Lds and PubSub; update solution entries, project references, Docker/CI paths, test fixture paths and documentation links with exact casing. **WoT guide scope.** This branch retains the detailed conversion guide that #4758 had replaced with an anchor stub. Conversion limits, verified document sets, preservation rules and provider ownership now live together in `WotNodeSetConversion.md`; binding/runtime guidance remains in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger prose was shortened, the oversized argument-table cell moved into its section, and existing anchors preserved. **SDK policy is a separate, explicit user-directed change.** `global.json` sets a stable minimum of `10.0.400`, `rollForward: latestMajor`, and `allowPrerelease: false`. Newer major SDKs are permitted; CI's explicit SDK pins are unchanged. README, GettingStarted, DeveloperGuide and Recycling instructions reflect the possible analyzer differences. The image-contract regression fix accepts compatible stable SDK images instead of requiring exact equality with the floor. The latest four rename/index threads are resolved after publication. Three older threads remain open: universal obsolete-removal timing, unconditional nonce-strength wording, and a blanket Vision NativeAOT claim. Existing implementation/evidence constraints are explained in their inline replies. The two-level TOCs remain as requested. ## Validation - `git diff --check` and staged diff checks: passed. The untouched upstream AutoID NodeSet retains its provenance bytes, including two existing license-header trailing spaces. - `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled migration documents and manifests synchronized. - Documentation/path validation: **101** documents retain two rendered TOC levels (**2,228** entries), with idempotent formatting. **52** new relative links and their anchors passed exact-case checks. No tracked references to the old guide/sample paths remain. Both relocated provider-contract sections are preserved in the destination. - `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects and the existing FluentApi/Lds/PubSub projects. Updated solution and pump ProjectReference paths also passed exact-case existence checks. - Latest focused commands, run from the repository root: ```powershell # Passed: 61 channel send-order and reconnect tests. dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal # Passed: 5 pump datasheet tests; builds the renamed pump sample. dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal # Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models. dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal # Passed: build, zero warnings/errors. dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal # Passed: dependent sample-test project build, zero warnings/errors. dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal ``` Earlier focused integration checks passed 2,620 tests across AutoID, event queues/scheduling, source generation, MCP/package profiles and WoT. Both reviewed Mermaid diagrams rendered locally without network access. These checks are not a full solution run. The full solution has **not** been rerun for this review. Earlier `dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did not pass: three AOT projects rejected VSTest and 20 Vision tests failed during graphics initialization with `OutOfMemoryException`. Remaining long-running tests were stopped at the user's request. A focused Vision rerun passed 558 tests and reproduced those 20 failures. The earlier `dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was blocked by the former SDK pin and was not retried after the policy change. The user approved proceeding with those limitations documented; current remote CI is not claimed green. ## Related Issues - Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and diagnostic-tool documentation refresh. #4841 supplies documentation and earlier results, not all newly requested measurements. - #4758 explains the conversion guide's earlier consolidation; the current provider contracts are now consolidated into `WotNodeSetConversion.md`. - #4839 supplies the reviewed API corrections; #4843 supplies the updated CTT issue record. - The original prose cleanup and SDK policy were user-requested without a source issue. No new runtime architecture is introduced by these documentation edits. ## 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. - [ ] 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. - [ ] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Roman Ettlinger <romanett98@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
575 lines
29 KiB
Markdown
575 lines
29 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`) provides asynchronous operations over 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.
|
|
|
|
`UaFileStream` also provides synchronous `Stream` compatibility members;
|
|
prefer its asynchronous methods to avoid blocking. See [Streams](#streams).
|
|
|
|
## 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)
|
|
- [Use a file known by its NodeId](#use-a-file-known-by-its-nodeid)
|
|
- [Path syntax](#path-syntax)
|
|
- [Path resolution and caching](#path-resolution-and-caching)
|
|
- [Type classification (FileType vs FileDirectoryType)](#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 (Part 5 §C.5)](#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)
|
|
- [Physical provider confinement](#physical-provider-confinement)
|
|
- [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);
|
|
```
|
|
|
|
### Use a file known by its NodeId
|
|
|
|
A `FileType` NodeId can come from somewhere other than a path below the
|
|
root, for example from a reference or a method result. Create the
|
|
`UaFileInfo` directly instead of falling back to the raw `FileTypeClient`
|
|
proxy:
|
|
|
|
```csharp
|
|
// The containing directory is known: every operation is available and
|
|
// the path is the directory's path followed by the BrowseName.
|
|
UaDirectoryInfo reports = await fs.GetDirectoryAsync("/Reports");
|
|
var report = new UaFileInfo(reports, reportNodeId, new QualifiedName("summary.json"));
|
|
await report.RefreshAsync();
|
|
await report.DeleteAsync();
|
|
|
|
// Only the NodeId is known: the file is detached. It can be refreshed,
|
|
// opened, read and written, but DeleteAsync, MoveToAsync and CopyToAsync
|
|
// throw InvalidOperationException, because those are methods of the
|
|
// containing FileDirectoryType.
|
|
var result = new UaFileInfo(fs, resultFileNodeId, new QualifiedName("result.bin"));
|
|
byte[] bytes = await result.ReadAllBytesAsync();
|
|
bool detached = result.IsDetached; // true; Parent is null
|
|
```
|
|
|
|
## 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.
|
|
|
|
`FileNodeId` and `Handle` expose the server-allocated identities for companion
|
|
methods that exchange metadata for an open transfer, such as OPC 40083
|
|
production datasets. They do not transfer lifecycle ownership: still commit
|
|
or dispose the wrapper rather than closing the handle separately. Cleanup
|
|
after a closed session uses the normal `BadSecureChannelClosed` protocol
|
|
failure instead of dereferencing a missing channel.
|
|
|
|
For a bounded generated-file download, `CopyToAsync` accepts a caller-owned
|
|
writable stream and a non-negative byte limit:
|
|
|
|
```csharp
|
|
var temp = new TemporaryFileTransferClient(managedSession, transferObject);
|
|
long copied = await temp.CopyToAsync(
|
|
generateOptions: default, destination, maxBytes: 64 * 1024 * 1024, ct);
|
|
```
|
|
|
|
With a `ManagedSession`, this operation captures one `SessionRequestContext`
|
|
for generation, readiness polling, copying and cleanup. Recovery invalidates
|
|
the operation instead of sending the old handle to a replacement session.
|
|
An already-dispatched generation response is retained even if the caller
|
|
cancels before it returns. A valid returned file node and handle are retained
|
|
for cleanup before non-Good statuses or malformed response metadata are rejected.
|
|
Close calls the standard `FileType.Close` declaration directly, without method
|
|
discovery, using an independent five-second deadline. Transfer and cleanup
|
|
failures are both reported when both occur.
|
|
|
|
The destination remains open and may contain partial data on failure. An
|
|
ordinary or custom session remains caller-owned; prevent its replacement during
|
|
the transfer. The operation allows at most 1024 file reads of at most 64 KiB
|
|
(or a smaller configured chunk size), including the end-of-file probe, and at
|
|
most 64 generation-state reads.
|
|
|
|
## 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.
|
|
Materialized `FileDirectoryBinder` bindings supplied with a `ServerSystemContext`
|
|
attach to that server's session lifecycle on the first open. Explicit session
|
|
closure and expiry release only that session's handles and pending reservations;
|
|
other sessions keep their readers and positions. Late opens from a closing,
|
|
expired, or removed session return `BadSessionClosed`. No historical session-ID
|
|
list is retained. Binding disposal removes the lifecycle subscription, so the
|
|
application must dispose the binding when its directory is removed or its node
|
|
manager shuts down. A context without a server session manager has no automatic
|
|
session lifecycle; such standalone bindings remain owned by their caller.
|
|
`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.
|
|
|
|
### Physical provider confinement
|
|
|
|
`PhysicalFileSystemProvider` rejects paths outside its configured root and
|
|
rejects symbolic links, Windows junctions, and other reparse points with
|
|
`UnauthorizedAccessException`. This no-link policy applies even to links
|
|
whose targets are inside the mount, dangling links, and the configured root
|
|
and every existing ancestor of that root. Use a physical root path without
|
|
linked ancestors (for example, avoid the `/var` alias on systems where it is
|
|
a symlink). The checks run before root creation and again on operations.
|
|
On .NET 8 and later, the ReferenceServer resolves system temporary-directory
|
|
aliases before selecting its default mount; provider-relative links remain rejected.
|
|
Enumeration fails on a linked entry rather than exposing its target metadata.
|
|
Recursive copy, move, and delete validate the source tree one directory level
|
|
at a time, before entering any child directory. Copy validates its source tree
|
|
before creating the destination; deletion does not use unchecked recursive
|
|
traversal. Ordinary in-root files, directories, and creation of missing paths
|
|
remain supported.
|
|
|
|
These are **path-based checks, not a race-free filesystem sandbox**. An
|
|
untrusted local process could replace a checked file or ancestor between
|
|
validation and I/O (TOCTOU). Protect the mount and its ancestors with host
|
|
permissions so untrusted processes cannot rename, replace, or introduce
|
|
entries while provider operations run. Existing open handles are not
|
|
revalidated. Hard links and filesystem mount points that are not reported as
|
|
reparse points are outside this policy; use a dedicated filesystem or
|
|
OS-level isolation when local filesystem attackers are in scope. Recursive
|
|
operations are not transactional and concurrent changes or I/O errors can
|
|
leave partial results.
|
|
|
|
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).
|