Skip to content

Getting Started ​

This page wires the module into a host and walks one file through its life. Why each step exists is in ADR 0010; the exact contract of every field and route is in the specification.

Prerequisites ​

  • PostgreSQL for the module's own schema.
  • An S3-compatible, Azure Blob or Google Cloud Storage bucket, configured in the GrydStorage section that AddGrydObjectStorage reads. MinIO works through the S3 provider (ServiceUrl + ForcePathStyle).
  • GrydJobs, because the confirmation hands the file to the scan job through the job queue, and the purges run as recurring jobs.
  • clamd reachable from the API, for every profile that does not waive the antivirus. See Antivirus & Quarantine for running it.

1. Install ​

bash
dotnet add package GrydFiles                         # Core + Application + Infrastructure + API
dotnet add package GrydFiles.Infrastructure.ClamAv   # the clamd adapter

2. Register ​

The order is the one the module documents: object storage first, then the module, then its engine.

csharp
using Gryd.Infrastructure.Extensions;
using GrydFiles;
using GrydFiles.Infrastructure;
using GrydFiles.Infrastructure.ClamAv;

builder.Services.AddGrydJobs(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString("GrydJobs");
});

builder.Services.AddGrydObjectStorage(builder.Configuration);
builder.Services.AddGrydFiles(builder.Configuration);
builder.Services.AddGrydFilesClamAv(builder.Configuration);
builder.Services.AddGrydFilesHealthCheck();   // optional: the scanner in /health

The engine is its own call. AddGrydFilesClamAv registers ClamAV and binds GrydClamAv for the daemon's address, so the module's builder is not involved. Another engine (GuardDuty, an ICAP appliance) goes through the builder instead: AddGrydFiles(configuration, files => files.UseScanner<MyScanner>()). Do not combine the two: AddGrydFilesClamAv replaces whatever UseScanner<T>() registered.

UseScanner<T>() is the only method on the builder. There is no default profile: every upload names a profile of the GrydFiles:Profiles section, and an upload without a known one is refused with FILE_PROFILE_UNKNOWN. The framework itself declares only two profiles, gryd.report and gryd.notification-attachment, for the modules that store files here; a product declares its own in its appsettings (see Configuration).

The module refuses to start on an incoherent section — a profile ceiling above what clamd accepts, a retention mode without its days, a scanner timeout of zero. That is deliberate (D15): the failure is at deploy time, not on the first large upload. It also refuses to start with no engine at all. Without an IFileScanner the scan job cannot run, so every upload would stay in quarantine while the host reported itself healthy.

GrydReports and GrydNotifications

Both store their files here and refuse to start without their profile. Register object storage, GrydJobs and GrydFiles with the scanner before either module. The gryd-api and gryd-module templates do it for you when Reports or Notifications is included.

3. Migrate ​

The module owns its schema and its migration history table, so it can sit next to any other module's migrations.

csharp
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    await app.ApplyGrydFilesMigrationsAsync();
}

4. One file, end to end ​

Over HTTP, the client makes three calls to the API and two to the bucket. The API never sees the bytes (RN-GF-06).

http
### 1. Reserve — requires upload:files
POST /api/v1/files
Content-Type: application/json

{
  "profile": "nexio.attachment",
  "originalFileName": "cotacao.pdf",
  "contentType": "application/pdf",
  "declaredSizeBytes": 184320
}

The answer carries the fileId, the uploadUrl, when it expires and the requiredHeaders the PUT must repeat — exactly those, since the signature covers them.

http
### 2. Send the bytes straight to the bucket — not to the API
PUT {uploadUrl}
Content-Type: application/pdf

<the file>

### 3. Confirm — requires upload:files. Answers at once, with status Scanning.
POST /api/v1/files/{fileId}/complete
Content-Type: application/json

{}

The scan runs in the background. When GET /api/v1/files/{fileId} reports Available, the file can be referenced and downloaded:

http
### 4. Say who holds it — requires upload:files
POST /api/v1/files/{fileId}/references
Content-Type: application/json

{ "ownerScope": "nexio.attachment", "ownerKey": "REQ-2026-0042" }

### 5. A short URL to download it — requires read:files
GET /api/v1/files/{fileId}/download-url?purpose=preview

The download URL is followed from the browser, straight to the bucket, and answers Content-Disposition: attachment with the original name and Content-Type: application/octet-stream. When the owner lets go, DELETE /api/v1/files/{fileId}/references/{referenceId} releases the reference, and the retention job purges the object once the grace period has passed.

A reference can only be created over an Available file. A file still being scanned answers FILE_NOT_AVAILABLE_CONFLICT, an infected one FILE_INFECTED_CONFLICT — the screen should tell "still being checked" from "we found something".

5. From another module in the same host ​

A module running in the same process does not call the routes. It uses IFileService, which applies the same rules as the HTTP surface for the current caller:

csharp
public sealed class AttachQuote(IFileService files)
{
    public async Task<Result> HandleAsync(Guid fileId, string requisition, CancellationToken ct)
    {
        var reference = await files.AddReferenceAsync(
            fileId,
            new AddFileReferenceRequest { OwnerScope = "nexio.attachment", OwnerKey = requisition },
            ct);

        return reference.IsSuccess
            ? Result.Success()
            : Result.Failure(reference.ErrorMessage!, reference.ErrorCode);
    }
}

Content the server produces itself — a rendered report, for instance — goes through IServerFileStore.StoreAsync instead: an explicit tenant, no user required, and the reference the owner expects is created in the same write that releases the file after the scan. This port has no route, and a test keeps it that way.

Next steps ​

Released under the MIT License.