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 —downloadandupload). - Service (BLL):
src/services/file.ts(uploadLocal,downloadLocal,uploadGCloud,downloadGCloud,deleteGCloud,initGCloud). - Repository (DAL):
src/db/api/file.ts(FileDBApi—replaceRelationFiles,_addFiles,_removeLegacyFiles); persists/removesfilerows for entity relations. Note: this DAL imports@/services/fileto calldeleteGCloud(see Behavior / Notes). - Model:
src/db/models/file.ts. - Shared used:
api/http/request.ts(paramStr),middlewares/upload.ts(processFilemulter 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 todownloadGCloudwhenNODE_ENV === 'production'orNEXT_PUBLIC_BACK_APIis set, otherwisedownloadLocal.- Local: missing
privateUrl->404; otherwise streams viares.downloadfromconfig.uploadDir. - GCloud: serves
${hash}/${privateUrl}from the bucket; file missing or error ->404{ message }.
- Local: missing
POST /api/file/upload/:table/:field-> uploads a single file. Requires JWT authentication (passport.authenticate('jwt')). The folder is computed as:table/:field. Dispatches touploadGCloudwhenNODE_ENV === 'production'orNEXT_PUBLIC_BACK_APIis set, otherwiseuploadLocal. The multipart form field name isfile; the destination filename is taken from the request body fieldfilename.- Local: no
req.currentUser->403; missing file ->400; missingfilename->500; success ->200(empty body). Errors ->500with the stringified error. - GCloud: missing file ->
400{ message }; success ->200{ message, url }whereurlis the publicstorage.googleapis.comURL; errors ->500{ message }.
- Local: no
Access Rules
- Upload requires a valid JWT (route-level passport auth). The local upload path additionally
rejects when
req.currentUseris absent (403) and when anentityvalidation is supplied (403); the controller callsuploadLocalwithentity: 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 namedfile. The controller passesmaxFileSize: 10 * 1024 * 1024touploadLocal, though that value is not consulted byuploadLocal(the limit comes from multer). services/file.tsstill operates directly on ExpressRequest/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 forservices/file.tsandservices/auth.ts.src/db/api/file.tsimports@/services/file(callingdeleteGCloudwhen 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.replaceRelationFilessyncs a relation's files: it deletes existingfilerows not present in the input (removing the GCloud object first whenprivateUrlis set) and creates rows for inputs markednew.
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.
Related
search.md(the other backend infrastructure slice).- Architecture contract:
backend/docs/backend-architecture.md.