Files & Attributes
Everything about a file beyond its bytes: permissions, ownership, timestamps, links, checksums, metadata, tags, versions, and signed URLs — plus raw server commands and free space.
Every method here is an extension on IStorageService and returns storage.unsupported when the
connection cannot do it. Check Capabilities first when you need to know in advance.
What a listing tells you
StorageItem carries, wherever the provider reports them:
| Property | Source |
|---|---|
UnixMode, Permissions (rwxr-xr-x) |
FTP listings, SFTP, local (Unix) |
Owner, Group |
FTP listings |
OwnerId, GroupId |
SFTP |
LinkTarget |
FTP listings, local |
Created, LastAccessed |
local, FTP listings (Created), SFTP (LastAccessed) |
IsHidden |
dot-files; local hidden attribute |
ETag, VersionId, ContentType, metadata |
object stores, WebDAV; local files get a weak ETag (W/"…") from their write time, creation time, and size |
ModifiedPrecision |
how coarse LastModified is when a listing gives only minutes or days (FTP LIST without MLSD); null when exact |
Sha256 |
only on an item returned by a verified upload or streamed write |
Permissions, ownership, timestamps, and links
await files.SetPermissionsAsync("reports/q3.csv", "640");
await files.SetPermissionsRecursiveAsync("public", fileMode: 0x1A4, directoryMode: 0x1ED); // 0644 / 0755
await files.SetOwnerAsync("reports/q3.csv", ownerId: 1001, groupId: 1001);
await files.SetTimestampsAsync("reports/q3.csv", lastModified: sourceTime);
await files.CreateLinkAsync("current", "releases/v42");
var link = await files.ReadLinkAsync("current");
| Local | FTP | SFTP | |
|---|---|---|---|
Permissions (Permissions) |
Unix only | SITE CHMOD |
yes, incl. setuid/setgid/sticky |
Owner/group (Ownership) |
no | no | numeric IDs |
Timestamps (SetTimestamps) |
modified + accessed | modified only (MFMT or MDTM) |
modified + accessed |
Create link (CreateLinks) |
yes (relative) | no | yes |
Read link (ReadLinks) |
yes | from listings | no (SSH.NET lacks readlink) |
Link targets must stay inside the mounted root. On Windows, creating local links needs Developer Mode or the symbolic-link privilege.
Server-side checksums
ComputeChecksumAsync and VerifyChecksumAsync ask the server for a stored digest first and only
download the content when there is none; StorageChecksum.Source says which happened.
GetServerChecksumAsync returns only the server's value, and StorageChecksumMode.ComputeOnly
forces a download.
Result<StorageChecksumVerification> verified = await files.VerifyChecksumAsync(
"large.bin", expectedSha256Hex);
| Provider | Server digest |
|---|---|
| S3 | MD5 from single-part ETags of objects without SSE-KMS or SSE-C; SHA-256 when stored with the object |
| Azure Blob | MD5 (Content-MD5) |
| Google Cloud Storage | MD5 of non-composite objects |
| Swift | MD5 ETag, except segmented large objects |
| FTP | HASH/XMD5/XSHA256/XSHA512 when the server offers them |
| SFTP, WebDAV, Local | none (always computed) |
Computed checksums stream through a pooled buffer using MD5, SHA-256, SHA-384, or SHA-512. MD5 is there for interoperability; prefer SHA-256 or stronger for security-sensitive checks.
Metadata, tags, versions, and signed URLs
| 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 (per server, see below) | read/list/delete | read/write |
| FTP / FTPS | physical | no | no | no | no | no |
| SFTP | physical | no | no | no | no | no |
| WebDAV | physical | discovered properties, 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 | yes/yes/yes | read/list/delete | when signing credentials permit |
| OpenStack Swift | virtual | read/write | no | create (update/delete checked just before) | no list or delete; downloads and server-side copies can read a version id | no |
The optional contracts behind these are IStorageMetadataService (merge or replace user metadata,
optionally matching an ETag or version), IStorageTagService (up to ten portable tags),
IStorageSignedUrlService (read or write URLs with bounded expiry), and IStorageVersionService (version
pages and exact-version deletes). The extension methods below, with EnumerateVersionPagesAsync, call them
and return storage.unsupported where a connection has none. A relayed transfer that pins an older
SourceVersionId needs IStorageVersionService on the source.
var metadata = await files.GetMetadataAsync("asset.bin");
await files.SetTagsAsync(
"asset.bin",
new Dictionary<string, string> { ["tier"] = "archive" },
new StorageTagUpdateOptions { Mode = StorageTagUpdateMode.Merge });
var signed = await files.CreateSignedUrlAsync("asset.bin", new StorageSignedUrlOptions
{
Method = StorageSignedUrlMethod.Read,
ExpiresIn = TimeSpan.FromMinutes(10)
});
var versions = await files.ListVersionsAsync("asset.bin");
await files.DeleteVersionAsync("asset.bin", "provider-version-id");
Treat signed URLs as credentials and never log them.
StorageMutationCondition applies ETag/version guards to uploads and deletes:
- Uploads with an ETag or version
Conditionare atomic on Azure Blob, Google Cloud, and S3 servers that enforceIf-Matchon bothPutObjectandCopyObject(AWS; underConditionalRequests = Autothis is probed). Everywhere else, MinIO included once the probe has run, the content is staged and the condition checked immediately before the staged file replaces the destination. An upload returns the stored item, not how its condition was enforced; ask the connection withGetConditionEnforcementAsync(MatchVersion)(see Guaranteed transfers; on MinIO it answersCheckedBeforeCommit), or checkStorageFeature.ConditionalUpdate, which is what decides the staging. - Create-only uploads (
Overwrite = false, not otherwise staged) are atomic on Local, WebDAV, S3 servers that enforceIf-None-MatchonPutObject(AWS and MinIO), Azure Blob, Google Cloud, and Swift. FTP and SFTP have no atomic create-if-absent and do not declareStorageFeature.ConditionalCreate; a caller that must not race should check that flag (MinIO's is cleared after the probe, because its copies do not enforce it) or askGetConditionEnforcementAsync(StorageConditionKind.CreateOnly). Local overwrites are staged too: the new content is written to a temporary file in the same folder and moved over the target, so a reader never sees a half-written file. - Deletes are atomic on Azure Blob, Google Cloud, and S3 servers that enforce
If-MatchonDeleteObject. Swift, and S3 servers that ignore it (MinIO), check the condition immediately before the delete. Local, FTP, SFTP, and WebDAV refuse a conditional delete withstorage.unsupported. - Swift ignores
If-Matchon writes, so it declares onlyConditionalCreate. - Local ETags are weak (
W/"…"): a different one proves the file changed, but a matching one does not prove it did not (two versions written within the file system's clock resolution can share it). - A WebDAV upload never replaces a folder: an existing collection at the destination is
storage.conflict.
await files.UploadAsync("settings.json", replacement, new StorageUploadOptions
{
Condition = new StorageMutationCondition { ExpectedETag = current.Value!.ETag }
});
Raw commands
With AllowRawCommands: true on an FTP or SFTP connection, ExecuteCommandAsync sends a raw FTP
command (SITE ..., SYST) or runs an SSH shell command as the connection's account:
var reply = await files.ExecuteCommandAsync("SITE CHMOD 640 reports/q3.csv");
if (reply.IsSuccess && !reply.Value!.Succeeded)
Console.WriteLine($"Server said {reply.Value.Code}: {reply.Value.Output}");
It is off by default because commands are not confined to the connection's Root (an SSH command starts
in the account's home folder). A rejected command or non-zero exit comes back as a result with
Succeeded = false, not as an error; an empty or multi-line FTP command is storage.invalid_content.
Servers that allow only SFTP (ForceCommand internal-sftp) refuse shell commands.
Free space
GetSpaceAsync reports free and used space: SFTP through statvfs@openssh.com, FTP through AVBL
where the server implements it (available bytes only; total and used stay null), and local connections
from the volume. WebDAV and object stores return storage.unsupported.
var space = await files.GetSpaceAsync();
if (space.IsSuccess && space.Value!.AvailableBytes < 10L * 1024 * 1024 * 1024)
Console.WriteLine("Less than 10 GiB left.");