@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
@@ -0,0 +1,136 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { JSONFilePreset } from 'lowdb/node';
4
+ import { createHttpError, createSerialQueue, createUniqueId, isObject, isSafeKey, isSystemError, resolveDatabasePath } from './utils.js';
5
+ const RESOURCE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
6
+ export const validateJsonValue = (value, path, ancestors = new WeakSet()) => {
7
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
8
+ return;
9
+ }
10
+ if (typeof value === 'number') {
11
+ if (!Number.isFinite(value)) {
12
+ throw new Error(`${path} должен содержать конечное число`);
13
+ }
14
+ return;
15
+ }
16
+ if (typeof value !== 'object') {
17
+ throw new Error(`${path} содержит значение, несовместимое с JSON`);
18
+ }
19
+ if (ancestors.has(value)) {
20
+ throw new Error(`${path} содержит циклическую ссылку`);
21
+ }
22
+ const prototype = Object.getPrototypeOf(value);
23
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
24
+ throw new Error(`${path} должен содержать обычный JSON-объект`);
25
+ }
26
+ ancestors.add(value);
27
+ if (Array.isArray(value)) {
28
+ for (let index = 0; index < value.length; index += 1) {
29
+ if (!Object.hasOwn(value, index)) {
30
+ throw new Error(`${path}[${index}] отсутствует; разреженные массивы несовместимы с JSON`);
31
+ }
32
+ validateJsonValue(value[index], `${path}[${index}]`, ancestors);
33
+ }
34
+ }
35
+ else {
36
+ Object.entries(value).forEach(([key, item]) => {
37
+ validateJsonValue(item, `${path}.${key}`, ancestors);
38
+ });
39
+ }
40
+ ancestors.delete(value);
41
+ };
42
+ export const validateDatabase = (data) => {
43
+ if (!isObject(data)) {
44
+ throw new Error('База данных должна содержать JSON-объект');
45
+ }
46
+ validateJsonValue(data, 'База данных');
47
+ Object.entries(data).forEach(([resource, records]) => {
48
+ if (!RESOURCE_NAME_PATTERN.test(resource) || !isSafeKey(resource)) {
49
+ throw new Error(`Недопустимое имя ресурса «${resource}»`);
50
+ }
51
+ if (!Array.isArray(records)) {
52
+ throw new Error(`Ресурс «${resource}» должен содержать JSON-массив`);
53
+ }
54
+ const ids = new Set();
55
+ records.forEach((record, index) => {
56
+ if (!isObject(record)) {
57
+ throw new Error(`Запись ${index} ресурса «${resource}» должна содержать JSON-объект`);
58
+ }
59
+ if (typeof record.id !== 'string' && !(typeof record.id === 'number' && Number.isFinite(record.id))) {
60
+ throw new Error(`Запись ${index} ресурса «${resource}» должна содержать строковый или числовой id`);
61
+ }
62
+ if (String(record.id) === '') {
63
+ throw new Error(`Запись ${index} ресурса «${resource}» должна содержать непустой id`);
64
+ }
65
+ const id = String(record.id);
66
+ if (ids.has(id)) {
67
+ throw new Error(`Ресурс «${resource}» содержит повторяющийся id «${id}»`);
68
+ }
69
+ ids.add(id);
70
+ });
71
+ });
72
+ return data;
73
+ };
74
+ export const readJsonObjectFile = async (path, label) => {
75
+ const resolvedPath = resolve(path);
76
+ let source;
77
+ try {
78
+ source = await readFile(resolvedPath, 'utf8');
79
+ }
80
+ catch (error) {
81
+ if (isSystemError(error) && error.code === 'ENOENT') {
82
+ throw new Error(`${label} не найден: ${resolvedPath}`);
83
+ }
84
+ throw error;
85
+ }
86
+ const value = JSON.parse(source);
87
+ if (!isObject(value)) {
88
+ throw new Error(`${label} должен содержать JSON-объект`);
89
+ }
90
+ return value;
91
+ };
92
+ export const readDatabaseFile = async (databasePath) => validateDatabase(await readJsonObjectFile(databasePath, 'Файл базы данных'));
93
+ const createDiskDatabaseStore = async (databasePath) => {
94
+ const resolvedDatabasePath = resolveDatabasePath(databasePath);
95
+ const initialData = await readDatabaseFile(resolvedDatabasePath);
96
+ const database = await JSONFilePreset(resolvedDatabasePath, initialData);
97
+ const schedule = createSerialQueue();
98
+ const read = async () => {
99
+ database.data = await readDatabaseFile(resolvedDatabasePath);
100
+ return database.data;
101
+ };
102
+ const update = (operation) => schedule(async () => {
103
+ await read();
104
+ const result = operation(database);
105
+ validateDatabase(database.data);
106
+ await database.write();
107
+ return result;
108
+ });
109
+ return { database, path: resolvedDatabasePath, read, update };
110
+ };
111
+ const createMemoryDatabaseStore = (sourceData) => {
112
+ validateDatabase(sourceData);
113
+ const database = { data: structuredClone(sourceData) };
114
+ const schedule = createSerialQueue();
115
+ const read = async () => database.data;
116
+ const update = (operation) => schedule(() => {
117
+ const draft = { data: structuredClone(database.data) };
118
+ const result = operation(draft);
119
+ validateDatabase(draft.data);
120
+ database.data = draft.data;
121
+ return result;
122
+ });
123
+ return { database, read, update };
124
+ };
125
+ /** Creates a disk- or memory-backed database with serialized updates. */
126
+ export const createDatabaseStore = async (config) => (config.data != null ? createMemoryDatabaseStore(config.data) : createDiskDatabaseStore(config.path));
127
+ export const getCollection = (database, resource) => {
128
+ const collection = isSafeKey(resource) ? database.data[resource] : undefined;
129
+ if (!Array.isArray(collection)) {
130
+ throw createHttpError(404, 'Ресурс не найден');
131
+ }
132
+ return collection;
133
+ };
134
+ export const findItemIndex = (collection, id) => collection.findIndex((item) => String(item.id) === String(id));
135
+ export const createId = (collection) => createUniqueId((id) => findItemIndex(collection, id) !== -1);
136
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type { Readable } from 'node:stream';
2
+ import type { OpenapiSchema } from '../types.js';
3
+ export interface StoredFileMetadata {
4
+ directory: string;
5
+ mimeType: string;
6
+ name: string;
7
+ }
8
+ export interface FileRecord extends StoredFileMetadata {
9
+ size: number;
10
+ }
11
+ export interface FileMetadata extends FileRecord {
12
+ downloadUrl: string;
13
+ metadataUrl: string;
14
+ url: string;
15
+ }
16
+ export interface FileUpload extends StoredFileMetadata {
17
+ maxFileSize: number;
18
+ override: boolean;
19
+ stream: Readable;
20
+ }
21
+ export interface FileUpdate {
22
+ directory?: string;
23
+ name?: string;
24
+ }
25
+ export interface FileStore {
26
+ get(path: string): Promise<{
27
+ file: FileRecord;
28
+ stream: Readable;
29
+ }>;
30
+ metadata(path: string): Promise<FileRecord>;
31
+ remove(path: string): Promise<void>;
32
+ update(path: string, update: FileUpdate): Promise<FileRecord>;
33
+ upload(upload: FileUpload): Promise<{
34
+ created: boolean;
35
+ file: FileRecord;
36
+ }>;
37
+ }
38
+ export declare const FILE_HEADERS: {
39
+ readonly directory: {
40
+ readonly key: 'content-directory';
41
+ readonly name: 'Content-Directory';
42
+ };
43
+ readonly name: {
44
+ readonly key: 'content-name';
45
+ readonly name: 'Content-Name';
46
+ };
47
+ readonly override: {
48
+ readonly key: 'content-override';
49
+ readonly name: 'Content-Override';
50
+ };
51
+ };
52
+ export declare const FILE_ROUTES: {
53
+ readonly download: '/_files/download';
54
+ readonly metadata: '/_files/metadata';
55
+ readonly storage: '/_files/storage';
56
+ };
57
+ export declare const PATCH_BODY_LIMIT: number;
58
+ export declare const FILE_METADATA_SCHEMA: OpenapiSchema;
59
+ export declare const FILE_UPDATE_SCHEMA: OpenapiSchema;
60
+ export declare const getFileKey: ({ directory, name }: Pick<StoredFileMetadata, 'directory' | 'name'>) => string;
61
+ export declare const validateName: (value: unknown, source: string) => string;
62
+ export declare const validateDirectory: (value: unknown, source: string) => string;
63
+ export declare const normalizeMimeType: (value: unknown, source?: string) => string;
64
+ export declare const normalizeStoredFileMetadata: (value: unknown, source: string) => StoredFileMetadata;
65
+ export declare const getPathLocation: (path: string) => Pick<StoredFileMetadata, 'directory' | 'name'>;
66
+ export declare const createFileMetadata: (file: FileRecord) => FileMetadata;
67
+ export declare const getDownloadName: (name: string) => string;
@@ -0,0 +1,106 @@
1
+ import { basename } from 'node:path';
2
+ import { createHttpError, isObject } from '../utils.js';
3
+ export const FILE_HEADERS = {
4
+ directory: { key: 'content-directory', name: 'Content-Directory' },
5
+ name: { key: 'content-name', name: 'Content-Name' },
6
+ override: { key: 'content-override', name: 'Content-Override' },
7
+ };
8
+ export const FILE_ROUTES = {
9
+ download: '/_files/download',
10
+ metadata: '/_files/metadata',
11
+ storage: '/_files/storage',
12
+ };
13
+ export const PATCH_BODY_LIMIT = 64 * 1024;
14
+ const MIME_TYPE_PATTERN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+\/[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
15
+ export const FILE_METADATA_SCHEMA = {
16
+ properties: {
17
+ directory: { type: 'string' },
18
+ downloadUrl: { format: 'uri-reference', type: 'string' },
19
+ metadataUrl: { format: 'uri-reference', type: 'string' },
20
+ mimeType: { type: 'string' },
21
+ name: { type: 'string' },
22
+ size: { minimum: 0, type: 'integer' },
23
+ url: { format: 'uri-reference', type: 'string' },
24
+ },
25
+ required: ['directory', 'downloadUrl', 'metadataUrl', 'mimeType', 'name', 'size', 'url'],
26
+ type: 'object',
27
+ };
28
+ export const FILE_UPDATE_SCHEMA = {
29
+ additionalProperties: false,
30
+ anyOf: [{ required: ['directory'] }, { required: ['name'] }],
31
+ properties: { directory: { type: 'string' }, name: { type: 'string' } },
32
+ type: 'object',
33
+ };
34
+ export const getFileKey = ({ directory, name }) => [directory, name].filter(Boolean).join('/');
35
+ const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
36
+ const hasControlCharacter = (value) => [...value].some((character) => {
37
+ const code = character.charCodeAt(0);
38
+ return code <= 31 || code === 127;
39
+ });
40
+ export const validateName = (value, source) => {
41
+ if (typeof value !== 'string' ||
42
+ value === '' ||
43
+ value === '.' ||
44
+ value === '..' ||
45
+ value.endsWith('.') ||
46
+ value.endsWith(' ') ||
47
+ /[<>:"/\\|?*]/.test(value) ||
48
+ hasControlCharacter(value) ||
49
+ WINDOWS_RESERVED_NAME.test(value)) {
50
+ throw createHttpError(400, `${source} должен содержать безопасное имя файла`);
51
+ }
52
+ return value;
53
+ };
54
+ export const validateDirectory = (value, source) => {
55
+ if (typeof value !== 'string') {
56
+ throw createHttpError(400, `${source} должен содержать безопасный относительный путь`);
57
+ }
58
+ if (value === '') {
59
+ return value;
60
+ }
61
+ const parts = value.split('/');
62
+ if (value.startsWith('/') || value.endsWith('/') || value.includes('\\') || value.includes('\0') || parts.some((part) => part === '' || part === '.' || part === '..')) {
63
+ throw createHttpError(400, `${source} должен содержать безопасный относительный путь`);
64
+ }
65
+ return value;
66
+ };
67
+ export const normalizeMimeType = (value, source = 'Заголовок Content-Type') => {
68
+ const mimeType = typeof value === 'string' ? value.split(';', 1)[0].trim().toLowerCase() : '';
69
+ if (mimeType === '' || !MIME_TYPE_PATTERN.test(mimeType)) {
70
+ throw createHttpError(400, `${source} должен содержать корректный MIME-тип`);
71
+ }
72
+ return mimeType;
73
+ };
74
+ export const normalizeStoredFileMetadata = (value, source) => {
75
+ if (!isObject(value)) {
76
+ throw new Error(`${source} должна содержать JSON-объект`);
77
+ }
78
+ try {
79
+ return {
80
+ directory: validateDirectory(value.directory ?? '', `${source}.directory`),
81
+ mimeType: normalizeMimeType(value.mimeType, `${source}.mimeType`),
82
+ name: validateName(value.name, `${source}.name`),
83
+ };
84
+ }
85
+ catch (error) {
86
+ throw new Error(error instanceof Error ? error.message : String(error), { cause: error });
87
+ }
88
+ };
89
+ export const getPathLocation = (path) => {
90
+ const normalizedPath = validateDirectory(path, 'Путь файла');
91
+ const parts = normalizedPath.split('/');
92
+ const name = validateName(parts.pop(), 'Путь файла');
93
+ return { directory: parts.join('/'), name };
94
+ };
95
+ const encodeFilePath = (file) => getFileKey(file).split('/').map(encodeURIComponent).join('/');
96
+ export const createFileMetadata = (file) => {
97
+ const path = encodeFilePath(file);
98
+ return {
99
+ ...file,
100
+ downloadUrl: `${FILE_ROUTES.download}/${path}`,
101
+ metadataUrl: `${FILE_ROUTES.metadata}/${path}`,
102
+ url: `${FILE_ROUTES.storage}/${path}`,
103
+ };
104
+ };
105
+ export const getDownloadName = (name) => encodeURIComponent(basename(name)).replaceAll("'", '%27');
106
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/files/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { type FileStore } from './contract.js';
2
+ export declare const createDiskFileStore: ({ directory: sourceDirectoryPath, metadata: sourceMetadataPath }: {
3
+ directory: string;
4
+ metadata: string;
5
+ }) => Promise<FileStore>;
@@ -0,0 +1,313 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { access, lstat, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
5
+ import { Transform } from 'node:stream';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import { createHttpError, createSerialQueue, isSystemError } from '../utils.js';
8
+ import { getFileKey, normalizeStoredFileMetadata } from './contract.js';
9
+ const createSizeLimiter = (maxFileSize, onSize) => {
10
+ let size = 0;
11
+ return new Transform({
12
+ transform(chunk, _encoding, callback) {
13
+ size += chunk.length;
14
+ if (size > maxFileSize) {
15
+ callback(createHttpError(413, `Размер файла не должен превышать ${maxFileSize} байт`));
16
+ return;
17
+ }
18
+ onSize(size);
19
+ callback(null, chunk);
20
+ },
21
+ });
22
+ };
23
+ const pathExists = async (path) => {
24
+ try {
25
+ await access(path);
26
+ return true;
27
+ }
28
+ catch (error) {
29
+ if (isSystemError(error) && error.code === 'ENOENT') {
30
+ return false;
31
+ }
32
+ throw error;
33
+ }
34
+ };
35
+ const isPathInside = (rootPath, targetPath) => {
36
+ const relativePath = relative(rootPath, targetPath);
37
+ return relativePath === '' || (relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
38
+ };
39
+ const readMetadata = async (metadataPath) => {
40
+ let source;
41
+ try {
42
+ source = JSON.parse(await readFile(metadataPath, 'utf8'));
43
+ }
44
+ catch (error) {
45
+ if (isSystemError(error) && error.code === 'ENOENT') {
46
+ return new Map();
47
+ }
48
+ throw error;
49
+ }
50
+ if (!Array.isArray(source)) {
51
+ throw new Error(`Файл метаданных ${metadataPath} должен содержать JSON-массив`);
52
+ }
53
+ const files = new Map();
54
+ source.forEach((value, index) => {
55
+ const file = normalizeStoredFileMetadata(value, `Запись ${index} в файле метаданных ${metadataPath}`);
56
+ const path = getFileKey(file);
57
+ if (files.has(path)) {
58
+ throw new Error(`Файл метаданных ${metadataPath} содержит повторяющийся путь «${path}»`);
59
+ }
60
+ files.set(path, file);
61
+ });
62
+ return files;
63
+ };
64
+ const writeMetadata = async (metadataPath, files) => {
65
+ const temporaryPath = `${metadataPath}.${randomBytes(6).toString('hex')}.tmp`;
66
+ try {
67
+ await writeFile(temporaryPath, JSON.stringify([...files.values()], null, 2), { encoding: 'utf8', flag: 'wx' });
68
+ await rename(temporaryPath, metadataPath);
69
+ }
70
+ catch (error) {
71
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
72
+ throw error;
73
+ }
74
+ };
75
+ const getFileSize = async (path) => {
76
+ try {
77
+ return (await stat(path)).size;
78
+ }
79
+ catch (error) {
80
+ if (isSystemError(error) && error.code === 'ENOENT') {
81
+ throw createHttpError(404, 'Файл не найден');
82
+ }
83
+ throw error;
84
+ }
85
+ };
86
+ export const createDiskFileStore = async ({ directory: sourceDirectoryPath, metadata: sourceMetadataPath }) => {
87
+ const directoryPath = resolve(sourceDirectoryPath);
88
+ const metadataPath = resolve(sourceMetadataPath);
89
+ const stagingPath = resolve(directoryPath, '.deep-json-server');
90
+ const schedule = createSerialQueue();
91
+ const pendingCleanup = new Set();
92
+ await Promise.all([mkdir(directoryPath, { recursive: true }), mkdir(dirname(metadataPath), { recursive: true }), mkdir(stagingPath, { recursive: true })]);
93
+ const realDirectoryPath = await realpath(directoryPath);
94
+ let files = await readMetadata(metadataPath);
95
+ const commitMetadata = async (nextFiles, rollback, rollbackMessage) => {
96
+ try {
97
+ await writeMetadata(metadataPath, nextFiles);
98
+ }
99
+ catch (error) {
100
+ try {
101
+ await rollback();
102
+ }
103
+ catch (rollbackError) {
104
+ throw new AggregateError([error, rollbackError], rollbackMessage);
105
+ }
106
+ throw error;
107
+ }
108
+ files = nextFiles;
109
+ };
110
+ const assertContained = (path) => {
111
+ if (!isPathInside(realDirectoryPath, path)) {
112
+ throw createHttpError(400, 'Путь файла выходит за пределы директории хранения');
113
+ }
114
+ };
115
+ const resolveFilePath = (path) => {
116
+ const filePath = resolve(directoryPath, path);
117
+ if (filePath === directoryPath || !isPathInside(directoryPath, filePath) || filePath === metadataPath || isPathInside(stagingPath, filePath)) {
118
+ throw createHttpError(400, 'Путь файла выходит за пределы директории хранения');
119
+ }
120
+ return filePath;
121
+ };
122
+ const assertNoSymlinks = async (targetPath) => {
123
+ const relativePath = relative(directoryPath, targetPath);
124
+ const parts = relativePath === '' ? [] : relativePath.split(sep);
125
+ let currentPath = directoryPath;
126
+ for (const part of parts) {
127
+ currentPath = resolve(currentPath, part);
128
+ try {
129
+ if ((await lstat(currentPath)).isSymbolicLink()) {
130
+ throw createHttpError(400, 'Путь файла не должен содержать символические ссылки');
131
+ }
132
+ }
133
+ catch (error) {
134
+ if (!isSystemError(error) || error.code !== 'ENOENT') {
135
+ throw error;
136
+ }
137
+ }
138
+ }
139
+ };
140
+ const prepareTargetPath = async (path) => {
141
+ const filePath = resolveFilePath(path);
142
+ const targetDirectory = dirname(filePath);
143
+ const parts = relative(directoryPath, targetDirectory).split(sep).filter(Boolean);
144
+ let currentPath = directoryPath;
145
+ for (const part of parts) {
146
+ currentPath = resolve(currentPath, part);
147
+ try {
148
+ await mkdir(currentPath);
149
+ }
150
+ catch (error) {
151
+ if (!isSystemError(error) || error.code !== 'EEXIST') {
152
+ throw error;
153
+ }
154
+ }
155
+ const currentStats = await lstat(currentPath);
156
+ if (currentStats.isSymbolicLink() || !currentStats.isDirectory()) {
157
+ throw createHttpError(400, 'Путь файла должен содержать только обычные директории');
158
+ }
159
+ assertContained(await realpath(currentPath));
160
+ }
161
+ if (await pathExists(filePath)) {
162
+ await assertNoSymlinks(filePath);
163
+ assertContained(await realpath(filePath));
164
+ }
165
+ return filePath;
166
+ };
167
+ const resolveExistingPath = async (path) => {
168
+ const filePath = resolveFilePath(path);
169
+ try {
170
+ await assertNoSymlinks(filePath);
171
+ assertContained(await realpath(filePath));
172
+ }
173
+ catch (error) {
174
+ if (isSystemError(error) && error.code === 'ENOENT') {
175
+ throw createHttpError(404, 'Файл не найден');
176
+ }
177
+ throw error;
178
+ }
179
+ return filePath;
180
+ };
181
+ const cleanupLater = async (path) => {
182
+ try {
183
+ await rm(path, { force: true });
184
+ pendingCleanup.delete(path);
185
+ }
186
+ catch {
187
+ pendingCleanup.add(path);
188
+ }
189
+ };
190
+ const flushCleanup = async () => {
191
+ await Promise.all([...pendingCleanup].map(cleanupLater));
192
+ };
193
+ const findFile = (path) => {
194
+ const file = files.get(path);
195
+ if (file == null) {
196
+ throw createHttpError(404, 'Файл не найден');
197
+ }
198
+ return file;
199
+ };
200
+ const metadata = async (path) => {
201
+ const file = findFile(path);
202
+ const filePath = await resolveExistingPath(path);
203
+ return { ...file, size: await getFileSize(filePath) };
204
+ };
205
+ const get = async (path) => {
206
+ const file = findFile(path);
207
+ const filePath = await resolveExistingPath(path);
208
+ const handle = await open(filePath, 'r');
209
+ try {
210
+ const size = (await handle.stat()).size;
211
+ return { file: { ...file, size }, stream: handle.createReadStream() };
212
+ }
213
+ catch (error) {
214
+ await handle.close();
215
+ throw error;
216
+ }
217
+ };
218
+ const upload = async ({ directory, maxFileSize, mimeType, name, override, stream }) => {
219
+ const storedFile = { directory, mimeType, name };
220
+ const key = getFileKey(storedFile);
221
+ if (files.has(key) && !override) {
222
+ throw createHttpError(409, 'Файл уже существует');
223
+ }
224
+ const stagedPath = resolve(stagingPath, `${randomBytes(12).toString('hex')}.upload`);
225
+ let size = 0;
226
+ try {
227
+ await pipeline(stream, createSizeLimiter(maxFileSize, (value) => {
228
+ size = value;
229
+ }), createWriteStream(stagedPath, { flags: 'wx' }));
230
+ return await schedule(async () => {
231
+ await flushCleanup();
232
+ const path = await prepareTargetPath(key);
233
+ const existsOnDisk = await pathExists(path);
234
+ const exists = files.has(key) || existsOnDisk;
235
+ if (exists && !override) {
236
+ throw createHttpError(409, 'Файл уже существует');
237
+ }
238
+ const backupPath = `${path}.${randomBytes(6).toString('hex')}.backup`;
239
+ let backedUp = false;
240
+ let installed = false;
241
+ let committed = false;
242
+ try {
243
+ if (existsOnDisk) {
244
+ await rename(path, backupPath);
245
+ backedUp = true;
246
+ }
247
+ await rename(stagedPath, path);
248
+ installed = true;
249
+ const nextFiles = new Map(files);
250
+ nextFiles.set(key, storedFile);
251
+ await writeMetadata(metadataPath, nextFiles);
252
+ files = nextFiles;
253
+ committed = true;
254
+ return { created: !exists, file: { ...storedFile, size } };
255
+ }
256
+ catch (error) {
257
+ const rollbackErrors = [];
258
+ if (installed) {
259
+ await rm(path, { force: true }).catch((rollbackError) => rollbackErrors.push(rollbackError));
260
+ }
261
+ if (backedUp) {
262
+ await rename(backupPath, path).catch((rollbackError) => rollbackErrors.push(rollbackError));
263
+ }
264
+ if (rollbackErrors.length > 0) {
265
+ throw new AggregateError([error, ...rollbackErrors], 'Не удалось сохранить файл и полностью откатить операцию');
266
+ }
267
+ throw error;
268
+ }
269
+ finally {
270
+ if (committed && backedUp) {
271
+ await cleanupLater(backupPath);
272
+ }
273
+ }
274
+ });
275
+ }
276
+ finally {
277
+ await cleanupLater(stagedPath);
278
+ }
279
+ };
280
+ const update = (sourcePath, updates) => schedule(async () => {
281
+ await flushCleanup();
282
+ const file = findFile(sourcePath);
283
+ const updatedFile = { ...file, ...updates };
284
+ const targetPath = getFileKey(updatedFile);
285
+ const sourceFilePath = await resolveExistingPath(sourcePath);
286
+ if (sourcePath === targetPath) {
287
+ return { ...file, size: await getFileSize(sourceFilePath) };
288
+ }
289
+ const targetFilePath = await prepareTargetPath(targetPath);
290
+ if (files.has(targetPath) || (await pathExists(targetFilePath))) {
291
+ throw createHttpError(409, 'Файл с таким путём уже существует');
292
+ }
293
+ await rename(sourceFilePath, targetFilePath);
294
+ const nextFiles = new Map(files);
295
+ nextFiles.delete(sourcePath);
296
+ nextFiles.set(targetPath, updatedFile);
297
+ await commitMetadata(nextFiles, () => rename(targetFilePath, sourceFilePath), 'Не удалось обновить файл и откатить перемещение');
298
+ return { ...updatedFile, size: await getFileSize(targetFilePath) };
299
+ });
300
+ const remove = (path) => schedule(async () => {
301
+ await flushCleanup();
302
+ findFile(path);
303
+ const filePath = await resolveExistingPath(path);
304
+ const temporaryPath = `${filePath}.${randomBytes(6).toString('hex')}.delete`;
305
+ await rename(filePath, temporaryPath);
306
+ const nextFiles = new Map(files);
307
+ nextFiles.delete(path);
308
+ await commitMetadata(nextFiles, () => rename(temporaryPath, filePath), 'Не удалось удалить файл и откатить операцию');
309
+ await cleanupLater(temporaryPath);
310
+ });
311
+ return { get, metadata, remove, update, upload };
312
+ };
313
+ //# sourceMappingURL=disk-store.js.map