@kollors/deep-json-server 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +12 -9
  2. package/README.ru.md +12 -9
  3. package/dist/bin/deep-json-server.d.ts +2 -0
  4. package/dist/bin/deep-json-server.js +11 -0
  5. package/dist/bin/deep-json-server.js.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/src/cli.d.ts +4 -0
  10. package/dist/src/cli.js +80 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/config.d.ts +54 -0
  13. package/dist/src/config.js +145 -0
  14. package/dist/src/config.js.map +1 -0
  15. package/{src → dist/src}/constants.js +1 -0
  16. package/dist/src/constants.js.map +1 -0
  17. package/dist/src/database.d.ts +20 -0
  18. package/dist/src/database.js +136 -0
  19. package/dist/src/database.js.map +1 -0
  20. package/dist/src/files/contract.d.ts +67 -0
  21. package/dist/src/files/contract.js +106 -0
  22. package/dist/src/files/contract.js.map +1 -0
  23. package/dist/src/files/disk-store.d.ts +5 -0
  24. package/dist/src/files/disk-store.js +313 -0
  25. package/dist/src/files/disk-store.js.map +1 -0
  26. package/dist/src/files/index.d.ts +5 -0
  27. package/dist/src/files/index.js +6 -0
  28. package/dist/src/files/index.js.map +1 -0
  29. package/dist/src/files/memory-store.d.ts +3 -0
  30. package/dist/src/files/memory-store.js +80 -0
  31. package/dist/src/files/memory-store.js.map +1 -0
  32. package/dist/src/files/routes.d.ts +6 -0
  33. package/dist/src/files/routes.js +77 -0
  34. package/dist/src/files/routes.js.map +1 -0
  35. package/dist/src/openapi/config.d.ts +11 -0
  36. package/dist/src/openapi/config.js +204 -0
  37. package/dist/src/openapi/config.js.map +1 -0
  38. package/dist/src/openapi/document.d.ts +9 -0
  39. package/dist/src/openapi/document.js +338 -0
  40. package/dist/src/openapi/document.js.map +1 -0
  41. package/dist/src/openapi/index.d.ts +10 -0
  42. package/dist/src/openapi/index.js +27 -0
  43. package/dist/src/openapi/index.js.map +1 -0
  44. package/dist/src/openapi/inference.d.ts +14 -0
  45. package/dist/src/openapi/inference.js +145 -0
  46. package/dist/src/openapi/inference.js.map +1 -0
  47. package/dist/src/query/filter.d.ts +6 -0
  48. package/dist/src/query/filter.js +248 -0
  49. package/dist/src/query/filter.js.map +1 -0
  50. package/{src → dist/src}/query/index.js +1 -0
  51. package/dist/src/query/index.js.map +1 -0
  52. package/dist/src/query/pagination.d.ts +11 -0
  53. package/dist/src/query/pagination.js +28 -0
  54. package/dist/src/query/pagination.js.map +1 -0
  55. package/dist/src/query/sort.d.ts +1 -0
  56. package/dist/src/query/sort.js +54 -0
  57. package/dist/src/query/sort.js.map +1 -0
  58. package/dist/src/relation-metadata.d.ts +10 -0
  59. package/dist/src/relation-metadata.js +32 -0
  60. package/dist/src/relation-metadata.js.map +1 -0
  61. package/dist/src/relations.d.ts +12 -0
  62. package/dist/src/relations.js +155 -0
  63. package/dist/src/relations.js.map +1 -0
  64. package/dist/src/server.d.ts +13 -0
  65. package/dist/src/server.js +188 -0
  66. package/dist/src/server.js.map +1 -0
  67. package/dist/src/types.d.ts +52 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/src/utils.d.ts +20 -0
  71. package/dist/src/utils.js +63 -0
  72. package/dist/src/utils.js.map +1 -0
  73. package/package.json +16 -15
  74. package/bin/deep-json-server.js +0 -11
  75. package/index.js +0 -12
  76. package/src/cli.js +0 -91
  77. package/src/config.js +0 -203
  78. package/src/database.js +0 -148
  79. package/src/files.js +0 -633
  80. package/src/openapi/config.js +0 -124
  81. package/src/openapi/document.js +0 -394
  82. package/src/openapi/index.js +0 -36
  83. package/src/openapi/inference.js +0 -187
  84. package/src/query/filter.js +0 -283
  85. package/src/query/pagination.js +0 -44
  86. package/src/query/sort.js +0 -63
  87. package/src/relation-metadata.js +0 -41
  88. package/src/relations.js +0 -173
  89. package/src/server.js +0 -250
  90. package/src/utils.js +0 -84
  91. package/types/index.d.ts +0 -21
  92. package/types/src/config.d.ts +0 -73
  93. package/types/src/database.d.ts +0 -33
  94. package/types/src/files.d.ts +0 -64
  95. package/types/src/openapi/config.d.ts +0 -3
  96. package/types/src/openapi/document.d.ts +0 -11
  97. package/types/src/openapi/index.d.ts +0 -9
  98. package/types/src/openapi/inference.d.ts +0 -9
  99. package/types/src/query/filter.d.ts +0 -3
  100. package/types/src/query/pagination.d.ts +0 -8
  101. package/types/src/query/sort.d.ts +0 -1
  102. package/types/src/relation-metadata.d.ts +0 -9
  103. package/types/src/relations.d.ts +0 -3
  104. package/types/src/server.d.ts +0 -14
  105. package/types/src/utils.d.ts +0 -18
  106. /package/{types → dist}/src/constants.d.ts +0 -0
  107. /package/{types → dist}/src/query/index.d.ts +0 -0
package/src/files.js DELETED
@@ -1,633 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { createReadStream, createWriteStream } from 'node:fs';
3
- import { access, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
4
- import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
5
- import { Readable, Transform } from 'node:stream';
6
- import { pipeline } from 'node:stream/promises';
7
- import { createHttpError, createSerialQueue, isObject } from './utils.js';
8
-
9
- /** @typedef {{ directory: string, mimeType: string, name: string }} StoredFileMetadata */
10
- /** @typedef {StoredFileMetadata & { size: number }} FileRecord */
11
- /** @typedef {FileRecord & { downloadUrl: string, metadataUrl: string, url: string }} FileMetadata */
12
- /** @typedef {{ directory: string, maxFileSize: number, mimeType: string, name: string, override: boolean, stream: Readable }} FileUpload */
13
- /** @typedef {{ directory?: string, name?: string }} FileUpdate */
14
- /**
15
- * @typedef {object} FileStore
16
- * @property {(path: string) => Promise<{ file: FileRecord, stream: Readable }>} get Returns file metadata and contents.
17
- * @property {(path: string) => Promise<FileRecord>} metadata Returns file metadata.
18
- * @property {(path: string) => Promise<void>} remove Deletes a file.
19
- * @property {(path: string, update: FileUpdate) => Promise<FileRecord>} update Renames or moves a file.
20
- * @property {(upload: FileUpload) => Promise<{ created: boolean, file: FileRecord }>} upload Stores a file.
21
- */
22
-
23
- const PATCH_BODY_LIMIT = 64 * 1024;
24
-
25
- const getFileKey = ({ directory, name }) => [directory, name].filter(Boolean).join('/');
26
- const hasValidFileFields = (file) =>
27
- isObject(file) && typeof file.directory === 'string' && typeof file.mimeType === 'string' && file.mimeType !== '' && typeof file.name === 'string' && file.name !== '';
28
-
29
- const validateName = (value, source) => {
30
- if (typeof value !== 'string' || value === '' || value === '.' || value === '..' || value.includes('/') || value.includes('\\') || value.includes('\0')) {
31
- throw createHttpError(400, `${source} должен содержать безопасное имя файла`);
32
- }
33
-
34
- return value;
35
- };
36
-
37
- const validateDirectory = (value, source) => {
38
- if (typeof value !== 'string') {
39
- throw createHttpError(400, `${source} должен содержать безопасный относительный путь`);
40
- }
41
-
42
- if (value === '') {
43
- return value;
44
- }
45
-
46
- const parts = value.split('/');
47
-
48
- if (value.startsWith('/') || value.endsWith('/') || value.includes('\\') || value.includes('\0') || parts.some((part) => part === '' || part === '.' || part === '..')) {
49
- throw createHttpError(400, `${source} должен содержать безопасный относительный путь`);
50
- }
51
-
52
- return value;
53
- };
54
-
55
- const validateStoredMetadata = (file) => {
56
- if (!hasValidFileFields(file)) {
57
- return false;
58
- }
59
-
60
- try {
61
- validateName(file.name, 'Имя файла');
62
- validateDirectory(file.directory, 'Директория файла');
63
- return true;
64
- } catch {
65
- return false;
66
- }
67
- };
68
-
69
- const validateFileMetadata = (files, metadataPath) => {
70
- if (!Array.isArray(files)) {
71
- throw new Error(`Файл метаданных ${metadataPath} должен содержать JSON-массив`);
72
- }
73
-
74
- const paths = new Set();
75
-
76
- files.forEach((file, index) => {
77
- if (!validateStoredMetadata(file)) {
78
- throw new Error(`Некорректная запись ${index} в файле метаданных ${metadataPath}`);
79
- }
80
-
81
- const path = getFileKey(file);
82
-
83
- if (paths.has(path)) {
84
- throw new Error(`Файл метаданных ${metadataPath} содержит повторяющийся путь «${path}»`);
85
- }
86
-
87
- paths.add(path);
88
- });
89
-
90
- return files;
91
- };
92
-
93
- const readMetadata = async (metadataPath) => {
94
- try {
95
- return validateFileMetadata(JSON.parse(await readFile(metadataPath, 'utf8')), metadataPath);
96
- } catch (error) {
97
- if (error?.code === 'ENOENT') {
98
- return [];
99
- }
100
-
101
- throw error;
102
- }
103
- };
104
-
105
- const writeMetadata = async (metadataPath, files) => {
106
- const temporaryPath = `${metadataPath}.${randomBytes(6).toString('hex')}.tmp`;
107
-
108
- try {
109
- // Atomic replacement prevents readers from seeing partially written JSON.
110
- await writeFile(temporaryPath, JSON.stringify(files, null, 2), { encoding: 'utf8', flag: 'wx' });
111
- await rename(temporaryPath, metadataPath);
112
- } catch (error) {
113
- await rm(temporaryPath, { force: true });
114
- throw error;
115
- }
116
- };
117
-
118
- const decodeHeader = (value, name) => {
119
- if (typeof value !== 'string') {
120
- throw createHttpError(400, `Заголовок ${name} обязателен`);
121
- }
122
-
123
- try {
124
- return decodeURIComponent(value);
125
- } catch {
126
- throw createHttpError(400, `Заголовок ${name} содержит некорректное значение`);
127
- }
128
- };
129
-
130
- const getContentName = (value) => validateName(decodeHeader(value, 'Content-Name'), 'Заголовок Content-Name');
131
- const getContentDirectory = (value) => (value == null ? '' : validateDirectory(decodeHeader(value, 'Content-Directory'), 'Заголовок Content-Directory'));
132
-
133
- const getContentOverride = (value) => {
134
- if (value == null || value === 'false') {
135
- return false;
136
- }
137
-
138
- if (value === 'true') {
139
- return true;
140
- }
141
-
142
- throw createHttpError(400, 'Заголовок Content-Override должен содержать true или false');
143
- };
144
-
145
- const getMimeType = (value) => {
146
- const mimeType = typeof value === 'string' ? value.split(';', 1)[0].trim().toLowerCase() : '';
147
-
148
- if (mimeType === '' || !/^[\w!#$&^.+-]+\/[\w!#$&^.+-]+$/.test(mimeType)) {
149
- throw createHttpError(400, 'Заголовок Content-Type должен содержать корректный MIME-тип');
150
- }
151
-
152
- return mimeType;
153
- };
154
-
155
- const getPathLocation = (path) => {
156
- const normalizedPath = validateDirectory(path, 'Путь файла');
157
- const parts = normalizedPath.split('/');
158
- const name = validateName(parts.pop(), 'Путь файла');
159
-
160
- return { directory: parts.join('/'), name };
161
- };
162
-
163
- const encodeFilePath = (file) => getFileKey(file).split('/').map(encodeURIComponent).join('/');
164
-
165
- const createFileMetadata = (file) => {
166
- const path = encodeFilePath(file);
167
-
168
- return {
169
- ...file,
170
- downloadUrl: `/_files/download/${path}`,
171
- metadataUrl: `/_files/metadata/${path}`,
172
- url: `/_files/storage/${path}`,
173
- };
174
- };
175
-
176
- const createSizeLimiter = (maxFileSize, onSize) => {
177
- let size = 0;
178
-
179
- return new Transform({
180
- transform(chunk, _encoding, callback) {
181
- size += chunk.length;
182
-
183
- if (size > maxFileSize) {
184
- callback(createHttpError(413, `Размер файла не должен превышать ${maxFileSize} байт`));
185
- return;
186
- }
187
-
188
- onSize(size);
189
- callback(null, chunk);
190
- },
191
- });
192
- };
193
-
194
- const pathExists = async (path) => {
195
- try {
196
- await access(path);
197
- return true;
198
- } catch (error) {
199
- if (error?.code === 'ENOENT') {
200
- return false;
201
- }
202
-
203
- throw error;
204
- }
205
- };
206
-
207
- const getFileSize = async (path) => {
208
- try {
209
- return (await stat(path)).size;
210
- } catch (error) {
211
- if (error?.code === 'ENOENT') {
212
- throw createHttpError(404, 'Файл не найден');
213
- }
214
-
215
- throw error;
216
- }
217
- };
218
-
219
- const createDiskFileStore = async ({ directory: sourceDirectoryPath, metadata: sourceMetadataPath }) => {
220
- if (typeof sourceDirectoryPath !== 'string' || sourceDirectoryPath.trim() === '') {
221
- throw new Error('Путь к директории файлов не должен быть пустым');
222
- }
223
-
224
- if (typeof sourceMetadataPath !== 'string' || sourceMetadataPath.trim() === '') {
225
- throw new Error('Путь к файлу метаданных не должен быть пустым');
226
- }
227
-
228
- const directoryPath = resolve(sourceDirectoryPath);
229
- const metadataPath = resolve(sourceMetadataPath);
230
- const schedule = createSerialQueue();
231
-
232
- await Promise.all([mkdir(directoryPath, { recursive: true }), mkdir(dirname(metadataPath), { recursive: true })]);
233
- await readMetadata(metadataPath);
234
-
235
- const resolveFilePath = (path) => {
236
- const filePath = resolve(directoryPath, path);
237
- const relativePath = relative(directoryPath, filePath);
238
-
239
- if (relativePath === '' || relativePath.startsWith('..') || isAbsolute(relativePath) || filePath === metadataPath) {
240
- throw createHttpError(400, 'Путь файла выходит за пределы директории хранения');
241
- }
242
-
243
- return filePath;
244
- };
245
-
246
- const getMetadata = async (path) => {
247
- const files = await readMetadata(metadataPath);
248
- const file = files.find((item) => getFileKey(item) === path);
249
-
250
- if (file == null) {
251
- throw createHttpError(404, 'Файл не найден');
252
- }
253
-
254
- const filePath = resolveFilePath(path);
255
-
256
- return { ...file, size: await getFileSize(filePath) };
257
- };
258
-
259
- const metadata = (path) => schedule(async () => getMetadata(path));
260
- const get = (path) =>
261
- schedule(async () => {
262
- const file = await getMetadata(path);
263
-
264
- return { file, stream: createReadStream(resolveFilePath(path)) };
265
- });
266
-
267
- const upload = ({ directory, maxFileSize, mimeType, name, override, stream }) =>
268
- schedule(async () => {
269
- const files = await readMetadata(metadataPath);
270
- const storedFile = { directory, mimeType, name };
271
- const file = { ...storedFile, size: 0 };
272
- const key = getFileKey(file);
273
- const index = files.findIndex((item) => getFileKey(item) === key);
274
- const path = resolveFilePath(key);
275
- const exists = index !== -1 || (await pathExists(path));
276
-
277
- if (exists && !override) {
278
- throw createHttpError(409, 'Файл уже существует');
279
- }
280
-
281
- await mkdir(dirname(path), { recursive: true });
282
-
283
- const temporaryPath = `${path}.${randomBytes(6).toString('hex')}.upload`;
284
- const backupPath = `${path}.${randomBytes(6).toString('hex')}.backup`;
285
- let backedUp = false;
286
- let installed = false;
287
-
288
- try {
289
- await pipeline(
290
- stream,
291
- createSizeLimiter(maxFileSize, (size) => (file.size = size)),
292
- createWriteStream(temporaryPath, { flags: 'wx' }),
293
- );
294
-
295
- if (await pathExists(path)) {
296
- await rename(path, backupPath);
297
- backedUp = true;
298
- }
299
-
300
- await rename(temporaryPath, path);
301
- installed = true;
302
-
303
- if (index === -1) {
304
- files.push(storedFile);
305
- } else {
306
- files.splice(index, 1, storedFile);
307
- }
308
-
309
- await writeMetadata(metadataPath, files);
310
- await rm(backupPath, { force: true });
311
-
312
- return { created: !exists, file };
313
- } catch (error) {
314
- await rm(temporaryPath, { force: true });
315
-
316
- if (installed) {
317
- await rm(path, { force: true });
318
- }
319
-
320
- if (backedUp) {
321
- await rename(backupPath, path);
322
- }
323
-
324
- throw error;
325
- }
326
- });
327
-
328
- const update = (sourcePath, updates) =>
329
- schedule(async () => {
330
- const files = await readMetadata(metadataPath);
331
- const index = files.findIndex((file) => getFileKey(file) === sourcePath);
332
-
333
- if (index === -1) {
334
- throw createHttpError(404, 'Файл не найден');
335
- }
336
-
337
- const file = files[index];
338
- const updatedFile = { ...file, ...updates };
339
- const targetPath = getFileKey(updatedFile);
340
- const sourceFilePath = resolveFilePath(sourcePath);
341
-
342
- if (!(await pathExists(sourceFilePath))) {
343
- throw createHttpError(404, 'Файл не найден');
344
- }
345
-
346
- if (sourcePath === targetPath) {
347
- return { ...file, size: await getFileSize(sourceFilePath) };
348
- }
349
-
350
- if (files.some((item, itemIndex) => itemIndex !== index && getFileKey(item) === targetPath) || (await pathExists(resolveFilePath(targetPath)))) {
351
- throw createHttpError(409, 'Файл с таким путём уже существует');
352
- }
353
-
354
- const targetFilePath = resolveFilePath(targetPath);
355
-
356
- await mkdir(dirname(targetFilePath), { recursive: true });
357
- await rename(sourceFilePath, targetFilePath);
358
- files.splice(index, 1, updatedFile);
359
-
360
- try {
361
- await writeMetadata(metadataPath, files);
362
- } catch (error) {
363
- await rename(targetFilePath, sourceFilePath);
364
- throw error;
365
- }
366
-
367
- return { ...updatedFile, size: await getFileSize(targetFilePath) };
368
- });
369
-
370
- const remove = (path) =>
371
- schedule(async () => {
372
- const files = await readMetadata(metadataPath);
373
- const index = files.findIndex((file) => getFileKey(file) === path);
374
-
375
- if (index === -1) {
376
- throw createHttpError(404, 'Файл не найден');
377
- }
378
-
379
- files.splice(index, 1);
380
-
381
- const filePath = resolveFilePath(path);
382
- const temporaryPath = `${filePath}.${randomBytes(6).toString('hex')}.delete`;
383
-
384
- try {
385
- await rename(filePath, temporaryPath);
386
- } catch (error) {
387
- if (error?.code === 'ENOENT') {
388
- throw createHttpError(404, 'Файл не найден');
389
- }
390
-
391
- throw error;
392
- }
393
-
394
- try {
395
- await writeMetadata(metadataPath, files);
396
- } catch (error) {
397
- await rename(temporaryPath, filePath);
398
- throw error;
399
- }
400
-
401
- await rm(temporaryPath, { force: true }).catch(() => undefined);
402
- });
403
-
404
- return { get, metadata, remove, update, upload };
405
- };
406
-
407
- const createMemoryFileStore = (sourceFiles) => {
408
- const paths = new Set();
409
- const storedFiles = sourceFiles.map((sourceFile, index) => {
410
- const directory = sourceFile.directory ?? '';
411
- const file = { directory, mimeType: sourceFile.mimeType, name: sourceFile.name, size: sourceFile.content?.length };
412
-
413
- if (!validateStoredMetadata(file) || !Number.isInteger(file.size) || file.size < 0 || !(sourceFile.content instanceof Uint8Array)) {
414
- throw new Error(`Некорректная запись ${index} в config.files.data`);
415
- }
416
-
417
- const path = getFileKey(file);
418
-
419
- if (paths.has(path)) {
420
- throw new Error(`config.files.data содержит повторяющийся путь «${path}»`);
421
- }
422
-
423
- paths.add(path);
424
-
425
- return { content: Buffer.from(sourceFile.content), file };
426
- });
427
- const schedule = createSerialQueue();
428
-
429
- const findStoredFile = (path) => {
430
- const storedFile = storedFiles.find(({ file }) => getFileKey(file) === path);
431
-
432
- if (storedFile == null) {
433
- throw createHttpError(404, 'Файл не найден');
434
- }
435
-
436
- return storedFile;
437
- };
438
-
439
- const metadata = (path) => schedule(async () => findStoredFile(path).file);
440
-
441
- const get = (path) =>
442
- schedule(async () => {
443
- const storedFile = findStoredFile(path);
444
-
445
- return { file: storedFile.file, stream: Readable.from([storedFile.content]) };
446
- });
447
-
448
- const upload = ({ directory, maxFileSize, mimeType, name, override, stream }) =>
449
- schedule(async () => {
450
- const chunks = [];
451
- let size = 0;
452
-
453
- for await (const chunk of stream) {
454
- const buffer = Buffer.from(chunk);
455
-
456
- size += buffer.length;
457
-
458
- if (size > maxFileSize) {
459
- throw createHttpError(413, `Размер файла не должен превышать ${maxFileSize} байт`);
460
- }
461
-
462
- chunks.push(buffer);
463
- }
464
-
465
- const file = { directory, mimeType, name, size };
466
- const path = getFileKey(file);
467
- const index = storedFiles.findIndex((item) => getFileKey(item.file) === path);
468
-
469
- if (index !== -1 && !override) {
470
- throw createHttpError(409, 'Файл уже существует');
471
- }
472
-
473
- const storedFile = { content: Buffer.concat(chunks), file };
474
-
475
- if (index === -1) {
476
- storedFiles.push(storedFile);
477
- } else {
478
- storedFiles.splice(index, 1, storedFile);
479
- }
480
-
481
- return { created: index === -1, file };
482
- });
483
-
484
- const update = (sourcePath, updates) =>
485
- schedule(async () => {
486
- const index = storedFiles.findIndex(({ file }) => getFileKey(file) === sourcePath);
487
-
488
- if (index === -1) {
489
- throw createHttpError(404, 'Файл не найден');
490
- }
491
-
492
- const storedFile = storedFiles[index];
493
- const file = { ...storedFile.file, ...updates };
494
- const targetPath = getFileKey(file);
495
-
496
- if (sourcePath !== targetPath && storedFiles.some((item, itemIndex) => itemIndex !== index && getFileKey(item.file) === targetPath)) {
497
- throw createHttpError(409, 'Файл с таким путём уже существует');
498
- }
499
-
500
- storedFiles.splice(index, 1, { ...storedFile, file });
501
-
502
- return file;
503
- });
504
-
505
- const remove = (path) =>
506
- schedule(async () => {
507
- const index = storedFiles.findIndex(({ file }) => getFileKey(file) === path);
508
-
509
- if (index === -1) {
510
- throw createHttpError(404, 'Файл не найден');
511
- }
512
-
513
- storedFiles.splice(index, 1);
514
- });
515
-
516
- return { get, metadata, remove, update, upload };
517
- };
518
-
519
- const readJsonObject = async (stream) => {
520
- const chunks = [];
521
- let size = 0;
522
-
523
- for await (const chunk of stream) {
524
- const buffer = Buffer.from(chunk);
525
-
526
- size += buffer.length;
527
-
528
- if (size > PATCH_BODY_LIMIT) {
529
- throw createHttpError(413, `Размер тела запроса не должен превышать ${PATCH_BODY_LIMIT} байт`);
530
- }
531
-
532
- chunks.push(buffer);
533
- }
534
-
535
- let body;
536
-
537
- try {
538
- body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
539
- } catch {
540
- throw createHttpError(400, 'Тело запроса должно содержать корректный JSON');
541
- }
542
-
543
- if (!isObject(body)) {
544
- throw createHttpError(400, 'Тело запроса должно быть JSON-объектом');
545
- }
546
-
547
- const unknownKey = Object.keys(body).find((key) => key !== 'directory' && key !== 'name');
548
-
549
- if (unknownKey != null) {
550
- throw createHttpError(400, `Неизвестный ключ body.${unknownKey}`);
551
- }
552
-
553
- if (body.directory == null && body.name == null) {
554
- throw createHttpError(400, 'Укажите новое имя или директорию файла');
555
- }
556
-
557
- return {
558
- ...(body.directory != null && { directory: validateDirectory(body.directory, 'Ключ body.directory') }),
559
- ...(body.name != null && { name: validateName(body.name, 'Ключ body.name') }),
560
- };
561
- };
562
-
563
- /**
564
- * Creates a disk- or memory-backed file store with serialized operations.
565
- * @param {import('./config.js').FilesConfig} config File storage configuration.
566
- * @returns {Promise<FileStore>} File store.
567
- */
568
- export const createFileStore = async (config) => ('data' in config ? createMemoryFileStore(config.data) : createDiskFileStore(config));
569
-
570
- const getDownloadName = (name) => encodeURIComponent(basename(name)).replaceAll("'", '%27');
571
-
572
- const sendFile = async (store, path, reply, disposition) => {
573
- const { file, stream } = await store.get(path);
574
-
575
- reply.header('Content-Disposition', `${disposition}; filename*=UTF-8''${getDownloadName(file.name)}`);
576
- reply.type(file.mimeType);
577
-
578
- return reply.send(stream);
579
- };
580
-
581
- export const registerFileRoutes = (fastify, { maxFileSize, store }) => {
582
- fastify.register((fileServer, _options, done) => {
583
- fileServer.removeAllContentTypeParsers();
584
- fileServer.addContentTypeParser('*', (_request, payload, parserDone) => parserDone(null, payload));
585
-
586
- fileServer.post('/_files/storage', async (request, reply) => {
587
- const contentLength = Number(request.headers['content-length']);
588
-
589
- if (Number.isFinite(contentLength) && contentLength > maxFileSize) {
590
- throw createHttpError(413, `Размер файла не должен превышать ${maxFileSize} байт`);
591
- }
592
-
593
- const result = await store.upload({
594
- directory: getContentDirectory(request.headers['content-directory']),
595
- maxFileSize,
596
- mimeType: getMimeType(request.headers['content-type']),
597
- name: getContentName(request.headers['content-name']),
598
- override: getContentOverride(request.headers['content-override']),
599
- stream: request.body,
600
- });
601
-
602
- return reply.code(result.created ? 201 : 200).send(createFileMetadata(result.file));
603
- });
604
-
605
- fileServer.get('/_files/storage/*', async (request, reply) => sendFile(store, getFileKey(getPathLocation(request.params['*'])), reply, 'inline'));
606
- fileServer.get('/_files/download/*', async (request, reply) => sendFile(store, getFileKey(getPathLocation(request.params['*'])), reply, 'attachment'));
607
-
608
- fileServer.get('/_files/metadata/*', async (request) => {
609
- const file = await store.metadata(getFileKey(getPathLocation(request.params['*'])));
610
-
611
- return createFileMetadata(file);
612
- });
613
-
614
- fileServer.patch('/_files/storage/*', async (request) => {
615
- if (getMimeType(request.headers['content-type']) !== 'application/json') {
616
- throw createHttpError(415, 'Для изменения файла используйте Content-Type: application/json');
617
- }
618
-
619
- const sourcePath = getFileKey(getPathLocation(request.params['*']));
620
- const update = await readJsonObject(request.body);
621
-
622
- return createFileMetadata(await store.update(sourcePath, update));
623
- });
624
-
625
- fileServer.delete('/_files/storage/*', async (request, reply) => {
626
- await store.remove(getFileKey(getPathLocation(request.params['*'])));
627
-
628
- return reply.code(204).send();
629
- });
630
-
631
- done();
632
- });
633
- };