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