Decode.Storage.FileSystem 3.0.0

dotnet add package Decode.Storage.FileSystem --version 3.0.0
                    
NuGet\Install-Package Decode.Storage.FileSystem -Version 3.0.0
                    
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="Decode.Storage.FileSystem" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Decode.Storage.FileSystem" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Decode.Storage.FileSystem" />
                    
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 Decode.Storage.FileSystem --version 3.0.0
                    
#r "nuget: Decode.Storage.FileSystem, 3.0.0"
                    
#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 Decode.Storage.FileSystem@3.0.0
                    
#: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=Decode.Storage.FileSystem&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Decode.Storage.FileSystem&version=3.0.0
                    
Install as a Cake Tool

Decode.Storage.FileSystem

Local FileSystem storage implementation of IStorageService for the Decode.Storage ecosystem.

📦 Installation

dotnet add package Decode.Storage.FileSystem

🛠️ Usage

1. Register Services

In your Program.cs or Startup.cs:

using Decode.Storage.FileSystem.Extensions;

builder.Services.AddFileSystemStorage(options =>
{
    options.BasePath = "C:\\StorageRoot"; // Or load from configuration
});

This registration automatically registers IFileValidator (implemented by FileSignatureValidator) as a singleton in your dependency injection container.

2. Inject and Use in Services

using Decode.Storage.Abstractions;

public class DocumentService
{
    private readonly IStorageService _storage;

    public DocumentService(IStorageService storage)
    {
        _storage = storage;
    }

    public async Task SaveReportAsync(string fileName, Stream content)
    {
        // Writes to a temporary file first, then moves it into place, so readers never observe a
        // partially written file. See the note on atomicity below.
        string savedPath = await _storage.UploadAsync($"reports/{fileName}", content, "application/pdf");
    }

    public async Task<StorageFile?> GetReportAsync(string fileName)
    {
        // Safe streaming and reading
        return await _storage.DownloadAsync($"reports/{fileName}");
    }
}

✍️ Write semantics and atomicity

UploadAsync always writes the content to a temporary file in the destination directory first, then moves it into place with File.Move(temp, final, overwrite: true). The replacement is atomic on every supported target: a reader never sees a half-written file, the destination is never absent, and a crash mid-write leaves the original intact.

Changed in 3.0.0. Until 2.1.0 the netstandard2.1 build had no overwriting File.Move overload and fell back to delete-then-move, which opened a window where the destination did not exist. That was the only known behavioural difference between targets in the whole ecosystem, and it disappeared with the target.

Concurrency is otherwise unsynchronized by design: FileSystemStorageService holds no locks, and the temporary file name is unique per call. Two concurrent uploads to the same path therefore both succeed and the last Move wins — the file is never corrupted, but which write survives is not defined. Serialize at the application level if you need a specific winner.

🔒 Path Containment (changed in 2.0.0)

Every path is resolved against BasePath and verified to stay inside it. Paths that are rooted, or that resolve outside the base directory, are rejected with ArgumentException:

await _storage.UploadAsync("reports/2026/q1.pdf", content); // ok
await _storage.UploadAsync("../../etc/passwd", content);    // ArgumentException
await _storage.UploadAsync(@"C:\Windows\System32\x.dll", content); // ArgumentException

Versions before 2.0.0 sanitized by stripping ".." substrings and then called Path.Combine(basePath, cleanPath). Path.Combine discards its first argument when the second is rooted, so any absolute path escaped the storage root entirely — arbitrary read, write and delete across the filesystem. The substring stripping was also lossy: a legitimate name like report..v2.pdf was silently rewritten to reportv2.pdf.

Both are fixed: containment is now proven by canonicalizing the resolved path, and file names are never rewritten.

Note on GetUrlAsync. It returns a file:// URI containing the absolute server path. That leaks your directory layout if handed to a client — treat it as internal-only.

📄 License

MIT License.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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
3.0.0 102 9/14/2026
2.1.0 93 9/13/2026
2.0.2 86 9/13/2026
2.0.1 92 9/13/2026
2.0.0 128 7/28/2026
1.0.3 125 6/22/2026