2026-06-10 18:27:19 +02:00

5.4 KiB

File Backend

Purpose

The file slice handles binary file upload and download. Uploaded files are stored either on local disk (development) or in Google Cloud Storage (production), selected at request time. The file table records file metadata (name, URLs, owning relation) and is written indirectly by the file DAL when entity relations are persisted, not by the upload endpoint itself.

Slice Files (by layer)

  • Route: src/routes/file.ts (thin wiring; GET /download, POST /upload/:table/:field).
  • Controller: src/api/controllers/file.controller.ts (custom — download and upload).
  • Service (BLL): src/services/file.ts (uploadLocal, downloadLocal, uploadGCloud, downloadGCloud, deleteGCloud, initGCloud).
  • Repository (DAL): src/db/api/file.ts (FileDBApireplaceRelationFiles, _addFiles, _removeLegacyFiles); persists/removes file rows for entity relations. Note: this DAL imports @/services/file to call deleteGCloud (see Behavior / Notes).
  • Model: src/db/models/file.ts.
  • Shared used: api/http/request.ts (paramStr), middlewares/upload.ts (processFile multer wrapper), shared/config.ts (config.uploadDir, config.gcloud.bucket, config.gcloud.hash), shared/errors/validation.ts (ValidationError, in the DAL).

API

  • GET /api/file/download?privateUrl=<path> -> downloads the file. No authentication middleware on this route. The controller dispatches to downloadGCloud when NODE_ENV === 'production' or NEXT_PUBLIC_BACK_API is set, otherwise downloadLocal.
    • Local: missing privateUrl -> 404; otherwise streams via res.download from config.uploadDir.
    • GCloud: serves ${hash}/${privateUrl} from the bucket; file missing or error -> 404 { message }.
  • POST /api/file/upload/:table/:field -> uploads a single file. Requires JWT authentication (passport.authenticate('jwt')). The folder is computed as :table/:field. Dispatches to uploadGCloud when NODE_ENV === 'production' or NEXT_PUBLIC_BACK_API is set, otherwise uploadLocal. The multipart form field name is file; the destination filename is taken from the request body field filename.
    • Local: no req.currentUser -> 403; missing file -> 400; missing filename -> 500; success -> 200 (empty body). Errors -> 500 with the stringified error.
    • GCloud: missing file -> 400 { message }; success -> 200 { message, url } where url is the public storage.googleapis.com URL; errors -> 500 { message }.

Access Rules

  • Upload requires a valid JWT (route-level passport auth). The local upload path additionally rejects when req.currentUser is absent (403) and when an entity validation is supplied (403); the controller calls uploadLocal with entity: null, so the entity branch is not exercised from this endpoint.
  • Download has no authentication middleware and performs no ownership check; access is governed solely by knowing the privateUrl.

Tenant Scope

None enforced in this slice. Neither upload nor download filters by organization; files are keyed by the :table/:field folder, the filename, and (for GCloud) the configured hash prefix. The file model has no organizationId column.

Data Contract

file model columns: id (UUID, PK), belongsTo (string, nullable), belongsToId (UUID, nullable), belongsToColumn (string, nullable), name (string, required, non-empty), sizeInBytes (integer, nullable), privateUrl (string, nullable), publicUrl (string, required, non-empty), createdAt, updatedAt, deletedAt (paranoid soft delete), createdById (UUID, nullable), updatedById (UUID, nullable). Associations: belongsTo users as createdBy and updatedBy.

In the DAL (replaceRelationFiles / _addFiles), each new file requires name and publicUrl, otherwise ValidationError('iam.errors.fileNameRequired') is raised.

Behavior / Notes

  • Storage backend is chosen per request from NODE_ENV / NEXT_PUBLIC_BACK_API: GCloud in production, local disk otherwise.
  • The upload middleware (src/middlewares/upload.ts) uses multer memory storage with a 10 MB file size limit on the field named file. The controller passes maxFileSize: 10 * 1024 * 1024 to uploadLocal, though that value is not consulted by uploadLocal (the limit comes from multer).
  • services/file.ts still operates directly on Express Request/Response (streaming upload/download). This is a documented architecture exception: the import-boundaries test (src/shared/architecture/import-boundaries.test.ts) allows the BLL→HTTP dependency only for services/file.ts and services/auth.ts.
  • src/db/api/file.ts imports @/services/file (calling deleteGCloud when removing legacy files), which is a DAL→BLL dependency. The same import-boundaries test caps DAL→BLL violations at one, and this file is the allowed one.
  • FileDBApi.replaceRelationFiles syncs a relation's files: it deletes existing file rows not present in the input (removing the GCloud object first when privateUrl is set) and creates rows for inputs marked new.

Tests

No dedicated file unit/e2e test exists. The architecture test src/shared/architecture/import-boundaries.test.ts references this slice by name in its BLL→HTTP and DAL→BLL debt-ceiling assertions.

  • search.md (the other backend infrastructure slice).
  • Architecture contract: backend/docs/backend-architecture.md.