Eviworx
Docs

Attachments & File Settings API

The central attachment system accepts files for 12 entity types — with virus scanning (ClamAV), file settings per entity type, retention periods and automatic cleanup of orphaned files. All entities use the same system — incl. eLibrary, custom reports and email signatures.

🔒
Features
✓ Virus scan before download (ClamAV)
✓ Scan status (PENDING → SCANNING → CLEAN/INFECTED)
✓ Quarantine for infected files
✓ Blocked extensions (.exe, .bat, .sh …)
✓ Check of the actual file content
✓ Size limit (100 MB global, per entity type)
✓ Max files per entity (maxFilesPerEntity)
✓ Soft-delete until the period expires
✓ Retention period per entity type (retentionDays)
✓ Orphaned files removed after 24 h

Supported Entity Types

Entity-Type Description Example
TICKETScreenshots, logsPOST /api/attachments/TICKET/:ticketId
INCIDENTPIR reports, screenshotsPOST /api/attachments/INCIDENT/:incidentId
PROBLEMRoot cause analysesPOST /api/attachments/PROBLEM/:problemId
CHANGEImplementation plans, rollback proceduresPOST /api/attachments/CHANGE/:changeId
ASSETPurchase orders, warranty docsPOST /api/attachments/ASSET/:assetId
CONTRACTSigned contracts (PDF)POST /api/attachments/CONTRACT/:contractId
LICENSELicense certificatesPOST /api/attachments/LICENSE/:licenseId
KB_ARTICLEScreenshots, diagramsPOST /api/attachments/KB_ARTICLE/:articleId
WORKFLOWWorkflow approvalsPOST /api/attachments/WORKFLOW/:workflowId
CUSTOM_REPORTGenerated reports (CSV/PDF)POST /api/attachments/CUSTOM_REPORT/:reportId
ELIBRARY_DOCUMENTeLibrary documents (unified attachment)POST /api/attachments/ELIBRARY_DOCUMENT/:docId
EMAIL_SIGNATUREInline images for email signaturesPOST /api/attachments/EMAIL_SIGNATURE/:signatureId

Authentication & Permissions

Attachments inherit the rights of the record they belong to — with exactly the same rules as there (incl. ownership, substitute, mailbox/group scoping, approvers, asset type lock):

  • List / metadata / download: requires view access to the record.
  • Upload / delete: requires edit access to the record.
  • No access to the parent entity → always 404, never 403: about a record the caller may not see, the API does not even reveal that it exists.
  • There is NO permission that allows deleting or downloading independently of the record — whoever may edit the record may delete.
Entity-Type View / edit as Special case
TICKETview / edit ticketmailbox + group + substitute + participant
INCIDENTview / edit incident+ substitute; assigned approvers may also view
PROBLEMview / edit problem+ substitute + assigned group
CHANGEview / edit changerequester, assignee, approvers + substitute
ASSETview / modify assetasset type lock — rights per asset type
CONTRACTview / edit contracteditAll or editOwn as owner
LICENSEview / edit licenseediting requires licenses.update
KB_ARTICLEview / edit articlevisibility, status, grants; edit with editAll or editOwn as author
WORKFLOWview workflow instanceinitiator, step assignment (incl. substitute) or workflows.viewAllInstances
CUSTOM_REPORTview report and customReports.export / changes only by report owner or deleteAllarchived reports: no access
ELIBRARY_DOCUMENTeLibrary visibilityarchived documents only with elibrary.viewArchived (else 404)
EMAIL_SIGNATUREsignature/settings permissioninline images (CID)

Special case CUSTOM_REPORT: Here, view access to the report is not enough. The attachment endpoints apply the same rights as the report endpoints: downloading additionally requires customReports.export, deleting/replacing is limited to the report owner or deleteAll. Otherwise /api/attachments/:id/download and the delete endpoint would bypass exactly what the report routes protect — a mere viewer of a shared report could pull or delete other people's export files.

This way every attachment is always governed by the same rights as its record. For the rights models see Permissions & RBAC.

Endpoints Overview

Attachment Operations

Method Endpoint Description
POST/api/attachments/:entityType/:entityIdUpload file
GET/api/attachments/:entityType/:entityIdList all attachments of an entity
GET/api/attachments/:idGet attachment metadata
GET/api/attachments/:id/downloadDownload file
GET/api/attachments/:id/thumbnailGet image thumbnail (WebP)
DELETE/api/attachments/:idDelete attachment (soft-delete)
GET/api/attachments/settings/:entityTypeFile settings for entity type

File Settings (Admin)

Method Endpoint Description
GET/api/settings/file-settingsGet all entity settings
GET/api/settings/file-settings/global/settingsGet global settings
PUT/api/settings/file-settings/global/settingsUpdate global settings
GET/api/settings/file-settings/:entityTypeGet entity settings
PUT/api/settings/file-settings/:entityTypeUpdate entity settings
POST/api/settings/file-settings/:entityType/resetReset settings (defaults)

Mount / Permissions / UI: All file-settings routes live under /api/settings/file-settings. Reading (GET) requires settings.viewGeneral, writing (PUT/POST reset) requires settings.editGeneral. In the UI: Admin Center → System → File Settings (/admin/file-settings) — with a "Global" tab (global defaults, /admin/file-settings?tab=global) and one tab per entity type (ticket, incident, problem, change, asset, …). Per-type settings override the global defaults (see settings hierarchy below).

API Examples

Upload File (to Ticket)

POST /api/attachments/TICKET/:ticketId
Content-Type: multipart/form-data
// JavaScript
const formData = new FormData();
formData.append('file', fileBlob, 'error-screenshot.png');

const response = await fetch(`/api/attachments/TICKET/${ticketId}`, {
  method: 'POST',
  body: formData,
  credentials: 'include'
});

const attachment = await response.json();

Response (201 Created)

{
  "id": "clx...",
  "entityType": "TICKET",
  "entityId": "clx-ticket-123",
  "originalFileName": "error-screenshot.png",
  "mimeType": "image/png",
  "fileSize": 125340,
  "scanStatus": "PENDING",
  "thumbnailPath": null,
  "downloadAvailable": false,
  "uploadedById": "clx-user",
  "uploadedBy": { "id": "clx-user", "name": "John Doe" },
  "uploadedApiKeyId": null,
  "uploadedApiKey": null,
  "uploadedActorName": null,
  "createdAt": "2026-01-28T11:00:00Z",
  "updatedAt": "2026-01-28T11:00:00Z"
}

The response is the row itself, without a wrapper. The uploader comes as a triple: either uploadedById + uploadedBy (user) or uploadedApiKeyId + uploadedApiKey (API key); uploadedActorName is the name snapshot when neither applies (system uploads or a deleted originator).

Check Scan Status

GET /api/attachments/:id

The scan status is part of an attachment's metadata and of the list. The following examples show only the relevant fields.

Response (Excerpt, During Scan)

{
  "id": "clx...",
  "scanStatus": "SCANNING",
  "downloadAvailable": false,
  "thumbnailPath": null,
  "updatedAt": "2026-01-28T11:00:05Z"
}

Response (Excerpt, After Scan - CLEAN)

{
  "id": "clx...",
  "scanStatus": "CLEAN",
  "downloadAvailable": true,
  "thumbnailPath": "thumbnails/ticket/clx-ticket-123/2026/01/2f...c9.webp",
  "updatedAt": "2026-01-28T11:00:12Z"
}

Response (Excerpt, INFECTED)

{
  "id": "clx...",
  "scanStatus": "INFECTED",
  "downloadAvailable": false,
  "thumbnailPath": null,
  "updatedAt": "2026-01-28T11:00:15Z"
}
Note: Infected files are moved to quarantine and cannot be downloaded. The uploading user is notified.

Download File

GET /api/attachments/:id/download

Response

# Response-Headers:
Content-Type: image/png
Content-Disposition: attachment; filename="error-screenshot.png"
X-Content-Type-Options: nosniff
Content-Security-Policy: sandbox

# Response-Body: Binary File-Data
Security: Files can only be downloaded when the scan status is CLEAN or SKIPPED. On PENDING/SCANNING the download answers 423 SCAN_PENDING, on SCAN_ERROR 423 SCAN_ERROR and on INFECTED 451 INFECTED. The block cannot be lifted for anyone.

Get Thumbnail (Image Preview)

GET /api/attachments/:id/thumbnail
# Response-Headers:
Content-Type: image/webp
Cache-Control: private, max-age=3600
X-Content-Type-Options: nosniff

# Response-Body: WebP thumbnail (max. 320px, fit inside)
Note: Thumbnails are generated automatically for raster images (JPEG/PNG/GIF/WebP — no SVG) after a successful scan (CLEAN/SKIPPED), provided generateThumbnails is enabled for the entity type. The same rights as for download apply (view access to the record), and the thumbnail is only served if the attachment is downloadable. No image, no thumbnail or no access → 404. Non-images keep the generic file icon in the UI.

List All Attachments of Entity

GET /api/attachments/TICKET/:ticketId

Response

{
  "data": [
    {
      "id": "clx-1",
      "entityType": "TICKET",
      "entityId": "clx-ticket-123",
      "originalFileName": "error-screenshot.png",
      "mimeType": "image/png",
      "fileSize": 125340,
      "scanStatus": "CLEAN",
      "thumbnailPath": "thumbnails/ticket/clx-ticket-123/2026/01/2f...c9.webp",
      "downloadAvailable": true,
      "uploadedById": "clx-user",
      "uploadedBy": { "id": "clx-user", "name": "John Doe" },
      "uploadedApiKeyId": null,
      "uploadedApiKey": null,
      "uploadedActorName": null,
      "createdAt": "2026-01-28T11:00:00Z",
      "updatedAt": "2026-01-28T11:00:12Z"
    },
    {
      "id": "clx-2",
      "entityType": "TICKET",
      "entityId": "clx-ticket-123",
      "originalFileName": "windows-event-log.txt",
      "mimeType": "text/plain",
      "fileSize": 45600,
      "scanStatus": "CLEAN",
      "thumbnailPath": null,
      "downloadAvailable": true,
      "uploadedById": "clx-user",
      "uploadedBy": { "id": "clx-user", "name": "John Doe" },
      "uploadedApiKeyId": null,
      "uploadedApiKey": null,
      "uploadedActorName": null,
      "createdAt": "2026-01-28T11:05:00Z",
      "updatedAt": "2026-01-28T11:05:09Z"
    }
  ]
}

The list is unpaginated — its upper bound is maxFilesPerEntity from the file settings. Deleted attachments are not included.

Delete Attachment

DELETE /api/attachments/:id

Response (204 No Content)

Automatically:

  • The attachment is flagged as deleted (soft-delete) and disappears from the list
  • The file remains in place for the retention period
  • The cleanup job then removes file and row for good

Attachments follow their record

  • Into the trash: When a ticket, incident, problem, change, asset, contract or license is deleted, its attachments follow — including on a bulk delete.
  • And back: Restoring brings back the attachments that fell with the record. Attachments deleted individually beforehand stay deleted, and whatever retention has meanwhile purged for good does not return.
  • Exception, custom report: A report is hard-deleted — so its export files fall immediately and permanently with it, including quarantine copies and thumbnails.

Virus Scan Flow

  1. After the upload the attachment has the scan status PENDING (downloadAvailable: false).
  2. ClamAV scans the file; meanwhile the status is SCANNING.
  3. Result CLEAN: the file can be downloaded; for images the thumbnail is created.
  4. Result INFECTED: the file is moved to quarantine and cannot be downloaded; the uploading user is notified.
  5. If the scan fails, the status is SCAN_ERROR; stuck scans are re-queued by the cleanup job (see below).

How scanner, worker and storage are isolated from each other is described on the pages Security and Container Architecture.

File Settings (Entity-Level)

Get Settings for TICKET

GET /api/settings/file-settings/TICKET

Response

{
  "entityType": "TICKET",
  "enabled": true,
  "maxFileSize": 52428800,
  "maxFilesPerEntity": 10,
  "allowedExtensions": [".pdf", ".jpg", ".jpeg", ".png", ".gif", ".doc", ".docx", ".xls", ".xlsx", ".txt", ".csv", ".zip"],
  "allowedMimeTypes": [
    "application/pdf",
    "image/jpeg",
    "image/png",
    "image/gif",
    "application/msword",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "application/vnd.ms-excel",
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "text/plain",
    "text/csv",
    "application/zip"
  ],
  "blockedExtensions": [".exe", ".bat", ".sh", ".cmd", ".msi", ".dll", ".js", ".vbs", ".ps1"],
  "generateThumbnails": true,
  "retentionDays": 0,
  "allowUnknownMimes": false
}
Note: The generateThumbnails field controls automatic generation of image thumbnails (WebP, max. 320px) for raster images (JPEG/PNG/GIF/WebP; no SVG). Thumbnails are created after a successful scan (CLEAN/SKIPPED) and served via GET /api/attachments/:id/thumbnail. Can be disabled per entity type.

Update Settings (Admin)

PUT /api/settings/file-settings/TICKET
{
  "maxFileSize": 104857600,
  "maxFilesPerEntity": 20,
  "retentionDays": 365,
  "allowedExtensions": [".pdf", ".jpg", ".png", ".docx", ".xlsx", ".log"]
}

File Settings (Global-Level)

Get Global Settings

GET /api/settings/file-settings/global/settings

Response

{
  "id": "global",
  "schemaVersion": 1,
  "virusScanEnabled": true,
  "virusScanOnUpload": true,
  "clamavRequestTimeoutMs": 30000,
  "scanStuckTimeoutMinutes": 10,
  "globalBlockedExtensions": [".exe", ".bat", ".sh", ".cmd", ".msi", ".dll", ".scr", ".pif", ".vbs", ".js", ".jar", ".ps1"],
  "defaultStorageProvider": "DISK",
  "uploadDirectory": "/app/uploads",
  "orphanCleanupEnabled": true,
  "orphanRetentionHours": 24,
  "globalMaxFileSize": 104857600
}
Note: The quarantine path is set at installation via the QUARANTINE_DIR environment variable (default /app/quarantine, a dedicated Docker volume separate from the uploads volume) and is deliberately not editable in the UI, so it cannot accidentally point to an unsuitable location.

Update Global Settings (Admin)

PUT /api/settings/file-settings/global/settings
{
  "virusScanEnabled": true,
  "globalMaxFileSize": 157286400,
  "scanStuckTimeoutMinutes": 15,
  "orphanRetentionHours": 48
}

Virus Scan Status

Status Description Download?
PENDINGWaiting for scan (in queue)
SCANNINGCurrently being scanned
CLEANNo virus found
INFECTEDVirus found (in quarantine)
SCAN_ERRORScan failed
SKIPPEDScan disabled (config)

Settings Hierarchy

Effective Settings Calculation:

1. Global Settings (Base):
   └─ globalMaxFileSize: 100MB
   └─ globalBlockedExtensions: [.exe, .bat, ...]
   └─ virusScanEnabled: true

2. Entity Settings (Override):
   └─ TICKET.maxFileSize: 50MB (smaller than global)
   └─ TICKET.maxFilesPerEntity: 10
   └─ TICKET.allowedExtensions: [.pdf, .jpg, ...]

3. Effective Settings (Merged):
   └─ maxFileSize: min(global, entity) = 50MB
   └─ blockedExtensions: global blacklist + entity blacklist
   └─ allowedExtensions: entity (if set)
   └─ virusScanEnabled: global (cannot be disabled per entity)

Example:

Global: 100MB
TICKET: 50MB
CONTRACT: 150MB → Effective: 100MB (global limit)

Global blocked: [.exe, .bat]
TICKET blocked: [.zip]
Effective: [.exe, .bat, .zip]

Error Handling

errorCodeHTTPDescription
NOT_FOUND404The attachment or the parent entity does not exist — OR the caller may not see or edit it. Both cases answer alike: the API does not even reveal that a foreign record exists.
FORBIDDEN403An API key called one of the six user routes — uploading, reading, downloading and deleting are bound to a signed-in user. Exception: GET /settings/:entityType answers API keys as well.
UPLOADS_DISABLED403Uploads are switched off for this entity type — both on upload and when reading the settings
NO_FILE400No multipart field file in the request
VALIDATION_ERROR400Schema violation with a field path — for instance a lowercase entity type: the path parameter is strictly uppercase (TICKET, not ticket)
FILE_TOO_LARGE413File larger than the effective limit (the stricter of the global and entity setting)
EXTENSION_BLOCKED415Extension is in blockedExtensions
EXTENSION_NOT_ALLOWED415Extension is not in allowedExtensions
MIME_TYPE_NOT_ALLOWED415MIME type is not in allowedMimeTypes
ARCHIVE_REQUIRES_VIRUS_SCAN415An archive is not accepted while the virus scan is off
UNKNOWN_FILE_TYPE415The content matches no known type and allowUnknownMimes is off
BINARY_FILE_AS_TEXT415Declared as text while the content is binary
TEXT_TYPE_NOT_ALLOWED415The detected text type is not permitted
EXTENSION_CONTENT_MISMATCH415The extension does not match the detected CONTENT — for instance text as .pdf or an image as .txt. The check runs against the actual content, not the MIME type the browser reports; for extensions without a known content family, allowedMimeTypes and allowUnknownMimes still decide.
MAX_FILES_EXCEEDED409The entity already carries maxFilesPerEntity attachments
DUPLICATE_FILE409The same record already carries a file with identical CONTENT (hash, not name). details.existingId names the existing row.
SCAN_PENDING423Download locked: the virus scan is still running
SCAN_ERROR423Download locked: the file could not be scanned
INFECTED451Download blocked: the file is quarantined
FILE_GONE410The row exists, the file is missing from storage
FILE_UPLOAD_RATE_LIMIT_EXCEEDED429Too many uploads in a short time

Downloading unscanned files is not available to anyone — there is no parameter and no permission that lifts the block.

Use Cases

Use Case 1: Ticket with Screenshot

// 1. Create ticket
const ticket = await fetch('/api/tickets', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',
  body: JSON.stringify({
    title: 'Error on login page',
    description: 'See attached screenshot'
  })
}).then(r => r.json());

// 2. Upload screenshot
const formData = new FormData();
formData.append('file', screenshotBlob, 'login-error.png');

const upload = await fetch(`/api/attachments/TICKET/${ticket.id}`, {
  method: 'POST',
  body: formData,
  credentials: 'include'
}).then(r => r.json());

// 3. Poll scan status (every 2s)
const pollStatus = async () => {
  const status = await fetch(`/api/attachments/${upload.id}`, {
    credentials: 'include'
  }).then(r => r.json());

  if (status.scanStatus === 'CLEAN') {
    console.log('File is safe, download available!');
    return true;
  } else if (status.scanStatus === 'INFECTED') {
    alert('File is infected! Contact IT.');
    return true;
  }
  return false; // Keep polling
};

Use Case 2: Upload Contract PDFs

// Check settings (what is allowed?)
const settings = await fetch('/api/attachments/settings/CONTRACT', {
  credentials: 'include'
}).then(r => r.json());

console.log('Max File Size:', settings.maxFileSize / 1024 / 1024, 'MB');
console.log('Allowed:', settings.allowedExtensions);

// Upload PDF
const formData = new FormData();
formData.append('file', pdfBlob, 'signed-contract-2026.pdf');

await fetch(`/api/attachments/CONTRACT/${contractId}`, {
  method: 'POST',
  body: formData,
  credentials: 'include'
});

Use Case 3: Configure Global Settings

# Admin: disable virus scan (development)
PUT /api/settings/file-settings/global/settings
{
  "virusScanEnabled": false
}

# Admin: increase max file size (for large reports)
PUT /api/settings/file-settings/global/settings
{
  "globalMaxFileSize": 209715200
}

# Admin: extend orphan-cleanup window
PUT /api/settings/file-settings/global/settings
{
  "orphanRetentionHours": 72
}

Best Practices

💡 Tips

1. Upload Validation

  • • Load settings BEFORE upload (GET /attachments/settings/:entityType)
  • • Client-side validation (maxFileSize, allowedExtensions)
  • • Server validates again (defense-in-depth)
  • • The server checks the actual file content

2. Virus Scan

  • • Poll every 2s for scan status (not too frequently)
  • • Timeout after 2min (if scan hangs)
  • • User feedback on SCANNING ("Please wait...")
  • • On INFECTED: user notification + alert to IT

3. Retention

  • • Set retentionDays per entity type (tickets: 365 days, contracts: 0 = unlimited)
  • • CronJob: attachment_cleanup runs daily
  • • Deleted attachments are kept for retentionDays days, after which the cleanup job removes them permanently (0 = never)
  • • Orphan cleanup: uploads without DB entry deleted after 24h

4. Performance

  • • Thumbnail generation (generateThumbnails) creates WebP previews for image attachments — can be disabled per entity type
  • • Increase ClamAV timeout for large files (clamavRequestTimeoutMs)
  • • Set max files limit (keeps the database lean)
  • • Keep orphan cleanup active (prevents full disks)

Integration with Entities

Usage with Different Entities:

Tickets:
POST /api/attachments/TICKET/:ticketId
• Screenshots of error messages
• Log files
• User uploads (evidence)

Incidents:
POST /api/attachments/INCIDENT/:incidentId
• Post-Incident-Review (PIR) reports
• Screenshots from monitoring
• Network diagrams

Problems:
POST /api/attachments/PROBLEM/:problemId
• Root-cause-analysis reports
• Vendor analysis reports
• Interim-solution documentation

Changes:
POST /api/attachments/CHANGE/:changeId
• Implementation-Plans
• Rollback-Procedures
• Approval-Documents

Assets:
POST /api/attachments/ASSET/:assetId
• Purchase-Orders
• Warranty-Certificates
• Invoices

Contracts:
POST /api/attachments/CONTRACT/:contractId
• Signed Contract-PDFs
• Amendments
• Renewal-Notices

Licenses:
POST /api/attachments/LICENSE/:licenseId
• License-Certificates
• Activation-Instructions

KB-Articles:
POST /api/attachments/KB_ARTICLE/:articleId
• Screenshots for how-to guides
• Diagrams
• PDFs

Workflows:
POST /api/attachments/WORKFLOW/:workflowId
• Approval-Documents
• Supporting-Documents

Custom Reports:
POST /api/attachments/CUSTOM_REPORT/:reportId
• Generated CSV/PDF reports
eLibrary:
POST /api/attachments/ELIBRARY_DOCUMENT/:docId
• eLibrary documents (unified attachment)
E-Mail-Signaturen:
POST /api/attachments/EMAIL_SIGNATURE/:signatureId
• Inline images (CID references)

Cleanup & Maintenance

Automatic Cleanup Jobs

A job of the attachment_cleanup action runs six operations in a set order; which of them run is set by the operations parameter (without it: all six).

operationDescription
stuck_scansA scan stuck on SCANNING for too long is set to SCAN_ERROR and re-queued — but only while its file still exists. Rows without a file are not re-queued but reported.
file_goneLive rows whose file is missing and that are older than the grace period are deleted by the system (a note on the record plus an audit entry); the retention step of the same run clears them for good.
retentionPermanently remove deleted attachments once their retention has elapsed — row, file and thumbnail.
orphansDelete files without a matching row after the grace period.
infectedRemove quarantined files after their own retention period.
signature_draftsClean up image uploads from the signature editor that were never saved.
Note: Matching image thumbnails are included. The periods themselves live in the file settings, not on the job.

CronJob Configuration

{
  "name": "Attachment Cleanup - Daily",
  "category": "MAINTENANCE",
  "trigger": {
    "type": "cron",
    "schedule": {
      "cronExpression": "0 3 * * *"
    }
  },
  "actions": [
    {
      "type": "attachment_cleanup",
      "parameters": {
        "operations": ["stuck_scans", "file_gone", "retention", "orphans", "infected", "signature_drafts"],
        "fileGoneDryRun": false
      }
    }
  ]
}
Note: The attachment system is the same for all twelve entity types: one API, the rights of the respective record and a shared virus scan.

Related Documentation

Virus scan (containers)
Container Architecture — clamav + av-worker
Security — Zero-Trust Virus-Scan
Rights & cleanup
CronJobs — attachment_cleanup