UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

227 lines (225 loc) 8.45 kB
import async_handler_default from "../utils/async-handler.js"; import { isMimeTypeAllowed } from "../services/files/lib/is-mime-type-allowed.js"; import { FilesService } from "../services/files.js"; import { respond } from "../middleware/respond.js"; import { sanitizeQuery } from "../utils/sanitize-query.js"; import { MetaService } from "../services/meta.js"; import use_collection_default from "../middleware/use-collection.js"; import { validateBatch } from "../middleware/validate-batch.js"; import is_locked_default from "../middleware/is-locked.js"; import path from "path"; import { useEnv } from "@directus/env"; import { ErrorCode, InvalidPayloadError, isDirectusError } from "@directus/errors"; import express from "express"; import bytes from "bytes"; import formatTitle from "@directus/format-title"; import Joi from "joi"; import Busboy from "busboy"; //#region src/controllers/files.ts const router = express.Router(); const env = useEnv(); router.use(use_collection_default("directus_files")); router.use(is_locked_default("files")); const multipartHandler = (req, res, next) => { if (req.is("multipart/form-data") === false) return next(); let headers; if (req.headers["content-type"]) headers = req.headers; else headers = { ...req.headers, "content-type": "application/octet-stream" }; const busboy = Busboy({ headers, defParamCharset: "utf8", limits: { fileSize: bytes.parse(env["FILES_MAX_UPLOAD_SIZE"]) ?? void 0 } }); const savedFiles = []; const service = new FilesService({ accountability: req.accountability, schema: req.schema }); const existingPrimaryKey = req.params["pk"] || void 0; /** * The order of the fields in multipart/form-data is important. We require that all fields * are provided _before_ the files. This allows us to set the storage location, and create * the row in directus_files async during the upload of the actual file. */ let payload = {}; let fileCount = 0; busboy.on("field", (fieldname, val) => { let fieldValue = val; if (typeof fieldValue === "string" && fieldValue.trim() === "null") fieldValue = null; if (typeof fieldValue === "string" && fieldValue.trim() === "false") fieldValue = false; if (typeof fieldValue === "string" && fieldValue.trim() === "true") fieldValue = true; payload[fieldname] = fieldValue; }); busboy.on("file", async (_fieldname, fileStream, { filename, mimeType }) => { if (!filename) return busboy.emit("error", new InvalidPayloadError({ reason: `File is missing filename` })); if (isMimeTypeAllowed(mimeType, env["FILES_MIME_TYPE_ALLOW_LIST"]) === false) return busboy.emit("error", new InvalidPayloadError({ reason: `File is of invalid content type` })); fileCount++; if (!existingPrimaryKey) { if (!payload.title) payload.title = formatTitle(path.parse(filename).name); } payload.filename_download ||= filename; const payloadWithRequiredFields = { ...payload, type: mimeType }; payload = {}; try { const primaryKey = await service.uploadOne(fileStream, payloadWithRequiredFields, existingPrimaryKey); savedFiles.push(primaryKey); tryDone(); } catch (error) { busboy.emit("error", error); } }); busboy.on("error", (error) => { next(error); }); busboy.on("close", () => { tryDone(); }); req.pipe(busboy); function tryDone() { if (savedFiles.length === fileCount) { if (fileCount === 0) return next(new InvalidPayloadError({ reason: `No files were included in the body` })); res.locals["savedFiles"] = savedFiles; return next(); } } }; router.post("/", async_handler_default(multipartHandler), async_handler_default(async (req, res, next) => { const service = new FilesService({ accountability: req.accountability, schema: req.schema }); let keys = []; if (req.is("multipart/form-data")) keys = res.locals["savedFiles"]; else keys = await service.createOne(req.body); try { if (Array.isArray(keys) && keys.length > 1) { const records = await service.readMany(keys, req.sanitizedQuery); res.locals["payload"] = { data: records }; } else { const key = Array.isArray(keys) ? keys[0] : keys; const record = await service.readOne(key, req.sanitizedQuery); res.locals["payload"] = { data: record }; } } catch (error) { if (isDirectusError(error, ErrorCode.Forbidden)) return next(); throw error; } return next(); }), respond); const importSchema = Joi.object({ url: Joi.string().required(), data: Joi.object(), options: Joi.object({ filterMimeType: Joi.array().items(Joi.string()) }) }); router.post("/import", async_handler_default(async (req, res, next) => { const { error } = importSchema.validate(req.body); if (error) throw new InvalidPayloadError({ reason: error.message }); const service = new FilesService({ accountability: req.accountability, schema: req.schema }); const primaryKey = await service.importOne(req.body.url, req.body.data, req.body.options); try { const record = await service.readOne(primaryKey, req.sanitizedQuery); res.locals["payload"] = { data: record || null }; } catch (error$1) { if (isDirectusError(error$1, ErrorCode.Forbidden)) return next(); throw error$1; } return next(); }), respond); const readHandler = async_handler_default(async (req, res, next) => { const service = new FilesService({ accountability: req.accountability, schema: req.schema }); const metaService = new MetaService({ accountability: req.accountability, schema: req.schema }); let result; if (req.singleton) result = await service.readSingleton(req.sanitizedQuery); else if (req.body.keys) result = await service.readMany(req.body.keys, req.sanitizedQuery); else result = await service.readByQuery(req.sanitizedQuery); const meta = await metaService.getMetaForQuery("directus_files", req.sanitizedQuery); res.locals["payload"] = { data: result, meta }; return next(); }); router.get("/", validateBatch("read"), readHandler, respond); router.search("/", validateBatch("read"), readHandler, respond); router.get("/:pk", async_handler_default(async (req, res, next) => { const record = await new FilesService({ accountability: req.accountability, schema: req.schema }).readOne(req.params["pk"], req.sanitizedQuery); res.locals["payload"] = { data: record || null }; return next(); }), respond); router.patch("/", validateBatch("update"), async_handler_default(async (req, res, next) => { const service = new FilesService({ accountability: req.accountability, schema: req.schema }); let keys = []; if (Array.isArray(req.body)) keys = await service.updateBatch(req.body); else if (req.body.keys) keys = await service.updateMany(req.body.keys, req.body.data); else { const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability); keys = await service.updateByQuery(sanitizedQuery, req.body.data); } try { const result = await service.readMany(keys, req.sanitizedQuery); res.locals["payload"] = { data: result || null }; } catch (error) { if (isDirectusError(error, ErrorCode.Forbidden)) return next(); throw error; } return next(); }), respond); router.patch("/:pk", async_handler_default(multipartHandler), async_handler_default(async (req, res, next) => { const service = new FilesService({ accountability: req.accountability, schema: req.schema }); await service.updateOne(req.params["pk"], req.body); try { const record = await service.readOne(req.params["pk"], req.sanitizedQuery); res.locals["payload"] = { data: record || null }; } catch (error) { if (isDirectusError(error, ErrorCode.Forbidden)) return next(); throw error; } return next(); }), respond); router.delete("/", validateBatch("delete"), async_handler_default(async (req, _res, next) => { const service = new FilesService({ accountability: req.accountability, schema: req.schema }); if (Array.isArray(req.body)) await service.deleteMany(req.body); else if (req.body.keys) await service.deleteMany(req.body.keys); else { const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability); await service.deleteByQuery(sanitizedQuery); } return next(); }), respond); router.delete("/:pk", async_handler_default(async (req, _res, next) => { await new FilesService({ accountability: req.accountability, schema: req.schema }).deleteOne(req.params["pk"]); return next(); }), respond); var files_default = router; //#endregion export { files_default as default, multipartHandler };