CodeLogic.Storage 4.6.76-preview

This is a prerelease version of CodeLogic.Storage.
This package has a SemVer 2.0.0 package version: 4.6.76-preview+2a346ca.
There is a newer version of this package available.
See the version list below for details.
dotnet add package CodeLogic.Storage --version 4.6.76-preview
                    
NuGet\Install-Package CodeLogic.Storage -Version 4.6.76-preview
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CodeLogic.Storage" Version="4.6.76-preview" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CodeLogic.Storage" Version="4.6.76-preview" />
                    
Directory.Packages.props
<PackageReference Include="CodeLogic.Storage" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CodeLogic.Storage --version 4.6.76-preview
                    
#r "nuget: CodeLogic.Storage, 4.6.76-preview"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CodeLogic.Storage@4.6.76-preview
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CodeLogic.Storage&version=4.6.76-preview&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=CodeLogic.Storage&version=4.6.76-preview&prerelease
                    
Install as a Cake Tool

CodeLogic.Storage

NuGet License: MIT

Provider-neutral, root-scoped storage for CodeLogic 4 and .NET 10. One API mounts local/UNC, S3-compatible, FTP/FTPS, SFTP, WebDAV, Azure Blob, Google Cloud Storage, and OpenStack Swift connections.

Install and load

dotnet add package CodeLogic.Storage
using CL.Storage;

await Libraries.LoadAsync<StorageLibrary>();
await CodeLogic.ConfigureAsync();
await CodeLogic.StartAsync();

var storage = Libraries.Get<StorageLibrary>();
IStorageService media = storage.GetStorage("media");

Every connection mounts exactly one local root, bucket/container prefix, or remote directory. Paths passed to IStorageService are relative slash-separated paths below that mount; rooted paths and .. escapes are rejected.

Providers and configuration

Provider connections live in typed, case-insensitive Connections dictionaries. Connection IDs must be unique across all sections.

Configuration section Connection model Mounted resource
storage.local LocalConnectionConfig local directory or UNC share
storage.s3 S3ConnectionConfig bucket plus optional prefix
storage.ftp FtpConnectionConfig FTP/FTPS directory
storage.sftp SftpConnectionConfig SFTP directory
storage.webdav WebDavConnectionConfig WebDAV endpoint plus root
storage.azure AzureBlobConnectionConfig Blob container plus prefix
storage.gcs GoogleCloudConnectionConfig GCS bucket plus prefix
storage.swift SwiftConnectionConfig Swift container plus prefix

The storage section selects DefaultConnection, controls the byte-buffering limit, and enables bounded health probes. Example config.storage.s3.json:

{
  "Connections": {
    "media": {
      "Enabled": true,
      "Bucket": "company-media",
      "Prefix": "production",
      "Region": "eu-north-1",
      "AuthenticationMode": "DefaultCredentialChain"
    },
    "minio": {
      "Enabled": true,
      "Bucket": "documents",
      "ServiceUrl": "https://minio.example.com",
      "ForcePathStyle": true,
      "AuthenticationMode": "StaticCredentials",
      "AccessKey": "...",
      "SecretKey": "..."
    }
  }
}

Clear-text custom S3 or WebDAV endpoints require AllowInsecureHttp = true. SFTP requires at least one SHA-256 host-key fingerprint. FTPS and WebDAV use normal certificate validation by default and optionally accept configured SHA-256 certificate pins; there is no accept-any switch.

Common API

await using var source = File.OpenRead("photo.jpg");
Result<StorageItem> uploaded = await media.UploadAsync(
    "photos/photo.jpg",
    source,
    new StorageUploadOptions
    {
        Overwrite = false,
        ContentType = "image/jpeg",
        Metadata = new Dictionary<string, string> { ["owner"] = "42" }
    });

Result<StoragePage> page = await media.ListAsync("photos", new StorageListOptions
{
    Recursive = true,
    PageSize = 250
});

Result<byte[]> range = await media.DownloadBytesAsync(
    "photos/photo.jpg",
    new StorageDownloadOptions { Offset = 1024, Length = 4096 });

Result deleted = await media.DeleteAsync(
    "photos",
    new StorageDeleteOptions { Recursive = true });

The common contract includes info/exists, paged recursive listing, physical or virtual directory creation, streaming and bounded byte uploads/downloads, ranges, delete, copy, move, and cancellation. Caller upload streams remain open. Returned download streams own their provider response and registry lease and must be disposed.

Use EnumeratePagesAsync or EnumerateItemsAsync to walk continuation tokens without buffering a complete remote tree. Bounded, order-preserving helpers are available for batch info, delete, copy, and move operations.

Safe transfers

The library can copy or move files and complete directory trees between any two mounted connections:

Result copied = await storage.CopyAsync(
    "primary", "exports/2026",
    "archive", "yearly/2026",
    new StorageTransferOptions
    {
        Overwrite = true,
        MetadataPreservation = StorageMetadataPreservation.BestEffort
    });

Result moved = await storage.MoveAsync(
    "incoming", "ready/item.bin",
    "processed", "item.bin");

Cross-provider data uses a System.IO.Pipelines relay capped at 1 MiB. Each destination file is uploaded to a unique staging name and committed only after the complete source stream succeeds. Existing destination files are backed up and restored if a later directory item fails. A move deletes its source only after the entire destination commits. Equal paths and a directory destination below its source are rejected.

The normal IStorageService.CopyAsync and MoveAsync methods use the same coordinator for recursive work. Safe same-provider file copies remain server-side when the provider can guarantee them.

Local directory trees can be transferred without manually registering a temporary local connection:

Result<StorageDirectoryTransferReport> upload = await storage.UploadDirectoryAsync(
    @"C:\exports\2026", "archive", "yearly/2026");

Result<StorageDirectoryTransferReport> download = await storage.DownloadDirectoryAsync(
    "archive", "yearly/2026", @"C:\restore\2026");

Links/reparse points in a local upload are rejected rather than followed. Reports contain file, directory, and byte counts.

File, text, JSON, progress, and integrity helpers

StorageServiceExtensions adds:

  • UploadFileAsync and atomic DownloadToFileAsync;
  • bounded ReadTextAsync / WriteTextAsync with explicit encodings;
  • bounded ReadJsonAsync<T> / WriteJsonAsync<T>;
  • UploadWithProgressAsync / DownloadWithProgressAsync;
  • streaming ComputeChecksumAsync / VerifyChecksumAsync using MD5, SHA-256, SHA-384, or SHA-512.
var progress = new Progress<StorageTransferProgress>(value =>
    Console.WriteLine($"{value.BytesTransferred} bytes"));

await media.UploadWithProgressAsync("large.bin", input, progress);
Result<StorageChecksumVerification> verified = await media.VerifyChecksumAsync(
    "large.bin", expectedSha256Hex);

MD5 is supplied only for interoperability; prefer SHA-256 or stronger for security-sensitive checks.

Capabilities and advanced contracts

Capabilities are granular flags plus provider limits. Check them at runtime rather than inferring behavior from a provider name:

if (media.Capabilities.Supports(StorageFeature.MetadataWrite))
    await media.SetMetadataAsync("photo.jpg", new Dictionary<string, string> { ["reviewed"] = "yes" });
Provider Directories Metadata Tags Conditional create/update/delete Versions Signed URLs
Local / UNC physical no no create no no
S3-compatible virtual read/write read/write yes/yes/yes read/list/delete read/write
FTP / FTPS physical no no no no no
SFTP physical no no no no no
WebDAV physical discovered properties are read-only no create no no
Azure Blob virtual read/write read/write yes/yes/yes read/list/delete SAS when credentials permit
Google Cloud Storage virtual read/write no portable contract yes/yes/yes read/list/delete when signing credentials permit
OpenStack Swift virtual read/write no portable contract yes/yes/yes endpoint-specific/native no portable TempURL contract

Advanced functionality stays out of the basic interface and is exposed through capability-gated optional contracts:

  • IStorageMetadataService: merge or replace user metadata, optionally matching ETag/version;
  • IStorageTagService: read, merge, or replace up to ten portable object tags;
  • IStorageSignedUrlService: temporary read or write URLs with bounded expiry;
  • IStorageVersionService: exact-object version pages and exact-version deletion.

Convenience extension methods (GetMetadataAsync, SetMetadataAsync, GetTagsAsync, SetTagsAsync, CreateSignedUrlAsync, ListVersionsAsync, EnumerateVersionPagesAsync, and DeleteVersionAsync) return storage.unsupported when the active backend does not implement the operation.

Atomic upload/delete identity checks use StorageMutationCondition:

await media.UploadAsync("settings.json", replacement, new StorageUploadOptions
{
    Condition = new StorageMutationCondition
    {
        ExpectedETag = current.Value!.ETag,
        ExpectedVersionId = current.Value.VersionId
    }
});

Providers that cannot enforce the condition atomically reject it instead of performing a racy check-then-write.

Runtime connections, health, and native clients

await storage.AddOrUpdateConnectionAsync("backup", new SftpConnectionConfig
{
    Host = "sftp.example.com",
    Username = "backup",
    AuthenticationMode = SftpAuthenticationMode.PrivateKey,
    PrivateKeyPath = @"C:\keys\backup_ed25519",
    HostKeyFingerprints = ["SHA256:..."]
});

Result health = await storage.CheckConnectionHealthAsync("backup");

Runtime changes can be persisted to the typed provider JSON section or installed for the process only with persist: false. Stable service proxies and active download/native leases keep an old backend alive until in-flight operations drain during replacement or shutdown.

Reusable native SDK clients and scoped session clients remain available as an escape hatch:

IAmazonS3 s3 = storage.GetNativeClient<IAmazonS3>("media");

var opened = await storage.OpenNativeConnectionAsync<AsyncFtpClient>("legacy-ftp");
if (opened.IsSuccess)
{
    await using var lease = opened.Value!;
    AsyncFtpClient ftp = lease.Client;
}

Do not dispose reusable clients returned by GetNativeClient; dispose session leases.

Failures and compatibility

Expected failures use stable storage.* error codes such as storage.not_found, storage.conflict, storage.unauthorized, storage.too_large, and storage.unsupported. Incomplete cleanup/source deletion is reported as storage.partial_failure with sanitized state and error codes. Provider bodies, credentials, and signed query strings are not exposed. Caller cancellation propagates as OperationCanceledException.

Migrating from the legacy S3-only package? See MIGRATION.md.

Requirements

  • CodeLogic 4
  • .NET 10

MIT license.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.6.79 15 8/2/2026
4.6.78 17 8/2/2026
4.6.77-preview 12 8/2/2026
4.6.76-preview 19 8/2/2026
4.6.75-preview 20 8/1/2026

# Changelog

## Unreleased

### Added

- Added mounted local/UNC, S3-compatible, FTP/FTPS, SFTP, WebDAV, Azure Blob, Google Cloud
 Storage, and OpenStack Swift backends behind one `IStorageService` contract.
- Added typed provider configuration, runtime add/update/remove persistence, stable service proxies,
 per-connection health, custom-backend registration, native clients, scoped native sessions, and
 asynchronous library disposal.
- Added granular `StorageFeature` flags and provider limits for page, object, metadata, batch, and
 multipart boundaries.
- Added bounded cross-connection file and recursive-directory copy/move, plus rollback-safe local
 directory upload/download reports.
- Added page/item async enumeration and bounded order-preserving batch info/delete/copy/move helpers.
- Added file, text, JSON, progress, and streaming checksum convenience APIs.
- Added optional metadata, object-tag, signed-URL, and object-version contracts. S3 and Azure Blob
 support bounded tag reads plus merge/replace updates. S3, Azure Blob, and GCS support exact version
 listing/deletion; S3, Azure, and signing-capable GCS credentials support temporary URLs.
- Added atomic ETag/version upload and delete conditions where providers can enforce them.
- Added typed write/delete/copy/move and cross-connection completion events.
- Added a migration guide from `CodeLogic.StorageS3`.

### Safety

- Centralized path normalization and source/destination relationship checks; equal transfers and
 directory moves/copies below their source are rejected by the library and direct backends.
- Staged local, FTP, SFTP, and WebDAV overwrites so a failed upload cannot truncate existing data.
- Made recursive transfers use unique staging objects, preserve caller upload streams, hold registry
 leases, and cap relay read-ahead at 1 MiB.
- Back up pre-existing destination files and restore them when a later directory item fails. Incomplete
 cleanup or source deletion returns sanitized `storage.partial_failure` state.
- Propagate caller cancellation and keep provider response/session ownership attached to returned
 download streams.
- Enforce byte-buffering, metadata, multipart, serialization, and batch limits.
- Removed certificate and host-key bypass behavior. Clear-text custom endpoints require explicit opt-in;
 SFTP host-key trust is mandatory; FTPS/WebDAV certificate pins use SHA-256.
- Reject header injection, transport-managed custom headers, unsafe endpoint URL components, malformed
 metadata, and unsupported version/metadata options instead of silently ignoring them.
- Sanitized public provider errors so credentials, signed query strings, raw response bodies, and
 provider exception messages are not exposed.

### Changed

- Recursive service copy/move now always uses the safe coordinator; same-provider file staging remains
 server-side when the backend advertises a safe native copy.
- Object-provider listing distinguishes an exact file path from a virtual directory and treats root
 directory creation as an idempotent no-op.
- S3 uploads use explicit bounded multipart handling for seekable and non-seekable streams.
- Google Cloud downloads stream through a bounded pipe and listings use real provider paging.
- WebDAV metadata-read capability is now callable through `IStorageMetadataService`; property writes
 remain explicitly unsupported by the portable adapter.