Skip to content
bitzorcas
中EN

Concept

Files and file assets

A source-verified Files manual covering upload sessions, object verification, asset state, download authorization, deletion, storage adapters, and production gaps.

Last updated

Files promotes bytes in object storage into an authorized, traceable FileAsset that a business module can reference. It has server-generated storage keys, tenant-scoped aggregates, upload finalization, an owner download policy, dual-ORM persistence, and S3 presigned access. It does not yet have malware scanning, quotas, trusted server-side hashing, upload-expiry enforcement, or reclamation of deleted objects.

1. What the module solves

Object storage understands buckets, keys, and bytes. It does not understand tenant ownership, business attachment rules, authorization, or whether an order may bind an object. Files supplies that asset boundary:

  • FileAsset records the tenant, owner, original name, declared metadata, observed object facts, and lifecycle;
  • application use cases create direct-upload capabilities, finalize uploads, grant downloads, and delete assets;
  • IFileStorage isolates Local, S3-compatible, and connector providers;
  • consuming modules persist a FileId and continue to own their business attachment rules;
  • FileAssetSummary is an immutable event/cross-module snapshot, not a mutable aggregate.

The module does not own business attachment tables, image transformation, document previews, content extraction, versioning, or compliance retention decisions.

2. Current end-to-end path

FileAssetSummarypersist FileId only

Authenticated client

Generated /api/files/* endpoints

Mediator
authn / authz / transaction / activity audit

Files commands and query

FileAsset
SysFileAsset

IFileStorage

Local / S3 / connector

Calling module
Ticket / Chat / Export

There are two independent sources of truth: the database stores asset state, while object storage stores bytes. The current code has no atomic transaction across them, so every create, finalize, and delete operation must account for asymmetric failure.

3. HTTP use cases

Method and routeMessageActionCurrent result
POST /api/files/upload-sessionCreateUploadSessionCommandCreateUploadSession with FileId, StorageKey, and URL
POST /api/files/{fileId}/finalizeFinalizeUploadCommandUpdateFileAssetSummary
GET /api/files/{fileId}/downloadGetFileDownloadAccessQueryViewA nominal five-minute download descriptor
DELETE /api/files/{fileId}DeleteFileCommandDeleteSoft-delete and publish a deleted summary

All four messages implement IAuthorizedRequest. The governance catalog declares files.file.create/view/update/delete. After generic View authorization, download also runs FileAssetOwnerPolicy. Delete has no owner policy, so a caller with Delete permission can delete any visible file in the effective tenant.

4. Lifecycle and the binding invariant

create sessionmetadata verifiedfinalizehandler verifies andadvances twicedeletedeletedeleterepeat finalize / conflictrepeat delete / conflict

PendingUpload

Uploaded

Finalized

Deleted

CanBindToBusiness() returns true only for Finalized. Object existence, an Uploaded database row, possession of a storage key, or possession of an upload URL cannot replace this domain decision.

FileAsset.Restore is an ORM materialization factory. It is not a business restore operation. There is no restore endpoint or Deleted-to-Finalized transition.

5. Trust boundaries

DataSourceCurrent trust level
TenantIdICurrentUser.User.TenantIdServer context, but not EffectiveTenantId
OwnerIdcurrent UserId, otherwise ClientIdServer-derived
OwnerType / VisibilityclientNon-empty and length checks only
FileNameclientNever enters StorageKey; still reaches DTOs and headers
Size / ContentTypecreate commandBaseline for finalize comparison
ContentHashfinalize command or object metadataFalls back to the client when a provider has no SHA-256
StorageKeyserver UUID v7tenants/{tenantId}/{fileId}

A file name such as ../file.pdf cannot traverse the current direct-upload key because the key is entirely server-generated. The name remains untrusted display data and needs context-specific escaping in responses, logs, and UI.

6. Persistence model

FileAsset is the unified aggregate persisted to SysFileAsset by both ORMs:

  • tenant filtering and soft deletion are enabled;
  • StorageKey has a global unique index;
  • Tenant + Owner and Tenant + Status have non-unique indexes;
  • StatusName maps to the Status column and fails closed on unknown persisted values;
  • non-Pending rows require ContentHash, and Finalized requires FinalizedAt;
  • StorageKey must start with tenants/{tenantId}/ or {tenantId}/.

Factories validate declared column bounds before persistence: FileId 36, OwnerType 50, OwnerId 64, Visibility 20, StorageKey 512, ContentHash/ETag 128, ContentType 100, and FileName 255.

7. Storage-provider reality matrix

ProviderDirect I/OPresigned uploadMetadata authorityPresigned downloadIntended use
UnavailableFileStorageNoNoNoNoFail-closed shell
LocalFileStorageYesNoTrusted length; null type/hashNoPort tests/custom direct I/O, not the HTTP flow
S3CompatibleFileStoreYesYesLength/type/ETag; SHA from user metadataYesCurrent production path
FileStoreAdapterProvider-dependentProvider-dependentCommon length onlyProvider-dependentBridge exists; API composition does not call it

The Production/Staging startup guard accepts only Minio and requires Endpoint, AccessKey, and SecretKey. Default appsettings.json selects Unavailable.

8. Highest-priority implementation gaps

  1. No maximum size, tenant quota, MIME allowlist, magic-byte validation, or malware scanning.
  2. Presigned PUT has no content-length, content-type, or checksum condition.
  3. UploadExpiresAt is persisted but never checked by finalize.
  4. S3 without x-amz-meta-sha256 trusts the client hash; the connector path also trusts client content type.
  5. Repeated finalize and delete return conflicts; there is no request idempotency or result replay.
  6. Create signs a URL before saving the database row, leaving a capability if Save fails.
  7. Finalize persists and publishes inside the database path, but rollback cannot remove the external object.
  8. Delete only soft-deletes metadata and never invokes object-storage deletion.
  9. Orphan cleanup immediately selects every PendingUpload, ignores UploadExpiresAt, and never selects Deleted files.
  10. Download exposes StorageKey; the connector ignores requested TTL and file name, so ExpiresAt may be inaccurate.

9. Correct responsibility for a caller

For a Ticket attachment, the Ticket module still owns the binding rule:

Ticket-to-file binding boundary
// Read asset facts through a Files contract, not by querying SysFileAsset directly.
var asset = await fileAssets.GetSummaryAsync(command.FileId, cancellationToken);
// The business owner decides whether this asset may become a ticket attachment.
if (asset.IsFailure || asset.Value.Status != FileAssetStatus.Finalized)
return Result.Failure(TicketErrors.AttachmentNotReady);
if (asset.Value.TenantId != currentTenant.EffectiveTenantId)
return Result.Failure(TicketErrors.AttachmentTenantMismatch);
// Persist only the stable FileId, never a URL, bucket, or storage key.
ticket.Attach(command.FileId, command.DisplayName);

This is a consumption example based on current public contracts, not a verbatim source copy. A real caller must also close the OwnerType/OwnerId relationship. The current create route fixes OwnerId to the caller identity; it cannot register an arbitrary business object ID.

10. Reading path

  1. Upload sessions and direct upload
  2. Finalization and content integrity
  3. Download authorization and owner policy
  4. Storage composition, deletion, and cleanup
  5. Testing, operations, and GA gates

Related foundations: Authorization, Multitenancy, Auditing, and GDPR.

11. Minimal source review

Terminal window
# List all four public use cases, routes, and authorization actions.
rg -n "GenerateEndpoint|AuthorizationAction" src/Platform/Files -g '*.cs'
# Search for real scanning, quota, and expiry enforcement; expect no production hit today.
rg -n "Virus|Malware|Quota|UploadExpiresAt.*clock|MagicBytes" \
src/Platform/Files src/Hosts -g '*.cs'
# Prove whether deletion reaches object storage; DeleteFile currently has no IFileStorage.
rg -n "DeleteAsync|DeleteFileCommandHandler" \
src/Platform/Files src/Framework/BitzOrcas.Infrastructure.SqlSugar -g '*.cs'

Back to the module catalog

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%