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
<PackageReference Include="Decode.Storage.FileSystem" Version="3.0.0" />
<PackageVersion Include="Decode.Storage.FileSystem" Version="3.0.0" />
<PackageReference Include="Decode.Storage.FileSystem" />
paket add Decode.Storage.FileSystem --version 3.0.0
#r "nuget: Decode.Storage.FileSystem, 3.0.0"
#:package Decode.Storage.FileSystem@3.0.0
#addin nuget:?package=Decode.Storage.FileSystem&version=3.0.0
#tool nuget:?package=Decode.Storage.FileSystem&version=3.0.0
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.1build had no overwritingFile.Moveoverload 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 afile://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 | Versions 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. |
-
net10.0
- Decode.Storage.Abstractions (>= 3.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
-
net8.0
- Decode.Storage.Abstractions (>= 3.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
-
net9.0
- Decode.Storage.Abstractions (>= 3.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.