@wener/common 2.0.2 → 2.0.5
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/lib/ai/qwen3vl/index.js +2 -0
- package/lib/ai/qwen3vl/index.js.map +1 -0
- package/lib/ai/qwen3vl/utils.js +31 -0
- package/lib/ai/qwen3vl/utils.js.map +1 -0
- package/lib/ai/vision/DocLayoutElementTypeSchema.js +28 -0
- package/lib/ai/vision/DocLayoutElementTypeSchema.js.map +1 -0
- package/lib/ai/vision/ImageAnnotationSchema.js +50 -0
- package/lib/ai/vision/ImageAnnotationSchema.js.map +1 -0
- package/lib/ai/vision/index.js +3 -0
- package/lib/ai/vision/index.js.map +1 -0
- package/lib/ai/vision/resolveImageAnnotation.js +105 -0
- package/lib/ai/vision/resolveImageAnnotation.js.map +1 -0
- package/lib/cn/ChineseResidentIdNo.js +22 -15
- package/lib/cn/ChineseResidentIdNo.js.map +1 -0
- package/lib/cn/ChineseResidentIdNo.test.js +1 -1
- package/lib/cn/DivisionCode.js +30 -25
- package/lib/cn/DivisionCode.js.map +1 -0
- package/lib/cn/DivisionCode.test.js +1 -1
- package/lib/cn/Mod11.js +38 -81
- package/lib/cn/Mod11.js.map +1 -0
- package/lib/cn/Mod31.js +41 -90
- package/lib/cn/Mod31.js.map +1 -0
- package/lib/cn/UnifiedSocialCreditCode.js +43 -34
- package/lib/cn/UnifiedSocialCreditCode.js.map +1 -0
- package/lib/cn/UnifiedSocialCreditCode.test.js +1 -1
- package/lib/cn/formatChineseAmount.js +77 -0
- package/lib/cn/formatChineseAmount.js.map +1 -0
- package/lib/cn/index.js +7 -1
- package/lib/cn/index.js.map +1 -0
- package/lib/cn/parseChineseNumber.js +94 -0
- package/lib/cn/parseChineseNumber.js.map +1 -0
- package/lib/cn/parseChineseNumber.test.js +278 -0
- package/lib/cn/pinyin/cartesianProduct.js +22 -0
- package/lib/cn/pinyin/cartesianProduct.js.map +1 -0
- package/lib/cn/pinyin/cartesianProduct.test.js +179 -0
- package/lib/cn/pinyin/data.json +23573 -0
- package/lib/cn/pinyin/loader.js +14 -0
- package/lib/cn/pinyin/loader.js.map +1 -0
- package/lib/cn/pinyin/preload.js +3 -0
- package/lib/cn/pinyin/preload.js.map +1 -0
- package/lib/cn/pinyin/toPinyin.test.js +167 -0
- package/lib/cn/pinyin/toPinyinPure.js +33 -0
- package/lib/cn/pinyin/toPinyinPure.js.map +1 -0
- package/lib/cn/pinyin/transform.js +14 -0
- package/lib/cn/pinyin/transform.js.map +1 -0
- package/lib/cn/types.d.js +2 -0
- package/lib/cn/types.d.js.map +1 -0
- package/lib/consola/createStandardConsolaReporter.js +6 -6
- package/lib/consola/createStandardConsolaReporter.js.map +1 -0
- package/lib/consola/formatLogObject.js +66 -146
- package/lib/consola/formatLogObject.js.map +1 -0
- package/lib/consola/formatLogObject.test.js +184 -0
- package/lib/consola/index.js +1 -0
- package/lib/consola/index.js.map +1 -0
- package/lib/data/formatSort.js +6 -5
- package/lib/data/formatSort.js.map +1 -0
- package/lib/data/index.js +1 -0
- package/lib/data/index.js.map +1 -0
- package/lib/data/maybeNumber.js +6 -8
- package/lib/data/maybeNumber.js.map +1 -0
- package/lib/data/parseSort.js +22 -28
- package/lib/data/parseSort.js.map +1 -0
- package/lib/data/resolvePagination.js +13 -17
- package/lib/data/resolvePagination.js.map +1 -0
- package/lib/data/types.d.js +2 -0
- package/lib/data/types.d.js.map +1 -0
- package/lib/dayjs/dayjs.js +21 -19
- package/lib/dayjs/dayjs.js.map +1 -0
- package/lib/dayjs/formatDuration.js +15 -14
- package/lib/dayjs/formatDuration.js.map +1 -0
- package/lib/dayjs/index.js +2 -0
- package/lib/dayjs/index.js.map +1 -0
- package/lib/dayjs/parseDuration.js +5 -8
- package/lib/dayjs/parseDuration.js.map +1 -0
- package/lib/dayjs/parseRelativeTime.js +90 -0
- package/lib/dayjs/parseRelativeTime.js.map +1 -0
- package/lib/dayjs/parseRelativeTime.test.js +247 -0
- package/lib/dayjs/resolveRelativeTime.js +87 -0
- package/lib/dayjs/resolveRelativeTime.js.map +1 -0
- package/lib/dayjs/resolveRelativeTime.test.js +310 -0
- package/lib/decimal/index.js +1 -0
- package/lib/decimal/index.js.map +1 -0
- package/lib/decimal/parseDecimal.js +3 -1
- package/lib/decimal/parseDecimal.js.map +1 -0
- package/lib/drain3/Drain.js +356 -0
- package/lib/drain3/Drain.js.map +1 -0
- package/lib/drain3/LogCluster.js +38 -0
- package/lib/drain3/LogCluster.js.map +1 -0
- package/lib/drain3/Node.js +39 -0
- package/lib/drain3/Node.js.map +1 -0
- package/lib/drain3/TemplateMiner.js +204 -0
- package/lib/drain3/TemplateMiner.js.map +1 -0
- package/lib/drain3/index.js +31 -0
- package/lib/drain3/index.js.map +1 -0
- package/lib/drain3/persistence/FilePersistence.js +24 -0
- package/lib/drain3/persistence/FilePersistence.js.map +1 -0
- package/lib/drain3/persistence/MemoryPersistence.js +18 -0
- package/lib/drain3/persistence/MemoryPersistence.js.map +1 -0
- package/lib/drain3/persistence/PersistenceHandler.js +5 -0
- package/lib/drain3/persistence/PersistenceHandler.js.map +1 -0
- package/lib/drain3/types.js +7 -0
- package/lib/drain3/types.js.map +1 -0
- package/lib/emittery/emitter.js +10 -0
- package/lib/emittery/emitter.js.map +1 -0
- package/lib/emittery/index.js +2 -0
- package/lib/emittery/index.js.map +1 -0
- package/lib/foundation/schema/SexType.js +5 -3
- package/lib/foundation/schema/SexType.js.map +1 -0
- package/lib/foundation/schema/index.js +1 -0
- package/lib/foundation/schema/index.js.map +1 -0
- package/lib/foundation/schema/parseSexType.js +1 -0
- package/lib/foundation/schema/parseSexType.js.map +1 -0
- package/lib/foundation/schema/types.js +4 -2
- package/lib/foundation/schema/types.js.map +1 -0
- package/lib/fs/FileSystemError.js +23 -0
- package/lib/fs/FileSystemError.js.map +1 -0
- package/lib/fs/IFileSystem.d.js +3 -0
- package/lib/fs/IFileSystem.d.js.map +1 -0
- package/lib/fs/MemoryFileSystem.test.js +188 -0
- package/lib/fs/createBrowserFileSystem.js +250 -0
- package/lib/fs/createBrowserFileSystem.js.map +1 -0
- package/lib/fs/createMemoryFileSystem.js +517 -0
- package/lib/fs/createMemoryFileSystem.js.map +1 -0
- package/lib/fs/createSandboxFileSystem.js +108 -0
- package/lib/fs/createSandboxFileSystem.js.map +1 -0
- package/lib/fs/createWebDavFileSystem.js +154 -0
- package/lib/fs/createWebDavFileSystem.js.map +1 -0
- package/lib/fs/createWebFileSystem.js +225 -0
- package/lib/fs/createWebFileSystem.js.map +1 -0
- package/lib/fs/findMimeType.js +17 -0
- package/lib/fs/findMimeType.js.map +1 -0
- package/lib/fs/index.js +8 -0
- package/lib/fs/index.js.map +1 -0
- package/lib/fs/minio/createMinioFileSystem.js +974 -0
- package/lib/fs/minio/createMinioFileSystem.js.map +1 -0
- package/lib/fs/minio/index.js +2 -0
- package/lib/fs/minio/index.js.map +1 -0
- package/lib/fs/orpc/FileSystemContract.js +93 -0
- package/lib/fs/orpc/FileSystemContract.js.map +1 -0
- package/lib/fs/orpc/createContractClientFileSystem.js +93 -0
- package/lib/fs/orpc/createContractClientFileSystem.js.map +1 -0
- package/lib/fs/orpc/index.js +3 -0
- package/lib/fs/orpc/index.js.map +1 -0
- package/lib/fs/orpc/server/createFileSystemContractImpl.js +63 -0
- package/lib/fs/orpc/server/createFileSystemContractImpl.js.map +1 -0
- package/lib/fs/orpc/server/index.js +2 -0
- package/lib/fs/orpc/server/index.js.map +1 -0
- package/lib/fs/s3/createS3MiniFileSystem.js +753 -0
- package/lib/fs/s3/createS3MiniFileSystem.js.map +1 -0
- package/lib/fs/s3/index.js +2 -0
- package/lib/fs/s3/index.js.map +1 -0
- package/lib/fs/s3/s3mini.test.js +584 -0
- package/lib/fs/scandir.js +59 -0
- package/lib/fs/scandir.js.map +1 -0
- package/lib/fs/server/createDatabaseFileSystem.js +750 -0
- package/lib/fs/server/createDatabaseFileSystem.js.map +1 -0
- package/lib/fs/server/createNodeFileSystem.js +426 -0
- package/lib/fs/server/createNodeFileSystem.js.map +1 -0
- package/lib/fs/server/dbfs.test.js +221 -0
- package/lib/fs/server/index.js +2 -0
- package/lib/fs/server/index.js.map +1 -0
- package/lib/fs/server/loadTestDatabase.js +127 -0
- package/lib/fs/server/loadTestDatabase.js.map +1 -0
- package/lib/fs/tests/runFileSystemTest.js +319 -0
- package/lib/fs/tests/runFileSystemTest.js.map +1 -0
- package/lib/fs/types.js +27 -0
- package/lib/fs/types.js.map +1 -0
- package/lib/fs/utils/getFileUrl.js +35 -0
- package/lib/fs/utils/getFileUrl.js.map +1 -0
- package/lib/fs/utils.js +22 -0
- package/lib/fs/utils.js.map +1 -0
- package/lib/fs/webdav/index.js +2 -0
- package/lib/fs/webdav/index.js.map +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -0
- package/lib/jsonschema/JsonSchema.js +146 -172
- package/lib/jsonschema/JsonSchema.js.map +1 -0
- package/lib/jsonschema/forEachJsonSchema.js +44 -0
- package/lib/jsonschema/forEachJsonSchema.js.map +1 -0
- package/lib/jsonschema/index.js +2 -0
- package/lib/jsonschema/index.js.map +1 -0
- package/lib/jsonschema/types.d.js +2 -0
- package/lib/jsonschema/types.d.js.map +1 -0
- package/lib/meta/defineFileType.js +20 -103
- package/lib/meta/defineFileType.js.map +1 -0
- package/lib/meta/defineInit.js +31 -250
- package/lib/meta/defineInit.js.map +1 -0
- package/lib/meta/defineMetadata.js +24 -140
- package/lib/meta/defineMetadata.js.map +1 -0
- package/lib/meta/index.js +1 -0
- package/lib/meta/index.js.map +1 -0
- package/lib/orpc/createOpenApiContractClient.js +27 -0
- package/lib/orpc/createOpenApiContractClient.js.map +1 -0
- package/lib/orpc/createRpcContractClient.js +34 -0
- package/lib/orpc/createRpcContractClient.js.map +1 -0
- package/lib/orpc/index.js +3 -0
- package/lib/orpc/index.js.map +1 -0
- package/lib/orpc/resolveLinkPlugins.js +28 -0
- package/lib/orpc/resolveLinkPlugins.js.map +1 -0
- package/lib/password/PHC.js +63 -87
- package/lib/password/PHC.js.map +1 -0
- package/lib/password/PHC.test.js +11 -3
- package/lib/password/Password.js +29 -294
- package/lib/password/Password.js.map +1 -0
- package/lib/password/Password.test.js +35 -22
- package/lib/password/createArgon2PasswordAlgorithm.js +35 -191
- package/lib/password/createArgon2PasswordAlgorithm.js.map +1 -0
- package/lib/password/createBase64PasswordAlgorithm.js +8 -141
- package/lib/password/createBase64PasswordAlgorithm.js.map +1 -0
- package/lib/password/createBcryptPasswordAlgorithm.js +13 -168
- package/lib/password/createBcryptPasswordAlgorithm.js.map +1 -0
- package/lib/password/createPBKDF2PasswordAlgorithm.js +46 -228
- package/lib/password/createPBKDF2PasswordAlgorithm.js.map +1 -0
- package/lib/password/createScryptPasswordAlgorithm.js +55 -211
- package/lib/password/createScryptPasswordAlgorithm.js.map +1 -0
- package/lib/password/index.js +1 -0
- package/lib/password/index.js.map +1 -0
- package/lib/password/server/index.js +1 -0
- package/lib/password/server/index.js.map +1 -0
- package/lib/resource/Identifiable.js +2 -0
- package/lib/resource/Identifiable.js.map +1 -0
- package/lib/resource/ListQuery.js +21 -93
- package/lib/resource/ListQuery.js.map +1 -0
- package/lib/resource/getTitleOfResource.js +3 -5
- package/lib/resource/getTitleOfResource.js.map +1 -0
- package/lib/resource/index.js +1 -0
- package/lib/resource/index.js.map +1 -0
- package/lib/resource/schema/AnyResourceSchema.js +2 -1
- package/lib/resource/schema/AnyResourceSchema.js.map +1 -0
- package/lib/resource/schema/BaseResourceSchema.js +2 -1
- package/lib/resource/schema/BaseResourceSchema.js.map +1 -0
- package/lib/resource/schema/ResourceActionType.js +6 -4
- package/lib/resource/schema/ResourceActionType.js.map +1 -0
- package/lib/resource/schema/ResourceStatus.js +5 -3
- package/lib/resource/schema/ResourceStatus.js.map +1 -0
- package/lib/resource/schema/ResourceType.js +5 -3
- package/lib/resource/schema/ResourceType.js.map +1 -0
- package/lib/resource/schema/index.js +1 -0
- package/lib/resource/schema/index.js.map +1 -0
- package/lib/resource/schema/types.js +16 -20
- package/lib/resource/schema/types.js.map +1 -0
- package/lib/s3/formatS3Url.js +65 -0
- package/lib/s3/formatS3Url.js.map +1 -0
- package/lib/s3/formatS3Url.test.js +262 -0
- package/lib/s3/index.js +3 -0
- package/lib/s3/index.js.map +1 -0
- package/lib/s3/parseS3Url.js +65 -0
- package/lib/s3/parseS3Url.js.map +1 -0
- package/lib/s3/parseS3Url.test.js +270 -0
- package/lib/schema/SchemaRegistry.js +38 -38
- package/lib/schema/SchemaRegistry.js.map +1 -0
- package/lib/schema/TypeSchema.d.js +2 -0
- package/lib/schema/TypeSchema.d.js.map +1 -0
- package/lib/schema/createSchemaData.js +26 -125
- package/lib/schema/createSchemaData.js.map +1 -0
- package/lib/schema/findJsonSchemaByPath.js +13 -36
- package/lib/schema/findJsonSchemaByPath.js.map +1 -0
- package/lib/schema/formatZodError.js +138 -0
- package/lib/schema/formatZodError.js.map +1 -0
- package/lib/schema/formatZodError.test.js +196 -0
- package/lib/schema/getSchemaCache.js +5 -5
- package/lib/schema/getSchemaCache.js.map +1 -0
- package/lib/schema/getSchemaOptions.js +8 -11
- package/lib/schema/getSchemaOptions.js.map +1 -0
- package/lib/schema/index.js +2 -1
- package/lib/schema/index.js.map +1 -0
- package/lib/schema/toJsonSchema.js +50 -293
- package/lib/schema/toJsonSchema.js.map +1 -0
- package/lib/schema/validate.js +34 -46
- package/lib/schema/validate.js.map +1 -0
- package/lib/tools/generateSchema.js +39 -197
- package/lib/tools/generateSchema.js.map +1 -0
- package/lib/tools/renderJsonSchemaToMarkdownDoc.js +55 -143
- package/lib/tools/renderJsonSchemaToMarkdownDoc.js.map +1 -0
- package/lib/utils/buildBaseUrl.js +13 -0
- package/lib/utils/buildBaseUrl.js.map +1 -0
- package/lib/utils/buildRedactorFormSchema.js +59 -0
- package/lib/utils/buildRedactorFormSchema.js.map +1 -0
- package/lib/utils/getEstimateProcessTime.js +12 -11
- package/lib/utils/getEstimateProcessTime.js.map +1 -0
- package/lib/utils/index.js +3 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/resolveFeatureOptions.js +12 -0
- package/lib/utils/resolveFeatureOptions.js.map +1 -0
- package/package.json +80 -13
- package/src/ai/qwen3vl/index.ts +1 -0
- package/src/ai/qwen3vl/utils.ts +36 -0
- package/src/ai/vision/DocLayoutElementTypeSchema.ts +30 -0
- package/src/ai/vision/ImageAnnotationSchema.ts +60 -0
- package/src/ai/vision/index.ts +2 -0
- package/src/ai/vision/resolveImageAnnotation.ts +135 -0
- package/src/cn/ChineseResidentIdNo.test.ts +1 -1
- package/src/cn/ChineseResidentIdNo.ts +9 -1
- package/src/cn/DivisionCode.test.ts +1 -1
- package/src/cn/DivisionCode.ts +8 -0
- package/src/cn/Mod11.ts +1 -1
- package/src/cn/UnifiedSocialCreditCode.test.ts +1 -1
- package/src/cn/UnifiedSocialCreditCode.ts +15 -0
- package/src/cn/__snapshots__/ChineseResidentIdNo.test.ts.snap +1 -1
- package/src/cn/formatChineseAmount.ts +61 -0
- package/src/cn/index.ts +7 -1
- package/src/cn/parseChineseNumber.test.ts +159 -0
- package/src/cn/parseChineseNumber.ts +97 -0
- package/src/cn/pinyin/cartesianProduct.test.ts +64 -0
- package/src/cn/pinyin/cartesianProduct.ts +24 -0
- package/src/cn/pinyin/data.json +23573 -0
- package/src/cn/pinyin/loader.ts +12 -0
- package/src/cn/pinyin/preload.ts +3 -0
- package/src/cn/pinyin/toPinyin.test.ts +12 -0
- package/src/cn/pinyin/toPinyinPure.ts +43 -0
- package/src/cn/pinyin/transform.ts +12 -0
- package/src/consola/formatLogObject.test.ts +27 -0
- package/src/consola/formatLogObject.ts +40 -12
- package/src/data/maybeNumber.ts +1 -1
- package/src/data/parseSort.test.ts +0 -1
- package/src/data/types.d.ts +2 -2
- package/src/dayjs/dayjs.ts +18 -18
- package/src/dayjs/formatDuration.ts +2 -2
- package/src/dayjs/index.ts +3 -1
- package/src/dayjs/parseRelativeTime.test.ts +185 -0
- package/src/dayjs/parseRelativeTime.ts +115 -0
- package/src/dayjs/resolveRelativeTime.test.ts +357 -0
- package/src/dayjs/resolveRelativeTime.ts +164 -0
- package/src/drain3/Drain.test.ts +378 -0
- package/src/drain3/Drain.ts +394 -0
- package/src/drain3/LogCluster.ts +46 -0
- package/src/drain3/Node.ts +53 -0
- package/src/drain3/TemplateMiner.ts +246 -0
- package/src/drain3/index.ts +36 -0
- package/src/drain3/persistence/FilePersistence.ts +24 -0
- package/src/drain3/persistence/MemoryPersistence.ts +23 -0
- package/src/drain3/persistence/PersistenceHandler.ts +19 -0
- package/src/drain3/types.ts +75 -0
- package/src/emittery/emitter.ts +9 -0
- package/src/emittery/index.ts +1 -0
- package/src/fs/FileSystemError.ts +26 -0
- package/src/fs/IFileSystem.d.ts +101 -0
- package/src/fs/MemoryFileSystem.test.ts +37 -0
- package/src/fs/createBrowserFileSystem.ts +293 -0
- package/src/fs/createMemoryFileSystem.ts +600 -0
- package/src/fs/createSandboxFileSystem.ts +136 -0
- package/src/fs/createWebDavFileSystem.ts +190 -0
- package/src/fs/createWebFileSystem.ts +242 -0
- package/src/fs/findMimeType.ts +20 -0
- package/src/fs/index.ts +8 -0
- package/src/fs/minio/createMinioFileSystem.ts +1148 -0
- package/src/fs/minio/index.ts +1 -0
- package/src/fs/orpc/FileSystemContract.ts +92 -0
- package/src/fs/orpc/createContractClientFileSystem.ts +115 -0
- package/src/fs/orpc/index.ts +2 -0
- package/src/fs/orpc/server/createFileSystemContractImpl.ts +64 -0
- package/src/fs/orpc/server/index.ts +1 -0
- package/src/fs/s3/createS3MiniFileSystem.ts +871 -0
- package/src/fs/s3/index.ts +1 -0
- package/src/fs/s3/s3fs.test.ts +441 -0
- package/src/fs/s3/s3mini.test.ts +264 -0
- package/src/fs/scandir.ts +75 -0
- package/src/fs/server/createDatabaseFileSystem.ts +668 -0
- package/src/fs/server/createNodeFileSystem.ts +518 -0
- package/src/fs/server/dbfs.test.ts +48 -0
- package/src/fs/server/index.ts +1 -0
- package/src/fs/server/loadTestDatabase.ts +131 -0
- package/src/fs/tests/runFileSystemTest.ts +289 -0
- package/src/fs/types.ts +29 -0
- package/src/fs/utils/getFileUrl.ts +44 -0
- package/src/fs/utils.ts +23 -0
- package/src/fs/webdav/index.ts +1 -0
- package/src/jsonschema/JsonSchema.ts +118 -110
- package/src/jsonschema/forEachJsonSchema.ts +50 -0
- package/src/jsonschema/index.ts +1 -0
- package/src/jsonschema/types.d.ts +1 -1
- package/src/meta/defineMetadata.ts +1 -1
- package/src/orpc/createOpenApiContractClient.ts +52 -0
- package/src/orpc/createRpcContractClient.ts +50 -0
- package/src/orpc/index.ts +2 -0
- package/src/orpc/resolveLinkPlugins.ts +29 -0
- package/src/password/PHC.ts +6 -6
- package/src/password/Password.test.ts +1 -1
- package/src/password/createArgon2PasswordAlgorithm.ts +1 -1
- package/src/password/createBase64PasswordAlgorithm.ts +2 -2
- package/src/password/createBcryptPasswordAlgorithm.ts +4 -2
- package/src/password/createPBKDF2PasswordAlgorithm.ts +4 -4
- package/src/password/createScryptPasswordAlgorithm.ts +4 -4
- package/src/resource/ListQuery.ts +4 -1
- package/src/resource/index.ts +2 -2
- package/src/resource/schema/AnyResourceSchema.ts +16 -2
- package/src/s3/formatS3Url.test.ts +254 -0
- package/src/s3/formatS3Url.ts +84 -0
- package/src/s3/index.ts +2 -0
- package/src/s3/parseS3Url.test.ts +258 -0
- package/src/s3/parseS3Url.ts +88 -0
- package/src/schema/SchemaRegistry.ts +35 -33
- package/src/schema/TypeSchema.d.ts +6 -6
- package/src/schema/createSchemaData.ts +4 -4
- package/src/schema/findJsonSchemaByPath.ts +4 -4
- package/src/schema/formatZodError.test.ts +197 -0
- package/src/schema/formatZodError.ts +139 -0
- package/src/schema/getSchemaOptions.ts +2 -2
- package/src/schema/index.ts +1 -1
- package/src/schema/toJsonSchema.ts +6 -6
- package/src/schema/validate.ts +1 -1
- package/src/utils/buildBaseUrl.ts +12 -0
- package/src/utils/buildRedactorFormSchema.ts +85 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/resolveFeatureOptions.ts +14 -0
- package/src/cn/ChineseResidentIdNo.mod.ts +0 -7
- package/src/cn/DivisionCode.mod.ts +0 -7
- package/src/cn/UnifiedSocialCreditCode.mod.ts +0 -7
- package/src/cn/mod.ts +0 -3
- package/src/schema/SchemaRegistry.mod.ts +0 -1
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
import { basename, dirname, normalize } from 'pathe';
|
|
2
|
+
import { PassThrough, Readable, type Writable } from 'node:stream';
|
|
3
|
+
import { parseS3Url, type ParseS3UrlOptions } from '@wener/common/s3';
|
|
4
|
+
import { Client } from 'minio';
|
|
5
|
+
import type {
|
|
6
|
+
CopyOptions,
|
|
7
|
+
CreateReadStreamOptions,
|
|
8
|
+
CreateWriteStreamOptions,
|
|
9
|
+
IFileStat,
|
|
10
|
+
IFileSystem,
|
|
11
|
+
MkdirOptions,
|
|
12
|
+
ReaddirOptions,
|
|
13
|
+
ReadFileOptions,
|
|
14
|
+
RenameOptions,
|
|
15
|
+
RmOptions,
|
|
16
|
+
StatOptions,
|
|
17
|
+
WriteFileOptions,
|
|
18
|
+
} from '../IFileSystem';
|
|
19
|
+
|
|
20
|
+
export type CreateMinioFileSystemOptions = ParseS3UrlOptions & {
|
|
21
|
+
client?: Client;
|
|
22
|
+
/**
|
|
23
|
+
* Optional prefix to scope all operations within a specific folder in the bucket.
|
|
24
|
+
* All file operations will be relative to this prefix.
|
|
25
|
+
* Should not include leading or trailing slashes.
|
|
26
|
+
* Example: 'data/uploads' or 'backups/2024'
|
|
27
|
+
*/
|
|
28
|
+
prefix?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function createMinioFileSystem(options: CreateMinioFileSystemOptions = {}): MinioFS {
|
|
32
|
+
const parsed = parseS3Url(options);
|
|
33
|
+
if (!parsed) {
|
|
34
|
+
throw new Error('S3 URL or connection options are required');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { client, prefix } = options;
|
|
38
|
+
if (!client && (!parsed.endpoint || !parsed.bucket)) {
|
|
39
|
+
throw new Error('S3 endpoint and bucket are required when client is not provided');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let minioClient: Client;
|
|
43
|
+
let bucket: string;
|
|
44
|
+
|
|
45
|
+
if (client) {
|
|
46
|
+
minioClient = client;
|
|
47
|
+
bucket = parsed.bucket || '';
|
|
48
|
+
} else {
|
|
49
|
+
bucket = parsed.bucket || '';
|
|
50
|
+
|
|
51
|
+
// Import Minio dynamically to avoid requiring it as a dependency
|
|
52
|
+
minioClient = new Client({
|
|
53
|
+
endPoint: parsed.endpoint,
|
|
54
|
+
port: parsed.port,
|
|
55
|
+
useSSL: parsed.useSsl ?? true,
|
|
56
|
+
accessKey: parsed.accessKeyId || '',
|
|
57
|
+
secretKey: parsed.secretAccessKey || '',
|
|
58
|
+
region: parsed.region,
|
|
59
|
+
pathStyle: parsed.pathStyle,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Normalize prefix: remove leading/trailing slashes
|
|
64
|
+
const normalizedPrefix = prefix ? prefix.replace(/^\/+/, '').replace(/\/+$/, '') : '';
|
|
65
|
+
|
|
66
|
+
return new MinioFS(minioClient, bucket, normalizedPrefix);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class MinioFS implements IFileSystem {
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly client: Client,
|
|
72
|
+
private readonly bucket: string,
|
|
73
|
+
private readonly prefix: string = '',
|
|
74
|
+
) {}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalize path to S3 key format (remove leading slash, handle relative paths)
|
|
78
|
+
* and prepend the prefix if one is set
|
|
79
|
+
*/
|
|
80
|
+
private normalizeKey(path: string): string {
|
|
81
|
+
if (!path || path === '/') {
|
|
82
|
+
return this.prefix;
|
|
83
|
+
}
|
|
84
|
+
const normalized = normalize(path).replace(/^\/+/, '').replace(/\\/g, '/');
|
|
85
|
+
|
|
86
|
+
// Prepend prefix if set
|
|
87
|
+
if (this.prefix) {
|
|
88
|
+
return this.prefix + '/' + normalized;
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Remove prefix from S3 key to get the file system path
|
|
95
|
+
*/
|
|
96
|
+
private stripPrefix(key: string): string {
|
|
97
|
+
if (!key) {
|
|
98
|
+
return '/';
|
|
99
|
+
}
|
|
100
|
+
// If key is just the prefix (or empty after stripping), return root
|
|
101
|
+
if (this.prefix && key === this.prefix) {
|
|
102
|
+
return '/';
|
|
103
|
+
}
|
|
104
|
+
if (!this.prefix || !key.startsWith(this.prefix + '/')) {
|
|
105
|
+
return key.startsWith('/') ? key : '/' + key;
|
|
106
|
+
}
|
|
107
|
+
const withoutPrefix = key.slice(this.prefix.length);
|
|
108
|
+
return withoutPrefix || '/';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convert S3 key back to file system path (strip prefix and add leading slash)
|
|
113
|
+
*/
|
|
114
|
+
private keyToPath(key: string): string {
|
|
115
|
+
if (!key) {
|
|
116
|
+
return '/';
|
|
117
|
+
}
|
|
118
|
+
return this.stripPrefix(key);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get directory path from a key
|
|
123
|
+
*/
|
|
124
|
+
private getDirectory(key: string): string {
|
|
125
|
+
if (!key) {
|
|
126
|
+
return '/';
|
|
127
|
+
}
|
|
128
|
+
const dir = dirname(key).replace(/\\/g, '/');
|
|
129
|
+
return dir === '.' ? '/' : '/' + dir;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if a key represents a directory (ends with /)
|
|
134
|
+
*/
|
|
135
|
+
private isDirectoryKey(key: string): boolean {
|
|
136
|
+
return key.endsWith('/');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convert MinIO object metadata to IFileStat
|
|
141
|
+
*/
|
|
142
|
+
private toFileStat(key: string, obj: { name: string; size: number; lastModified: Date; etag?: string }): IFileStat {
|
|
143
|
+
const isDir = this.isDirectoryKey(key);
|
|
144
|
+
const path = this.keyToPath(key);
|
|
145
|
+
const directory = this.getDirectory(key);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
directory,
|
|
149
|
+
path,
|
|
150
|
+
name: isDir ? basename(key.slice(0, -1)) || basename(directory) || '/' : basename(key),
|
|
151
|
+
kind: isDir ? 'directory' : 'file',
|
|
152
|
+
mtime: obj.lastModified ? new Date(obj.lastModified).getTime() : Date.now(),
|
|
153
|
+
size: obj.size || 0,
|
|
154
|
+
meta: {
|
|
155
|
+
...(obj.etag ? { etag: obj.etag.replace(/"/g, '') } : {}),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private checkAborted(signal?: AbortSignal): void {
|
|
161
|
+
if (signal?.aborted) {
|
|
162
|
+
throw new Error('The operation was aborted');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async readdir(dir: string, options: ReaddirOptions = {}): Promise<IFileStat[]> {
|
|
167
|
+
const { glob, recursive, depth = 1, kind, hidden = true, signal } = options;
|
|
168
|
+
this.checkAborted(signal);
|
|
169
|
+
|
|
170
|
+
const dirPrefix = this.normalizeKey(dir);
|
|
171
|
+
const prefixWithSlash = dirPrefix ? (dirPrefix.endsWith('/') ? dirPrefix : dirPrefix + '/') : '';
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// MinIO listObjects supports recursive option
|
|
175
|
+
// When recursive=false, MinIO uses delimiter='/' internally to return CommonPrefixes
|
|
176
|
+
const objects: Array<{ name: string; prefix?: string; size: number; lastModified: Date; etag: string }> = [];
|
|
177
|
+
const commonPrefixes: string[] = [];
|
|
178
|
+
|
|
179
|
+
// MinIO listObjects returns a stream
|
|
180
|
+
// When recursive=false, it returns both objects and prefixes (CommonPrefixes)
|
|
181
|
+
const objectStream = this.client.listObjects(this.bucket, prefixWithSlash, recursive);
|
|
182
|
+
|
|
183
|
+
await new Promise<void>((resolve, reject) => {
|
|
184
|
+
objectStream.on('data', (obj: any) => {
|
|
185
|
+
if (obj.name) {
|
|
186
|
+
objects.push({
|
|
187
|
+
name: obj.name,
|
|
188
|
+
size: obj.size || 0,
|
|
189
|
+
lastModified: obj.lastModified || new Date(),
|
|
190
|
+
etag: obj.etag || '',
|
|
191
|
+
});
|
|
192
|
+
} else if (obj.prefix) {
|
|
193
|
+
// CommonPrefixes from MinIO
|
|
194
|
+
commonPrefixes.push(obj.prefix);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
objectStream.on('end', () => {
|
|
199
|
+
resolve();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
objectStream.on('error', (err: any) => {
|
|
203
|
+
reject(err);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (signal) {
|
|
207
|
+
signal.addEventListener('abort', () => {
|
|
208
|
+
objectStream.destroy();
|
|
209
|
+
reject(new Error('The operation was aborted'));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
let results: IFileStat[] = [];
|
|
215
|
+
|
|
216
|
+
// Process CommonPrefixes (directories) first
|
|
217
|
+
for (const prefix of commonPrefixes) {
|
|
218
|
+
this.checkAborted(signal);
|
|
219
|
+
|
|
220
|
+
// Skip if not under our prefix
|
|
221
|
+
if (prefixWithSlash && !prefix.startsWith(prefixWithSlash)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get relative path
|
|
226
|
+
const relativePrefix = prefixWithSlash ? prefix.slice(prefixWithSlash.length) : prefix;
|
|
227
|
+
const dirName = relativePrefix.replace(/\/$/, ''); // Remove trailing slash
|
|
228
|
+
|
|
229
|
+
if (!dirName) continue;
|
|
230
|
+
|
|
231
|
+
// For non-recursive, only show immediate children
|
|
232
|
+
if (!recursive && depth === 1) {
|
|
233
|
+
const firstSlash = dirName.indexOf('/');
|
|
234
|
+
if (firstSlash >= 0) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const dirKey = prefix.endsWith('/') ? prefix : prefix + '/';
|
|
240
|
+
const stat = this.toFileStat(dirKey, {
|
|
241
|
+
name: dirKey,
|
|
242
|
+
size: 0,
|
|
243
|
+
lastModified: new Date(),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Filter by hidden
|
|
247
|
+
if (!hidden && stat.name.startsWith('.')) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Filter by kind
|
|
252
|
+
if (kind && stat.kind !== kind) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
results.push(stat);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Process objects (files) to convert to IFileStat
|
|
260
|
+
const seenDirs = new Set<string>();
|
|
261
|
+
for (const obj of objects) {
|
|
262
|
+
this.checkAborted(signal);
|
|
263
|
+
|
|
264
|
+
const key = obj.name;
|
|
265
|
+
if (!key) continue;
|
|
266
|
+
|
|
267
|
+
// Skip if not under our prefix
|
|
268
|
+
if (prefixWithSlash && !key.startsWith(prefixWithSlash)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Strip the prefix from the key for path conversion
|
|
273
|
+
const relativeKey = prefixWithSlash ? key.slice(prefixWithSlash.length) : key;
|
|
274
|
+
|
|
275
|
+
// Calculate depth: count the number of slashes in the relative path
|
|
276
|
+
// depth=1 means immediate children (no slashes), depth=2 means one level deep (one slash), etc.
|
|
277
|
+
const depthLevel = (relativeKey.match(/\//g) || []).length + 1;
|
|
278
|
+
|
|
279
|
+
// Filter by depth
|
|
280
|
+
if (depthLevel > depth) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const isDir = this.isDirectoryKey(key);
|
|
285
|
+
|
|
286
|
+
// Track directories to avoid duplicates
|
|
287
|
+
if (isDir) {
|
|
288
|
+
const dirKey = key.slice(0, -1);
|
|
289
|
+
if (seenDirs.has(dirKey)) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
seenDirs.add(dirKey);
|
|
293
|
+
} else {
|
|
294
|
+
// For files in recursive mode, always include them
|
|
295
|
+
// For non-recursive mode, check if parent directory was already added
|
|
296
|
+
if (!recursive) {
|
|
297
|
+
const parentDir = dirname(key).replace(/\\/g, '/') + '/';
|
|
298
|
+
if (seenDirs.has(parentDir.slice(0, -1))) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const stat = this.toFileStat(key, {
|
|
305
|
+
name: key,
|
|
306
|
+
size: obj.size || 0,
|
|
307
|
+
lastModified: obj.lastModified,
|
|
308
|
+
etag: obj.etag,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Filter by hidden
|
|
312
|
+
if (!hidden && stat.name.startsWith('.')) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Filter by kind
|
|
317
|
+
if (kind && stat.kind !== kind) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
results.push(stat);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Handle recursive with depth > 1
|
|
325
|
+
if (!recursive && depth > 1) {
|
|
326
|
+
const subdirs = results.filter((entry) => entry.kind === 'directory');
|
|
327
|
+
for (const subdir of subdirs) {
|
|
328
|
+
this.checkAborted(signal);
|
|
329
|
+
const maxDepth = depth - 1;
|
|
330
|
+
if (maxDepth > 0) {
|
|
331
|
+
const subEntries = await this.readdir(subdir.path, {
|
|
332
|
+
...options,
|
|
333
|
+
depth: maxDepth,
|
|
334
|
+
});
|
|
335
|
+
results = [...results, ...subEntries];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Handle glob filtering
|
|
341
|
+
if (glob) {
|
|
342
|
+
const { matcher } = await import('micromatch');
|
|
343
|
+
const match = matcher(glob);
|
|
344
|
+
results = results.filter((entry) => match(entry.path));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return results;
|
|
348
|
+
} catch (error: any) {
|
|
349
|
+
if (error.code === 'NoSuchKey' || error.message?.includes('404') || error.code === 'NotFound') {
|
|
350
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
351
|
+
}
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async stat(entry: string, options: StatOptions = {}): Promise<IFileStat> {
|
|
357
|
+
const { signal } = options;
|
|
358
|
+
this.checkAborted(signal);
|
|
359
|
+
|
|
360
|
+
const key = this.normalizeKey(entry);
|
|
361
|
+
if (!key) {
|
|
362
|
+
// Root directory
|
|
363
|
+
return {
|
|
364
|
+
directory: '/',
|
|
365
|
+
path: '/',
|
|
366
|
+
name: '/',
|
|
367
|
+
kind: 'directory',
|
|
368
|
+
mtime: Date.now(),
|
|
369
|
+
size: 0,
|
|
370
|
+
meta: {},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
// Try to get object stat
|
|
376
|
+
const stat = await this.client.statObject(this.bucket, key);
|
|
377
|
+
return this.toFileStat(key, {
|
|
378
|
+
name: key,
|
|
379
|
+
size: stat.size,
|
|
380
|
+
lastModified: stat.lastModified,
|
|
381
|
+
etag: stat.etag,
|
|
382
|
+
});
|
|
383
|
+
} catch (error: any) {
|
|
384
|
+
// If object not found, try checking if it's a directory
|
|
385
|
+
if (error.code === 'NotFound' || error.code === 'NoSuchKey') {
|
|
386
|
+
const dirKey = key.endsWith('/') ? key : key + '/';
|
|
387
|
+
try {
|
|
388
|
+
// List objects with this prefix to check if it's a directory
|
|
389
|
+
const objectStream = this.client.listObjects(this.bucket, dirKey, false);
|
|
390
|
+
let hasObjects = false;
|
|
391
|
+
|
|
392
|
+
await new Promise<void>((resolve, reject) => {
|
|
393
|
+
objectStream.on('data', () => {
|
|
394
|
+
hasObjects = true;
|
|
395
|
+
objectStream.destroy();
|
|
396
|
+
resolve();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
objectStream.on('end', () => {
|
|
400
|
+
resolve();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
objectStream.on('error', reject);
|
|
404
|
+
|
|
405
|
+
if (signal) {
|
|
406
|
+
signal.addEventListener('abort', () => {
|
|
407
|
+
objectStream.destroy();
|
|
408
|
+
reject(new Error('The operation was aborted'));
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (hasObjects) {
|
|
414
|
+
// It's a directory
|
|
415
|
+
return {
|
|
416
|
+
directory: this.getDirectory(key),
|
|
417
|
+
path: this.keyToPath(key),
|
|
418
|
+
name: basename(key.replace(/\/$/, '')) || '/',
|
|
419
|
+
kind: 'directory',
|
|
420
|
+
mtime: Date.now(),
|
|
421
|
+
size: 0,
|
|
422
|
+
meta: {},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
// Ignore listing errors
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
throw new Error(`File not found: ${entry}`);
|
|
430
|
+
}
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async mkdir(path: string, options: MkdirOptions = {}): Promise<void> {
|
|
436
|
+
const { recursive = false, signal } = options;
|
|
437
|
+
this.checkAborted(signal);
|
|
438
|
+
|
|
439
|
+
// In S3, directories don't actually exist - they're just prefixes
|
|
440
|
+
// Optionally create a marker object (empty object with trailing slash)
|
|
441
|
+
const key = this.normalizeKey(path);
|
|
442
|
+
if (!key) {
|
|
443
|
+
return; // Root directory, nothing to do
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Ensure it ends with / to indicate directory
|
|
447
|
+
const dirKey = key.endsWith('/') ? key : key + '/';
|
|
448
|
+
|
|
449
|
+
// Try to create a marker object (0-byte object)
|
|
450
|
+
try {
|
|
451
|
+
const stream = new PassThrough();
|
|
452
|
+
stream.end();
|
|
453
|
+
await this.client.putObject(this.bucket, dirKey, stream, 0, {
|
|
454
|
+
'Content-Type': 'application/x-directory',
|
|
455
|
+
});
|
|
456
|
+
} catch (error: any) {
|
|
457
|
+
// If it already exists or we don't have permission, that's okay for mkdir
|
|
458
|
+
if (error.code !== 'NoSuchBucket' && error.code !== 'AccessDenied') {
|
|
459
|
+
// Ignore other errors for mkdir
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async readFile(path: string, options?: ReadFileOptions & { encoding: 'text' }): Promise<string>;
|
|
465
|
+
async readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
466
|
+
async readFile(path: string, options: ReadFileOptions = {}): Promise<string | Uint8Array> {
|
|
467
|
+
const { encoding = 'binary', signal, onDownloadProgress } = options;
|
|
468
|
+
this.checkAborted(signal);
|
|
469
|
+
|
|
470
|
+
const key = this.normalizeKey(path);
|
|
471
|
+
if (!key) {
|
|
472
|
+
throw new Error('Cannot read root directory');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
// MinIO getObject returns Promise<Readable>
|
|
477
|
+
const nodeStream = await this.client.getObject(this.bucket, key);
|
|
478
|
+
const chunks: Buffer[] = [];
|
|
479
|
+
let loaded = 0;
|
|
480
|
+
let total = 0;
|
|
481
|
+
|
|
482
|
+
// Get object size for progress if available
|
|
483
|
+
try {
|
|
484
|
+
const stat = await this.client.statObject(this.bucket, key);
|
|
485
|
+
total = stat.size;
|
|
486
|
+
} catch {
|
|
487
|
+
// Ignore if stat fails
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
nodeStream.on('data', (chunk: Buffer) => {
|
|
492
|
+
chunks.push(chunk);
|
|
493
|
+
loaded += chunk.length;
|
|
494
|
+
if (onDownloadProgress) {
|
|
495
|
+
onDownloadProgress({ loaded, total: total || -1 });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
nodeStream.on('end', () => {
|
|
500
|
+
const buffer = Buffer.concat(chunks);
|
|
501
|
+
if (encoding === 'text') {
|
|
502
|
+
resolve(buffer.toString('utf-8'));
|
|
503
|
+
} else {
|
|
504
|
+
resolve(new Uint8Array(buffer));
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
nodeStream.on('error', reject);
|
|
509
|
+
|
|
510
|
+
if (signal) {
|
|
511
|
+
signal.addEventListener('abort', () => {
|
|
512
|
+
if (nodeStream.destroy) {
|
|
513
|
+
nodeStream.destroy(new Error('The operation was aborted'));
|
|
514
|
+
}
|
|
515
|
+
reject(new Error('The operation was aborted'));
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
} catch (error: any) {
|
|
520
|
+
if (error.code === 'NoSuchKey' || error.code === 'NotFound') {
|
|
521
|
+
throw new Error(`File not found: ${path}`);
|
|
522
|
+
}
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async writeFile(
|
|
528
|
+
path: string,
|
|
529
|
+
data: string | ArrayBuffer | ArrayBufferView<ArrayBufferLike> | ReadableStream | Buffer | Readable,
|
|
530
|
+
options: WriteFileOptions = {},
|
|
531
|
+
): Promise<void> {
|
|
532
|
+
const { signal, overwrite = true, onUploadProgress } = options;
|
|
533
|
+
this.checkAborted(signal);
|
|
534
|
+
|
|
535
|
+
const key = this.normalizeKey(path);
|
|
536
|
+
if (!key) {
|
|
537
|
+
throw new Error('Cannot write to root directory');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Check if file exists and overwrite is false
|
|
541
|
+
if (!overwrite) {
|
|
542
|
+
const exists = await this.exists(path);
|
|
543
|
+
if (exists) {
|
|
544
|
+
throw new Error(`File already exists: ${path}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Convert data to stream or buffer
|
|
549
|
+
let stream: Readable;
|
|
550
|
+
let size: number;
|
|
551
|
+
|
|
552
|
+
if (data instanceof Readable) {
|
|
553
|
+
stream = data;
|
|
554
|
+
size = 0; // Unknown size
|
|
555
|
+
} else if (data instanceof ReadableStream) {
|
|
556
|
+
// Convert Web ReadableStream to Node Readable
|
|
557
|
+
stream = Readable.fromWeb(data as import('node:stream/web').ReadableStream);
|
|
558
|
+
size = 0;
|
|
559
|
+
} else {
|
|
560
|
+
let buffer: Buffer;
|
|
561
|
+
if (data instanceof ArrayBuffer) {
|
|
562
|
+
buffer = Buffer.from(data);
|
|
563
|
+
} else if (Buffer.isBuffer(data)) {
|
|
564
|
+
buffer = data;
|
|
565
|
+
} else if (typeof data === 'string') {
|
|
566
|
+
buffer = Buffer.from(data, 'utf-8');
|
|
567
|
+
} else {
|
|
568
|
+
// ArrayBufferView
|
|
569
|
+
buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
size = buffer.length;
|
|
573
|
+
stream = new Readable();
|
|
574
|
+
stream.push(buffer);
|
|
575
|
+
stream.push(null);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Wrap stream for progress tracking if needed
|
|
579
|
+
if (onUploadProgress) {
|
|
580
|
+
let loaded = 0;
|
|
581
|
+
const progressStream = new PassThrough();
|
|
582
|
+
stream.on('data', (chunk: Buffer) => {
|
|
583
|
+
loaded += chunk.length;
|
|
584
|
+
onUploadProgress({ loaded, total: size || -1 });
|
|
585
|
+
});
|
|
586
|
+
stream.pipe(progressStream);
|
|
587
|
+
stream = progressStream;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
await this.client.putObject(this.bucket, key, stream, size);
|
|
592
|
+
} catch (error: any) {
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async rm(path: string, options: RmOptions = {}): Promise<void> {
|
|
598
|
+
const { recursive = false, force = false, signal } = options;
|
|
599
|
+
this.checkAborted(signal);
|
|
600
|
+
|
|
601
|
+
const key = this.normalizeKey(path);
|
|
602
|
+
if (!key) {
|
|
603
|
+
throw new Error('Cannot remove root directory');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
if (recursive) {
|
|
608
|
+
// List all objects with this prefix
|
|
609
|
+
const prefix = key.endsWith('/') ? key : key + '/';
|
|
610
|
+
const objectStream = this.client.listObjects(this.bucket, prefix, true);
|
|
611
|
+
const keys: string[] = [];
|
|
612
|
+
|
|
613
|
+
await new Promise<void>((resolve, reject) => {
|
|
614
|
+
objectStream.on('data', (obj: any) => {
|
|
615
|
+
if (obj.name) {
|
|
616
|
+
keys.push(obj.name);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
objectStream.on('end', () => {
|
|
621
|
+
resolve();
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
objectStream.on('error', reject);
|
|
625
|
+
|
|
626
|
+
if (signal) {
|
|
627
|
+
signal.addEventListener('abort', () => {
|
|
628
|
+
objectStream.destroy();
|
|
629
|
+
reject(new Error('The operation was aborted'));
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Delete all objects
|
|
635
|
+
if (keys.length > 0) {
|
|
636
|
+
await this.client.removeObjects(this.bucket, keys);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Also delete the marker object if it exists
|
|
640
|
+
const markerKey = prefix;
|
|
641
|
+
try {
|
|
642
|
+
await this.client.removeObject(this.bucket, markerKey);
|
|
643
|
+
} catch {
|
|
644
|
+
// Ignore if marker doesn't exist
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
// Delete single object
|
|
648
|
+
try {
|
|
649
|
+
await this.client.removeObject(this.bucket, key);
|
|
650
|
+
} catch (error: any) {
|
|
651
|
+
// Check if it's a directory with files
|
|
652
|
+
if (error.code === 'NoSuchKey' || error.code === 'NotFound') {
|
|
653
|
+
const prefix = key.endsWith('/') ? key : key + '/';
|
|
654
|
+
const objectStream = this.client.listObjects(this.bucket, prefix, false);
|
|
655
|
+
let hasObjects = false;
|
|
656
|
+
|
|
657
|
+
await new Promise<void>((resolve) => {
|
|
658
|
+
objectStream.on('data', () => {
|
|
659
|
+
hasObjects = true;
|
|
660
|
+
objectStream.destroy();
|
|
661
|
+
resolve();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
objectStream.on('end', () => {
|
|
665
|
+
resolve();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
objectStream.on('error', () => {
|
|
669
|
+
resolve();
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
if (hasObjects) {
|
|
674
|
+
if (!force) {
|
|
675
|
+
throw new Error('Directory not empty');
|
|
676
|
+
}
|
|
677
|
+
// If force, delete recursively
|
|
678
|
+
const recursiveStream = this.client.listObjects(this.bucket, prefix, true);
|
|
679
|
+
const keys: string[] = [];
|
|
680
|
+
await new Promise<void>((resolve) => {
|
|
681
|
+
recursiveStream.on('data', (obj: any) => {
|
|
682
|
+
if (obj.name) {
|
|
683
|
+
keys.push(obj.name);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
recursiveStream.on('end', () => resolve());
|
|
687
|
+
recursiveStream.on('error', () => resolve());
|
|
688
|
+
});
|
|
689
|
+
if (keys.length > 0) {
|
|
690
|
+
await this.client.removeObjects(this.bucket, keys);
|
|
691
|
+
}
|
|
692
|
+
// Also try to remove the marker
|
|
693
|
+
try {
|
|
694
|
+
await this.client.removeObject(this.bucket, prefix);
|
|
695
|
+
} catch {
|
|
696
|
+
// Ignore
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
// File doesn't exist
|
|
700
|
+
if (!force) {
|
|
701
|
+
throw new Error('File not found');
|
|
702
|
+
}
|
|
703
|
+
// If force, just return without error
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
} else if (!force) {
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} catch (error: any) {
|
|
712
|
+
if (force && (error.code === 'NoSuchKey' || error.code === 'NotFound')) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (force && error.message === 'File not found') {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async rename(oldPath: string, newPath: string, options: RenameOptions = {}): Promise<void> {
|
|
723
|
+
const { signal, overwrite = false } = options;
|
|
724
|
+
this.checkAborted(signal);
|
|
725
|
+
|
|
726
|
+
const oldKey = this.normalizeKey(oldPath);
|
|
727
|
+
const newKey = this.normalizeKey(newPath);
|
|
728
|
+
|
|
729
|
+
if (!oldKey) {
|
|
730
|
+
throw new Error('Cannot rename root directory');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Check if target exists and overwrite is false
|
|
734
|
+
if (!overwrite) {
|
|
735
|
+
const exists = await this.exists(newPath);
|
|
736
|
+
if (exists) {
|
|
737
|
+
throw new Error(`Destination already exists: ${newPath}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
// Check if it's a directory (has objects with prefix)
|
|
743
|
+
const isDir = oldKey.endsWith('/');
|
|
744
|
+
const prefix = isDir ? oldKey : oldKey + '/';
|
|
745
|
+
|
|
746
|
+
const objectStream = this.client.listObjects(this.bucket, prefix, true);
|
|
747
|
+
const objects: Array<{ name: string }> = [];
|
|
748
|
+
|
|
749
|
+
await new Promise<void>((resolve, reject) => {
|
|
750
|
+
objectStream.on('data', (obj: any) => {
|
|
751
|
+
if (obj.name) {
|
|
752
|
+
objects.push({ name: obj.name });
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
objectStream.on('end', () => {
|
|
757
|
+
resolve();
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
objectStream.on('error', reject);
|
|
761
|
+
|
|
762
|
+
if (signal) {
|
|
763
|
+
signal.addEventListener('abort', () => {
|
|
764
|
+
objectStream.destroy();
|
|
765
|
+
reject(new Error('The operation was aborted'));
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
if (objects.length > 0) {
|
|
771
|
+
// It's a directory or has multiple objects, move all
|
|
772
|
+
const newPrefix = newKey.endsWith('/') ? newKey : newKey + '/';
|
|
773
|
+
|
|
774
|
+
// Move all objects
|
|
775
|
+
await Promise.all(
|
|
776
|
+
objects.map(async (obj) => {
|
|
777
|
+
const objKey = obj.name;
|
|
778
|
+
if (!objKey) return;
|
|
779
|
+
|
|
780
|
+
const relativeKey = objKey.slice(prefix.length);
|
|
781
|
+
const newObjKey = newPrefix + relativeKey;
|
|
782
|
+
|
|
783
|
+
// Copy then delete
|
|
784
|
+
// MinIO copyObject signature: copyObject(bucketName, objectName, sourceObject)
|
|
785
|
+
// sourceObject should be a string in format "bucket/object"
|
|
786
|
+
await this.client.copyObject(this.bucket, newObjKey, `${this.bucket}/${objKey}`);
|
|
787
|
+
await this.client.removeObject(this.bucket, objKey);
|
|
788
|
+
}),
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
// Move marker object if it exists (skip if it's a directory marker with data)
|
|
792
|
+
try {
|
|
793
|
+
// Check if it's a directory marker (ends with /)
|
|
794
|
+
if (prefix.endsWith('/')) {
|
|
795
|
+
// Try to copy the marker, but skip if it fails due to "contains data payload"
|
|
796
|
+
try {
|
|
797
|
+
await this.client.copyObject(this.bucket, newPrefix, `${this.bucket}/${prefix}`);
|
|
798
|
+
await this.client.removeObject(this.bucket, prefix);
|
|
799
|
+
} catch (error: any) {
|
|
800
|
+
// If error is about data payload, just remove the old marker
|
|
801
|
+
// Directory markers are optional in S3
|
|
802
|
+
if (error.message?.includes('data payload') || error.code === 'InvalidRequest') {
|
|
803
|
+
await this.client.removeObject(this.bucket, prefix);
|
|
804
|
+
} else {
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
await this.client.copyObject(this.bucket, newPrefix, `${this.bucket}/${prefix}`);
|
|
810
|
+
await this.client.removeObject(this.bucket, prefix);
|
|
811
|
+
}
|
|
812
|
+
} catch {
|
|
813
|
+
// Ignore if marker doesn't exist
|
|
814
|
+
}
|
|
815
|
+
} else {
|
|
816
|
+
// Single file - copy then delete
|
|
817
|
+
await this.client.copyObject(this.bucket, newKey, `${this.bucket}/${oldKey}`);
|
|
818
|
+
await this.client.removeObject(this.bucket, oldKey);
|
|
819
|
+
}
|
|
820
|
+
} catch (error: any) {
|
|
821
|
+
if (error.code === 'NoSuchKey' || error.code === 'NotFound') {
|
|
822
|
+
throw new Error(`Source file not found: ${oldPath}`);
|
|
823
|
+
}
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async exists(path: string): Promise<boolean> {
|
|
829
|
+
try {
|
|
830
|
+
const key = this.normalizeKey(path);
|
|
831
|
+
if (!key) {
|
|
832
|
+
return true; // Root always exists
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
await this.client.statObject(this.bucket, key);
|
|
836
|
+
return true;
|
|
837
|
+
} catch (error: any) {
|
|
838
|
+
if (error.code === 'NotFound' || error.code === 'NoSuchKey') {
|
|
839
|
+
// Check if it's a directory
|
|
840
|
+
const key = this.normalizeKey(path);
|
|
841
|
+
const dirKey = key.endsWith('/') ? key : key + '/';
|
|
842
|
+
try {
|
|
843
|
+
const objectStream = this.client.listObjects(this.bucket, dirKey, false);
|
|
844
|
+
let hasObjects = false;
|
|
845
|
+
|
|
846
|
+
await new Promise<void>((resolve) => {
|
|
847
|
+
objectStream.on('data', () => {
|
|
848
|
+
hasObjects = true;
|
|
849
|
+
objectStream.destroy();
|
|
850
|
+
resolve();
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
objectStream.on('end', () => {
|
|
854
|
+
resolve();
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
objectStream.on('error', () => {
|
|
858
|
+
resolve();
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
return hasObjects;
|
|
863
|
+
} catch {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async copy(src: string, dest: string, options: CopyOptions = {}): Promise<void> {
|
|
872
|
+
const { signal, overwrite = true, shallow = false } = options;
|
|
873
|
+
this.checkAborted(signal);
|
|
874
|
+
|
|
875
|
+
const srcKey = this.normalizeKey(src);
|
|
876
|
+
const destKey = this.normalizeKey(dest);
|
|
877
|
+
|
|
878
|
+
if (!srcKey) {
|
|
879
|
+
throw new Error('Cannot copy root directory');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Check if source exists
|
|
883
|
+
try {
|
|
884
|
+
const srcStat = await this.stat(src);
|
|
885
|
+
|
|
886
|
+
// Check if destination exists and overwrite is false
|
|
887
|
+
if (!overwrite) {
|
|
888
|
+
const exists = await this.exists(dest);
|
|
889
|
+
if (exists) {
|
|
890
|
+
throw new Error(`Destination already exists: ${dest}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (srcStat.kind === 'directory') {
|
|
895
|
+
// Copy directory recursively
|
|
896
|
+
const srcPrefix = srcKey.endsWith('/') ? srcKey : srcKey + '/';
|
|
897
|
+
const destPrefix = destKey.endsWith('/') ? destKey : destKey + '/';
|
|
898
|
+
|
|
899
|
+
const objectStream = this.client.listObjects(this.bucket, srcPrefix, !shallow);
|
|
900
|
+
const objects: Array<{ name: string }> = [];
|
|
901
|
+
|
|
902
|
+
await new Promise<void>((resolve, reject) => {
|
|
903
|
+
objectStream.on('data', (obj: any) => {
|
|
904
|
+
if (obj.name) {
|
|
905
|
+
objects.push({ name: obj.name });
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
objectStream.on('end', () => {
|
|
910
|
+
resolve();
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
objectStream.on('error', reject);
|
|
914
|
+
|
|
915
|
+
if (signal) {
|
|
916
|
+
signal.addEventListener('abort', () => {
|
|
917
|
+
objectStream.destroy();
|
|
918
|
+
reject(new Error('The operation was aborted'));
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// Copy all objects
|
|
924
|
+
await Promise.all(
|
|
925
|
+
objects.map(async (obj: { name: string }) => {
|
|
926
|
+
const objKey = obj.name;
|
|
927
|
+
if (!objKey) return;
|
|
928
|
+
|
|
929
|
+
const relativeKey = objKey.slice(srcPrefix.length);
|
|
930
|
+
const newObjKey = destPrefix + relativeKey;
|
|
931
|
+
|
|
932
|
+
await this.client.copyObject(this.bucket, newObjKey, `${this.bucket}/${objKey}`);
|
|
933
|
+
}),
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
// Copy marker object if it exists (skip if it's a directory marker with data)
|
|
937
|
+
try {
|
|
938
|
+
// Check if it's a directory marker (ends with /)
|
|
939
|
+
if (srcPrefix.endsWith('/')) {
|
|
940
|
+
// Try to copy the marker, but skip if it fails due to "contains data payload"
|
|
941
|
+
try {
|
|
942
|
+
await this.client.copyObject(this.bucket, destPrefix, `${this.bucket}/${srcPrefix}`);
|
|
943
|
+
} catch (error: any) {
|
|
944
|
+
// If error is about data payload, just skip copying the marker
|
|
945
|
+
// Directory markers are optional in S3
|
|
946
|
+
if (!error.message?.includes('data payload') && error.code !== 'InvalidRequest') {
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} else {
|
|
951
|
+
await this.client.copyObject(this.bucket, destPrefix, `${this.bucket}/${srcPrefix}`);
|
|
952
|
+
}
|
|
953
|
+
} catch {
|
|
954
|
+
// Ignore if marker doesn't exist
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
// Copy single file
|
|
958
|
+
await this.client.copyObject(this.bucket, destKey, `${this.bucket}/${srcKey}`);
|
|
959
|
+
}
|
|
960
|
+
} catch (error: any) {
|
|
961
|
+
if (error.code === 'NoSuchKey' || error.code === 'NotFound') {
|
|
962
|
+
throw new Error(`Source file not found: ${src}`);
|
|
963
|
+
}
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
createReadStream(path: string, options: CreateReadStreamOptions = {}): Readable {
|
|
969
|
+
const key = this.normalizeKey(path);
|
|
970
|
+
if (!key) {
|
|
971
|
+
throw new Error('Cannot read root directory');
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const { range, signal } = options;
|
|
975
|
+
|
|
976
|
+
// MinIO getObject and getPartialObject both return Promise<Readable>
|
|
977
|
+
// Create a Readable stream that waits for the promise to resolve
|
|
978
|
+
const client = this.client;
|
|
979
|
+
const bucket = this.bucket;
|
|
980
|
+
const readable = new Readable({
|
|
981
|
+
async read() {
|
|
982
|
+
// Only initialize once
|
|
983
|
+
if ((readable as any)._initialized) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
(readable as any)._initialized = true;
|
|
987
|
+
|
|
988
|
+
try {
|
|
989
|
+
let nodeStream: Readable;
|
|
990
|
+
|
|
991
|
+
if (range) {
|
|
992
|
+
// Use getPartialObject for range requests
|
|
993
|
+
// getPartialObject's length parameter: number of bytes to read (not end position)
|
|
994
|
+
// So for range {start: 0, end: 4}, we need length = end - start + 1 = 5
|
|
995
|
+
const length = range.end !== undefined ? range.end - range.start + 1 : undefined;
|
|
996
|
+
nodeStream = await client.getPartialObject(bucket, key, range.start, length);
|
|
997
|
+
} else {
|
|
998
|
+
nodeStream = await client.getObject(bucket, key);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Pipe the actual stream to our readable
|
|
1002
|
+
nodeStream.on('data', (chunk) => {
|
|
1003
|
+
if (!readable.push(chunk)) {
|
|
1004
|
+
// If push returns false, the stream is backpressured
|
|
1005
|
+
nodeStream.pause();
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
nodeStream.on('end', () => {
|
|
1010
|
+
readable.push(null);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
nodeStream.on('error', (err) => {
|
|
1014
|
+
readable.emit('error', err);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
if (signal) {
|
|
1018
|
+
signal.addEventListener('abort', () => {
|
|
1019
|
+
if (nodeStream.destroy) {
|
|
1020
|
+
nodeStream.destroy(new Error('The operation was aborted'));
|
|
1021
|
+
}
|
|
1022
|
+
readable.emit('error', new Error('The operation was aborted'));
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
} catch (error: any) {
|
|
1026
|
+
readable.emit('error', error);
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
return readable;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
createReadableStream(path: string, options: CreateReadStreamOptions = {}): ReadableStream {
|
|
1035
|
+
const key = this.normalizeKey(path);
|
|
1036
|
+
if (!key) {
|
|
1037
|
+
throw new Error('Cannot read root directory');
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const { range, signal } = options;
|
|
1041
|
+
|
|
1042
|
+
// MinIO getObject and getPartialObject both return Promise<Readable>
|
|
1043
|
+
const client = this.client;
|
|
1044
|
+
const bucket = this.bucket;
|
|
1045
|
+
return new ReadableStream({
|
|
1046
|
+
async start(controller) {
|
|
1047
|
+
try {
|
|
1048
|
+
let nodeStream: Readable;
|
|
1049
|
+
|
|
1050
|
+
if (range) {
|
|
1051
|
+
// Use getPartialObject for range requests
|
|
1052
|
+
// getPartialObject's length parameter: number of bytes to read (not end position)
|
|
1053
|
+
// So for range {start: 0, end: 4}, we need length = end - start + 1 = 5
|
|
1054
|
+
const length = range.end !== undefined ? range.end - range.start + 1 : undefined;
|
|
1055
|
+
nodeStream = await client.getPartialObject(bucket, key, range.start, length);
|
|
1056
|
+
} else {
|
|
1057
|
+
nodeStream = await client.getObject(bucket, key);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
nodeStream.on('data', (chunk) => {
|
|
1061
|
+
controller.enqueue(chunk);
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
nodeStream.on('end', () => {
|
|
1065
|
+
controller.close();
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
nodeStream.on('error', (err) => {
|
|
1069
|
+
controller.error(err);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
if (signal) {
|
|
1073
|
+
signal.addEventListener('abort', () => {
|
|
1074
|
+
if (nodeStream.destroy) {
|
|
1075
|
+
nodeStream.destroy(new Error('The operation was aborted'));
|
|
1076
|
+
}
|
|
1077
|
+
controller.error(new Error('The operation was aborted'));
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
} catch (error: any) {
|
|
1081
|
+
controller.error(error);
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
createWriteStream(path: string, options?: CreateWriteStreamOptions): Writable {
|
|
1088
|
+
throw new Error('Not implemented');
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
createWritableStream(path: string, options: CreateWriteStreamOptions = {}): WritableStream {
|
|
1092
|
+
const key = this.normalizeKey(path);
|
|
1093
|
+
if (!key) {
|
|
1094
|
+
throw new Error('Cannot write to root directory');
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const { signal, overwrite = true } = options;
|
|
1098
|
+
this.checkAborted(signal);
|
|
1099
|
+
|
|
1100
|
+
// Create a WritableStream that buffers data and uploads when done
|
|
1101
|
+
const buffer: Uint8Array[] = [];
|
|
1102
|
+
let controller: WritableStreamDefaultController;
|
|
1103
|
+
const client = this.client;
|
|
1104
|
+
const bucket = this.bucket;
|
|
1105
|
+
const checkAborted = this.checkAborted.bind(this);
|
|
1106
|
+
|
|
1107
|
+
return new WritableStream({
|
|
1108
|
+
start(ctrl) {
|
|
1109
|
+
controller = ctrl;
|
|
1110
|
+
},
|
|
1111
|
+
async write(chunk) {
|
|
1112
|
+
buffer.push(chunk);
|
|
1113
|
+
},
|
|
1114
|
+
async close() {
|
|
1115
|
+
try {
|
|
1116
|
+
checkAborted(signal);
|
|
1117
|
+
const data = Buffer.concat(buffer.map((chunk) => Buffer.from(chunk)));
|
|
1118
|
+
const stream = new Readable();
|
|
1119
|
+
stream.push(data);
|
|
1120
|
+
stream.push(null);
|
|
1121
|
+
await client.putObject(bucket, key, stream, data.length);
|
|
1122
|
+
// Controller closes automatically when close() completes successfully
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
controller.error(error);
|
|
1125
|
+
}
|
|
1126
|
+
},
|
|
1127
|
+
abort(reason) {
|
|
1128
|
+
buffer.length = 0;
|
|
1129
|
+
controller.error(reason);
|
|
1130
|
+
},
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
getUrl(path: IFileStat | string, options?: any): string | undefined {
|
|
1135
|
+
if (typeof path === 'object' && path?.kind !== 'file') {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const key = typeof path === 'string' ? this.normalizeKey(path) : this.normalizeKey(path.path);
|
|
1139
|
+
if (!key) {
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// MinIO supports presigned URLs, but getUrl is synchronous
|
|
1144
|
+
// We can't generate presigned URLs synchronously, so return undefined
|
|
1145
|
+
// For presigned URLs, users should use a separate method or await the promise
|
|
1146
|
+
return undefined;
|
|
1147
|
+
}
|
|
1148
|
+
}
|