@ryuzaki13/react-foundation-lib 2.16.0 → 2.17.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.
- package/README.md +4 -1
- package/dist/binary/getBase64DecodedSize.d.ts +7 -0
- package/dist/binary/getBase64DecodedSize.d.ts.map +1 -0
- package/dist/binary/index.d.ts +1 -0
- package/dist/binary/index.d.ts.map +1 -1
- package/dist/binary/index.js +8 -3
- package/dist/binary/index.js.map +1 -1
- package/dist/file-system/fileSystemTypes.d.ts +10 -0
- package/dist/file-system/fileSystemTypes.d.ts.map +1 -0
- package/dist/file-system/index.d.ts +4 -0
- package/dist/file-system/index.d.ts.map +1 -0
- package/dist/file-system/index.js +37 -0
- package/dist/file-system/index.js.map +1 -0
- package/dist/file-system/isFileSystemNotFoundError.d.ts +3 -0
- package/dist/file-system/isFileSystemNotFoundError.d.ts.map +1 -0
- package/dist/file-system/resolvePathInsideRoot.d.ts +9 -0
- package/dist/file-system/resolvePathInsideRoot.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/binary/README.mdx +32 -8
- package/src/file-system/README.mdx +64 -0
package/README.md
CHANGED
|
@@ -89,6 +89,7 @@ npm run pack:dry-run
|
|
|
89
89
|
| Tree table преобразования | [`tree-table`](./src/tree-table/index.ts), документация в общем справочнике [`table/README.mdx`](./src/table/README.mdx), тесты в [`tree-table`](./src/tree-table) |
|
|
90
90
|
| Excel export | [`excel`](./src/excel/index.ts), подробный README: [`excel/README.mdx`](./src/excel/README.mdx), тесты: [`excel.test.ts`](./src/excel/excel.test.ts), [`tableExport.test.ts`](./src/excel/tableExport.test.ts) |
|
|
91
91
|
| FileReader, изображения, base64 Blob | [`file`](./src/file/index.ts), [`binary`](./src/binary/index.ts) |
|
|
92
|
+
| Проверка серверного пути внутри root, ошибки отсутствующего файла | [`file-system`](./src/file-system/index.ts), Node.js API: [`file-system/README.mdx`](./src/file-system/README.mdx) |
|
|
92
93
|
| DOM overlay, portal, focus, download | [`dom`](./src/dom/index.ts), тест: [`dom.test.tsx`](./src/dom/dom.test.tsx) |
|
|
93
94
|
| Debounce/throttle, DnD sensors, search params, listbox | [`hooks`](./src/hooks/index.ts), подробный README: [`hooks/README.mdx`](./src/hooks/README.mdx), [`utils/keyboard.ts`](./src/utils/keyboard.ts) |
|
|
94
95
|
| Breakpoints, media queries и responsive props | [`media`](./src/media/index.ts), подробный README: [`media/README.mdx`](./src/media/README.mdx), тест: [`media.test.ts`](./src/media/media.test.ts) |
|
|
@@ -118,6 +119,7 @@ npm run pack:dry-run
|
|
|
118
119
|
| `/error-report` | [`error-report/README.mdx`](./src/error-report/README.mdx) |
|
|
119
120
|
| `/excel` | [`excel/README.mdx`](./src/excel/README.mdx) |
|
|
120
121
|
| `/file` | [`file/README.mdx`](./src/file/README.mdx) |
|
|
122
|
+
| `/file-system` | [Node.js: пути внутри хранилища](./src/file-system/README.mdx) |
|
|
121
123
|
| `/fingerprint` | [`fingerprint/README.mdx`](./src/fingerprint/README.mdx) |
|
|
122
124
|
| `/form` | [`form/README.mdx`](./src/form/README.mdx) |
|
|
123
125
|
| `/formatters` | [`formatters/README.mdx`](./src/formatters/README.mdx) и его предметные README |
|
|
@@ -528,10 +530,11 @@ Pipeline — runtime форматирования ячейки. Он прини
|
|
|
528
530
|
| API | Поведение |
|
|
529
531
|
| ---------------- | ------------------------------------------------------------------------------ |
|
|
530
532
|
| `binaryToBlob` | Base64/data URL/raw binary string -> `Blob`; валидирует пустые и битые данные. |
|
|
533
|
+
| `getBase64DecodedSize` | Проверяет стандартный Base64 и считает размер без декодирования; неверная форма -> `null`, пустая строка -> `0`. |
|
|
531
534
|
| `detectMimeType` | MIME по filename, data URL, byte signature, fallback `application/pdf`. |
|
|
532
535
|
| `useBinaryFile` | Hook: base64 string -> `{ blob, mime }` или error; object URL не создает. |
|
|
533
536
|
|
|
534
|
-
|
|
537
|
+
Тесты: [`binaryToBlob.test.ts`](./src/binary/binaryToBlob.test.ts), [`getBase64DecodedSize.test.ts`](./src/binary/getBase64DecodedSize.test.ts).
|
|
535
538
|
|
|
536
539
|
### `xml`
|
|
537
540
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Проверяет стандартную Base64-форму и считает байты без декодирования или создания Buffer/Blob.
|
|
3
|
+
* Невалидный payload возвращает null, пустая строка — 0; допустимый размер определяет вызывающий контракт.
|
|
4
|
+
* Data URL, whitespace, URL-safe алфавит и отсутствующий padding здесь не нормализуются.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getBase64DecodedSize(value: string): number | null;
|
|
7
|
+
//# sourceMappingURL=getBase64DecodedSize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getBase64DecodedSize.d.ts","sourceRoot":"","sources":["../../src/binary/getBase64DecodedSize.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMjE"}
|
package/dist/binary/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/binary/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/binary/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,cAAc,iBAAiB,CAAC"}
|
package/dist/binary/index.js
CHANGED
|
@@ -99,10 +99,15 @@ var i = "application/octet-stream", a = "application/pdf", o = /^data:([^,]*);ba
|
|
|
99
99
|
if (n) return n;
|
|
100
100
|
}
|
|
101
101
|
return a;
|
|
102
|
-
}
|
|
102
|
+
}, y = /[^A-Za-z0-9+/=]/;
|
|
103
|
+
function b(e) {
|
|
104
|
+
if (e.length % 4 != 0 || y.test(e)) return null;
|
|
105
|
+
let t = e.endsWith("==") ? 2 : +!!e.endsWith("="), n = e.indexOf("=");
|
|
106
|
+
return n !== -1 && n !== e.length - t ? null : Math.floor(e.length * 3 / 4) - t;
|
|
107
|
+
}
|
|
103
108
|
//#endregion
|
|
104
109
|
//#region src/binary/useBinaryFile.ts
|
|
105
|
-
function
|
|
110
|
+
function x(i) {
|
|
106
111
|
let [a, o] = r(null), [s, c] = r(null), l = n((e) => o(e)), u = n((e) => c(e));
|
|
107
112
|
return t(() => {
|
|
108
113
|
if (i) try {
|
|
@@ -121,6 +126,6 @@ function y(i) {
|
|
|
121
126
|
};
|
|
122
127
|
}
|
|
123
128
|
//#endregion
|
|
124
|
-
export { _ as binaryToBlob, v as detectMimeType,
|
|
129
|
+
export { _ as binaryToBlob, v as detectMimeType, b as getBase64DecodedSize, x as useBinaryFile };
|
|
125
130
|
|
|
126
131
|
//# sourceMappingURL=index.js.map
|
package/dist/binary/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/binary/binaryToBlob.ts","../../src/binary/useBinaryFile.ts"],"sourcesContent":["const DEFAULT_BLOB_MIME_TYPE = \"application/octet-stream\";\nconst DEFAULT_DOCUMENT_MIME_TYPE = \"application/pdf\";\nconst BASE64_DATA_URL_PATTERN = /^data:([^,]*);base64,([\\s\\S]*)$/i;\n\nconst MIME_TYPE_BY_EXTENSION: Record<string, string> = {\n\tbmp: \"image/bmp\",\n\tcsv: \"text/csv\",\n\tdoc: \"application/msword\",\n\tdocx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n\tgif: \"image/gif\",\n\tjpeg: \"image/jpeg\",\n\tjpg: \"image/jpeg\",\n\tjson: \"application/json\",\n\tpdf: \"application/pdf\",\n\tpng: \"image/png\",\n\tppt: \"application/vnd.ms-powerpoint\",\n\tpptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n\tsvg: \"image/svg+xml\",\n\ttif: \"image/tiff\",\n\ttiff: \"image/tiff\",\n\ttxt: \"text/plain\",\n\twebp: \"image/webp\",\n\txls: \"application/vnd.ms-excel\",\n\txlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\txml: \"application/xml\",\n\tzip: \"application/zip\"\n};\n\nconst normalizeBase64 = (value: string): string => {\n\tconst normalized = value.replace(/\\s/g, \"\").replace(/-/g, \"+\").replace(/_/g, \"/\");\n\tconst paddingRest = normalized.length % 4;\n\n\tif (paddingRest === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {\n\t\tthrow new Error(\"Некорректный формат base64\");\n\t}\n\n\treturn paddingRest === 0 ? normalized : `${normalized}${\"=\".repeat(4 - paddingRest)}`;\n};\n\nconst getBase64RawPayload = (binaryData: string): { data: string; mimeType: string | null } => {\n\tconst dataUrlMatch = binaryData.match(BASE64_DATA_URL_PATTERN);\n\n\tif (!dataUrlMatch) {\n\t\treturn { data: binaryData, mimeType: null };\n\t}\n\n\tconst [, rawMimeType, rawData] = dataUrlMatch;\n\tconst mimeType = rawMimeType.split(\";\")[0].trim();\n\n\treturn {\n\t\tdata: rawData,\n\t\tmimeType: mimeType || null\n\t};\n};\n\nconst getBase64Payload = (binaryData: string): { data: string; mimeType: string | null } => {\n\tconst payload = getBase64RawPayload(binaryData);\n\n\treturn {\n\t\tdata: normalizeBase64(payload.data),\n\t\tmimeType: payload.mimeType\n\t};\n};\n\nconst getDataUrlMimeType = (binaryData: string): string | null => {\n\tconst dataUrlMatch = binaryData.match(BASE64_DATA_URL_PATTERN);\n\n\tif (!dataUrlMatch) {\n\t\treturn null;\n\t}\n\n\tconst mimeType = dataUrlMatch[1].split(\";\")[0].trim();\n\n\treturn mimeType || null;\n};\n\nconst decodeBase64ToBytes = (binaryData: string): Uint8Array => {\n\tconst { data } = getBase64Payload(binaryData);\n\tconst binaryString = atob(data);\n\tconst byteArray = new Uint8Array(binaryString.length);\n\n\tfor (let index = 0; index < binaryString.length; index++) {\n\t\tbyteArray[index] = binaryString.charCodeAt(index);\n\t}\n\n\treturn byteArray;\n};\n\nconst binaryStringToBytes = (binaryData: string): Uint8Array => {\n\tconst byteArray = new Uint8Array(binaryData.length);\n\n\tfor (let index = 0; index < binaryData.length; index++) {\n\t\tbyteArray[index] = binaryData.charCodeAt(index);\n\t}\n\n\treturn byteArray;\n};\n\nconst copyBytesToArrayBuffer = (byteArray: Uint8Array): ArrayBuffer => {\n\tconst buffer = new ArrayBuffer(byteArray.byteLength);\n\tconst view = new Uint8Array(buffer);\n\n\tview.set(byteArray);\n\n\treturn buffer;\n};\n\nconst decodeHeaderFromBase64 = (binaryData: string): string | null => {\n\ttry {\n\t\tconst { data } = getBase64RawPayload(binaryData);\n\t\tconst headerData = data.slice(0, 128);\n\t\tconst binaryString = atob(normalizeBase64(headerData));\n\n\t\treturn binaryString;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst detectMimeTypeBySignature = (binaryData: string): string | null => {\n\tconst header = decodeHeaderFromBase64(binaryData);\n\n\tif (!header) {\n\t\treturn null;\n\t}\n\n\tconst bytes = new Uint8Array(header.length);\n\n\tfor (let index = 0; index < header.length; index++) {\n\t\tbytes[index] = header.charCodeAt(index);\n\t}\n\n\tif (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) {\n\t\treturn \"application/pdf\";\n\t}\n\n\tif (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {\n\t\treturn \"image/jpeg\";\n\t}\n\n\tif (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {\n\t\treturn \"image/png\";\n\t}\n\n\tif (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n\t\treturn \"image/gif\";\n\t}\n\n\tif (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {\n\t\treturn \"image/webp\";\n\t}\n\n\tif (bytes[0] === 0x50 && bytes[1] === 0x4b) {\n\t\treturn \"application/zip\";\n\t}\n\n\tconst textHeader = header\n\t\t.replace(/^\\uFEFF/, \"\")\n\t\t.trimStart()\n\t\t.slice(0, 16);\n\n\tif (textHeader.startsWith(\"<?xml\") || textHeader.startsWith(\"<\")) {\n\t\treturn \"application/xml\";\n\t}\n\n\treturn null;\n};\n\n/**\n * Преобразует base64 или бинарную строку в Blob.\n *\n * Data URL с префиксом `data:*;base64,` поддерживается, сам URL здесь не создаётся.\n */\nexport const binaryToBlob = (binaryData: string, mimeType: string = DEFAULT_BLOB_MIME_TYPE, isBase64 = true): Blob => {\n\tif (!binaryData) {\n\t\tthrow new Error(\"Не переданы бинарные данные\");\n\t}\n\n\ttry {\n\t\tconst byteArray = isBase64 ? decodeBase64ToBytes(binaryData) : binaryStringToBytes(binaryData);\n\n\t\tif (byteArray.length === 0) {\n\t\t\tthrow new Error(\"После преобразования не осталось данных\");\n\t\t}\n\n\t\treturn new Blob([copyBytesToArrayBuffer(byteArray)], { type: mimeType.trim() || DEFAULT_BLOB_MIME_TYPE });\n\t} catch (error) {\n\t\tif (error instanceof Error) {\n\t\t\tthrow error;\n\t\t}\n\t\tthrow new Error(\"Некорректный формат бинарных данных\");\n\t}\n};\n\n/**\n * Определяет MIME-тип по имени файла, data URL или сигнатуре base64-данных.\n *\n * Если определить тип не удалось, используется PDF: большинство документов в этом контуре приходят именно так.\n */\nexport const detectMimeType = (fileName?: string, binaryData?: string): string => {\n\tif (fileName) {\n\t\tconst extension = fileName.toLowerCase().split(\".\").pop();\n\n\t\tif (extension && MIME_TYPE_BY_EXTENSION[extension]) {\n\t\t\treturn MIME_TYPE_BY_EXTENSION[extension];\n\t\t}\n\t}\n\n\tif (binaryData) {\n\t\tconst dataUrlMimeType = getDataUrlMimeType(binaryData);\n\n\t\tif (dataUrlMimeType) {\n\t\t\treturn dataUrlMimeType;\n\t\t}\n\n\t\tconst signatureMimeType = detectMimeTypeBySignature(binaryData);\n\n\t\tif (signatureMimeType) {\n\t\t\treturn signatureMimeType;\n\t\t}\n\t}\n\n\treturn DEFAULT_DOCUMENT_MIME_TYPE;\n};\n","import { useEffect, useEffectEvent, useState } from \"react\";\n\nimport { logError } from \"../utils\";\n\nimport { binaryToBlob, detectMimeType } from \"./binaryToBlob\";\n\nexport function useBinaryFile(bin: string | undefined) {\n\tconst [entry, setEntry] = useState<{ blob: Blob; mime: string } | null>(null);\n\tconst [error, setError] = useState<string | null>(null);\n\n\tconst updateEntry = useEffectEvent((data: { blob: Blob; mime: string } | null) => setEntry(data));\n\tconst updateError = useEffectEvent((error: string | null) => setError(error));\n\n\tuseEffect(() => {\n\t\tif (bin) {\n\t\t\ttry {\n\t\t\t\tconst mime = detectMimeType(undefined, bin);\n\t\t\t\tconst blob = binaryToBlob(bin, mime, true);\n\n\t\t\t\tupdateEntry({ blob, mime });\n\t\t\t\tupdateError(null);\n\t\t\t} catch (e) {\n\t\t\t\tlogError(e);\n\n\t\t\t\tupdateError(String(e));\n\t\t\t\tupdateEntry(null);\n\t\t\t}\n\t\t} else {\n\t\t\tupdateEntry(null);\n\t\t\tupdateError(null);\n\t\t}\n\t}, [bin]);\n\n\treturn { data: entry, error };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/binary/binaryToBlob.ts","../../src/binary/getBase64DecodedSize.ts","../../src/binary/useBinaryFile.ts"],"sourcesContent":["const DEFAULT_BLOB_MIME_TYPE = \"application/octet-stream\";\nconst DEFAULT_DOCUMENT_MIME_TYPE = \"application/pdf\";\nconst BASE64_DATA_URL_PATTERN = /^data:([^,]*);base64,([\\s\\S]*)$/i;\n\nconst MIME_TYPE_BY_EXTENSION: Record<string, string> = {\n\tbmp: \"image/bmp\",\n\tcsv: \"text/csv\",\n\tdoc: \"application/msword\",\n\tdocx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n\tgif: \"image/gif\",\n\tjpeg: \"image/jpeg\",\n\tjpg: \"image/jpeg\",\n\tjson: \"application/json\",\n\tpdf: \"application/pdf\",\n\tpng: \"image/png\",\n\tppt: \"application/vnd.ms-powerpoint\",\n\tpptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n\tsvg: \"image/svg+xml\",\n\ttif: \"image/tiff\",\n\ttiff: \"image/tiff\",\n\ttxt: \"text/plain\",\n\twebp: \"image/webp\",\n\txls: \"application/vnd.ms-excel\",\n\txlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\txml: \"application/xml\",\n\tzip: \"application/zip\"\n};\n\nconst normalizeBase64 = (value: string): string => {\n\tconst normalized = value.replace(/\\s/g, \"\").replace(/-/g, \"+\").replace(/_/g, \"/\");\n\tconst paddingRest = normalized.length % 4;\n\n\tif (paddingRest === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {\n\t\tthrow new Error(\"Некорректный формат base64\");\n\t}\n\n\treturn paddingRest === 0 ? normalized : `${normalized}${\"=\".repeat(4 - paddingRest)}`;\n};\n\nconst getBase64RawPayload = (binaryData: string): { data: string; mimeType: string | null } => {\n\tconst dataUrlMatch = binaryData.match(BASE64_DATA_URL_PATTERN);\n\n\tif (!dataUrlMatch) {\n\t\treturn { data: binaryData, mimeType: null };\n\t}\n\n\tconst [, rawMimeType, rawData] = dataUrlMatch;\n\tconst mimeType = rawMimeType.split(\";\")[0].trim();\n\n\treturn {\n\t\tdata: rawData,\n\t\tmimeType: mimeType || null\n\t};\n};\n\nconst getBase64Payload = (binaryData: string): { data: string; mimeType: string | null } => {\n\tconst payload = getBase64RawPayload(binaryData);\n\n\treturn {\n\t\tdata: normalizeBase64(payload.data),\n\t\tmimeType: payload.mimeType\n\t};\n};\n\nconst getDataUrlMimeType = (binaryData: string): string | null => {\n\tconst dataUrlMatch = binaryData.match(BASE64_DATA_URL_PATTERN);\n\n\tif (!dataUrlMatch) {\n\t\treturn null;\n\t}\n\n\tconst mimeType = dataUrlMatch[1].split(\";\")[0].trim();\n\n\treturn mimeType || null;\n};\n\nconst decodeBase64ToBytes = (binaryData: string): Uint8Array => {\n\tconst { data } = getBase64Payload(binaryData);\n\tconst binaryString = atob(data);\n\tconst byteArray = new Uint8Array(binaryString.length);\n\n\tfor (let index = 0; index < binaryString.length; index++) {\n\t\tbyteArray[index] = binaryString.charCodeAt(index);\n\t}\n\n\treturn byteArray;\n};\n\nconst binaryStringToBytes = (binaryData: string): Uint8Array => {\n\tconst byteArray = new Uint8Array(binaryData.length);\n\n\tfor (let index = 0; index < binaryData.length; index++) {\n\t\tbyteArray[index] = binaryData.charCodeAt(index);\n\t}\n\n\treturn byteArray;\n};\n\nconst copyBytesToArrayBuffer = (byteArray: Uint8Array): ArrayBuffer => {\n\tconst buffer = new ArrayBuffer(byteArray.byteLength);\n\tconst view = new Uint8Array(buffer);\n\n\tview.set(byteArray);\n\n\treturn buffer;\n};\n\nconst decodeHeaderFromBase64 = (binaryData: string): string | null => {\n\ttry {\n\t\tconst { data } = getBase64RawPayload(binaryData);\n\t\tconst headerData = data.slice(0, 128);\n\t\tconst binaryString = atob(normalizeBase64(headerData));\n\n\t\treturn binaryString;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst detectMimeTypeBySignature = (binaryData: string): string | null => {\n\tconst header = decodeHeaderFromBase64(binaryData);\n\n\tif (!header) {\n\t\treturn null;\n\t}\n\n\tconst bytes = new Uint8Array(header.length);\n\n\tfor (let index = 0; index < header.length; index++) {\n\t\tbytes[index] = header.charCodeAt(index);\n\t}\n\n\tif (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) {\n\t\treturn \"application/pdf\";\n\t}\n\n\tif (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {\n\t\treturn \"image/jpeg\";\n\t}\n\n\tif (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {\n\t\treturn \"image/png\";\n\t}\n\n\tif (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n\t\treturn \"image/gif\";\n\t}\n\n\tif (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {\n\t\treturn \"image/webp\";\n\t}\n\n\tif (bytes[0] === 0x50 && bytes[1] === 0x4b) {\n\t\treturn \"application/zip\";\n\t}\n\n\tconst textHeader = header\n\t\t.replace(/^\\uFEFF/, \"\")\n\t\t.trimStart()\n\t\t.slice(0, 16);\n\n\tif (textHeader.startsWith(\"<?xml\") || textHeader.startsWith(\"<\")) {\n\t\treturn \"application/xml\";\n\t}\n\n\treturn null;\n};\n\n/**\n * Преобразует base64 или бинарную строку в Blob.\n *\n * Data URL с префиксом `data:*;base64,` поддерживается, сам URL здесь не создаётся.\n */\nexport const binaryToBlob = (binaryData: string, mimeType: string = DEFAULT_BLOB_MIME_TYPE, isBase64 = true): Blob => {\n\tif (!binaryData) {\n\t\tthrow new Error(\"Не переданы бинарные данные\");\n\t}\n\n\ttry {\n\t\tconst byteArray = isBase64 ? decodeBase64ToBytes(binaryData) : binaryStringToBytes(binaryData);\n\n\t\tif (byteArray.length === 0) {\n\t\t\tthrow new Error(\"После преобразования не осталось данных\");\n\t\t}\n\n\t\treturn new Blob([copyBytesToArrayBuffer(byteArray)], { type: mimeType.trim() || DEFAULT_BLOB_MIME_TYPE });\n\t} catch (error) {\n\t\tif (error instanceof Error) {\n\t\t\tthrow error;\n\t\t}\n\t\tthrow new Error(\"Некорректный формат бинарных данных\");\n\t}\n};\n\n/**\n * Определяет MIME-тип по имени файла, data URL или сигнатуре base64-данных.\n *\n * Если определить тип не удалось, используется PDF: большинство документов в этом контуре приходят именно так.\n */\nexport const detectMimeType = (fileName?: string, binaryData?: string): string => {\n\tif (fileName) {\n\t\tconst extension = fileName.toLowerCase().split(\".\").pop();\n\n\t\tif (extension && MIME_TYPE_BY_EXTENSION[extension]) {\n\t\t\treturn MIME_TYPE_BY_EXTENSION[extension];\n\t\t}\n\t}\n\n\tif (binaryData) {\n\t\tconst dataUrlMimeType = getDataUrlMimeType(binaryData);\n\n\t\tif (dataUrlMimeType) {\n\t\t\treturn dataUrlMimeType;\n\t\t}\n\n\t\tconst signatureMimeType = detectMimeTypeBySignature(binaryData);\n\n\t\tif (signatureMimeType) {\n\t\t\treturn signatureMimeType;\n\t\t}\n\t}\n\n\treturn DEFAULT_DOCUMENT_MIME_TYPE;\n};\n","const INVALID_BASE64_CHARACTER_PATTERN = /[^A-Za-z0-9+/=]/;\n\n/**\n * Проверяет стандартную Base64-форму и считает байты без декодирования или создания Buffer/Blob.\n * Невалидный payload возвращает null, пустая строка — 0; допустимый размер определяет вызывающий контракт.\n * Data URL, whitespace, URL-safe алфавит и отсутствующий padding здесь не нормализуются.\n */\nexport function getBase64DecodedSize(value: string): number | null {\n\tif (value.length % 4 !== 0 || INVALID_BASE64_CHARACTER_PATTERN.test(value)) return null;\n\tconst paddingLength = value.endsWith(\"==\") ? 2 : value.endsWith(\"=\") ? 1 : 0;\n\tconst firstPaddingIndex = value.indexOf(\"=\");\n\tif (firstPaddingIndex !== -1 && firstPaddingIndex !== value.length - paddingLength) return null;\n\treturn Math.floor((value.length * 3) / 4) - paddingLength;\n}\n","import { useEffect, useEffectEvent, useState } from \"react\";\n\nimport { logError } from \"../utils\";\n\nimport { binaryToBlob, detectMimeType } from \"./binaryToBlob\";\n\nexport function useBinaryFile(bin: string | undefined) {\n\tconst [entry, setEntry] = useState<{ blob: Blob; mime: string } | null>(null);\n\tconst [error, setError] = useState<string | null>(null);\n\n\tconst updateEntry = useEffectEvent((data: { blob: Blob; mime: string } | null) => setEntry(data));\n\tconst updateError = useEffectEvent((error: string | null) => setError(error));\n\n\tuseEffect(() => {\n\t\tif (bin) {\n\t\t\ttry {\n\t\t\t\tconst mime = detectMimeType(undefined, bin);\n\t\t\t\tconst blob = binaryToBlob(bin, mime, true);\n\n\t\t\t\tupdateEntry({ blob, mime });\n\t\t\t\tupdateError(null);\n\t\t\t} catch (e) {\n\t\t\t\tlogError(e);\n\n\t\t\t\tupdateError(String(e));\n\t\t\t\tupdateEntry(null);\n\t\t\t}\n\t\t} else {\n\t\t\tupdateEntry(null);\n\t\t\tupdateError(null);\n\t\t}\n\t}, [bin]);\n\n\treturn { data: entry, error };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Определяет, какую существующую часть пути дополнительно проверить через Node.js realpath. */
|
|
2
|
+
export type PathRealpathMode = "none" | "parent" | "target";
|
|
3
|
+
/** Root принадлежит серверной конфигурации; сегменты обозначают только путь внутри него. */
|
|
4
|
+
export type ResolvePathInsideRootOptions = Readonly<{
|
|
5
|
+
rootPath: string;
|
|
6
|
+
pathSegments: readonly string[];
|
|
7
|
+
/** По умолчанию проверяется существующий target, включая символические ссылки. */
|
|
8
|
+
realpathMode?: PathRealpathMode;
|
|
9
|
+
}>;
|
|
10
|
+
//# sourceMappingURL=fileSystemTypes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileSystemTypes.d.ts","sourceRoot":"","sources":["../../src/file-system/fileSystemTypes.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE5D,4FAA4F;AAC5F,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,kFAAkF;IAClF,YAAY,CAAC,EAAE,gBAAgB,CAAC;CAChC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file-system/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { i as e } from "../chunks/isRecord-DXDTW-jA.js";
|
|
2
|
+
import { realpath as t } from "node:fs/promises";
|
|
3
|
+
import { dirname as n, isAbsolute as r, relative as i, resolve as a, sep as o } from "node:path";
|
|
4
|
+
//#region src/file-system/isFileSystemNotFoundError.ts
|
|
5
|
+
function s(t) {
|
|
6
|
+
return e(t) ? t.code === "ENOENT" || t.code === "ENOTDIR" : !1;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/file-system/resolvePathInsideRoot.ts
|
|
10
|
+
function c(e) {
|
|
11
|
+
let t = e.trim();
|
|
12
|
+
if (!t) throw Error("Корневая папка хранилища не задана.");
|
|
13
|
+
return a(t);
|
|
14
|
+
}
|
|
15
|
+
function l(e, t) {
|
|
16
|
+
let n = i(e, t);
|
|
17
|
+
return n === "" || n !== ".." && !n.startsWith(`..${o}`) && !r(n);
|
|
18
|
+
}
|
|
19
|
+
async function u(e, r, i) {
|
|
20
|
+
let a;
|
|
21
|
+
try {
|
|
22
|
+
a = await t(e);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
throw Error("Корневая папка хранилища недоступна.", { cause: e });
|
|
25
|
+
}
|
|
26
|
+
let o = i === "parent" ? n(r) : r, s = await t(o);
|
|
27
|
+
if (!l(a, s)) throw Error("Путь выходит за пределы корневой папки хранилища.");
|
|
28
|
+
}
|
|
29
|
+
async function d({ rootPath: e, pathSegments: t, realpathMode: n = "target" }) {
|
|
30
|
+
let r = c(e), i = a(r, ...t);
|
|
31
|
+
if (!l(r, i)) throw Error("Путь выходит за пределы корневой папки хранилища.");
|
|
32
|
+
return n !== "none" && await u(r, i, n), i;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { s as isFileSystemNotFoundError, d as resolvePathInsideRoot };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/file-system/isFileSystemNotFoundError.ts","../../src/file-system/resolvePathInsideRoot.ts"],"sourcesContent":["import { isRecord } from \"../validators\";\n\n/** Отличает отсутствующий путь от ошибок доступа и I/O, которые вызывающий код обязан обработать отдельно. */\nexport function isFileSystemNotFoundError(error: unknown): boolean {\n\tif (!isRecord(error)) return false;\n\treturn error.code === \"ENOENT\" || error.code === \"ENOTDIR\";\n}\n","import { type PathRealpathMode, type ResolvePathInsideRootOptions } from \"./fileSystemTypes\";\n\nimport { realpath } from \"node:fs/promises\";\nimport { dirname, isAbsolute, relative, resolve, sep } from \"node:path\";\n\nfunction resolveStorageRoot(rootPath: string): string {\n\tconst normalizedRootPath = rootPath.trim();\n\tif (!normalizedRootPath) {\n\t\tthrow new Error(\"Корневая папка хранилища не задана.\");\n\t}\n\treturn resolve(normalizedRootPath);\n}\n\nfunction isResolvedPathInsideRoot(rootPath: string, candidatePath: string): boolean {\n\tconst relativePath = relative(rootPath, candidatePath);\n\t// Проверяется сегмент родителя, а не префикс имени: '..metadata' является допустимым файлом внутри root.\n\treturn relativePath === \"\" || (relativePath !== \"..\" && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));\n}\n\nasync function assertRealPathInsideRoot(\n\trootPath: string,\n\tcandidatePath: string,\n\trealpathMode: Exclude<PathRealpathMode, \"none\">\n): Promise<void> {\n\tlet realRootPath: string;\n\ttry {\n\t\trealRootPath = await realpath(rootPath);\n\t} catch (error) {\n\t\tthrow new Error(\"Корневая папка хранилища недоступна.\", { cause: error });\n\t}\n\tconst pathForRealCheck = realpathMode === \"parent\" ? dirname(candidatePath) : candidatePath;\n\tconst realCandidatePath = await realpath(pathForRealCheck);\n\tif (!isResolvedPathInsideRoot(realRootPath, realCandidatePath)) {\n\t\tthrow new Error(\"Путь выходит за пределы корневой папки хранилища.\");\n\t}\n}\n\n/**\n * Нормализует абсолютный путь и запрещает выход за доверенный root.\n * 'target' проверяет существующий файл, 'parent' — каталог перед созданием файла,\n * 'none' выполняет только лексическую проверку и не защищает от символических ссылок.\n * Проверка не открывает файл: защиту от смены пути между проверкой и I/O обеспечивает вызывающий код.\n */\nexport async function resolvePathInsideRoot({\n\trootPath,\n\tpathSegments,\n\trealpathMode = \"target\"\n}: ResolvePathInsideRootOptions): Promise<string> {\n\tconst resolvedRootPath = resolveStorageRoot(rootPath);\n\tconst resolvedCandidatePath = resolve(resolvedRootPath, ...pathSegments);\n\tif (!isResolvedPathInsideRoot(resolvedRootPath, resolvedCandidatePath)) {\n\t\tthrow new Error(\"Путь выходит за пределы корневой папки хранилища.\");\n\t}\n\tif (realpathMode !== \"none\") {\n\t\tawait assertRealPathInsideRoot(resolvedRootPath, resolvedCandidatePath, realpathMode);\n\t}\n\treturn resolvedCandidatePath;\n}\n"],"mappings":";;;;AAGA,SAAgB,EAA0B,GAAyB;CAElE,OADK,EAAS,CAAK,IACZ,EAAM,SAAS,YAAY,EAAM,SAAS,YADpB;AAE9B;;;ACDA,SAAS,EAAmB,GAA0B;CACrD,IAAM,IAAqB,EAAS,KAAK;CACzC,IAAI,CAAC,GACJ,MAAU,MAAM,qCAAqC;CAEtD,OAAO,EAAQ,CAAkB;AAClC;AAEA,SAAS,EAAyB,GAAkB,GAAgC;CACnF,IAAM,IAAe,EAAS,GAAU,CAAa;CAErD,OAAO,MAAiB,MAAO,MAAiB,QAAQ,CAAC,EAAa,WAAW,KAAK,GAAK,KAAK,CAAC,EAAW,CAAY;AACzH;AAEA,eAAe,EACd,GACA,GACA,GACgB;CAChB,IAAI;CACJ,IAAI;EACH,IAAe,MAAM,EAAS,CAAQ;CACvC,SAAS,GAAO;EACf,MAAU,MAAM,wCAAwC,EAAE,OAAO,EAAM,CAAC;CACzE;CACA,IAAM,IAAmB,MAAiB,WAAW,EAAQ,CAAa,IAAI,GACxE,IAAoB,MAAM,EAAS,CAAgB;CACzD,IAAI,CAAC,EAAyB,GAAc,CAAiB,GAC5D,MAAU,MAAM,mDAAmD;AAErE;AAQA,eAAsB,EAAsB,EAC3C,aACA,iBACA,kBAAe,YACkC;CACjD,IAAM,IAAmB,EAAmB,CAAQ,GAC9C,IAAwB,EAAQ,GAAkB,GAAG,CAAY;CACvE,IAAI,CAAC,EAAyB,GAAkB,CAAqB,GACpE,MAAU,MAAM,mDAAmD;CAKpE,OAHI,MAAiB,UACpB,MAAM,EAAyB,GAAkB,GAAuB,CAAY,GAE9E;AACR"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isFileSystemNotFoundError.d.ts","sourceRoot":"","sources":["../../src/file-system/isFileSystemNotFoundError.ts"],"names":[],"mappings":"AAEA,8GAA8G;AAC9G,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGjE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ResolvePathInsideRootOptions } from "./fileSystemTypes";
|
|
2
|
+
/**
|
|
3
|
+
* Нормализует абсолютный путь и запрещает выход за доверенный root.
|
|
4
|
+
* 'target' проверяет существующий файл, 'parent' — каталог перед созданием файла,
|
|
5
|
+
* 'none' выполняет только лексическую проверку и не защищает от символических ссылок.
|
|
6
|
+
* Проверка не открывает файл: защиту от смены пути между проверкой и I/O обеспечивает вызывающий код.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolvePathInsideRoot({ rootPath, pathSegments, realpathMode }: ResolvePathInsideRootOptions): Promise<string>;
|
|
9
|
+
//# sourceMappingURL=resolvePathInsideRoot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolvePathInsideRoot.d.ts","sourceRoot":"","sources":["../../src/file-system/resolvePathInsideRoot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAqC7F;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,EAC3C,QAAQ,EACR,YAAY,EACZ,YAAuB,EACvB,EAAE,4BAA4B,GAAG,OAAO,CAAC,MAAM,CAAC,CAUhD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryuzaki13/react-foundation-lib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "Reusable React and TypeScript foundation utilities, hooks, and primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryuzaki13",
|
|
@@ -97,6 +97,10 @@
|
|
|
97
97
|
"types": "./dist/file/index.d.ts",
|
|
98
98
|
"import": "./dist/file/index.js"
|
|
99
99
|
},
|
|
100
|
+
"./file-system": {
|
|
101
|
+
"types": "./dist/file-system/index.d.ts",
|
|
102
|
+
"import": "./dist/file-system/index.js"
|
|
103
|
+
},
|
|
100
104
|
"./fingerprint": {
|
|
101
105
|
"types": "./dist/fingerprint/index.d.ts",
|
|
102
106
|
"import": "./dist/fingerprint/index.js"
|
package/src/binary/README.mdx
CHANGED
|
@@ -4,7 +4,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
|
|
|
4
4
|
|
|
5
5
|
# Работа с бинарными данными
|
|
6
6
|
|
|
7
|
-
Модуль `binary` преобразует строковое бинарное содержимое в браузерный `Blob`, определяет предполагаемый MIME-тип документа и предоставляет React hook для типового сценария «base64 → `{ blob, mime }`».
|
|
7
|
+
Модуль `binary` преобразует строковое бинарное содержимое в браузерный `Blob`, определяет предполагаемый MIME-тип документа и предоставляет React hook для типового сценария «base64 → `{ blob, mime }`». Отдельная pure function `getBase64DecodedSize` проверяет стандартный Base64 payload и считает размер без декодирования в browser или server runtime.
|
|
8
8
|
|
|
9
9
|
Основной сценарий:
|
|
10
10
|
|
|
@@ -20,6 +20,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
|
|
|
20
20
|
|
|
21
21
|
- [Назначение и границы модуля](#назначение-и-границы-модуля)
|
|
22
22
|
- [Импорт](#импорт)
|
|
23
|
+
- [`getBase64DecodedSize`](#getbase64decodedsize)
|
|
23
24
|
- [Минимальные понятия](#минимальные-понятия)
|
|
24
25
|
- [Как выбрать API](#как-выбрать-api)
|
|
25
26
|
- [Быстрый старт](#быстрый-старт)
|
|
@@ -44,7 +45,8 @@ import { Meta } from "@storybook/addon-docs/blocks";
|
|
|
44
45
|
- нормализует пробелы, URL-safe алфавит и отсутствующий base64 padding;
|
|
45
46
|
- создаёт `Blob`;
|
|
46
47
|
- предполагает MIME по имени файла, объявлению data URL или небольшой сигнатуре;
|
|
47
|
-
- предоставляет React hook для base64
|
|
48
|
+
- предоставляет React hook для base64-данных;
|
|
49
|
+
- проверяет стандартную Base64-форму и считает размер без выделения decoded buffer через `getBase64DecodedSize`.
|
|
48
50
|
|
|
49
51
|
### Чего модуль не делает
|
|
50
52
|
|
|
@@ -75,7 +77,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
|
|
|
75
77
|
Импортируйте API только из опубликованного subpath `/binary`:
|
|
76
78
|
|
|
77
79
|
```ts
|
|
78
|
-
import { binaryToBlob, detectMimeType, useBinaryFile } from "@ryuzaki13/react-foundation-lib/binary";
|
|
80
|
+
import { binaryToBlob, detectMimeType, getBase64DecodedSize, useBinaryFile } from "@ryuzaki13/react-foundation-lib/binary";
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
Корневой импорт пакета не поддерживается:
|
|
@@ -112,6 +114,27 @@ npm install react react-dom
|
|
|
112
114
|
|
|
113
115
|
В тестовой или серверной среде эти Web APIs могут отсутствовать либо вести себя иначе. Наличие похожих globals в конкретной версии Node.js не превращает browser contract в гарантированный server API.
|
|
114
116
|
|
|
117
|
+
`getBase64DecodedSize` использует только строки и числа. Эта функция подходит для client-safe или server request validation и не вызывает browser API, React hooks, `Buffer` или Node.js imports. Существующий barrel `/binary` по-прежнему экспортирует React hook, поэтому его peer-контракт сохраняется.
|
|
118
|
+
|
|
119
|
+
## `getBase64DecodedSize`
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
function getBase64DecodedSize(value: string): number | null;
|
|
123
|
+
|
|
124
|
+
getBase64DecodedSize("AA=="); // 1 байт
|
|
125
|
+
getBase64DecodedSize("AAE="); // 2 байта
|
|
126
|
+
getBase64DecodedSize("QUJD"); // 3 байта
|
|
127
|
+
getBase64DecodedSize(""); // 0
|
|
128
|
+
getBase64DecodedSize("AA"); // null: отсутствует padding
|
|
129
|
+
getBase64DecodedSize("AAA\n"); // null: перенос строки недопустим
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Принимается стандартный алфавит `A–Z`, `a–z`, `0–9`, `+`, `/`, длина должна быть кратна четырём. Один или два символа `=` допустимы только в конце; неверный алфавит, положение или количество padding возвращают `null`. Пустая строка является корректной формой с размером `0`; domain contract отдельно решает, разрешён ли пустой файл.
|
|
133
|
+
|
|
134
|
+
Функция не нормализует whitespace, Base64URL, data URL или отсутствующий padding. Она проверяет форму и вычисляет количество decoded bytes, не декодируя содержимое и не проверяя canonical pad bits, MIME или формат файла. В отличие от browser `binaryToBlob`, это узкий API для проверки строгого transport payload до выделения decoded buffer. Ограничение длины входного HTTP body и лимит размера файла остаются у вызывающей boundary.
|
|
135
|
+
|
|
136
|
+
Время работы — `O(n)` по длине строки, дополнительная память — `O(1)`. Тесты: [`getBase64DecodedSize.test.ts`](./getBase64DecodedSize.test.ts).
|
|
137
|
+
|
|
115
138
|
## Минимальные понятия
|
|
116
139
|
|
|
117
140
|
### Байт
|
|
@@ -1307,11 +1330,12 @@ npm run test -- src/binary/binaryToBlob.test.ts
|
|
|
1307
1330
|
import { binaryToBlob, detectMimeType, useBinaryFile } from "@ryuzaki13/react-foundation-lib/binary";
|
|
1308
1331
|
```
|
|
1309
1332
|
|
|
1310
|
-
| API
|
|
1311
|
-
|
|
|
1312
|
-
| `binaryToBlob`
|
|
1313
|
-
| `
|
|
1314
|
-
| `
|
|
1333
|
+
| API | Вход | Результат | Ошибка |
|
|
1334
|
+
| ---------------------- | ------------------------------- | ----------------- | ------------------------------------------------ |
|
|
1335
|
+
| `binaryToBlob` | Строка, MIME, base64/raw флаг | Непустой `Blob` | Выбрасывает `Error`/browser decode error |
|
|
1336
|
+
| `getBase64DecodedSize` | Стандартный Base64 payload | Количество байтов | `null` для неверной формы; `0` для пустой строки |
|
|
1337
|
+
| `detectMimeType` | Filename и/или base64/data URL | MIME string | Не выбрасывает; fallback PDF |
|
|
1338
|
+
| `useBinaryFile` | Base64/data URL или `undefined` | `{ data, error }` | Логирует и возвращает строку ошибки |
|
|
1315
1339
|
|
|
1316
1340
|
### Defaults
|
|
1317
1341
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation Lib/API/File System" />
|
|
4
|
+
|
|
5
|
+
# Пути внутри серверного файлового хранилища
|
|
6
|
+
|
|
7
|
+
`@ryuzaki13/react-foundation-lib/file-system` — отдельный Node.js entrypoint для проверки путей перед серверным I/O. Он использует `node:fs/promises` и `node:path`; его нельзя импортировать в browser graph или изоморфный route loader.
|
|
8
|
+
|
|
9
|
+
Модуль выделен из проверок хранилища современного приложения. Он не знает доменную сущность, пользователя, БД, environment variables или способ публикации файла. Browser API чтения выбранного `File` остаётся в отдельном `/file`.
|
|
10
|
+
|
|
11
|
+
## Public API
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
isFileSystemNotFoundError,
|
|
16
|
+
resolvePathInsideRoot,
|
|
17
|
+
type PathRealpathMode,
|
|
18
|
+
type ResolvePathInsideRootOptions
|
|
19
|
+
} from "@ryuzaki13/react-foundation-lib/file-system";
|
|
20
|
+
|
|
21
|
+
const options = {
|
|
22
|
+
rootPath: "/srv/private-files",
|
|
23
|
+
pathSegments: ["documents", "file.pdf"],
|
|
24
|
+
realpathMode: "parent"
|
|
25
|
+
} satisfies ResolvePathInsideRootOptions;
|
|
26
|
+
|
|
27
|
+
const filePath = await resolvePathInsideRoot(options);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`resolvePathInsideRoot(options)` возвращает `Promise<string>` с нормализованным абсолютным путём. Относительный `rootPath` разрешается от `process.cwd()`, внешние пробелы удаляются, пустая конфигурация отвергается. `pathSegments` принимает readonly-массив. Сам root считается допустимым target, а выход через `..`, абсолютный путь вне root или соседний каталог с похожим именем отвергается.
|
|
31
|
+
|
|
32
|
+
| `PathRealpathMode` | Назначение и условия |
|
|
33
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `target` — по умолчанию | Root и target должны существовать. `realpath` проверяет, что фактический target после разрешения символических ссылок остаётся внутри фактического root. |
|
|
35
|
+
| `parent` | Root и непосредственный родитель target должны существовать. Режим нужен перед созданием нового файла; сам target может отсутствовать. Проверяется фактический путь родителя. |
|
|
36
|
+
| `none` | Только лексическая нормализация и проверка границ. Root и target могут отсутствовать. Символические ссылки не проверяются. |
|
|
37
|
+
|
|
38
|
+
Функция возвращает нормализованный исходный путь, а не заменяет его результатом `realpath`. Символическая ссылка внутри root разрешена, если проверяемый фактический путь остаётся внутри root; доверенный root тоже может быть символической ссылкой.
|
|
39
|
+
|
|
40
|
+
## Ошибки и граница I/O
|
|
41
|
+
|
|
42
|
+
Пустой root, недоступный root и выход за границы приводят к rejected promise. Ошибка доступа к root сохраняет исходную ошибку в `Error.cause`; ошибки `realpath` target или parent передаются вызывающему коду без замены.
|
|
43
|
+
|
|
44
|
+
`isFileSystemNotFoundError(error: unknown): boolean` распознаёт `ENOENT` и `ENOTDIR`. Ошибки доступа, диска или произвольный текст с названием кода не считаются отсутствующим файлом:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
try {
|
|
48
|
+
await resolvePathInsideRoot({ rootPath: "/srv/private-files", pathSegments: ["documents", "file.pdf"] });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (isFileSystemNotFoundError(error)) {
|
|
51
|
+
// Здесь вызывающий сценарий выбирает состояние отсутствующего файла.
|
|
52
|
+
} else {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Проверка пути не открывает файл, не создаёт каталог и не устанавливает filesystem lock. Между проверкой и I/O путь может измениться. Режим `parent` также не проверяет существующий target symlink. Вызывающий storage adapter выбирает безопасные open flags, контролирует владельца каталогов и выполняет фактическое чтение или запись.
|
|
59
|
+
|
|
60
|
+
Загрузка байтов, checksum, MIME validation, staging, публикация, удаление, транзакции и компенсация принадлежат соответствующим владельцам. Этот модуль предоставляет только проверку границ пути и распознавание отсутствующего пути.
|
|
61
|
+
|
|
62
|
+
## Проверки
|
|
63
|
+
|
|
64
|
+
Тесты используют реальные временные каталоги и символические ссылки: относительный root, dot-сегменты, абсолютные пути, соседние каталоги, parent/target, недоступный root и системные ошибки. Type-test проверяет публичные сигнатуры и readonly-массив сегментов.
|