Skip to content
Gustavo Viana edited this page Aug 4, 2026 · 7 revisions

FAQ / Troubleshooting

Quick answers to the questions newcomers hit most. Each entry links to the deeper page when there is one.

Choosing a driver

Which driver should I pick?

Use case Driver
Production on a single VM / on-prem LocalFileHub
Unit tests, in-process state MemoryFileHub
AWS S3 general-purpose buckets AmazonS3FileHub
Oracle Cloud Object Storage OracleObjectStorageFileHub
Plain FTP server FtpFileHub
Anything else (Azure Blob, GCS, SFTP, …) Write a custom driver

Is Azure Blob Storage supported?

Not yet, planned. Track on the issue tracker. Until then, write a custom driver against the Azure SDK — the Custom drivers page walks through the contract; the existing S3 driver is the closest reference.

Should I depend on IFileHub or on the driver marker (e.g. ILocalFileHub)?

Default to IFileHub. Use the marker only when a consumer genuinely needs a backend-specific guarantee. Prefer capability interfaces such as IUrlAccessible, ISignedUploadable, or IMultipartUploadSignable where available rather than casting directly to the driver type. Stream-based multipart writes use the common FileWriteOptions.StreamPreference API.

Testing

How do I mock FileHub?

Don't. Use MemoryFileHub instead — it's the real implementation, in-process, no shared state, zero setup. Mocking the interface re-implements the contract badly; the in-memory hub is the contract.

var hub = new MemoryFileHub();
var svc = new ReportService(hub);
svc.Save("report.pdf", "hello");
Assert.Equal("hello", hub.Root.OpenFile("report.pdf").ReadAllText());

More patterns: Testing.

Will MemoryFileHub behave the same as LocalFileHub in production?

For files, directories, sandbox enforcement, and read-only wrapping — yes. Search patterns (GetFiles("*.log")) now use the same globbing rules across both drivers (*, ?, [abc] character classes), so a pattern that matches in tests matches in production.

Search (GetFiles, glob patterns) is deliberately case-insensitive on every backend, including LocalFileHub on Linux — this diverges from Directory.GetFiles, which is case-sensitive there. A pattern that matches in tests matches in production regardless of host OS.

The differences that remain:

  • Disposal / handle exhaustion semantics — Memory has no OS handle limits.
  • File system case sensitivity for direct name lookups (OpenFile("A.txt") vs OpenFile("a.txt")) follows the host OS for LocalFileHub; MemoryFileHub is always case-insensitive.
  • Symlinks / reparse points — Memory has none.

When you need true disk behaviour in a test, point LocalFileHub at Path.GetTempPath() and clean up afterwards.

How do I exercise the cloud drivers in tests without hitting AWS / OCI?

Use MemoryFileHub for the service-under-test, since your code depends on IFileHub. The cloud driver's own internals are covered by FileHub's integration tests (tests/FileHub.AmazonS3.Tests/Integration/...) which only run when credentials are present.

Do not mock IAmazonS3 / ObjectStorageClient directly — those are SDK types; the driver wraps them behind an internal contract you can't reach. If you need to assert "driver X was called", inject MemoryFileHub and assert on its state instead.

Errors and exception types

What exception types does FileHub throw?

Condition Type
Missing file FileNotFoundException
Missing directory DirectoryNotFoundException
Sandbox escape (.., absolute, symlink out of root) FileHubException
Write on a read-only entry FileHubException
Invalid name (null, empty, ., .., path chars, or a //\ passed to Rename) ArgumentException
Access denied by the backend (all drivers incl. Local) UnauthorizedAccessException
Driver wraps an I/O-shaped backend failure FileHubException (inner = SDK exception)
Stream re-open on S3 / OCI while another is live InvalidOperationException
Copy succeeded, source delete failed during MoveTo PartialMoveException
Deleting a non-empty directory without recursive: true DirectoryNotEmptyException
Copy/move onto an existing entry (default overwrite: false), CreateFile(name) onto an existing target, or Rename onto a taken name FileAlreadyExistsException

FileHubException : IOException and FileAlreadyExistsException : FileHubException : IOException, so a generic IOException handler catches every FileHub error — the refined types just let you catch narrower where you want to.

UnauthorizedAccessException is surfaced as-is on every driver (including Local) — it is no longer wrapped in FileHubException.

Cloud driver fails at construction with "Access Denied" — what's wrong?

Most common cause: pointing a AmazonS3FileHub at an S3 Express One Zone (Directory) bucket. Those use a different auth flow (CreateSession) and are not supported. Verify the bucket type in the AWS console; if --x-s3 is at the end of the name, it's a Directory bucket.

For OCI: check that the user / instance principal has both OBJECT_READ and OBJECT_INSPECT on the bucket. The GetNamespace and HeadBucket calls at construction time need the latter.

"Cannot open a second stream on this file"

Object-storage and FTP drivers only allow one stream open per file at a time (read or write). Either dispose the first stream before opening the second, or use a different FileEntry instance.

using (var r = file.GetReadStream()) { /* read */ }
using (var w = file.GetWriteStream()) { /* write */ }  // OK — first one disposed

Cloud drivers — cost and behaviour

Why is dir.Delete() so slow / expensive on S3 / OCI?

Object storage has no native "delete prefix" primitive — the driver lists every object and issues one DeleteObject per key. Cost grows linearly with object count. For large prefixes, prefer an S3 Lifecycle rule / OCI Lifecycle policy instead. Details: S3 → Recursive delete cost, OCI → Recursive delete cost.

How do I change an object's metadata or storage class?

There is no metadata-only update API. Write the object again with FileWriteOptions (or the S3 subclass S3WriteOptions) — this re-uploads the bytes with the new metadata applied. Note that LocalFileHub and FtpFileHub silently ignore metadata fields (their backends have no per-object metadata API).

var file = hub.Root.OpenFile("doc.pdf");
var bytes = file.ReadAllBytes();

file.SetBytes(bytes, new S3WriteOptions { StorageClass = "GLACIER" });   // 1 PutObject

For storage-class transitions on large or many objects, prefer an S3 Lifecycle rule (background, no byte transfer through your process) over re-uploading — see S3 → Fields NOT exposed.

Pagination on cloud drivers is slow past page 10 — why?

Index-based offsets force the driver to walk every object up to the offset. Cloud stores paginate by string cursor. Use FileListOffset.FromName(lastSeen) for deep paging:

hub.Root.GetFiles(offset: FileListOffset.FromName(lastSeen), limit: 100);

Details: Usage → Pagination.

Sync API on S3 / OCI under ASP.NET (classic) / WPF — will it deadlock?

The cloud drivers route every sync overload through an internal SyncBridge that hops to the thread pool, defending against the common single-thread SynchronizationContext deadlock. It is not a universal guarantee — see Usage → Sync-over-async and deadlocks for the exact contract and edge cases. Default to async under any legacy UI / ASP.NET-classic host.

DI and lifetimes

What lifetime should my hub have?

  • Local / Memory — singleton.
  • Cloud (S3, OCI) — singleton. The hub owns the SDK HTTP client; recreating per request wastes connections.
  • FTP — singleton. The hub owns the control connection.
  • Per-tenant hubs — scoped, via AddNamedFileHubs(..., ServiceLifetime.Scoped).

Can I use IServiceProvider.GetKeyedService<IFileHub>("name") to fetch named hubs?

No. FileHub does not register hubs as keyed services. Inject INamedFileHubs and call GetByName("name") instead. The registry is scoped — don't capture it from a singleton.

Can I register two hubs of the same type with different roots?

Yes, via AddNamedFileHubs. Each entry gets its own name and lifetime. See Dependency Injection → Named hubs.

Sandbox and security

Can a user-supplied filename escape the sandbox?

No. Every relative name passes through ValidateName (blocks null, empty, ., .., path chars) plus ResolveSafePath (blocks paths that fall outside the root, including symlink targets on .NET 8+). Pass user input directly into CreateFile(userName) — it's the supported tenant-isolation pattern.

var hub = new LocalFileHub($@"C:\tenants\{tenantId}");
hub.Root.CreateFile(userSuppliedName);   // confined to that tenant

Details: Security.

How do I hand a read-only view of a directory to plugin / untrusted code?

Wrap with AsReadOnly(). Anything navigated from the wrapper is read-only too — there's no escape by walking the tree.

var ro = hub.Root.OpenDirectory("config").AsReadOnly();
RunPlugin(ro);   // any CreateFile / Delete / Rename throws FileHubException

Versioning and stability

Is FileHub production-ready?

The API is consolidating. Pre-v1 releases follow SemVer minor for breaking changes — pin to a minor ([0.x, 0.x+1)) until v1.0 ships. Drivers and the core ride the same version. File issues for anything surprising.

Reporting and contributing

I found a bug / I have a feature request

Open an issue on the issue tracker. Bug reports with a 10-line repro using MemoryFileHub get fixed fastest.

Where do I propose a new driver?

Drop a sketch in an issue first (which backend, which capabilities, what the auth surface looks like). Then implement against Custom drivers; the S3 / OCI / FTP drivers are reference implementations.

Clone this wiki locally