|
1 | | -import { FileSource } from './types.js' |
| 1 | +import { DirSource, FileMetadata, FileSource, SourcePart } from './types.js' |
2 | 2 | import { getFileName } from './utils.js' |
3 | 3 |
|
4 | | -export function getHttpSource(sourceId: string, options?: {requestInit?: RequestInit}): FileSource | undefined { |
| 4 | +function s3list(bucket: string, prefix: string) { |
| 5 | + const url = `https://${bucket}.s3.amazonaws.com/?list-type=2&prefix=${prefix}&delimiter=/` |
| 6 | + return fetch(url) |
| 7 | + .then(res => { |
| 8 | + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) |
| 9 | + return res.text() |
| 10 | + }) |
| 11 | + .then(text => { |
| 12 | + const results = [] |
| 13 | + |
| 14 | + // Parse regular objects (files and explicit directories) |
| 15 | + const contentsRegex = /<Contents>(.*?)<\/Contents>/gs |
| 16 | + const contentsMatches = text.match(contentsRegex) ?? [] |
| 17 | + |
| 18 | + for (const match of contentsMatches) { |
| 19 | + const keyMatch = /<Key>(.*?)<\/Key>/.exec(match) |
| 20 | + const lastModifiedMatch = /<LastModified>(.*?)<\/LastModified>/.exec(match) |
| 21 | + const sizeMatch = /<Size>(.*?)<\/Size>/.exec(match) |
| 22 | + const eTagMatch = /<ETag>"(.*?)"<\/ETag>/.exec(match) ?? /<ETag>"(.*?)"<\/ETag>/.exec(match) |
| 23 | + |
| 24 | + if (!keyMatch || !lastModifiedMatch) continue |
| 25 | + |
| 26 | + const key = keyMatch[1] |
| 27 | + const lastModified = lastModifiedMatch[1] |
| 28 | + const size = sizeMatch ? parseInt(sizeMatch[1] ?? '', 10) : undefined |
| 29 | + const eTag = eTagMatch ? eTagMatch[1] : undefined |
| 30 | + |
| 31 | + results.push({ key, lastModified, size, eTag }) |
| 32 | + } |
| 33 | + |
| 34 | + // Parse CommonPrefixes (virtual directories) |
| 35 | + const prefixRegex = /<CommonPrefixes>(.*?)<\/CommonPrefixes>/gs |
| 36 | + const prefixMatches = text.match(prefixRegex) ?? [] |
| 37 | + |
| 38 | + for (const match of prefixMatches) { |
| 39 | + const prefixMatch = /<Prefix>(.*?)<\/Prefix>/.exec(match) |
| 40 | + if (!prefixMatch) continue |
| 41 | + |
| 42 | + const key = prefixMatch[1] |
| 43 | + results.push({ |
| 44 | + key, |
| 45 | + lastModified: new Date().toISOString(), // No lastModified for CommonPrefixes |
| 46 | + size: 0, |
| 47 | + isCommonPrefix: true, |
| 48 | + }) |
| 49 | + } |
| 50 | + |
| 51 | + return results |
| 52 | + }) |
| 53 | +} |
| 54 | + |
| 55 | +function getSourceParts(sourceId: string): SourcePart[] { |
| 56 | + const [protocol, rest] = sourceId.split('://', 2) |
| 57 | + const parts = rest |
| 58 | + ? [`${protocol}://${rest.split('/', 1)[0]}`, ...rest.split('/').slice(1)] |
| 59 | + : sourceId.split('/') |
| 60 | + const sourceParts = [ |
| 61 | + ...parts.map((part, depth) => { |
| 62 | + const slashSuffix = depth === parts.length - 1 ? '' : '/' |
| 63 | + return { |
| 64 | + text: part + slashSuffix, |
| 65 | + sourceId: parts.slice(0, depth + 1).join('/') + slashSuffix, |
| 66 | + } |
| 67 | + }), |
| 68 | + ] |
| 69 | + if (sourceParts[sourceParts.length - 1]?.text === '') { |
| 70 | + sourceParts.pop() |
| 71 | + } |
| 72 | + return sourceParts |
| 73 | +} |
| 74 | + |
| 75 | +export function getHttpSource(sourceId: string, options?: {requestInit?: RequestInit}): FileSource | DirSource | undefined { |
5 | 76 | if (!URL.canParse(sourceId)) { |
6 | 77 | return undefined |
7 | 78 | } |
| 79 | + |
| 80 | + const sourceParts = getSourceParts(sourceId) |
| 81 | + |
| 82 | + if (sourceId.endsWith('/')) { |
| 83 | + const url = new URL(sourceId) |
| 84 | + const bucket = url.hostname.split('.')[0] |
| 85 | + const prefix = url.pathname.slice(1) |
| 86 | + |
| 87 | + if (!bucket) { |
| 88 | + return undefined |
| 89 | + } |
| 90 | + |
| 91 | + return { |
| 92 | + kind: 'directory', |
| 93 | + sourceId, |
| 94 | + sourceParts, |
| 95 | + prefix, |
| 96 | + listFiles: () => s3list(bucket, prefix).then(items => |
| 97 | + items |
| 98 | + .filter(item => item.key !== undefined) |
| 99 | + .map(item => { |
| 100 | + if (!item.key) { |
| 101 | + throw new Error('Key is undefined') |
| 102 | + } |
| 103 | + const isDirectory = item.key.endsWith('/') |
| 104 | + const itemSourceId = `https://${bucket}.s3.amazonaws.com/${item.key}` |
| 105 | + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing |
| 106 | + let name = item.key.split('/').pop() || item.key |
| 107 | + if (name && isDirectory) { |
| 108 | + name = name.replace(prefix, '') |
| 109 | + } |
| 110 | + return { |
| 111 | + name, |
| 112 | + lastModified: item.lastModified, |
| 113 | + sourceId: itemSourceId, |
| 114 | + kind: isDirectory ? 'directory' : 'file', |
| 115 | + } as FileMetadata |
| 116 | + }) |
| 117 | + ), |
| 118 | + } as DirSource |
| 119 | + } |
| 120 | + |
8 | 121 | return { |
9 | 122 | kind: 'file', |
10 | 123 | sourceId, |
11 | | - sourceParts: [{ text: sourceId, sourceId }], |
| 124 | + sourceParts, |
12 | 125 | fileName: getFileName(sourceId), |
13 | 126 | resolveUrl: sourceId, |
14 | 127 | requestInit: options?.requestInit, |
|
0 commit comments