File fields and attachments
Use IFileFieldService to upload directly for a File or Table<File>
field. Use IAttachmentService for a resource’s attachments list. Both reuse
the file transport, inspection and recovery mechanisms.
For public REST routes, numeric states, response variants and cancellation, read File API workflows. Catalogue Explorer selection is available in the native UI; it is not a published REST picker. The C# catalogue service is a separate upload surface.
| Intended result | Upload service | Destination | Completion evidence |
|---|---|---|---|
| Resource attachment | IAttachmentService | The resource owner | Available attachment under that owner |
| Scalar File or table File cell | IFileFieldService, then Entity mutation | Definition, field and record | Available file, committed field reference and field readback |
| File in the optional Moltaro catalogue | IFileCatalogService | Explicit user-defined folder | Available file in that folder |
In the WebApp, Upload file opens the computer’s file chooser. Select opens the Moltaro catalogue only while that module is enabled and the user has access. On the Collaboration Attachments tab, upload is the primary action; a comment is optional. Disabling the catalogue does not disable direct uploads.
The field itself stores a reference to FileReference. Catalogue membership
is a separate folder association. Reuse an existing published file with matching
source identity by keeping its reference ID: do not download the source again,
call an upload method, or require its former folder just to reuse it. Identical
bytes in unrelated files do not by themselves authorize sharing or deleting
references. Upload completion alone does not prove that the field was saved.
SVG documents use the same transfer and verification contracts. Preserve their
original name, bytes and declared MIME type. Accepted SVG content is delivered
as application/octet-stream with an attachment disposition and has no inline
preview; DetectedMimeType controls delivery, not the declared MimeType.
Integrity, configured malware scanning and active-markup rejection still apply.
The signature probe is not an SVG sanitizer: it does not prove the absence of
scripts, event handlers or embedded active content. Accepted SVG remains an
untrusted document and must never gain inline eligibility from its name or
declared type. A direct storage GET must carry the signed binary response type
and attachment disposition; the JSON authorization API’s headers do not prove
the headers returned by storage.
Do not rename SVG files, claim they are PNG images, or assign
LegacyUninspected to bypass verification. Card layouts expose preview and
download for saved File cells in tables through the parent record’s access
checks, just as for scalar File values.
Import from a Job
Section titled “Import from a Job”After completion commits, Moltaro sends a RabbitMQ wake-up to the verification
Worker. It drains eligible work continuously and checks again before sleeping.
The default 15-second recovery poll discovers lost notifications and due retries;
it is not a required pause after every file. Applications should observe the
durable upload status and wait for Available, rather than sleep for a guessed
Worker interval or publish their own wake-up messages. Catalogue uploads and
ordinary attachments use the same verification scheduling while keeping their
distinct placement contracts.
Inject IFileFieldService into a Net Operation Project service. It is available
to scheduled Jobs and ordinary functions. S3 uses managed multipart storage;
Database and Local storage receive a server stream and use ordinary descriptor
inspection. Inspect ManagedMultipartEnabled or LegacyStreamEnabled before
opening the stream. All three transports retain the upload operation and exact
replay identity; a completed replay does not consume the supplied stream.
It is not provided by standalone direct-database SDK registration.
Supply MoltaroFileFieldTarget with the Entity definition, immutable field ID
and existing instance ID. For a new record, supply its existing create operation
key and actual draft field values instead. A File cell uses the child field ID;
Moltaro checks both the parent Table and the child File permission. Existing
records need View, Update and field-write permission and must permit updates.
Scheduled system execution carries host-authenticated provenance; an actor ID
supplied in application data does not establish trust.
Keep FileReferenceId stable for one source file and IdempotencyKey stable
for one import version. No folder ID or attachment owner is supplied. Existing
published imports should take the reuse path before opening the source body.
UploadStreamAsync creates a file or replays that exact upload. A different
upload key for an existing file ID returns filesystem.upload.referenceConflict.
For a different source file, upload with a new file ID and save its reference
through the Entity mutation; the previous file’s contents remain unchanged.
IFileCatalogService remains available for an explicitly requested catalogue
placement when the FileSystem module is enabled; ordinary actors also need
folder access. It does not move existing references between owners or folders.
The complete helper below advances one import attempt. Register the helper in
your project and inject the two platform services. Pass the same request
metadata and mutation id when resuming the same source version. false means
that verification is still pending; retain progress and retry the Job later.
The caller opens and disposes request.ContentStream.
using System.Text.Json;using Moltaro.Package.NET;using Moltaro.Package.NET.Automation;
namespace Moltaro.Operational.Services;
public sealed class DirectFieldFileImporter( IFileFieldService files, IEntityAutomationCommandService entities){ public async Task<bool> TryAssignScalarAsync( MoltaroFileFieldUploadRequest request, string fieldKey, Guid expectedRowVersion, Guid stableMutationId, CancellationToken cancellationToken) { var instanceId = request.Target.InstanceId ?? throw new ArgumentException("This helper updates an existing record."); var policy = await files.GetUploadCapabilitiesAsync(cancellationToken); if (!policy.ManagedMultipartEnabled && !policy.LegacyStreamEnabled) throw new InvalidOperationException("Direct field transfer is unavailable.");
var transfer = await files.UploadStreamAsync(request, cancellationToken); if (!transfer.IsTerminal) transfer = await files.GetUploadAsync(transfer.UploadSessionId, cancellationToken) ?? throw new InvalidOperationException("The import session is unavailable.");
if (!transfer.IsTerminal) return false; if (transfer.State != MoltaroFileTransferStateEnum.Available) throw new InvalidOperationException($"File verification failed: {transfer.FailureCode}");
var changed = await entities.UpdateAsync(new EntityUpdateCommandRequest( stableMutationId, request.Target.EntityDefinitionIdOrKey, instanceId, new Dictionary<string, JsonElement> { [fieldKey] = JsonSerializer.SerializeToElement( transfer.FileReferenceId, new JsonSerializerOptions()) }, expectedRowVersion), cancellationToken); changed.ThrowIfFailed();
return true; }}After true, reload the record through your normal authorized read path and
check the expected field reference before marking the import complete. For
Table<File>, reconcile the intended rows and their file cells through Entity
mutation, preserving unrelated rows. Persist that row mapping across retries;
a fresh row id on each run creates duplicate documents.
Keep the generated context’s pending changes saved before starting a transfer. Stream upload, status reconciliation and abort cannot run inside an ambient package transaction. The caller owns and disposes the stream. The service never exposes provider credentials or signed storage URLs.
The body checksum and size are verified before publication. If the legacy system knows the creator, supply the corresponding workspace user id; explicit unknown ids fail. Omit an unknown historical creator instead of inventing one. Keep the historical timestamp separate from the runtime actor.
Retry and acceptance
Section titled “Retry and acceptance”Workspace administrators can observe a background upload through
GET /api/workspace/files/uploads/administration/{uploadSessionId}/progress.
Resolve this path against the installed workspace API and confirm that release
exposes it. It is a read-only diagnostic for the exact session, including system
automation; it does not grant file access or replace the creator-owned SDK
status/abort methods.
For managed storage, the API and storage signer must both support this observation contract. An older or unavailable signer leaves progress unknown; the runtime does not fall back to the resume operation, which renews activity.
Match UploadSessionId, SourceFunctionRunId and SourceJobId before acting on
an observation. DeclaredSizeBytes and the active-list ReservedBytes are not
proof of progress. ProviderConfirmedBytes and ProviderConfirmedPartCount
are non-null only when Observation is ProviderConfirmed (0). An actual empty
provider inventory returns zero. NotUploading (1), Expired (2),
SessionChanged (3) and ProviderUnavailable (4) return null counts, including
when a provider read races with a lifecycle transition. The observation does
not update activity, expiry or reservations and returns no provider locators,
ETags or signed URLs. Anonymous and ordinary callers are denied; a missing
session is HTTP404 for an administrator.
To establish interruption during a managed upload, observe Status=Uploading
and 0 < ProviderConfirmedBytes < DeclaredSizeBytes for the selected source
Job, then request cancellation through the administrative function-operations
Job endpoint. Confirm the actual terminal result before replaying. A completed
Job or reserved capacity alone does not establish an interrupted transfer.
After interruption, repeat the same logical operation with identical source facts and a fresh readable stream. Moltaro resumes or returns the completed operation. Do not put the current time or an arbitrary retry number into the idempotency key. A changed source version is a new logical operation.
Field GetUploadAsync and AbortUploadAsync take the session ID and enforce
its creator identity. Abort remains possible after destination access is lost.
Catalogue methods additionally take their original folder ID. Preserve the
returned failure code and retry information from MoltaroSdkException.
For Entity mutation failures also retain every structured error’s code, field
and arguments; a generic “upload failed” discards the failing boundary.
Acceptance must exercise the actual importer: upload, wait for availability, save the scalar/table reference, reload the record, and download the expected bytes. Repeat unchanged input and a stopped Job. A test that seeds catalogue files directly does not establish that the Job uploaded them correctly.