Skip to content

Commit 8e9823f

Browse files
committed
Extend file system to support S3
1 parent fc324e9 commit 8e9823f

3 files changed

Lines changed: 76 additions & 0 deletions

File tree

src/Globals.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export interface FileSystemConfig {
168168
example?: boolean
169169
isGithub?: boolean
170170
isZIB?: boolean
171+
isS3?: boolean // AWS S3 bucket with public access
171172
flask?: boolean // Flask filesystem supports OMX open matrix API - see https://github.com/simwrapper/omx-server
172173
}
173174

src/fileSystemConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ let fileSystems: FileSystemConfig[] = [
140140
thumbnail: 'images/thumb-localfiles.jpg',
141141
example: true,
142142
baseURL: 'https://aeqwrapper.s3.amazonaws.com/',
143+
isS3: true,
143144
},
144145
{
145146
name: 'VSP/ZIB LakeFS',

src/js/HTTPFileSystem.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ enum FileSystemType {
1717
GITHUB,
1818
FLASK,
1919
LAKEFS,
20+
S3,
2021
}
2122

2223
naturalSort.insensitive = true
@@ -42,6 +43,7 @@ class HTTPFileSystem {
4243
private isGithub: boolean
4344
private isZIB: boolean
4445
private isFlask: boolean
46+
private isS3: boolean
4547
private type: FileSystemType
4648
private fileLinkLookup: any = {}
4749

@@ -54,12 +56,14 @@ class HTTPFileSystem {
5456
this.isGithub = !!project.isGithub
5557
this.isFlask = !!project.flask
5658
this.isZIB = !!project.isZIB
59+
this.isS3 = !!project.isS3
5760

5861
this.type = FileSystemType.FETCH
5962
if (this.fsHandle) this.type = FileSystemType.CHROME
6063
if (this.isGithub) this.type = FileSystemType.GITHUB
6164
if (this.isFlask) this.type = FileSystemType.FLASK
6265
if (this.isZIB) this.type = FileSystemType.LAKEFS
66+
if (this.isS3) this.type = FileSystemType.S3
6367

6468
this.baseUrl = project.baseURL
6569
if (!project.baseURL.endsWith('/')) this.baseUrl += '/'
@@ -533,6 +537,9 @@ class HTTPFileSystem {
533537
case FileSystemType.FLASK:
534538
dirEntry = await this._getDirectoryFromAzure(stillScaryPath)
535539
break
540+
case FileSystemType.S3:
541+
dirEntry = await this._getDirectoryFromS3(stillScaryPath)
542+
break
536543
case FileSystemType.LAKEFS:
537544
case FileSystemType.FETCH:
538545
default:
@@ -617,6 +624,73 @@ class HTTPFileSystem {
617624
return contents
618625
}
619626

627+
async _getDirectoryFromS3(stillScaryPath: string): Promise<DirectoryEntry> {
628+
// S3 uses a list API with prefix and delimiter to simulate directories
629+
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
630+
631+
let prefix = stillScaryPath.replace(/^\/+/, '') // remove leading slashes
632+
prefix = prefix.replaceAll('//', '/')
633+
634+
// Build the S3 list URL with query parameters
635+
const listUrl = `${this.baseUrl}?list-type=2&delimiter=/&prefix=${encodeURIComponent(prefix)}`
636+
637+
const response = await fetch(listUrl)
638+
if (response.status !== 200) {
639+
console.warn('S3 list status:', response.status)
640+
throw response
641+
}
642+
643+
const xmlText = await response.text()
644+
return this.buildListFromS3Xml(xmlText, prefix)
645+
}
646+
647+
private buildListFromS3Xml(xmlText: string, prefix: string): DirectoryEntry {
648+
const dirs: string[] = []
649+
const files: string[] = []
650+
651+
const parser = new DOMParser()
652+
const xmlDoc = parser.parseFromString(xmlText, 'text/xml')
653+
654+
// Get files from <Contents> elements
655+
const contents = xmlDoc.getElementsByTagName('Contents')
656+
for (let i = 0; i < contents.length; i++) {
657+
const keyElement = contents[i].getElementsByTagName('Key')[0]
658+
if (keyElement && keyElement.textContent) {
659+
let key = keyElement.textContent
660+
// Remove the prefix to get the relative filename
661+
if (key.startsWith(prefix)) {
662+
key = key.substring(prefix.length)
663+
}
664+
// Skip if it's the directory itself or empty
665+
if (key && key !== '' && !key.endsWith('/')) {
666+
files.push(key)
667+
}
668+
}
669+
}
670+
671+
// Get directories from <CommonPrefixes> elements
672+
const commonPrefixes = xmlDoc.getElementsByTagName('CommonPrefixes')
673+
for (let i = 0; i < commonPrefixes.length; i++) {
674+
const prefixElement = commonPrefixes[i].getElementsByTagName('Prefix')[0]
675+
if (prefixElement && prefixElement.textContent) {
676+
let dirPath = prefixElement.textContent
677+
// Remove the base prefix to get the relative directory name
678+
if (dirPath.startsWith(prefix)) {
679+
dirPath = dirPath.substring(prefix.length)
680+
}
681+
// Remove trailing slash
682+
if (dirPath.endsWith('/')) {
683+
dirPath = dirPath.slice(0, -1)
684+
}
685+
if (dirPath && dirPath !== '') {
686+
dirs.push(dirPath)
687+
}
688+
}
689+
}
690+
691+
return { dirs, files, handles: {} }
692+
}
693+
620694
async _getDirectoryFromURL(stillScaryPath: string) {
621695
const response = await this._getFileResponse(stillScaryPath)
622696
// console.log(response)

0 commit comments

Comments
 (0)