@wener/common 2.0.3 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/lib/ai/qwen3vl/index.js +1 -1
  2. package/lib/ai/qwen3vl/utils.js +15 -15
  3. package/lib/ai/qwen3vl/utils.js.map +1 -1
  4. package/lib/ai/vision/DocLayoutElementTypeSchema.js +22 -22
  5. package/lib/ai/vision/ImageAnnotationSchema.js +63 -47
  6. package/lib/ai/vision/index.js +2 -2
  7. package/lib/ai/vision/resolveImageAnnotation.js +81 -95
  8. package/lib/cn/ChineseResidentIdNo.js +55 -41
  9. package/lib/cn/ChineseResidentIdNo.js.map +1 -1
  10. package/lib/cn/ChineseResidentIdNo.mod.js +6 -1
  11. package/lib/cn/ChineseResidentIdNo.test.js +22 -21
  12. package/lib/cn/DivisionCode.js +220 -235
  13. package/lib/cn/DivisionCode.mod.js +6 -1
  14. package/lib/cn/DivisionCode.test.js +92 -121
  15. package/lib/cn/Mod11.js +18 -37
  16. package/lib/cn/Mod11.js.map +1 -1
  17. package/lib/cn/Mod31.js +23 -41
  18. package/lib/cn/UnifiedSocialCreditCode.js +143 -137
  19. package/lib/cn/UnifiedSocialCreditCode.mod.js +6 -1
  20. package/lib/cn/UnifiedSocialCreditCode.test.js +21 -15
  21. package/lib/cn/formatChineseAmount.js +46 -71
  22. package/lib/cn/index.js +6 -6
  23. package/lib/cn/mod.js +5 -3
  24. package/lib/cn/parseChineseNumber.js +81 -85
  25. package/lib/cn/parseChineseNumber.test.js +183 -261
  26. package/lib/cn/pinyin/cartesianProduct.js +19 -19
  27. package/lib/cn/pinyin/cartesianProduct.test.js +78 -178
  28. package/lib/cn/pinyin/loader.js +13 -11
  29. package/lib/cn/pinyin/preload.js +2 -1
  30. package/lib/cn/pinyin/toPinyin.test.js +149 -161
  31. package/lib/cn/pinyin/toPinyinPure.js +28 -23
  32. package/lib/cn/pinyin/transform.js +11 -11
  33. package/lib/cn/types.d.js +2 -2
  34. package/lib/consola/createStandardConsolaReporter.js +14 -15
  35. package/lib/consola/formatLogObject.js +149 -133
  36. package/lib/consola/formatLogObject.js.map +1 -1
  37. package/lib/consola/formatLogObject.test.js +167 -178
  38. package/lib/consola/index.js +2 -2
  39. package/lib/data/formatSort.js +14 -12
  40. package/lib/data/formatSort.test.js +33 -33
  41. package/lib/data/index.js +3 -3
  42. package/lib/data/maybeNumber.js +23 -23
  43. package/lib/data/maybeNumber.js.map +1 -1
  44. package/lib/data/parseSort.js +75 -68
  45. package/lib/data/parseSort.test.js +196 -187
  46. package/lib/data/resolvePagination.js +38 -39
  47. package/lib/data/resolvePagination.test.js +228 -218
  48. package/lib/data/types.d.js +2 -2
  49. package/lib/data/types.d.js.map +1 -1
  50. package/lib/dayjs/dayjs.js +20 -20
  51. package/lib/dayjs/formatDuration.js +56 -56
  52. package/lib/dayjs/formatDuration.js.map +1 -1
  53. package/lib/dayjs/formatDuration.test.js +63 -77
  54. package/lib/dayjs/index.js +4 -4
  55. package/lib/dayjs/parseDuration.js +21 -26
  56. package/lib/dayjs/parseRelativeTime.js +65 -66
  57. package/lib/dayjs/parseRelativeTime.test.js +227 -243
  58. package/lib/dayjs/resolveRelativeTime.js +74 -144
  59. package/lib/dayjs/resolveRelativeTime.js.map +1 -1
  60. package/lib/dayjs/resolveRelativeTime.test.js +296 -307
  61. package/lib/decimal/index.js +1 -1
  62. package/lib/decimal/parseDecimal.js +12 -12
  63. package/lib/drain3/Drain.js +321 -0
  64. package/lib/drain3/Drain.js.map +1 -0
  65. package/lib/drain3/LogCluster.js +38 -0
  66. package/lib/drain3/LogCluster.js.map +1 -0
  67. package/lib/drain3/Node.js +39 -0
  68. package/lib/drain3/Node.js.map +1 -0
  69. package/lib/drain3/TemplateMiner.js +205 -0
  70. package/lib/drain3/TemplateMiner.js.map +1 -0
  71. package/lib/drain3/index.js +31 -0
  72. package/lib/drain3/index.js.map +1 -0
  73. package/lib/drain3/persistence/FilePersistence.js +24 -0
  74. package/lib/drain3/persistence/FilePersistence.js.map +1 -0
  75. package/lib/drain3/persistence/MemoryPersistence.js +18 -0
  76. package/lib/drain3/persistence/MemoryPersistence.js.map +1 -0
  77. package/lib/drain3/persistence/PersistenceHandler.js +5 -0
  78. package/lib/drain3/persistence/PersistenceHandler.js.map +1 -0
  79. package/lib/drain3/types.js +7 -0
  80. package/lib/drain3/types.js.map +1 -0
  81. package/lib/emittery/emitter.js +7 -7
  82. package/lib/emittery/index.js +1 -1
  83. package/lib/foundation/schema/SexType.js +15 -12
  84. package/lib/foundation/schema/index.js +1 -1
  85. package/lib/foundation/schema/parseSexType.js +15 -16
  86. package/lib/foundation/schema/types.js +8 -6
  87. package/lib/fs/FileSystemError.js +18 -18
  88. package/lib/fs/IFileSystem.d.js +2 -2
  89. package/lib/fs/IFileSystem.d.js.map +1 -1
  90. package/lib/fs/MemoryFileSystem.test.js +172 -181
  91. package/lib/fs/createBrowserFileSystem.js +222 -233
  92. package/lib/fs/createBrowserFileSystem.js.map +1 -1
  93. package/lib/fs/createMemoryFileSystem.js +473 -510
  94. package/lib/fs/createMemoryFileSystem.js.map +1 -1
  95. package/lib/fs/createSandboxFileSystem.js +102 -101
  96. package/lib/fs/createSandboxFileSystem.js.map +1 -1
  97. package/lib/fs/createWebDavFileSystem.js +162 -132
  98. package/lib/fs/createWebDavFileSystem.js.map +1 -1
  99. package/lib/fs/createWebFileSystem.js +202 -0
  100. package/lib/fs/createWebFileSystem.js.map +1 -0
  101. package/lib/fs/findMimeType.js +14 -14
  102. package/lib/fs/findMimeType.js.map +1 -1
  103. package/lib/fs/index.js +7 -7
  104. package/lib/fs/index.js.map +1 -1
  105. package/lib/fs/minio/createMinioFileSystem.js +977 -0
  106. package/lib/fs/minio/createMinioFileSystem.js.map +1 -0
  107. package/lib/fs/minio/index.js +2 -0
  108. package/lib/fs/minio/index.js.map +1 -0
  109. package/lib/fs/orpc/FileSystemContract.js +57 -57
  110. package/lib/fs/orpc/createContractClientFileSystem.js +88 -88
  111. package/lib/fs/orpc/createContractClientFileSystem.js.map +1 -1
  112. package/lib/fs/orpc/index.js +2 -2
  113. package/lib/fs/orpc/server/createFileSystemContractImpl.js +62 -60
  114. package/lib/fs/orpc/server/createFileSystemContractImpl.js.map +1 -1
  115. package/lib/fs/orpc/server/index.js +1 -1
  116. package/lib/fs/s3/createS3MiniFileSystem.js +756 -689
  117. package/lib/fs/s3/createS3MiniFileSystem.js.map +1 -1
  118. package/lib/fs/s3/index.js +1 -1
  119. package/lib/fs/s3/s3mini.test.js +524 -553
  120. package/lib/fs/scandir.js +56 -56
  121. package/lib/fs/server/createDatabaseFileSystem.js +834 -741
  122. package/lib/fs/server/createDatabaseFileSystem.js.map +1 -1
  123. package/lib/fs/server/createNodeFileSystem.js +407 -380
  124. package/lib/fs/server/createNodeFileSystem.js.map +1 -1
  125. package/lib/fs/server/dbfs.test.js +201 -214
  126. package/lib/fs/server/index.js +1 -1
  127. package/lib/fs/server/loadTestDatabase.js +40 -43
  128. package/lib/fs/tests/runFileSystemTest.js +352 -315
  129. package/lib/fs/tests/runFileSystemTest.js.map +1 -1
  130. package/lib/fs/types.js +17 -20
  131. package/lib/fs/utils/getFileUrl.js +24 -30
  132. package/lib/fs/utils.js +17 -17
  133. package/lib/fs/utils.js.map +1 -1
  134. package/lib/fs/webdav/index.js +2 -0
  135. package/lib/fs/webdav/index.js.map +1 -0
  136. package/lib/index.js +2 -2
  137. package/lib/jsonschema/JsonSchema.js +216 -155
  138. package/lib/jsonschema/JsonSchema.js.map +1 -1
  139. package/lib/jsonschema/JsonSchema.test.js +123 -124
  140. package/lib/jsonschema/forEachJsonSchema.js +41 -41
  141. package/lib/jsonschema/forEachJsonSchema.js.map +1 -1
  142. package/lib/jsonschema/index.js +2 -2
  143. package/lib/jsonschema/types.d.js +2 -2
  144. package/lib/jsonschema/types.d.js.map +1 -1
  145. package/lib/meta/defineFileType.js +32 -38
  146. package/lib/meta/defineInit.js +39 -35
  147. package/lib/meta/defineMetadata.js +37 -34
  148. package/lib/meta/defineMetadata.js.map +1 -1
  149. package/lib/meta/defineMetadata.test.js +13 -12
  150. package/lib/meta/index.js +3 -3
  151. package/lib/orpc/createOpenApiContractClient.js +26 -24
  152. package/lib/orpc/createOpenApiContractClient.js.map +1 -1
  153. package/lib/orpc/createRpcContractClient.js +37 -31
  154. package/lib/orpc/index.js +2 -2
  155. package/lib/orpc/resolveLinkPlugins.js +25 -25
  156. package/lib/password/PHC.js +187 -189
  157. package/lib/password/PHC.js.map +1 -1
  158. package/lib/password/PHC.test.js +517 -535
  159. package/lib/password/Password.js +85 -80
  160. package/lib/password/Password.test.js +330 -364
  161. package/lib/password/createArgon2PasswordAlgorithm.js +50 -51
  162. package/lib/password/createArgon2PasswordAlgorithm.js.map +1 -1
  163. package/lib/password/createBase64PasswordAlgorithm.js +11 -11
  164. package/lib/password/createBase64PasswordAlgorithm.js.map +1 -1
  165. package/lib/password/createBcryptPasswordAlgorithm.js +20 -18
  166. package/lib/password/createBcryptPasswordAlgorithm.js.map +1 -1
  167. package/lib/password/createPBKDF2PasswordAlgorithm.js +65 -52
  168. package/lib/password/createPBKDF2PasswordAlgorithm.js.map +1 -1
  169. package/lib/password/createScryptPasswordAlgorithm.js +74 -63
  170. package/lib/password/createScryptPasswordAlgorithm.js.map +1 -1
  171. package/lib/password/index.js +5 -5
  172. package/lib/password/server/index.js +1 -1
  173. package/lib/resource/Identifiable.js +2 -2
  174. package/lib/resource/ListQuery.js +42 -42
  175. package/lib/resource/ListQuery.js.map +1 -1
  176. package/lib/resource/getTitleOfResource.js +5 -5
  177. package/lib/resource/index.js +2 -2
  178. package/lib/resource/index.js.map +1 -1
  179. package/lib/resource/schema/AnyResourceSchema.js +91 -89
  180. package/lib/resource/schema/BaseResourceSchema.js +26 -26
  181. package/lib/resource/schema/ResourceActionType.js +117 -115
  182. package/lib/resource/schema/ResourceStatus.js +94 -92
  183. package/lib/resource/schema/ResourceType.js +25 -23
  184. package/lib/resource/schema/index.js +5 -5
  185. package/lib/resource/schema/types.js +86 -55
  186. package/lib/resource/schema/types.test.js +16 -13
  187. package/lib/s3/formatS3Url.js +60 -60
  188. package/lib/s3/formatS3Url.js.map +1 -1
  189. package/lib/s3/formatS3Url.test.js +238 -261
  190. package/lib/s3/index.js +2 -2
  191. package/lib/s3/parseS3Url.js +61 -60
  192. package/lib/s3/parseS3Url.js.map +1 -1
  193. package/lib/s3/parseS3Url.test.js +270 -269
  194. package/lib/schema/SchemaRegistry.js +41 -42
  195. package/lib/schema/SchemaRegistry.js.map +1 -1
  196. package/lib/schema/SchemaRegistry.mod.js +1 -1
  197. package/lib/schema/TypeSchema.d.js +2 -2
  198. package/lib/schema/TypeSchema.d.js.map +1 -1
  199. package/lib/schema/createSchemaData.js +113 -67
  200. package/lib/schema/createSchemaData.js.map +1 -1
  201. package/lib/schema/findJsonSchemaByPath.js +28 -23
  202. package/lib/schema/findJsonSchemaByPath.js.map +1 -1
  203. package/lib/schema/formatZodError.js +113 -134
  204. package/lib/schema/formatZodError.js.map +1 -1
  205. package/lib/schema/formatZodError.test.js +192 -195
  206. package/lib/schema/getSchemaCache.js +7 -7
  207. package/lib/schema/getSchemaOptions.js +17 -16
  208. package/lib/schema/index.js +6 -6
  209. package/lib/schema/toJsonSchema.js +196 -190
  210. package/lib/schema/toJsonSchema.js.map +1 -1
  211. package/lib/schema/toJsonSchema.test.js +34 -26
  212. package/lib/schema/validate.js +106 -97
  213. package/lib/schema/validate.js.map +1 -1
  214. package/lib/tools/generateSchema.js +40 -40
  215. package/lib/tools/renderJsonSchemaToMarkdownDoc.js +74 -74
  216. package/lib/utils/buildBaseUrl.js +8 -8
  217. package/lib/utils/buildRedactorFormSchema.js +55 -54
  218. package/lib/utils/buildRedactorFormSchema.js.map +1 -1
  219. package/lib/utils/getEstimateProcessTime.js +24 -19
  220. package/lib/utils/index.js +3 -3
  221. package/lib/utils/resolveFeatureOptions.js +9 -9
  222. package/package.json +37 -18
  223. package/src/ai/qwen3vl/utils.ts +1 -1
  224. package/src/ai/vision/index.ts +2 -2
  225. package/src/cn/ChineseResidentIdNo.ts +1 -1
  226. package/src/cn/Mod11.ts +1 -1
  227. package/src/cn/__snapshots__/ChineseResidentIdNo.test.ts.snap +1 -1
  228. package/src/cn/__snapshots__/UnifiedSocialCreditCode.test.ts.snap +0 -23
  229. package/src/cn/index.ts +1 -2
  230. package/src/cn/parseChineseNumber.test.ts +4 -4
  231. package/src/consola/formatLogObject.ts +6 -6
  232. package/src/consola/index.ts +1 -1
  233. package/src/data/index.ts +3 -4
  234. package/src/data/maybeNumber.ts +1 -1
  235. package/src/data/parseSort.test.ts +0 -1
  236. package/src/data/resolvePagination.ts +2 -2
  237. package/src/data/types.d.ts +2 -2
  238. package/src/dayjs/formatDuration.ts +10 -11
  239. package/src/dayjs/index.ts +1 -1
  240. package/src/dayjs/parseRelativeTime.ts +1 -1
  241. package/src/dayjs/resolveRelativeTime.ts +11 -14
  242. package/src/drain3/Drain.test.ts +378 -0
  243. package/src/drain3/Drain.ts +394 -0
  244. package/src/drain3/LogCluster.ts +46 -0
  245. package/src/drain3/Node.ts +53 -0
  246. package/src/drain3/TemplateMiner.ts +246 -0
  247. package/src/drain3/index.ts +34 -0
  248. package/src/drain3/persistence/FilePersistence.ts +24 -0
  249. package/src/drain3/persistence/MemoryPersistence.ts +23 -0
  250. package/src/drain3/persistence/PersistenceHandler.ts +19 -0
  251. package/src/drain3/types.ts +75 -0
  252. package/src/fs/IFileSystem.d.ts +1 -2
  253. package/src/fs/createBrowserFileSystem.ts +7 -5
  254. package/src/fs/createMemoryFileSystem.ts +9 -13
  255. package/src/fs/createSandboxFileSystem.ts +1 -1
  256. package/src/fs/createWebDavFileSystem.ts +30 -17
  257. package/src/fs/createWebFileSystem.ts +242 -0
  258. package/src/fs/findMimeType.ts +1 -4
  259. package/src/fs/index.ts +5 -5
  260. package/src/fs/minio/createMinioFileSystem.ts +1148 -0
  261. package/src/fs/minio/index.ts +1 -0
  262. package/src/fs/orpc/createContractClientFileSystem.ts +5 -5
  263. package/src/fs/orpc/server/createFileSystemContractImpl.ts +1 -1
  264. package/src/fs/s3/createS3MiniFileSystem.ts +120 -79
  265. package/src/fs/s3/s3fs.test.ts +441 -0
  266. package/src/fs/s3/s3mini.test.ts +2 -2
  267. package/src/fs/server/createDatabaseFileSystem.ts +78 -114
  268. package/src/fs/server/createNodeFileSystem.ts +32 -13
  269. package/src/fs/server/dbfs.test.ts +13 -8
  270. package/src/fs/server/index.ts +1 -0
  271. package/src/fs/server/loadTestDatabase.ts +8 -119
  272. package/src/fs/tests/runFileSystemTest.ts +29 -28
  273. package/src/fs/utils.ts +1 -1
  274. package/src/fs/webdav/index.ts +1 -0
  275. package/src/jsonschema/JsonSchema.ts +5 -5
  276. package/src/jsonschema/forEachJsonSchema.ts +1 -1
  277. package/src/jsonschema/index.ts +1 -1
  278. package/src/jsonschema/types.d.ts +1 -1
  279. package/src/meta/defineMetadata.ts +1 -1
  280. package/src/meta/index.ts +2 -3
  281. package/src/orm/createSqliteDialect.ts +17 -0
  282. package/src/orm/index.ts +1 -0
  283. package/src/orpc/createOpenApiContractClient.ts +3 -3
  284. package/src/orpc/index.ts +1 -1
  285. package/src/password/PHC.ts +3 -3
  286. package/src/password/createArgon2PasswordAlgorithm.ts +2 -2
  287. package/src/password/createBase64PasswordAlgorithm.ts +2 -2
  288. package/src/password/createBcryptPasswordAlgorithm.ts +4 -2
  289. package/src/password/createPBKDF2PasswordAlgorithm.ts +2 -2
  290. package/src/password/createScryptPasswordAlgorithm.ts +4 -4
  291. package/src/password/index.ts +2 -2
  292. package/src/resource/ListQuery.ts +4 -1
  293. package/src/resource/index.ts +3 -3
  294. package/src/resource/schema/index.ts +4 -4
  295. package/src/s3/formatS3Url.test.ts +1 -1
  296. package/src/s3/formatS3Url.ts +2 -2
  297. package/src/s3/index.ts +1 -1
  298. package/src/s3/parseS3Url.ts +1 -1
  299. package/src/schema/SchemaRegistry.ts +2 -2
  300. package/src/schema/TypeSchema.d.ts +6 -6
  301. package/src/schema/createSchemaData.ts +5 -5
  302. package/src/schema/findJsonSchemaByPath.ts +5 -5
  303. package/src/schema/formatZodError.test.ts +2 -1
  304. package/src/schema/formatZodError.ts +50 -62
  305. package/src/schema/index.ts +5 -5
  306. package/src/schema/toJsonSchema.ts +6 -6
  307. package/src/schema/validate.ts +2 -2
  308. package/src/utils/buildRedactorFormSchema.ts +4 -4
  309. package/src/utils/formatNumber.ts +18 -0
  310. package/src/utils/formatPercent.ts +17 -0
  311. package/src/utils/index.ts +3 -3
  312. package/src/utils/resolveFeatureOptions.ts +1 -1
@@ -1,705 +1,772 @@
1
- import { basename, dirname, normalize } from "node:path";
2
- import { Readable } from "node:stream";
3
- import { formatS3Url, parseS3Url } from "@wener/common/s3";
4
- import { S3mini, sanitizeETag } from "s3mini";
1
+ import { basename, dirname, normalize } from 'node:path';
2
+ import { Readable } from 'node:stream';
3
+ import { formatS3Url, parseS3Url } from '@wener/common/s3';
4
+ import { S3mini, sanitizeETag } from 's3mini';
5
5
  export function createS3MiniFileSystem(options = {}) {
6
- const parsed = parseS3Url(options);
7
- if (!parsed) {
8
- throw new Error('S3 URL or connection options are required');
9
- }
10
- const { client, prefix } = options;
11
- if (!client && (!parsed.endpoint || !parsed.bucket)) {
12
- throw new Error('S3 endpoint and bucket are required when client is not provided');
13
- }
14
- let s3mini;
15
- let bucket;
16
- if (client) {
17
- s3mini = client;
18
- bucket = client.bucketName || parsed.bucket || '';
19
- } else {
20
- bucket = parsed.bucket || '';
21
- // Construct full endpoint URL with bucket for S3mini
22
- const endpointUrl = formatS3Url(parsed, {
23
- credentials: false,
24
- useParams: false
25
- });
26
- s3mini = new S3mini({
27
- accessKeyId: parsed.accessKeyId || '',
28
- secretAccessKey: parsed.secretAccessKey || '',
29
- endpoint: endpointUrl,
30
- region: parsed.region
31
- });
32
- }
33
- // Normalize prefix: remove leading/trailing slashes, ensure it ends with / when not empty
34
- const normalizedPrefix = prefix ? prefix.replace(/^\/+/, '').replace(/\/+$/, '') : '';
35
- return new S3FS(s3mini, bucket, normalizedPrefix);
6
+ const parsed = parseS3Url(options);
7
+ if (!parsed) {
8
+ throw new Error('S3 URL or connection options are required');
9
+ }
10
+ const { client, prefix } = options;
11
+ if (!client && (!parsed.endpoint || !parsed.bucket)) {
12
+ throw new Error('S3 endpoint and bucket are required when client is not provided');
13
+ }
14
+ let s3mini;
15
+ let bucket;
16
+ if (client) {
17
+ s3mini = client;
18
+ bucket = client.bucketName || parsed.bucket || '';
19
+ } else {
20
+ bucket = parsed.bucket || '';
21
+ // Construct full endpoint URL with bucket for S3mini
22
+ const endpointUrl = formatS3Url(parsed, {
23
+ credentials: false,
24
+ useParams: false,
25
+ });
26
+ s3mini = new S3mini({
27
+ accessKeyId: parsed.accessKeyId || '',
28
+ secretAccessKey: parsed.secretAccessKey || '',
29
+ endpoint: endpointUrl,
30
+ region: parsed.region,
31
+ });
32
+ }
33
+ // Normalize prefix: remove leading/trailing slashes, ensure it ends with / when not empty
34
+ const normalizedPrefix = prefix ? prefix.replace(/^\/+/, '').replace(/\/+$/, '') : '';
35
+ return new S3FS(s3mini, bucket, normalizedPrefix);
36
36
  }
37
37
  let S3FS = class S3FS {
38
- client;
39
- bucket;
40
- prefix;
41
- constructor(client, bucket, prefix = ''){
42
- this.client = client;
43
- this.bucket = bucket;
44
- this.prefix = prefix;
45
- }
46
- /**
38
+ client;
39
+ _bucket;
40
+ prefix;
41
+ constructor(client, _bucket, prefix = '') {
42
+ this.client = client;
43
+ this._bucket = _bucket;
44
+ this.prefix = prefix;
45
+ }
46
+ /**
47
47
  * Normalize path to S3 key format (remove leading slash, handle relative paths)
48
48
  * and prepend the prefix if one is set
49
49
  */ normalizeKey(path) {
50
- if (!path || path === '/') {
51
- return this.prefix;
52
- }
53
- const normalized = normalize(path).replace(/^\/+/, '').replace(/\\/g, '/');
54
- // Prepend prefix if set
55
- if (this.prefix) {
56
- return this.prefix + '/' + normalized;
57
- }
58
- return normalized;
59
- }
60
- /**
50
+ if (!path || path === '/') {
51
+ return this.prefix;
52
+ }
53
+ const normalized = normalize(path).replace(/^\/+/, '').replace(/\\/g, '/');
54
+ // Prepend prefix if set
55
+ if (this.prefix) {
56
+ return `${this.prefix}/${normalized}`;
57
+ }
58
+ return normalized;
59
+ }
60
+ /**
61
61
  * Remove prefix from S3 key to get the file system path
62
62
  */ stripPrefix(key) {
63
- if (!this.prefix || !key.startsWith(this.prefix + '/')) {
64
- // If key doesn't start with prefix, return as-is (shouldn't happen normally)
65
- return key.startsWith('/') ? key : '/' + key;
66
- }
67
- const withoutPrefix = key.slice(this.prefix.length);
68
- return withoutPrefix || '/';
69
- }
70
- /**
63
+ if (!this.prefix || !key.startsWith(`${this.prefix}/`)) {
64
+ // If key doesn't start with prefix, return as-is (shouldn't happen normally)
65
+ return key.startsWith('/') ? key : `/${key}`;
66
+ }
67
+ const withoutPrefix = key.slice(this.prefix.length);
68
+ return withoutPrefix || '/';
69
+ }
70
+ /**
71
71
  * Convert S3 key back to file system path (strip prefix and add leading slash)
72
72
  */ keyToPath(key) {
73
- if (!key) {
74
- return '/';
75
- }
76
- return this.stripPrefix(key);
77
- }
78
- /**
73
+ if (!key) {
74
+ return '/';
75
+ }
76
+ return this.stripPrefix(key);
77
+ }
78
+ /**
79
79
  * Get directory path from a key
80
80
  */ getDirectory(key) {
81
- if (!key) {
82
- return '/';
83
- }
84
- const dir = dirname(key).replace(/\\/g, '/');
85
- return dir === '.' ? '/' : '/' + dir;
86
- }
87
- /**
81
+ if (!key) {
82
+ return '/';
83
+ }
84
+ const dir = dirname(key).replace(/\\/g, '/');
85
+ return dir === '.' ? '/' : `/${dir}`;
86
+ }
87
+ /**
88
88
  * Check if a key represents a directory (ends with /)
89
89
  */ isDirectoryKey(key) {
90
- return key.endsWith('/');
91
- }
92
- toFileStat(key, obj) {
93
- const isDir = this.isDirectoryKey(key);
94
- const path = this.keyToPath(key);
95
- const directory = this.getDirectory(key);
96
- const size = typeof obj.Size === 'string' ? parseInt(obj.Size, 10) || 0 : obj.Size || 0;
97
- const mtime = obj.LastModified ? new Date(obj.LastModified).getTime() : Date.now();
98
- return {
99
- directory,
100
- path,
101
- name: isDir ? basename(key.slice(0, -1)) || basename(directory) || '/' : basename(key),
102
- kind: isDir ? 'directory' : 'file',
103
- mtime,
104
- size,
105
- meta: obj.ETag ? {
106
- etag: sanitizeETag(obj.ETag)
107
- } : {}
108
- };
109
- }
110
- checkAborted(signal) {
111
- if (signal?.aborted) {
112
- throw new Error('The operation was aborted');
113
- }
114
- }
115
- async readdir(dir, options = {}) {
116
- const { glob, recursive, depth = 1, kind, hidden = true, signal } = options;
117
- this.checkAborted(signal);
118
- const dirPrefix = this.normalizeKey(dir);
119
- const prefixWithSlash = dirPrefix ? dirPrefix.endsWith('/') ? dirPrefix : dirPrefix + '/' : '';
120
- try {
121
- // When delimiter is '/', S3 returns:
122
- // - Files: Key without trailing /, Size > 0
123
- // - Directories: Key with trailing /, Size: 0
124
- const delimiter = recursive ? undefined : '/';
125
- const objects = await this.client.listObjects(delimiter ?? '', prefixWithSlash, undefined, {
126
- delimiter,
127
- signal
128
- });
129
- if (!objects || objects.length === 0) {
130
- return [];
131
- }
132
- let results = [];
133
- const seenPaths = new Set();
134
- for (const obj of objects){
135
- this.checkAborted(signal);
136
- const key = obj.Key || '';
137
- if (!key) continue;
138
- // Skip if not under our prefix
139
- if (prefixWithSlash && !key.startsWith(prefixWithSlash)) {
140
- continue;
141
- }
142
- // Strip the prefix from the key for relative path calculation
143
- const relativeKey = prefixWithSlash ? key.slice(prefixWithSlash.length) : key;
144
- // Skip empty relative keys (the directory itself)
145
- if (!relativeKey || relativeKey === '/') {
146
- continue;
147
- }
148
- // Directory: Key ends with '/' (from CommonPrefixes or explicit marker)
149
- const isDir = key.endsWith('/');
150
- // For non-recursive, only show immediate children
151
- if (!recursive && depth === 1) {
152
- // For directories, the relative key looks like "dirname/"
153
- // For files, the relative key looks like "filename"
154
- // We only want immediate children, so no more slashes in the middle
155
- const keyWithoutTrailingSlash = relativeKey.replace(/\/$/, '');
156
- if (keyWithoutTrailingSlash.includes('/')) {
157
- continue;
158
- }
159
- }
160
- // Deduplicate by path
161
- const pathKey = key.replace(/\/$/, ''); // Normalize for deduplication
162
- if (seenPaths.has(pathKey)) {
163
- continue;
164
- }
165
- seenPaths.add(pathKey);
166
- const stat = this.toFileStat(key, {
167
- Key: key,
168
- Size: obj.Size,
169
- LastModified: obj.LastModified,
170
- ETag: obj.ETag
171
- });
172
- // Filter by hidden
173
- if (!hidden && stat.name.startsWith('.')) {
174
- continue;
175
- }
176
- // Filter by kind
177
- if (kind && stat.kind !== kind) {
178
- continue;
179
- }
180
- results.push(stat);
181
- }
182
- // Handle recursive with depth > 1
183
- if (!recursive && depth > 1) {
184
- const subdirs = results.filter((entry)=>entry.kind === 'directory');
185
- for (const subdir of subdirs){
186
- this.checkAborted(signal);
187
- const maxDepth = depth - 1;
188
- if (maxDepth > 0) {
189
- const subEntries = await this.readdir(subdir.path, {
190
- ...options,
191
- depth: maxDepth
192
- });
193
- results = [
194
- ...results,
195
- ...subEntries
196
- ];
197
- }
198
- }
199
- }
200
- // Handle glob filtering
201
- if (glob) {
202
- const { matcher } = await import("micromatch");
203
- const match = matcher(glob);
204
- results = results.filter((entry)=>match(entry.path));
205
- }
206
- return results;
207
- } catch (error) {
208
- if (error.code === 'NoSuchKey' || error.message?.includes('404')) {
209
- throw new Error(`Directory not found: ${dir}`);
210
- }
211
- throw error;
212
- }
213
- }
214
- async stat(entry, options = {}) {
215
- const { signal } = options;
216
- this.checkAborted(signal);
217
- const key = this.normalizeKey(entry);
218
- if (!key) {
219
- // Root directory
220
- return {
221
- directory: '/',
222
- path: '/',
223
- name: '/',
224
- kind: 'directory',
225
- mtime: Date.now(),
226
- size: 0,
227
- meta: {}
228
- };
229
- }
230
- try {
231
- // Try to check if object exists and get metadata
232
- const exists = await this.client.objectExists(key, {
233
- signal
234
- });
235
- if (exists === true) {
236
- // Get ETag and size
237
- const etag = await this.client.getEtag(key, {
238
- signal
239
- });
240
- const size = await this.client.getContentLength(key);
241
- // Get LastModified from listing (since objectExists doesn't return it)
242
- const objects = await this.client.listObjects('/', key, 1, {
243
- delimiter: '/',
244
- signal
245
- });
246
- const obj = objects?.[0];
247
- return this.toFileStat(key, {
248
- Key: key,
249
- Size: size,
250
- LastModified: obj?.LastModified || new Date(),
251
- ETag: etag || undefined
252
- });
253
- }
254
- // If object not found, try checking if it's a directory (prefix listing)
255
- const dirKey = key.endsWith('/') ? key : key + '/';
256
- const objects = await this.client.listObjects('/', dirKey, 1, {
257
- delimiter: '/',
258
- signal
259
- });
260
- if (objects && objects.length > 0) {
261
- // It's a directory
262
- return {
263
- directory: this.getDirectory(key),
264
- path: this.keyToPath(key),
265
- name: basename(key.replace(/\/$/, '')) || '/',
266
- kind: 'directory',
267
- mtime: Date.now(),
268
- size: 0,
269
- meta: {}
270
- };
271
- }
272
- throw new Error(`File not found: ${entry}`);
273
- } catch (error) {
274
- if (error.message?.includes('not found') || error.code === 'NoSuchKey') {
275
- throw new Error(`File not found: ${entry}`);
276
- }
277
- throw error;
278
- }
279
- }
280
- async mkdir(path, options = {}) {
281
- const { recursive = false, signal } = options;
282
- this.checkAborted(signal);
283
- // In S3, directories don't actually exist - they're just prefixes
284
- // Optionally create a marker object (empty object with trailing slash)
285
- const key = this.normalizeKey(path);
286
- if (!key) {
287
- return; // Root directory, nothing to do
288
- }
289
- // Ensure it ends with / to indicate directory
290
- const dirKey = key.endsWith('/') ? key : key + '/';
291
- // Try to create a marker object (0-byte object)
292
- try {
293
- await this.client.putObject(dirKey, '', 'application/x-directory', undefined, undefined);
294
- } catch (error) {
295
- // If it already exists or we don't have permission, that's okay for mkdir
296
- if (!error.message?.includes('409') && !error.message?.includes('403')) {
297
- throw error;
298
- }
299
- }
300
- }
301
- async readFile(path, options = {}) {
302
- const { encoding = 'binary', signal, onDownloadProgress } = options;
303
- this.checkAborted(signal);
304
- const key = this.normalizeKey(path);
305
- if (!key) {
306
- throw new Error('Cannot read root directory');
307
- }
308
- try {
309
- // Use getObjectArrayBuffer for binary data
310
- const data = await this.client.getObjectArrayBuffer(key, {
311
- signal
312
- });
313
- if (!data) {
314
- throw new Error(`File not found: ${path}`);
315
- }
316
- // Handle progress reporting if needed
317
- if (onDownloadProgress) {
318
- onDownloadProgress({
319
- loaded: data.byteLength,
320
- total: data.byteLength
321
- });
322
- }
323
- if (encoding === 'text') {
324
- return new TextDecoder().decode(data);
325
- }
326
- return new Uint8Array(data);
327
- } catch (error) {
328
- if (error.code === 'NoSuchKey' || error.message?.includes('404')) {
329
- throw new Error(`File not found: ${path}`);
330
- }
331
- throw error;
332
- }
333
- }
334
- async writeFile(path, data, options = {}) {
335
- const { signal, overwrite = true, onUploadProgress } = options;
336
- this.checkAborted(signal);
337
- const key = this.normalizeKey(path);
338
- if (!key) {
339
- throw new Error('Cannot write to root directory');
340
- }
341
- // Check if file exists and overwrite is false
342
- if (!overwrite) {
343
- const exists = await this.exists(path);
344
- if (exists) {
345
- throw new Error(`File already exists: ${path}`);
346
- }
347
- }
348
- // Convert data to buffer or string
349
- let body;
350
- if (data instanceof Readable) {
351
- // For streams, we need to read them into a buffer
352
- const chunks = [];
353
- let loaded = 0;
354
- if (onUploadProgress) {
355
- data.on('data', (chunk)=>{
356
- chunks.push(chunk);
357
- loaded += chunk.length;
358
- onUploadProgress({
359
- loaded,
360
- total: -1
361
- });
362
- });
363
- } else {
364
- data.on('data', (chunk)=>{
365
- chunks.push(chunk);
366
- });
367
- }
368
- body = await new Promise((resolve, reject)=>{
369
- const allChunks = [];
370
- data.on('data', (chunk)=>allChunks.push(chunk));
371
- data.on('end', ()=>resolve(Buffer.concat(allChunks)));
372
- data.on('error', reject);
373
- if (signal) {
374
- signal.addEventListener('abort', ()=>{
375
- data.destroy();
376
- reject(new Error('The operation was aborted'));
377
- });
378
- }
379
- });
380
- } else if (data instanceof ArrayBuffer) {
381
- body = Buffer.from(data);
382
- } else if (data instanceof Buffer) {
383
- body = data;
384
- } else if (typeof data === 'string') {
385
- body = data;
386
- } else {
387
- // ArrayBufferView
388
- body = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
389
- }
390
- try {
391
- await this.client.putObject(key, body, undefined, undefined, undefined);
392
- } catch (error) {
393
- throw error;
394
- }
395
- }
396
- async rm(path, options = {}) {
397
- const { recursive = false, force = false, signal } = options;
398
- this.checkAborted(signal);
399
- const key = this.normalizeKey(path);
400
- if (!key) {
401
- throw new Error('Cannot remove root directory');
402
- }
403
- try {
404
- if (recursive) {
405
- // List all objects with this prefix (no delimiter = recursive)
406
- const prefix = key.endsWith('/') ? key : key + '/';
407
- const objects = await this.client.listObjects('', prefix, undefined, {
408
- signal
409
- });
410
- if (objects) {
411
- // Delete all objects
412
- const keys = objects.map((obj)=>obj.Key || '').filter(Boolean);
413
- if (keys.length > 0) {
414
- await this.client.deleteObjects(keys);
415
- }
416
- }
417
- // Also delete the marker object if it exists
418
- const markerKey = prefix;
419
- try {
420
- await this.client.deleteObject(markerKey);
421
- } catch {
422
- // Ignore if marker doesn't exist
423
- }
424
- } else {
425
- // Delete single object
426
- await this.client.deleteObject(key);
427
- }
428
- } catch (error) {
429
- if (force && (error.code === 'NoSuchKey' || error.message?.includes('404'))) {
430
- return;
431
- }
432
- throw error;
433
- }
434
- }
435
- async rename(oldPath, newPath, options = {}) {
436
- const { signal, overwrite = false } = options;
437
- this.checkAborted(signal);
438
- const oldKey = this.normalizeKey(oldPath);
439
- const newKey = this.normalizeKey(newPath);
440
- if (!oldKey) {
441
- throw new Error('Cannot rename root directory');
442
- }
443
- // Check if target exists and overwrite is false
444
- if (!overwrite) {
445
- const exists = await this.exists(newPath);
446
- if (exists) {
447
- throw new Error(`Destination already exists: ${newPath}`);
448
- }
449
- }
450
- try {
451
- // Check if it's a directory (has objects with prefix)
452
- const isDir = oldKey.endsWith('/');
453
- const prefix = isDir ? oldKey : oldKey + '/';
454
- const objects = await this.client.listObjects('', prefix, undefined, {
455
- signal
456
- });
457
- if (objects && objects.length > 0) {
458
- // It's a directory or has multiple objects, move all
459
- const newPrefix = newKey.endsWith('/') ? newKey : newKey + '/';
460
- // Move all objects
461
- await Promise.all(objects.map(async (obj)=>{
462
- const objKey = obj.Key || '';
463
- if (!objKey) return;
464
- const relativeKey = objKey.slice(prefix.length);
465
- const newObjKey = newPrefix + relativeKey;
466
- // Move object (copy + delete)
467
- await this.client.moveObject(objKey, newObjKey);
468
- }));
469
- // Move marker object if it exists
470
- try {
471
- await this.client.moveObject(prefix, newPrefix);
472
- } catch {
473
- // Ignore if marker doesn't exist
474
- }
475
- } else {
476
- // Single file - use moveObject
477
- await this.client.moveObject(oldKey, newKey);
478
- }
479
- } catch (error) {
480
- if (error.code === 'NoSuchKey' || error.message?.includes('404')) {
481
- throw new Error(`Source file not found: ${oldPath}`);
482
- }
483
- throw error;
484
- }
485
- }
486
- async exists(path) {
487
- try {
488
- const key = this.normalizeKey(path);
489
- if (!key) {
490
- return true; // Root always exists
491
- }
492
- const exists = await this.client.objectExists(key);
493
- if (exists === true) {
494
- return true;
495
- }
496
- // Check if it's a directory
497
- const dirKey = key.endsWith('/') ? key : key + '/';
498
- const objects = await this.client.listObjects('/', dirKey, 1, {
499
- delimiter: '/'
500
- });
501
- return objects !== null && objects.length > 0;
502
- } catch {
503
- return false;
504
- }
505
- }
506
- async copy(src, dest, options = {}) {
507
- const { signal, overwrite = true, shallow = false } = options;
508
- this.checkAborted(signal);
509
- const srcKey = this.normalizeKey(src);
510
- const destKey = this.normalizeKey(dest);
511
- if (!srcKey) {
512
- throw new Error('Cannot copy root directory');
513
- }
514
- // Check if source exists
515
- try {
516
- const srcStat = await this.stat(src);
517
- // Check if destination exists and overwrite is false
518
- if (!overwrite) {
519
- const exists = await this.exists(dest);
520
- if (exists) {
521
- throw new Error(`Destination already exists: ${dest}`);
522
- }
523
- }
524
- if (srcStat.kind === 'directory') {
525
- // Copy directory recursively
526
- const srcPrefix = srcKey.endsWith('/') ? srcKey : srcKey + '/';
527
- const destPrefix = destKey.endsWith('/') ? destKey : destKey + '/';
528
- const objects = await this.client.listObjects(shallow ? '/' : '', srcPrefix, undefined, {
529
- ...shallow ? {
530
- delimiter: '/'
531
- } : {},
532
- signal
533
- });
534
- if (objects) {
535
- // Copy all objects
536
- await Promise.all(objects.map(async (obj)=>{
537
- const objKey = obj.Key || '';
538
- if (!objKey) return;
539
- const relativeKey = objKey.slice(srcPrefix.length);
540
- const newObjKey = destPrefix + relativeKey;
541
- await this.client.copyObject(objKey, newObjKey);
542
- }));
543
- }
544
- // Copy marker object if it exists
545
- try {
546
- await this.client.copyObject(srcPrefix, destPrefix);
547
- } catch {
548
- // Ignore if marker doesn't exist
549
- }
550
- } else {
551
- // Copy single file
552
- await this.client.copyObject(srcKey, destKey);
553
- }
554
- } catch (error) {
555
- if (error.code === 'NoSuchKey' || error.message?.includes('404')) {
556
- throw new Error(`Source file not found: ${src}`);
557
- }
558
- throw error;
559
- }
560
- }
561
- createReadStream(path, options = {}) {
562
- const key = this.normalizeKey(path);
563
- if (!key) {
564
- throw new Error('Cannot read root directory');
565
- }
566
- const { range, signal } = options;
567
- // Use getObjectRaw with range support
568
- const responsePromise = this.client.getObjectRaw(key, !range, range?.start, range?.end, {
569
- signal
570
- }, undefined);
571
- // Convert Response to Readable stream
572
- let nodeStream = null;
573
- const stream = new Readable({
574
- async read () {
575
- if (!nodeStream) {
576
- try {
577
- const response = await responsePromise;
578
- if (!response.body) {
579
- this.emit('error', new Error('No response body'));
580
- return;
581
- }
582
- // Convert ReadableStream to Node Readable
583
- const reader = response.body.getReader();
584
- const decoder = new TextDecoder();
585
- nodeStream = new Readable({
586
- async read () {
587
- try {
588
- const { done, value } = await reader.read();
589
- if (done) {
590
- this.push(null);
591
- } else {
592
- this.push(Buffer.from(value));
593
- }
594
- } catch (err) {
595
- this.emit('error', err);
596
- }
597
- }
598
- });
599
- nodeStream.on('data', (chunk)=>{
600
- this.push(chunk);
601
- });
602
- nodeStream.on('end', ()=>{
603
- this.push(null);
604
- });
605
- nodeStream.on('error', (err)=>{
606
- this.emit('error', err);
607
- });
608
- } catch (err) {
609
- this.emit('error', err);
610
- }
611
- }
612
- }
613
- });
614
- signal?.addEventListener('abort', ()=>{
615
- stream.destroy(new Error('The operation was aborted'));
616
- });
617
- return stream;
618
- }
619
- createReadableStream(path, options = {}) {
620
- const key = this.normalizeKey(path);
621
- if (!key) {
622
- throw new Error('Cannot read root directory');
623
- }
624
- const { range, signal } = options;
625
- // Use getObjectRaw which returns a Response with ReadableStream
626
- const responsePromise = this.client.getObjectRaw(key, !range, range?.start, range?.end, {
627
- signal
628
- }, undefined);
629
- return new ReadableStream({
630
- async start (controller) {
631
- try {
632
- const response = await responsePromise;
633
- if (!response.body) {
634
- controller.error(new Error('No response body'));
635
- return;
636
- }
637
- const reader = response.body.getReader();
638
- signal?.addEventListener('abort', ()=>{
639
- reader.cancel(new Error('The operation was aborted'));
640
- controller.error(new Error('The operation was aborted'));
641
- });
642
- while(true){
643
- const { done, value } = await reader.read();
644
- if (done) {
645
- controller.close();
646
- break;
647
- }
648
- controller.enqueue(value);
649
- }
650
- } catch (err) {
651
- controller.error(err);
652
- }
653
- }
654
- });
655
- }
656
- createWritableStream(path, options = {}) {
657
- const key = this.normalizeKey(path);
658
- if (!key) {
659
- throw new Error('Cannot write to root directory');
660
- }
661
- const { signal, overwrite = true } = options;
662
- this.checkAborted(signal);
663
- // Create a WritableStream that buffers data and uploads when done
664
- const buffer = [];
665
- let controller;
666
- const client = this.client;
667
- const checkAborted = this.checkAborted.bind(this);
668
- return new WritableStream({
669
- start (ctrl) {
670
- controller = ctrl;
671
- },
672
- async write (chunk) {
673
- buffer.push(chunk);
674
- },
675
- async close () {
676
- try {
677
- checkAborted(signal);
678
- const data = Buffer.concat(buffer.map((chunk)=>Buffer.from(chunk)));
679
- await client.putObject(key, data, undefined, undefined, undefined);
680
- // Controller closes automatically when close() completes successfully
681
- } catch (error) {
682
- controller.error(error);
683
- }
684
- },
685
- abort (reason) {
686
- buffer.length = 0;
687
- controller.error(reason);
688
- }
689
- });
690
- }
691
- getUrl(path, options) {
692
- if (typeof path === 'object' && path?.kind !== 'file') {
693
- return;
694
- }
695
- const key = typeof path === 'string' ? this.normalizeKey(path) : this.normalizeKey(path.path);
696
- if (!key) {
697
- return;
698
- }
699
- // Construct URL from endpoint - S3mini doesn't provide presigned URLs in basic API
700
- // This is a fallback - real implementation would need presigned URLs
701
- return undefined;
702
- }
90
+ return key.endsWith('/');
91
+ }
92
+ toFileStat(key, obj) {
93
+ const isDir = this.isDirectoryKey(key);
94
+ const path = this.keyToPath(key);
95
+ const directory = this.getDirectory(key);
96
+ const size = typeof obj.Size === 'string' ? parseInt(obj.Size, 10) || 0 : obj.Size || 0;
97
+ const mtime = obj.LastModified ? new Date(obj.LastModified).getTime() : Date.now();
98
+ return {
99
+ directory,
100
+ path,
101
+ name: isDir ? basename(key.slice(0, -1)) || basename(directory) || '/' : basename(key),
102
+ kind: isDir ? 'directory' : 'file',
103
+ mtime,
104
+ size,
105
+ meta: obj.ETag
106
+ ? {
107
+ etag: sanitizeETag(obj.ETag),
108
+ }
109
+ : {},
110
+ };
111
+ }
112
+ checkAborted(signal) {
113
+ if (signal?.aborted) {
114
+ throw new Error('The operation was aborted');
115
+ }
116
+ }
117
+ async readdir(dir, options = {}) {
118
+ const { glob, recursive, depth = 1, kind, hidden = true, signal } = options;
119
+ this.checkAborted(signal);
120
+ const dirPrefix = this.normalizeKey(dir);
121
+ const prefixWithSlash = dirPrefix ? (dirPrefix.endsWith('/') ? dirPrefix : `${dirPrefix}/`) : '';
122
+ try {
123
+ const delimiter = recursive ? '' : '/';
124
+ // S3mini doesn't expose CommonPrefixes from delimiter-based listing.
125
+ // For non-recursive listing, we do two calls:
126
+ // 1. With delimiter to get direct files
127
+ // 2. Without delimiter to infer directory prefixes
128
+ let objects = [];
129
+ let commonPrefixes = [];
130
+ if (delimiter && !recursive) {
131
+ const directObjects = await this.client.listObjects(delimiter, prefixWithSlash, undefined, {
132
+ delimiter,
133
+ signal,
134
+ });
135
+ if (directObjects) {
136
+ objects = directObjects;
137
+ }
138
+ // Infer CommonPrefixes by listing all objects recursively
139
+ const allObjectsRecursive = await this.client.listObjects('', prefixWithSlash, 1000, {
140
+ signal,
141
+ });
142
+ if (allObjectsRecursive) {
143
+ const prefixSet = new Set();
144
+ for (const obj of allObjectsRecursive) {
145
+ const key = obj.Key || '';
146
+ if (!key || !key.startsWith(prefixWithSlash)) continue;
147
+ const relativeKey = key.slice(prefixWithSlash.length);
148
+ const firstSlash = relativeKey.indexOf('/');
149
+ if (firstSlash > 0) {
150
+ prefixSet.add(prefixWithSlash + relativeKey.slice(0, firstSlash + 1));
151
+ }
152
+ }
153
+ commonPrefixes = Array.from(prefixSet).sort();
154
+ }
155
+ } else {
156
+ const listResult = await this.client.listObjects(delimiter, prefixWithSlash, undefined, {
157
+ signal,
158
+ });
159
+ if (listResult) {
160
+ objects = listResult;
161
+ }
162
+ }
163
+ if (!objects.length && !commonPrefixes.length) {
164
+ return [];
165
+ }
166
+ let results = [];
167
+ // Process inferred CommonPrefixes (directories)
168
+ for (const prefix of commonPrefixes) {
169
+ this.checkAborted(signal);
170
+ if (prefixWithSlash && !prefix.startsWith(prefixWithSlash)) continue;
171
+ const relativePrefix = prefixWithSlash ? prefix.slice(prefixWithSlash.length) : prefix;
172
+ const dirName = relativePrefix.replace(/\/$/, '');
173
+ if (!dirName) continue;
174
+ if (!recursive && depth === 1) {
175
+ if (dirName.indexOf('/') >= 0) continue;
176
+ }
177
+ const dirKey = prefix.endsWith('/') ? prefix : `${prefix}/`;
178
+ const stat = this.toFileStat(dirKey, {
179
+ Key: dirKey,
180
+ Size: 0,
181
+ LastModified: new Date(),
182
+ });
183
+ if (!hidden && stat.name.startsWith('.')) continue;
184
+ if (kind && stat.kind !== kind) continue;
185
+ results.push(stat);
186
+ }
187
+ // Process objects (files and explicit directory markers)
188
+ const seenDirs = new Set();
189
+ for (const obj of objects) {
190
+ this.checkAborted(signal);
191
+ const key = obj.Key || '';
192
+ if (!key) continue;
193
+ if (prefixWithSlash && !key.startsWith(prefixWithSlash)) continue;
194
+ const relativeKey = prefixWithSlash ? key.slice(prefixWithSlash.length) : key;
195
+ if (!relativeKey || relativeKey === '/') continue;
196
+ if (!recursive && depth === 1) {
197
+ const firstSlash = relativeKey.indexOf('/');
198
+ if (firstSlash >= 0) continue;
199
+ }
200
+ const isDir = this.isDirectoryKey(key);
201
+ if (isDir) {
202
+ const dk = key.slice(0, -1);
203
+ if (seenDirs.has(dk)) continue;
204
+ seenDirs.add(dk);
205
+ }
206
+ const stat = this.toFileStat(key, {
207
+ Key: key,
208
+ Size: obj.Size,
209
+ LastModified: obj.LastModified,
210
+ ETag: obj.ETag,
211
+ });
212
+ if (!hidden && stat.name.startsWith('.')) continue;
213
+ if (kind && stat.kind !== kind) continue;
214
+ results.push(stat);
215
+ }
216
+ // Handle recursive with depth > 1
217
+ if (!recursive && depth > 1) {
218
+ const subdirs = results.filter((entry) => entry.kind === 'directory');
219
+ for (const subdir of subdirs) {
220
+ this.checkAborted(signal);
221
+ const maxDepth = depth - 1;
222
+ if (maxDepth > 0) {
223
+ const subEntries = await this.readdir(subdir.path, {
224
+ ...options,
225
+ depth: maxDepth,
226
+ });
227
+ results = [...results, ...subEntries];
228
+ }
229
+ }
230
+ }
231
+ // Handle glob filtering
232
+ if (glob) {
233
+ const { matcher } = await import('micromatch');
234
+ const match = matcher(glob);
235
+ results = results.filter((entry) => match(entry.path));
236
+ }
237
+ return results;
238
+ } catch (error) {
239
+ if (error.code === 'NoSuchKey' || error.message?.includes('404')) {
240
+ throw new Error(`Directory not found: ${dir}`);
241
+ }
242
+ throw error;
243
+ }
244
+ }
245
+ async stat(entry, options = {}) {
246
+ const { signal } = options;
247
+ this.checkAborted(signal);
248
+ const key = this.normalizeKey(entry);
249
+ if (!key) {
250
+ // Root directory
251
+ return {
252
+ directory: '/',
253
+ path: '/',
254
+ name: '/',
255
+ kind: 'directory',
256
+ mtime: Date.now(),
257
+ size: 0,
258
+ meta: {},
259
+ };
260
+ }
261
+ try {
262
+ // Try to check if object exists and get metadata
263
+ const exists = await this.client.objectExists(key, {
264
+ signal,
265
+ });
266
+ if (exists === true) {
267
+ // Get ETag and size
268
+ const etag = await this.client.getEtag(key, {
269
+ signal,
270
+ });
271
+ const size = await this.client.getContentLength(key);
272
+ // Get LastModified from listing (since objectExists doesn't return it)
273
+ const objects = await this.client.listObjects('/', key, 1, {
274
+ delimiter: '/',
275
+ signal,
276
+ });
277
+ const obj = objects?.[0];
278
+ return this.toFileStat(key, {
279
+ Key: key,
280
+ Size: size,
281
+ LastModified: obj?.LastModified || new Date(),
282
+ ETag: etag || undefined,
283
+ });
284
+ }
285
+ // If object not found, try checking if it's a directory (prefix listing)
286
+ const dirKey = key.endsWith('/') ? key : `${key}/`;
287
+ const objects = await this.client.listObjects('/', dirKey, 1, {
288
+ delimiter: '/',
289
+ signal,
290
+ });
291
+ if (objects && objects.length > 0) {
292
+ // It's a directory
293
+ return {
294
+ directory: this.getDirectory(key),
295
+ path: this.keyToPath(key),
296
+ name: basename(key.replace(/\/$/, '')) || '/',
297
+ kind: 'directory',
298
+ mtime: Date.now(),
299
+ size: 0,
300
+ meta: {},
301
+ };
302
+ }
303
+ throw new Error(`File not found: ${entry}`);
304
+ } catch (error) {
305
+ if (error.message?.includes('not found') || error.code === 'NoSuchKey') {
306
+ throw new Error(`File not found: ${entry}`);
307
+ }
308
+ throw error;
309
+ }
310
+ }
311
+ async mkdir(path, options = {}) {
312
+ const { recursive: _recursive = false, signal } = options;
313
+ this.checkAborted(signal);
314
+ // In S3, directories don't actually exist - they're just prefixes
315
+ // Optionally create a marker object (empty object with trailing slash)
316
+ const key = this.normalizeKey(path);
317
+ if (!key) {
318
+ return; // Root directory, nothing to do
319
+ }
320
+ // Ensure it ends with / to indicate directory
321
+ const dirKey = key.endsWith('/') ? key : `${key}/`;
322
+ // Try to create a marker object (0-byte object)
323
+ try {
324
+ await this.client.putObject(dirKey, '', 'application/x-directory', undefined, undefined);
325
+ } catch (error) {
326
+ // If it already exists or we don't have permission, that's okay for mkdir
327
+ if (!error.message?.includes('409') && !error.message?.includes('403')) {
328
+ throw error;
329
+ }
330
+ }
331
+ }
332
+ async readFile(path, options = {}) {
333
+ const { encoding = 'binary', signal, onDownloadProgress } = options;
334
+ this.checkAborted(signal);
335
+ const key = this.normalizeKey(path);
336
+ if (!key) {
337
+ throw new Error('Cannot read root directory');
338
+ }
339
+ try {
340
+ // Use getObjectArrayBuffer for binary data
341
+ const data = await this.client.getObjectArrayBuffer(key, {
342
+ signal,
343
+ });
344
+ if (!data) {
345
+ throw new Error(`File not found: ${path}`);
346
+ }
347
+ // Handle progress reporting if needed
348
+ if (onDownloadProgress) {
349
+ onDownloadProgress({
350
+ loaded: data.byteLength,
351
+ total: data.byteLength,
352
+ });
353
+ }
354
+ if (encoding === 'text') {
355
+ return new TextDecoder().decode(data);
356
+ }
357
+ return new Uint8Array(data);
358
+ } catch (error) {
359
+ if (error.code === 'NoSuchKey' || error.message?.includes('404')) {
360
+ throw new Error(`File not found: ${path}`);
361
+ }
362
+ throw error;
363
+ }
364
+ }
365
+ async writeFile(path, data, options = {}) {
366
+ const { signal, overwrite = true, onUploadProgress } = options;
367
+ this.checkAborted(signal);
368
+ const key = this.normalizeKey(path);
369
+ if (!key) {
370
+ throw new Error('Cannot write to root directory');
371
+ }
372
+ // Check if file exists and overwrite is false
373
+ if (!overwrite) {
374
+ const exists = await this.exists(path);
375
+ if (exists) {
376
+ throw new Error(`File already exists: ${path}`);
377
+ }
378
+ }
379
+ // Convert data to buffer or string
380
+ let body;
381
+ if (data instanceof ReadableStream) {
382
+ // Handle web ReadableStream
383
+ const reader = data.getReader();
384
+ const chunks = [];
385
+ let loaded = 0;
386
+ while (true) {
387
+ const { done, value } = await reader.read();
388
+ if (done) break;
389
+ if (value) {
390
+ chunks.push(value);
391
+ loaded += value.length;
392
+ if (onUploadProgress) {
393
+ onUploadProgress({
394
+ loaded,
395
+ total: -1,
396
+ });
397
+ }
398
+ }
399
+ }
400
+ body = Buffer.concat(chunks);
401
+ } else if (data instanceof Readable) {
402
+ // For streams, we need to read them into a buffer
403
+ const chunks = [];
404
+ let loaded = 0;
405
+ if (onUploadProgress) {
406
+ data.on('data', (chunk) => {
407
+ chunks.push(chunk);
408
+ loaded += chunk.length;
409
+ onUploadProgress({
410
+ loaded,
411
+ total: -1,
412
+ });
413
+ });
414
+ } else {
415
+ data.on('data', (chunk) => {
416
+ chunks.push(chunk);
417
+ });
418
+ }
419
+ body = await new Promise((resolve, reject) => {
420
+ const allChunks = [];
421
+ data.on('data', (chunk) => allChunks.push(chunk));
422
+ data.on('end', () => resolve(Buffer.concat(allChunks)));
423
+ data.on('error', reject);
424
+ if (signal) {
425
+ signal.addEventListener('abort', () => {
426
+ data.destroy();
427
+ reject(new Error('The operation was aborted'));
428
+ });
429
+ }
430
+ });
431
+ } else if (data instanceof ArrayBuffer) {
432
+ body = Buffer.from(data);
433
+ } else if (data instanceof Buffer) {
434
+ body = data;
435
+ } else if (typeof data === 'string') {
436
+ body = data;
437
+ } else {
438
+ // ArrayBufferView
439
+ body = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
440
+ }
441
+ await this.client.putObject(key, body, undefined, undefined, undefined);
442
+ }
443
+ async rm(path, options = {}) {
444
+ const { recursive = false, force = false, signal } = options;
445
+ this.checkAborted(signal);
446
+ const key = this.normalizeKey(path);
447
+ if (!key) {
448
+ throw new Error('Cannot remove root directory');
449
+ }
450
+ try {
451
+ if (recursive) {
452
+ // List all objects with this prefix (no delimiter = recursive)
453
+ const prefix = key.endsWith('/') ? key : `${key}/`;
454
+ const objects = await this.client.listObjects('', prefix, undefined, {
455
+ signal,
456
+ });
457
+ if (objects) {
458
+ // Delete all objects
459
+ const keys = objects.map((obj) => obj.Key || '').filter(Boolean);
460
+ if (keys.length > 0) {
461
+ await this.client.deleteObjects(keys);
462
+ }
463
+ }
464
+ // Also delete the marker object if it exists
465
+ const markerKey = prefix;
466
+ try {
467
+ await this.client.deleteObject(markerKey);
468
+ } catch {
469
+ // Ignore if marker doesn't exist
470
+ }
471
+ } else {
472
+ // Delete single object
473
+ await this.client.deleteObject(key);
474
+ }
475
+ } catch (error) {
476
+ if (force && (error.code === 'NoSuchKey' || error.message?.includes('404'))) {
477
+ return;
478
+ }
479
+ throw error;
480
+ }
481
+ }
482
+ async rename(oldPath, newPath, options = {}) {
483
+ const { signal, overwrite = false } = options;
484
+ this.checkAborted(signal);
485
+ const oldKey = this.normalizeKey(oldPath);
486
+ const newKey = this.normalizeKey(newPath);
487
+ if (!oldKey) {
488
+ throw new Error('Cannot rename root directory');
489
+ }
490
+ // Check if target exists and overwrite is false
491
+ if (!overwrite) {
492
+ const exists = await this.exists(newPath);
493
+ if (exists) {
494
+ throw new Error(`Destination already exists: ${newPath}`);
495
+ }
496
+ }
497
+ try {
498
+ // Check if it's a directory (has objects with prefix)
499
+ const isDir = oldKey.endsWith('/');
500
+ const prefix = isDir ? oldKey : `${oldKey}/`;
501
+ const objects = await this.client.listObjects('', prefix, undefined, {
502
+ signal,
503
+ });
504
+ if (objects && objects.length > 0) {
505
+ // It's a directory or has multiple objects, move all
506
+ const newPrefix = newKey.endsWith('/') ? newKey : `${newKey}/`;
507
+ // Move all objects
508
+ await Promise.all(
509
+ objects.map(async (obj) => {
510
+ const objKey = obj.Key || '';
511
+ if (!objKey) return;
512
+ const relativeKey = objKey.slice(prefix.length);
513
+ const newObjKey = newPrefix + relativeKey;
514
+ // Move object (copy + delete)
515
+ await this.client.moveObject(objKey, newObjKey);
516
+ }),
517
+ );
518
+ // Move marker object if it exists
519
+ try {
520
+ await this.client.moveObject(prefix, newPrefix);
521
+ } catch {
522
+ // Ignore if marker doesn't exist
523
+ }
524
+ } else {
525
+ // Single file - use moveObject
526
+ await this.client.moveObject(oldKey, newKey);
527
+ }
528
+ } catch (error) {
529
+ if (error.code === 'NoSuchKey' || error.message?.includes('404')) {
530
+ throw new Error(`Source file not found: ${oldPath}`);
531
+ }
532
+ throw error;
533
+ }
534
+ }
535
+ async exists(path) {
536
+ try {
537
+ const key = this.normalizeKey(path);
538
+ if (!key) {
539
+ return true; // Root always exists
540
+ }
541
+ const exists = await this.client.objectExists(key);
542
+ if (exists === true) {
543
+ return true;
544
+ }
545
+ // Check if it's a directory
546
+ const dirKey = key.endsWith('/') ? key : `${key}/`;
547
+ const objects = await this.client.listObjects('/', dirKey, 1, {
548
+ delimiter: '/',
549
+ });
550
+ return objects !== null && objects.length > 0;
551
+ } catch {
552
+ return false;
553
+ }
554
+ }
555
+ async copy(src, dest, options = {}) {
556
+ const { signal, overwrite = true, shallow = false } = options;
557
+ this.checkAborted(signal);
558
+ const srcKey = this.normalizeKey(src);
559
+ const destKey = this.normalizeKey(dest);
560
+ if (!srcKey) {
561
+ throw new Error('Cannot copy root directory');
562
+ }
563
+ // Check if source exists
564
+ try {
565
+ const srcStat = await this.stat(src);
566
+ // Check if destination exists and overwrite is false
567
+ if (!overwrite) {
568
+ const exists = await this.exists(dest);
569
+ if (exists) {
570
+ throw new Error(`Destination already exists: ${dest}`);
571
+ }
572
+ }
573
+ if (srcStat.kind === 'directory') {
574
+ // Copy directory recursively
575
+ const srcPrefix = srcKey.endsWith('/') ? srcKey : `${srcKey}/`;
576
+ const destPrefix = destKey.endsWith('/') ? destKey : `${destKey}/`;
577
+ const objects = await this.client.listObjects(shallow ? '/' : '', srcPrefix, undefined, {
578
+ ...(shallow
579
+ ? {
580
+ delimiter: '/',
581
+ }
582
+ : {}),
583
+ signal,
584
+ });
585
+ if (objects) {
586
+ // Copy all objects
587
+ await Promise.all(
588
+ objects.map(async (obj) => {
589
+ const objKey = obj.Key || '';
590
+ if (!objKey) return;
591
+ const relativeKey = objKey.slice(srcPrefix.length);
592
+ const newObjKey = destPrefix + relativeKey;
593
+ await this.client.copyObject(objKey, newObjKey);
594
+ }),
595
+ );
596
+ }
597
+ // Copy marker object if it exists
598
+ try {
599
+ await this.client.copyObject(srcPrefix, destPrefix);
600
+ } catch {
601
+ // Ignore if marker doesn't exist
602
+ }
603
+ } else {
604
+ // Copy single file
605
+ await this.client.copyObject(srcKey, destKey);
606
+ }
607
+ } catch (error) {
608
+ if (error.code === 'NoSuchKey' || error.message?.includes('404')) {
609
+ throw new Error(`Source file not found: ${src}`);
610
+ }
611
+ throw error;
612
+ }
613
+ }
614
+ createReadStream(path, options = {}) {
615
+ const key = this.normalizeKey(path);
616
+ if (!key) {
617
+ throw new Error('Cannot read root directory');
618
+ }
619
+ const { range, signal } = options;
620
+ // Use getObjectRaw with range support
621
+ const responsePromise = this.client.getObjectRaw(
622
+ key,
623
+ !range,
624
+ range?.start,
625
+ range?.end,
626
+ {
627
+ signal,
628
+ },
629
+ undefined,
630
+ );
631
+ // Convert Response to Readable stream
632
+ let nodeStream = null;
633
+ const stream = new Readable({
634
+ async read() {
635
+ if (!nodeStream) {
636
+ try {
637
+ const response = await responsePromise;
638
+ if (!response.body) {
639
+ this.emit('error', new Error('No response body'));
640
+ return;
641
+ }
642
+ // Convert ReadableStream to Node Readable
643
+ const reader = response.body.getReader();
644
+ const _decoder = new TextDecoder();
645
+ nodeStream = new Readable({
646
+ async read() {
647
+ try {
648
+ const { done, value } = await reader.read();
649
+ if (done) {
650
+ this.push(null);
651
+ } else {
652
+ this.push(Buffer.from(value));
653
+ }
654
+ } catch (err) {
655
+ this.emit('error', err);
656
+ }
657
+ },
658
+ });
659
+ nodeStream.on('data', (chunk) => {
660
+ this.push(chunk);
661
+ });
662
+ nodeStream.on('end', () => {
663
+ this.push(null);
664
+ });
665
+ nodeStream.on('error', (err) => {
666
+ this.emit('error', err);
667
+ });
668
+ } catch (err) {
669
+ this.emit('error', err);
670
+ }
671
+ }
672
+ },
673
+ });
674
+ signal?.addEventListener('abort', () => {
675
+ stream.destroy(new Error('The operation was aborted'));
676
+ });
677
+ return stream;
678
+ }
679
+ createReadableStream(path, options = {}) {
680
+ const key = this.normalizeKey(path);
681
+ if (!key) {
682
+ throw new Error('Cannot read root directory');
683
+ }
684
+ const { range, signal } = options;
685
+ // Use getObjectRaw which returns a Response with ReadableStream
686
+ const responsePromise = this.client.getObjectRaw(
687
+ key,
688
+ !range,
689
+ range?.start,
690
+ range?.end,
691
+ {
692
+ signal,
693
+ },
694
+ undefined,
695
+ );
696
+ return new ReadableStream({
697
+ async start(controller) {
698
+ try {
699
+ const response = await responsePromise;
700
+ if (!response.body) {
701
+ controller.error(new Error('No response body'));
702
+ return;
703
+ }
704
+ const reader = response.body.getReader();
705
+ signal?.addEventListener('abort', () => {
706
+ reader.cancel(new Error('The operation was aborted'));
707
+ controller.error(new Error('The operation was aborted'));
708
+ });
709
+ while (true) {
710
+ const { done, value } = await reader.read();
711
+ if (done) {
712
+ controller.close();
713
+ break;
714
+ }
715
+ controller.enqueue(value);
716
+ }
717
+ } catch (err) {
718
+ controller.error(err);
719
+ }
720
+ },
721
+ });
722
+ }
723
+ createWritableStream(path, options = {}) {
724
+ const key = this.normalizeKey(path);
725
+ if (!key) {
726
+ throw new Error('Cannot write to root directory');
727
+ }
728
+ const { signal, overwrite: _overwrite = true } = options;
729
+ this.checkAborted(signal);
730
+ // Create a WritableStream that buffers data and uploads when done
731
+ const buffer = [];
732
+ let controller;
733
+ const client = this.client;
734
+ const checkAborted = this.checkAborted.bind(this);
735
+ return new WritableStream({
736
+ start(ctrl) {
737
+ controller = ctrl;
738
+ },
739
+ async write(chunk) {
740
+ buffer.push(chunk);
741
+ },
742
+ async close() {
743
+ try {
744
+ checkAborted(signal);
745
+ const data = Buffer.concat(buffer.map((chunk) => Buffer.from(chunk)));
746
+ await client.putObject(key, data, undefined, undefined, undefined);
747
+ // Controller closes automatically when close() completes successfully
748
+ } catch (error) {
749
+ controller.error(error);
750
+ }
751
+ },
752
+ abort(reason) {
753
+ buffer.length = 0;
754
+ controller.error(reason);
755
+ },
756
+ });
757
+ }
758
+ getUrl(path, _options) {
759
+ if (typeof path === 'object' && path?.kind !== 'file') {
760
+ return;
761
+ }
762
+ const key = typeof path === 'string' ? this.normalizeKey(path) : this.normalizeKey(path.path);
763
+ if (!key) {
764
+ return;
765
+ }
766
+ // Construct URL from endpoint - S3mini doesn't provide presigned URLs in basic API
767
+ // This is a fallback - real implementation would need presigned URLs
768
+ return undefined;
769
+ }
703
770
  };
704
771
 
705
- //# sourceMappingURL=createS3MiniFileSystem.js.map
772
+ //# sourceMappingURL=createS3MiniFileSystem.js.map