directus

Форк
0
369 строк · 8.9 Кб
1
import { useEnv } from '@directus/env';
2
import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';
3
import formatTitle from '@directus/format-title';
4
import type { BusboyFileStream, PrimaryKey } from '@directus/types';
5
import { toArray } from '@directus/utils';
6
import Busboy from 'busboy';
7
import bytes from 'bytes';
8
import type { RequestHandler } from 'express';
9
import express from 'express';
10
import Joi from 'joi';
11
import { minimatch } from 'minimatch';
12
import path from 'path';
13
import { respond } from '../middleware/respond.js';
14
import useCollection from '../middleware/use-collection.js';
15
import { validateBatch } from '../middleware/validate-batch.js';
16
import { FilesService } from '../services/files.js';
17
import { MetaService } from '../services/meta.js';
18
import asyncHandler from '../utils/async-handler.js';
19
import { sanitizeQuery } from '../utils/sanitize-query.js';
20

21
const router = express.Router();
22
const env = useEnv();
23

24
router.use(useCollection('directus_files'));
25

26
export const multipartHandler: RequestHandler = (req, res, next) => {
27
	if (req.is('multipart/form-data') === false) return next();
28

29
	let headers;
30

31
	if (req.headers['content-type']) {
32
		headers = req.headers;
33
	} else {
34
		headers = {
35
			...req.headers,
36
			'content-type': 'application/octet-stream',
37
		};
38
	}
39

40
	const busboy = Busboy({
41
		headers,
42
		defParamCharset: 'utf8',
43
		limits: {
44
			fileSize: env['FILES_MAX_UPLOAD_SIZE'] ? bytes(env['FILES_MAX_UPLOAD_SIZE'] as string) : undefined,
45
		},
46
	});
47

48
	const savedFiles: PrimaryKey[] = [];
49
	const service = new FilesService({ accountability: req.accountability, schema: req.schema });
50

51
	const existingPrimaryKey = req.params['pk'] || undefined;
52

53
	/**
54
	 * The order of the fields in multipart/form-data is important. We require that all fields
55
	 * are provided _before_ the files. This allows us to set the storage location, and create
56
	 * the row in directus_files async during the upload of the actual file.
57
	 */
58

59
	let disk: string = toArray(env['STORAGE_LOCATIONS'] as string)[0]!;
60
	let payload: any = {};
61
	let fileCount = 0;
62

63
	busboy.on('field', (fieldname, val) => {
64
		let fieldValue: string | null | boolean = val;
65

66
		if (typeof fieldValue === 'string' && fieldValue.trim() === 'null') fieldValue = null;
67
		if (typeof fieldValue === 'string' && fieldValue.trim() === 'false') fieldValue = false;
68
		if (typeof fieldValue === 'string' && fieldValue.trim() === 'true') fieldValue = true;
69

70
		if (fieldname === 'storage') {
71
			disk = val;
72
		}
73

74
		payload[fieldname] = fieldValue;
75
	});
76

77
	busboy.on('file', async (_fieldname, fileStream: BusboyFileStream, { filename, mimeType }) => {
78
		if (!filename) {
79
			return busboy.emit('error', new InvalidPayloadError({ reason: `File is missing filename` }));
80
		}
81

82
		const allowedPatterns = toArray(env['FILES_MIME_TYPE_ALLOW_LIST'] as string | string[]);
83
		const mimeTypeAllowed = allowedPatterns.some((pattern) => minimatch(mimeType, pattern));
84

85
		if (mimeTypeAllowed === false) {
86
			return busboy.emit('error', new InvalidPayloadError({ reason: `File is of invalid content type` }));
87
		}
88

89
		fileCount++;
90

91
		if (!existingPrimaryKey) {
92
			if (!payload.title) {
93
				payload.title = formatTitle(path.parse(filename).name);
94
			}
95
		}
96

97
		payload.filename_download = filename;
98

99
		const payloadWithRequiredFields = {
100
			...payload,
101
			type: mimeType,
102
			storage: payload.storage || disk,
103
		};
104

105
		// Clear the payload for the next to-be-uploaded file
106
		payload = {};
107

108
		try {
109
			const primaryKey = await service.uploadOne(fileStream, payloadWithRequiredFields, existingPrimaryKey);
110
			savedFiles.push(primaryKey);
111
			tryDone();
112
		} catch (error: any) {
113
			busboy.emit('error', error);
114
		}
115

116
		return undefined;
117
	});
118

119
	busboy.on('error', (error: Error) => {
120
		next(error);
121
	});
122

123
	busboy.on('close', () => {
124
		tryDone();
125
	});
126

127
	req.pipe(busboy);
128

129
	function tryDone() {
130
		if (savedFiles.length === fileCount) {
131
			if (fileCount === 0) {
132
				return next(new InvalidPayloadError({ reason: `No files were included in the body` }));
133
			}
134

135
			res.locals['savedFiles'] = savedFiles;
136
			return next();
137
		}
138
	}
139
};
140

141
router.post(
142
	'/',
143
	asyncHandler(multipartHandler),
144
	asyncHandler(async (req, res, next) => {
145
		const service = new FilesService({
146
			accountability: req.accountability,
147
			schema: req.schema,
148
		});
149

150
		let keys: PrimaryKey | PrimaryKey[] = [];
151

152
		if (req.is('multipart/form-data')) {
153
			keys = res.locals['savedFiles'];
154
		} else {
155
			keys = await service.createOne(req.body);
156
		}
157

158
		try {
159
			if (Array.isArray(keys) && keys.length > 1) {
160
				const records = await service.readMany(keys, req.sanitizedQuery);
161

162
				res.locals['payload'] = {
163
					data: records,
164
				};
165
			} else {
166
				const key = Array.isArray(keys) ? keys[0]! : keys;
167
				const record = await service.readOne(key, req.sanitizedQuery);
168

169
				res.locals['payload'] = {
170
					data: record,
171
				};
172
			}
173
		} catch (error: any) {
174
			if (isDirectusError(error, ErrorCode.Forbidden)) {
175
				return next();
176
			}
177

178
			throw error;
179
		}
180

181
		return next();
182
	}),
183
	respond,
184
);
185

186
const importSchema = Joi.object({
187
	url: Joi.string().required(),
188
	data: Joi.object(),
189
});
190

191
router.post(
192
	'/import',
193
	asyncHandler(async (req, res, next) => {
194
		const { error } = importSchema.validate(req.body);
195

196
		if (error) {
197
			throw new InvalidPayloadError({ reason: error.message });
198
		}
199

200
		const service = new FilesService({
201
			accountability: req.accountability,
202
			schema: req.schema,
203
		});
204

205
		const primaryKey = await service.importOne(req.body.url, req.body.data);
206

207
		try {
208
			const record = await service.readOne(primaryKey, req.sanitizedQuery);
209
			res.locals['payload'] = { data: record || null };
210
		} catch (error: any) {
211
			if (isDirectusError(error, ErrorCode.Forbidden)) {
212
				return next();
213
			}
214

215
			throw error;
216
		}
217

218
		return next();
219
	}),
220
	respond,
221
);
222

223
const readHandler = asyncHandler(async (req, res, next) => {
224
	const service = new FilesService({
225
		accountability: req.accountability,
226
		schema: req.schema,
227
	});
228

229
	const metaService = new MetaService({
230
		accountability: req.accountability,
231
		schema: req.schema,
232
	});
233

234
	let result;
235

236
	if (req.singleton) {
237
		result = await service.readSingleton(req.sanitizedQuery);
238
	} else if (req.body.keys) {
239
		result = await service.readMany(req.body.keys, req.sanitizedQuery);
240
	} else {
241
		result = await service.readByQuery(req.sanitizedQuery);
242
	}
243

244
	const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
245

246
	res.locals['payload'] = { data: result, meta };
247
	return next();
248
});
249

250
router.get('/', validateBatch('read'), readHandler, respond);
251
router.search('/', validateBatch('read'), readHandler, respond);
252

253
router.get(
254
	'/:pk',
255
	asyncHandler(async (req, res, next) => {
256
		const service = new FilesService({
257
			accountability: req.accountability,
258
			schema: req.schema,
259
		});
260

261
		const record = await service.readOne(req.params['pk']!, req.sanitizedQuery);
262
		res.locals['payload'] = { data: record || null };
263
		return next();
264
	}),
265
	respond,
266
);
267

268
router.patch(
269
	'/',
270
	validateBatch('update'),
271
	asyncHandler(async (req, res, next) => {
272
		const service = new FilesService({
273
			accountability: req.accountability,
274
			schema: req.schema,
275
		});
276

277
		let keys: PrimaryKey[] = [];
278

279
		if (Array.isArray(req.body)) {
280
			keys = await service.updateBatch(req.body);
281
		} else if (req.body.keys) {
282
			keys = await service.updateMany(req.body.keys, req.body.data);
283
		} else {
284
			const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
285
			keys = await service.updateByQuery(sanitizedQuery, req.body.data);
286
		}
287

288
		try {
289
			const result = await service.readMany(keys, req.sanitizedQuery);
290
			res.locals['payload'] = { data: result || null };
291
		} catch (error: any) {
292
			if (isDirectusError(error, ErrorCode.Forbidden)) {
293
				return next();
294
			}
295

296
			throw error;
297
		}
298

299
		return next();
300
	}),
301
	respond,
302
);
303

304
router.patch(
305
	'/:pk',
306
	asyncHandler(multipartHandler),
307
	asyncHandler(async (req, res, next) => {
308
		const service = new FilesService({
309
			accountability: req.accountability,
310
			schema: req.schema,
311
		});
312

313
		await service.updateOne(req.params['pk']!, req.body);
314

315
		try {
316
			const record = await service.readOne(req.params['pk']!, req.sanitizedQuery);
317
			res.locals['payload'] = { data: record || null };
318
		} catch (error: any) {
319
			if (isDirectusError(error, ErrorCode.Forbidden)) {
320
				return next();
321
			}
322

323
			throw error;
324
		}
325

326
		return next();
327
	}),
328
	respond,
329
);
330

331
router.delete(
332
	'/',
333
	validateBatch('delete'),
334
	asyncHandler(async (req, _res, next) => {
335
		const service = new FilesService({
336
			accountability: req.accountability,
337
			schema: req.schema,
338
		});
339

340
		if (Array.isArray(req.body)) {
341
			await service.deleteMany(req.body);
342
		} else if (req.body.keys) {
343
			await service.deleteMany(req.body.keys);
344
		} else {
345
			const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
346
			await service.deleteByQuery(sanitizedQuery);
347
		}
348

349
		return next();
350
	}),
351
	respond,
352
);
353

354
router.delete(
355
	'/:pk',
356
	asyncHandler(async (req, _res, next) => {
357
		const service = new FilesService({
358
			accountability: req.accountability,
359
			schema: req.schema,
360
		});
361

362
		await service.deleteOne(req.params['pk']!);
363

364
		return next();
365
	}),
366
	respond,
367
);
368

369
export default router;
370

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.