Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit 2a45124

Browse files
committed
fix(plugins/plugin-s3): ls /s3 fails poorly with wildcards
Fixes #5691
1 parent e0a164a commit 2a45124

File tree

1 file changed

+76
-50
lines changed

1 file changed

+76
-50
lines changed

plugins/plugin-s3/src/vfs/index.ts

Lines changed: 76 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import { basename, join } from 'path'
18+
import * as micromatch from 'micromatch'
1819
import { Client, ClientOptions, CopyConditions } from 'minio'
1920
import { Arguments, CodedError, REPL, flatten, inBrowser, i18n } from '@kui-shell/core'
2021
import { DirEntry, FStat, GlobStats, VFS, mount } from '@kui-shell/plugin-bash-like/fs'
@@ -40,8 +41,10 @@ class S3VFSResponder extends S3VFS implements VFS {
4041
this.client = new Client(options)
4142
}
4243

43-
public async ls(_, filepaths: string[]) {
44-
return flatten(await Promise.all(filepaths.map(filepath => this.dirstat(filepath.replace(this.s3Prefix, '')))))
44+
public async ls({ parsedOptions }: Parameters<VFS['ls']>[0], filepaths: string[]) {
45+
return flatten(
46+
await Promise.all(filepaths.map(filepath => this.dirstat(filepath.replace(this.s3Prefix, ''), parsedOptions.d)))
47+
)
4548
}
4649

4750
/** Degenerate case for `ls /s3`: list all buckets */
@@ -71,75 +74,98 @@ class S3VFSResponder extends S3VFS implements VFS {
7174
}))
7275
}
7376

74-
/** This is incomplete */
75-
private globToRegExp(prefix: string, suffixGlob: string): RegExp {
76-
return new RegExp('^' + prefix + suffixGlob.replace(/\*/g, '.*') + '$')
77+
private async listBucketsMatching(pattern: string): Promise<DirEntry[]> {
78+
const allBuckets = await this.listBuckets()
79+
return allBuckets.filter(_ => micromatch.isMatch(_.name, pattern))
7780
}
7881

7982
/** Enumerate matching objects */
80-
private listObjects(filepath: string): Promise<DirEntry[]> {
81-
const [, bucketName, prefix, wildcardSuffix] = filepath.match(/([^/]+)\/?([^*]*)(.*)/)
83+
private async listObjects(filepath: string, dashD): Promise<DirEntry[]> {
84+
const [, bucketName, bucketNameSlash, prefix, wildcardSuffix] = filepath.match(/([^/*]+)(\/?)([^*]*)(.*)/)
8285

8386
const pattern =
8487
prefix.length === 0 && (wildcardSuffix.length === 0 || wildcardSuffix === '*')
85-
? /.*/ // e.g. ls /s3/myBucket
86-
: this.globToRegExp(prefix, wildcardSuffix) // e.g. ls /s3/myBucket/he*lo
87-
88-
return this.listObjectsMatching(bucketName, prefix, pattern)
88+
? '*' // e.g. ls /s3/myBucket
89+
: wildcardSuffix // e.g. ls /s3/myBucket/he*lo
90+
91+
if (!bucketNameSlash && dashD) {
92+
// ls -d /s3/myBuck*
93+
return this.listBucketsMatching(filepath)
94+
} else if (!bucketNameSlash) {
95+
// ls /s3/myBuck*
96+
const buckets = await this.listBucketsMatching(filepath)
97+
return flatten(
98+
await Promise.all(buckets.map(bucketEntry => this.listObjectsMatching(bucketEntry.name, prefix, pattern, true)))
99+
)
100+
} else {
101+
// ls /s3/myBucket/myObj*
102+
return this.listObjectsMatching(bucketName, prefix, pattern)
103+
}
89104
}
90105

91106
/** Enumerate objects with a suffix wildcard, e.g. C* */
92-
private async listObjectsMatching(bucketName: string, prefix: string, pattern: RegExp): Promise<DirEntry[]> {
93-
const objectStream = await this.client.listObjects(bucketName, prefix)
107+
private async listObjectsMatching(
108+
bucketName: string,
109+
prefix: string,
110+
pattern: string,
111+
displayFullPath = false
112+
): Promise<DirEntry[]> {
113+
try {
114+
const objectStream = await this.client.listObjects(bucketName, prefix)
94115

95-
return new Promise((resolve, reject) => {
96-
const objects: DirEntry[] = []
116+
return new Promise((resolve, reject) => {
117+
const objects: DirEntry[] = []
97118

98-
objectStream.on('end', () => resolve(objects))
99-
objectStream.on('close', () => resolve(objects))
119+
objectStream.on('end', () => resolve(objects))
120+
objectStream.on('close', () => resolve(objects))
100121

101-
objectStream.on('error', err => {
102-
console.error('Error in S3Vfs.listObjects', err)
103-
const error: CodedError = new Error(err.message || 'Error listing s3 objects')
104-
error.code = err['httpstatuscode'] || err['code'] // missing types in @types/minio
105-
reject(error)
106-
})
122+
objectStream.on('error', err => {
123+
console.error('Error in S3Vfs.listObjects', err)
124+
const error: CodedError = new Error(err.message || 'Error listing s3 objects')
125+
error.code = err['httpstatuscode'] || err['code'] // missing types in @types/minio
126+
reject(error)
127+
})
107128

108-
objectStream.on('data', ({ name, size, lastModified }) => {
109-
if (pattern.test(name)) {
110-
objects.push({
111-
name,
112-
path: join(this.mountPath, bucketName, name),
113-
stats: {
114-
size,
115-
mtimeMs: lastModified.getTime(),
116-
mode: 0,
117-
uid,
118-
gid
119-
},
120-
nameForDisplay: name,
121-
dirent: {
122-
isFile: true,
123-
isDirectory: false,
124-
isSymbolicLink: false,
125-
isSpecial: false,
126-
isExecutable: false,
127-
permissions: '',
128-
username
129-
}
130-
})
131-
}
129+
objectStream.on('data', ({ name, size, lastModified }) => {
130+
if (!pattern || micromatch.isMatch(name, pattern)) {
131+
const path = join(this.mountPath, bucketName, name)
132+
133+
objects.push({
134+
name,
135+
path,
136+
stats: {
137+
size,
138+
mtimeMs: lastModified.getTime(),
139+
mode: 0,
140+
uid,
141+
gid
142+
},
143+
nameForDisplay: displayFullPath ? path : name,
144+
dirent: {
145+
isFile: true,
146+
isDirectory: false,
147+
isSymbolicLink: false,
148+
isSpecial: false,
149+
isExecutable: false,
150+
permissions: '',
151+
username
152+
}
153+
})
154+
}
155+
})
132156
})
133-
})
157+
} catch (err) {
158+
throw new Error(err.message)
159+
}
134160
}
135161

136162
/** Enumerate the objects specified by the given filepath */
137-
private async dirstat(filepath: string): Promise<DirEntry[]> {
163+
private async dirstat(filepath: string, dashD: boolean): Promise<DirEntry[]> {
138164
try {
139165
if (filepath.length === 0) {
140166
return this.listBuckets()
141167
} else {
142-
return this.listObjects(filepath)
168+
return this.listObjects(filepath, dashD)
143169
}
144170
} catch (err) {
145171
console.error('Error in S3VFS.ls', err)

0 commit comments

Comments
 (0)