Decode.Storage.FileSystem 2.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Decode.Storage.FileSystem --version 2.1.0
                    
NuGet\Install-Package Decode.Storage.FileSystem -Version 2.1.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="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Decode.Storage.FileSystem" Version="2.1.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 2.1.0
                    
#r "nuget: Decode.Storage.FileSystem, 2.1.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@2.1.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=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Decode.Storage.FileSystem&version=2.1.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, so a reader never sees a half-written file. How that temporary file becomes the final one depends on the target framework:

Target Replacement Guarantee
net8.0, net9.0 File.Move(temp, final, overwrite: true) Atomic. The destination is never absent, and a crash mid-write leaves the original intact.
netstandard2.1 File.Delete(final) then File.Move(temp, final) Not atomic. File.Move has no overwriting overload on this target, so there is a window in which the destination does not exist — a concurrent reader can observe a missing file, and a crash inside the window loses the original without producing the replacement.

If you deploy on netstandard2.1 and overwrite files that are read concurrently, do not rely on this method for atomic replacement.

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 net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 was computed.  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. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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 104 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