UNPKG

@firesystem/s3

Version:
1,140 lines (1,139 loc) 35.2 kB
// src/S3FileSystem.ts import { normalizePath, dirname, basename, join, TypedEventEmitter, FileSystemEvents, } from "@firesystem/core"; import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsV2Command, HeadObjectCommand, DeleteObjectsCommand, } from "@aws-sdk/client-s3"; var S3FileSystem = class { constructor(config) { this.watchers = []; this.events = new TypedEventEmitter(); this.config = { ...config, prefix: config.prefix || "/", mode: config.mode || "strict", }; if (this.config.prefix !== "/") { this.config.prefix = normalizePath(this.config.prefix); if (!this.config.prefix.endsWith("/")) { this.config.prefix += "/"; } this.config.prefix = this.config.prefix.substring(1); } this.client = new S3Client({ region: config.region, credentials: config.credentials, ...config.clientOptions, }); } async initialize() { const startTime = Date.now(); this.events.emit(FileSystemEvents.INITIALIZING, void 0); try { if ( this.config.mode === "strict" && this.config.prefix !== "/" && this.config.prefix !== "" ) { await this.ensureDirectoryMarker(this.config.prefix); } this.events.emit(FileSystemEvents.INITIALIZED, { duration: Date.now() - startTime, }); } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "initialize", path: "/", error, }); throw error; } } toS3Key(path) { const normalized = normalizePath(path); if (normalized === "/") { return this.config.prefix === "/" ? "" : this.config.prefix; } const withoutLeadingSlash = normalized.substring(1); if (this.config.prefix === "/" || this.config.prefix === "") { return withoutLeadingSlash; } return this.config.prefix + withoutLeadingSlash; } fromS3Key(key) { if (this.config.prefix === "/" || this.config.prefix === "") { return "/" + key; } if (key.startsWith(this.config.prefix)) { const withoutPrefix = key.substring(this.config.prefix.length); return "/" + withoutPrefix; } return "/" + key; } async streamToString(stream) { const chunks = []; for await (const chunk of stream) { chunks.push(chunk); } return Buffer.concat(chunks).toString("utf-8"); } async ensureDirectoryMarker(s3Key) { const key = s3Key.endsWith("/") ? s3Key : s3Key + "/"; try { await this.client.send( new PutObjectCommand({ Bucket: this.config.bucket, Key: key, Body: "", ContentType: "application/x-directory", Metadata: { "x-amz-meta-type": "directory", "x-amz-meta-created": /* @__PURE__ */ new Date().toISOString(), }, }), ); } catch (error) { throw new Error(`Failed to create directory marker: ${error}`); } } async ensureParentExists(path) { if (this.config.mode !== "strict") return; const parent = dirname(path); if (parent === "/" || parent === ".") return; const s3Key = this.toS3Key(parent); await this.ensureDirectoryMarker(s3Key); } parseS3Metadata(metadata) { const result = {}; if (metadata) { if (metadata["x-amz-meta-created"]) { result.created = new Date(metadata["x-amz-meta-created"]); } if (metadata["x-amz-meta-modified"]) { result.modified = new Date(metadata["x-amz-meta-modified"]); } if (metadata["x-amz-meta-type"]) { result.type = metadata["x-amz-meta-type"]; } if (metadata["x-amz-meta-custom"]) { try { result.fileMetadata = JSON.parse(metadata["x-amz-meta-custom"]); } catch {} } } return result; } createS3Metadata(type, metadata, created) { const now = /* @__PURE__ */ new Date(); const s3Metadata = { "x-amz-meta-type": type, "x-amz-meta-created": (created || now).toISOString(), "x-amz-meta-modified": now.toISOString(), }; if (metadata) { s3Metadata["x-amz-meta-custom"] = JSON.stringify(metadata); } return s3Metadata; } async readFile(path) { const normalized = normalizePath(path); const startTime = Date.now(); const operationId = `read-${normalized}-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "readFile", path: normalized, id: operationId, }); try { const s3Key = this.toS3Key(normalized); const dirKey = s3Key.endsWith("/") ? s3Key : s3Key + "/"; try { const dirCheck = await this.client.send( new HeadObjectCommand({ Bucket: this.config.bucket, Key: dirKey, }), ); if (dirCheck) { throw new Error( `EISDIR: illegal operation on a directory, read '${normalized}'`, ); } } catch (error) { if (error.name !== "NoSuchKey" && !error.message?.includes("EISDIR")) { throw error; } } const response = await this.client.send( new GetObjectCommand({ Bucket: this.config.bucket, Key: s3Key, }), ); if (!response.Body) { throw new Error(`File not found: ${normalized}`); } const parsedMetadata = this.parseS3Metadata(response.Metadata); let type = parsedMetadata.type; if (!type && this.config.mode === "lenient") { type = s3Key.endsWith("/") ? "directory" : "file"; } if (type === "directory" || s3Key.endsWith("/")) { throw new Error( `EISDIR: illegal operation on a directory, read '${normalized}'`, ); } let content; if (parsedMetadata.fileMetadata?.isBinary) { const base64Content = await this.streamToString(response.Body); const buffer = Buffer.from(base64Content, "base64"); content = buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength, ); } else { content = await this.streamToString(response.Body); } const result = { path: normalized, name: basename(normalized), type: type || "file", size: response.ContentLength || 0, created: parsedMetadata.created || new Date(response.LastModified || Date.now()), modified: parsedMetadata.modified || new Date(response.LastModified || Date.now()), metadata: parsedMetadata.fileMetadata, content, }; this.events.emit(FileSystemEvents.FILE_READ, { path: normalized, size: result.size || 0, }); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "readFile", path: normalized, id: operationId, duration: Date.now() - startTime, }); return result; } catch (error) { if (error.name === "NoSuchKey") { const notFoundError = new Error( `ENOENT: no such file or directory, open '${normalized}'`, ); this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "readFile", path: normalized, error: notFoundError, }); throw notFoundError; } this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "readFile", path: normalized, error, }); throw error; } } async writeFile(path, content, metadata) { const normalized = normalizePath(path); const startTime = Date.now(); const operationId = `write-${normalized}-${startTime}`; let contentStr; let isBinary = false; let originalSize; if (content === null || content === void 0) { contentStr = ""; originalSize = 0; } else if (typeof content === "string") { contentStr = content; originalSize = Buffer.byteLength(contentStr); } else if (content instanceof ArrayBuffer) { contentStr = Buffer.from(content).toString("base64"); isBinary = true; originalSize = content.byteLength; } else { contentStr = JSON.stringify(content); originalSize = Buffer.byteLength(contentStr); } const size = originalSize; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "writeFile", path: normalized, id: operationId, }); this.events.emit(FileSystemEvents.FILE_WRITING, { path: normalized, size }); try { if (this.config.mode === "strict") { const parent = dirname(normalized); if (parent !== "/" && parent !== ".") { const parentExists = await this.exists(parent); if (!parentExists) { throw new Error( `ENOENT: no such file or directory, open '${normalized}'`, ); } } } await this.ensureParentExists(normalized); const s3Key = this.toS3Key(normalized); let existingCreated; try { const existing = await this.client.send( new HeadObjectCommand({ Bucket: this.config.bucket, Key: s3Key, }), ); if (existing.Metadata?.["x-amz-meta-created"]) { existingCreated = new Date(existing.Metadata["x-amz-meta-created"]); } } catch {} const finalMetadata = isBinary ? { ...metadata, isBinary: true } : metadata; const s3Metadata = this.createS3Metadata( "file", finalMetadata, existingCreated, ); await this.client.send( new PutObjectCommand({ Bucket: this.config.bucket, Key: s3Key, Body: contentStr, ContentType: "text/plain", Metadata: s3Metadata, }), ); const now = /* @__PURE__ */ new Date(); const result = { path: normalized, name: basename(normalized), type: "file", size, created: existingCreated || new Date(s3Metadata["x-amz-meta-created"]), modified: now, metadata: finalMetadata, content, }; this.notifyWatchers({ type: existingCreated ? "updated" : "created", path: normalized, timestamp: now, }); this.events.emit(FileSystemEvents.FILE_WRITTEN, { path: normalized, size, }); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "writeFile", path: normalized, id: operationId, duration: Date.now() - startTime, }); return result; } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "writeFile", path: normalized, error, }); throw error; } } async deleteFile(path) { const normalized = normalizePath(path); const startTime = Date.now(); const operationId = `delete-${normalized}-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "deleteFile", path: normalized, id: operationId, }); this.events.emit(FileSystemEvents.FILE_DELETING, { path: normalized }); try { const exists = await this.exists(normalized); if (!exists) { throw new Error( `ENOENT: no such file or directory, unlink '${normalized}'`, ); } const s3Key = this.toS3Key(normalized); if (await this.isDirectory(normalized)) { const entries = await this.readDir(normalized); if (entries.length > 0) { throw new Error( `ENOTEMPTY: directory not empty, rmdir '${normalized}'`, ); } } await this.client.send( new DeleteObjectCommand({ Bucket: this.config.bucket, Key: s3Key, }), ); this.notifyWatchers({ type: "deleted", path: normalized, timestamp: /* @__PURE__ */ new Date(), }); this.events.emit(FileSystemEvents.FILE_DELETED, { path: normalized }); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "deleteFile", path: normalized, id: operationId, duration: Date.now() - startTime, }); } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "deleteFile", path: normalized, error, }); throw error; } } async exists(path) { const normalized = normalizePath(path); try { const s3Key = this.toS3Key(normalized); try { await this.client.send( new HeadObjectCommand({ Bucket: this.config.bucket, Key: s3Key, }), ); return true; } catch { if (this.config.mode === "lenient") { const prefix = s3Key.endsWith("/") ? s3Key : s3Key + "/"; const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: prefix, MaxKeys: 1, }), ); return (response.Contents?.length || 0) > 0; } const dirKey = s3Key.endsWith("/") ? s3Key : s3Key + "/"; try { await this.client.send( new HeadObjectCommand({ Bucket: this.config.bucket, Key: dirKey, }), ); return true; } catch { return false; } } } catch { return false; } } async readDir(path) { const normalized = normalizePath(path); const startTime = Date.now(); const operationId = `readdir-${normalized}-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "readDir", path: normalized, id: operationId, }); try { if (normalized !== "/") { const exists = await this.exists(normalized); if (!exists) { throw new Error( `ENOENT: no such file or directory, scandir '${normalized}'`, ); } } const s3Prefix = this.toS3Key(normalized); const prefix = s3Prefix === "" ? "" : s3Prefix.endsWith("/") ? s3Prefix : s3Prefix + "/"; const entries = []; const seenPaths = /* @__PURE__ */ new Set(); let continuationToken; do { const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: prefix, Delimiter: "/", ContinuationToken: continuationToken, }), ); if (response.CommonPrefixes) { for (const commonPrefix of response.CommonPrefixes) { if (commonPrefix.Prefix) { const path2 = this.fromS3Key(commonPrefix.Prefix); if (!seenPaths.has(path2)) { seenPaths.add(path2); entries.push({ path: path2, name: basename( path2.endsWith("/") ? path2.slice(0, -1) : path2, ), type: "directory", size: 0, created: /* @__PURE__ */ new Date(), modified: /* @__PURE__ */ new Date(), }); } } } } if (response.Contents) { for (const object of response.Contents) { if (object.Key) { if (object.Key.endsWith("/")) continue; const relativePath = object.Key.substring(prefix.length); if (relativePath.includes("/")) continue; const path2 = this.fromS3Key(object.Key); if (!seenPaths.has(path2)) { seenPaths.add(path2); entries.push({ path: path2, name: basename(path2), type: "file", size: object.Size || 0, created: object.LastModified || /* @__PURE__ */ new Date(), modified: object.LastModified || /* @__PURE__ */ new Date(), }); } } } } continuationToken = response.NextContinuationToken; } while (continuationToken); this.events.emit(FileSystemEvents.DIR_READ, { path: normalized, count: entries.length, }); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "readDir", path: normalized, id: operationId, duration: Date.now() - startTime, }); return entries; } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "readDir", path: normalized, error, }); throw error; } } async mkdir(path, recursive) { const normalized = normalizePath(path); const startTime = Date.now(); const operationId = `mkdir-${normalized}-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "mkdir", path: normalized, id: operationId, }); this.events.emit(FileSystemEvents.DIR_CREATING, { path: normalized, recursive: !!recursive, }); try { if (normalized === "/") { throw new Error(`EEXIST: file already exists, mkdir '${normalized}'`); } if (await this.exists(normalized)) { throw new Error(`EEXIST: file already exists, mkdir '${normalized}'`); } if (this.config.mode === "lenient") { const now2 = /* @__PURE__ */ new Date(); const result2 = { path: normalized, name: basename(normalized), type: "directory", size: 0, created: now2, modified: now2, }; this.notifyWatchers({ type: "created", path: normalized, timestamp: now2, }); this.events.emit(FileSystemEvents.DIR_CREATED, { path: normalized }); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "mkdir", path: normalized, id: operationId, duration: Date.now() - startTime, }); return result2; } if (recursive) { const parts = normalized.split("/").filter(Boolean); let currentPath = ""; for (const part of parts) { currentPath += "/" + part; const s3Key = this.toS3Key(currentPath); await this.ensureDirectoryMarker(s3Key); } } else { const parent = dirname(normalized); if (parent !== "/" && parent !== ".") { const parentExists = await this.exists(parent); if (!parentExists) { throw new Error( `ENOENT: no such file or directory, mkdir '${normalized}'`, ); } } await this.ensureParentExists(normalized); const s3Key = this.toS3Key(normalized); await this.ensureDirectoryMarker(s3Key); } const now = /* @__PURE__ */ new Date(); const result = { path: normalized, name: basename(normalized), type: "directory", size: 0, created: now, modified: now, }; this.notifyWatchers({ type: "created", path: normalized, timestamp: now, }); this.events.emit(FileSystemEvents.DIR_CREATED, { path: normalized }); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "mkdir", path: normalized, id: operationId, duration: Date.now() - startTime, }); return result; } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "mkdir", path: normalized, error, }); throw error; } } async rmdir(path, recursive) { const normalized = normalizePath(path); const startTime = Date.now(); const operationId = `rmdir-${normalized}-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "rmdir", path: normalized, id: operationId, }); this.events.emit(FileSystemEvents.DIR_DELETING, { path: normalized, recursive: !!recursive, }); try { if (normalized === "/") { throw new Error( `EBUSY: resource busy or locked, rmdir '${normalized}'`, ); } const exists = await this.exists(normalized); if (!exists) { throw new Error( `ENOENT: no such file or directory, rmdir '${normalized}'`, ); } if (!(await this.isDirectory(normalized))) { throw new Error(`ENOTDIR: not a directory, rmdir '${normalized}'`); } const s3Prefix = this.toS3Key(normalized); const prefix = s3Prefix.endsWith("/") ? s3Prefix : s3Prefix + "/"; if (recursive) { const objectsToDelete = []; let continuationToken; do { const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: prefix, ContinuationToken: continuationToken, }), ); if (response.Contents) { for (const object of response.Contents) { if (object.Key) { objectsToDelete.push({ Key: object.Key }); } } } continuationToken = response.NextContinuationToken; } while (continuationToken); const batchSize = 1e3; for (let i = 0; i < objectsToDelete.length; i += batchSize) { const batch = objectsToDelete.slice(i, i + batchSize); await this.client.send( new DeleteObjectsCommand({ Bucket: this.config.bucket, Delete: { Objects: batch }, }), ); } } else { const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: prefix, MaxKeys: 2, // We only need to know if there's more than the dir marker }), ); const hasContents = (response.Contents?.length || 0) > 1 || (response.Contents?.length === 1 && !response.Contents[0].Key?.endsWith("/")); if (hasContents) { throw new Error( `ENOTEMPTY: directory not empty, rmdir '${normalized}'`, ); } await this.client.send( new DeleteObjectCommand({ Bucket: this.config.bucket, Key: prefix, }), ); } this.notifyWatchers({ type: "deleted", path: normalized, timestamp: /* @__PURE__ */ new Date(), }); this.events.emit(FileSystemEvents.DIR_DELETED, { path: normalized }); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "rmdir", path: normalized, id: operationId, duration: Date.now() - startTime, }); } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "rmdir", path: normalized, error, }); throw error; } } async rename(oldPath, newPath) { const normalizedOld = normalizePath(oldPath); const normalizedNew = normalizePath(newPath); const startTime = Date.now(); const operationId = `rename-${normalizedOld}-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "rename", path: normalizedOld, id: operationId, }); try { if (!(await this.exists(normalizedOld))) { throw new Error( `ENOENT: no such file or directory, rename '${normalizedOld}' -> '${normalizedNew}'`, ); } if (await this.exists(normalizedNew)) { throw new Error( `EEXIST: file already exists, rename '${normalizedOld}' -> '${normalizedNew}'`, ); } const newParent = dirname(normalizedNew); if ( newParent !== "/" && newParent !== "." && !(await this.exists(newParent)) ) { throw new Error( `ENOENT: no such file or directory, rename '${normalizedOld}' -> '${normalizedNew}'`, ); } const oldEntry = await this.readFile(normalizedOld); if (oldEntry.type === "file") { await this.writeFile( normalizedNew, oldEntry.content, oldEntry.metadata, ); await this.deleteFile(normalizedOld); } else { await this.move([normalizedOld], dirname(normalizedNew)); } const result = await this.readFile(normalizedNew); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "rename", path: normalizedOld, id: operationId, duration: Date.now() - startTime, }); return result; } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "rename", path: normalizedOld, error, }); throw error; } } async move(sourcePaths, targetPath) { const normalizedTarget = normalizePath(targetPath); const startTime = Date.now(); const operationId = `move-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "move", path: sourcePaths[0], id: operationId, }); try { if (!(await this.exists(normalizedTarget))) { throw new Error( `ENOENT: no such file or directory, open '${normalizedTarget}'`, ); } if (!(await this.isDirectory(normalizedTarget))) { throw new Error(`ENOTDIR: not a directory, open '${normalizedTarget}'`); } for (const sourcePath of sourcePaths) { const normalizedSource = normalizePath(sourcePath); const targetFilePath = join( normalizedTarget, basename(normalizedSource), ); const sourceEntry = await this.readFile(normalizedSource); await this.writeFile( targetFilePath, sourceEntry.content, sourceEntry.metadata, ); await this.deleteFile(normalizedSource); } this.events.emit(FileSystemEvents.OPERATION_END, { operation: "move", path: sourcePaths[0], id: operationId, duration: Date.now() - startTime, }); } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "move", path: sourcePaths[0], error, }); throw error; } } async copy(sourcePath, targetPath) { const normalizedSource = normalizePath(sourcePath); const normalizedTarget = normalizePath(targetPath); const startTime = Date.now(); const operationId = `copy-${normalizedSource}-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "copy", path: normalizedSource, id: operationId, }); try { if (await this.isDirectory(normalizedSource)) { throw new Error( `EISDIR: illegal operation on a directory, open '${normalizedSource}'`, ); } const sourceEntry = await this.readFile(normalizedSource); const result = await this.writeFile( normalizedTarget, sourceEntry.content, sourceEntry.metadata, ); this.events.emit(FileSystemEvents.OPERATION_END, { operation: "copy", path: normalizedSource, id: operationId, duration: Date.now() - startTime, }); return result; } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "copy", path: normalizedSource, error, }); throw error; } } watch(pattern, callback) { const id = Math.random().toString(36).substr(2, 9); const listener = { id, pattern, callback }; this.watchers.push(listener); return { dispose: () => { const index = this.watchers.findIndex((w) => w.id === id); if (index !== -1) { this.watchers.splice(index, 1); } }, }; } notifyWatchers(event) { for (const watcher of this.watchers) { if ( watcher.pattern === "*" || event.path.includes(watcher.pattern) || this.matchGlob(event.path, watcher.pattern) ) { watcher.callback(event); } } } matchGlob(path, pattern) { if (pattern === "*") { return path.split("/").length === 2; } if (pattern === "**") { return true; } let regex = pattern .replace(/\*\*/g, "___DOUBLE_STAR___") .replace(/\*/g, "[^/]*") .replace(/___DOUBLE_STAR___/g, ".*") .replace(/\?/g, ".") .replace(/\//g, "\\/"); return new RegExp("^" + regex + "$").test(path); } async isDirectory(path) { const normalized = normalizePath(path); const s3Key = this.toS3Key(normalized); const dirKey = s3Key.endsWith("/") ? s3Key : s3Key + "/"; try { const response = await this.client.send( new HeadObjectCommand({ Bucket: this.config.bucket, Key: dirKey, }), ); return response.Metadata?.["x-amz-meta-type"] === "directory"; } catch { const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: dirKey, MaxKeys: 1, }), ); return (response.Contents?.length || 0) > 0; } } async stat(path) { const normalized = normalizePath(path); try { if (await this.isDirectory(normalized)) { const s3Key = this.toS3Key(normalized); const dirKey = s3Key.endsWith("/") ? s3Key : s3Key + "/"; try { const response = await this.client.send( new HeadObjectCommand({ Bucket: this.config.bucket, Key: dirKey, }), ); const metadata = this.parseS3Metadata(response.Metadata); return { path: normalized, size: 0, type: "directory", created: metadata.created || new Date(response.LastModified || Date.now()), modified: metadata.modified || new Date(response.LastModified || Date.now()), }; } catch { return { path: normalized, size: 0, type: "directory", created: /* @__PURE__ */ new Date(), modified: /* @__PURE__ */ new Date(), }; } } const entry = await this.readFile(normalized); return { path: entry.path, size: entry.size || 0, type: entry.type, created: entry.created || /* @__PURE__ */ new Date(), modified: entry.modified || /* @__PURE__ */ new Date(), }; } catch (error) { if (error.message?.includes("EISDIR")) { return { path: normalized, size: 0, type: "directory", created: /* @__PURE__ */ new Date(), modified: /* @__PURE__ */ new Date(), }; } throw new Error(`Failed to stat ${normalized}: ${error}`); } } async glob(pattern) { const results = []; const seenPaths = /* @__PURE__ */ new Set(); const prefix = pattern.split("*")[0].replace(/^\//, ""); const s3Prefix = this.config.prefix === "/" ? prefix : this.config.prefix + prefix; let continuationToken; do { const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: s3Prefix, ContinuationToken: continuationToken, }), ); if (response.Contents) { for (const object of response.Contents) { if (object.Key) { const path = this.fromS3Key(object.Key); if (!object.Key.endsWith("/") && this.matchGlob(path, pattern)) { seenPaths.add(path); } if (pattern === "*" || pattern === "**") { const parts = path.split("/").filter(Boolean); for (let i = 1; i <= parts.length - 1; i++) { const dirPath = "/" + parts.slice(0, i).join("/"); if (this.matchGlob(dirPath, pattern)) { seenPaths.add(dirPath); } } } } } } continuationToken = response.NextContinuationToken; } while (continuationToken); return Array.from(seenPaths).sort(); } async clear() { const startTime = Date.now(); const operationId = `clear-${startTime}`; this.events.emit(FileSystemEvents.OPERATION_START, { operation: "clear", path: "/", id: operationId, }); try { const objectsToDelete = []; let continuationToken; do { const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: this.config.prefix === "/" ? "" : this.config.prefix, ContinuationToken: continuationToken, }), ); if (response.Contents) { for (const object of response.Contents) { if (object.Key) { objectsToDelete.push({ Key: object.Key }); } } } continuationToken = response.NextContinuationToken; } while (continuationToken); const batchSize = 1e3; for (let i = 0; i < objectsToDelete.length; i += batchSize) { const batch = objectsToDelete.slice(i, i + batchSize); await this.client.send( new DeleteObjectsCommand({ Bucket: this.config.bucket, Delete: { Objects: batch }, }), ); } this.events.emit(FileSystemEvents.OPERATION_END, { operation: "clear", path: "/", id: operationId, duration: Date.now() - startTime, }); } catch (error) { this.events.emit(FileSystemEvents.OPERATION_ERROR, { operation: "clear", path: "/", error, }); throw error; } } async size() { let totalSize = 0; let continuationToken; do { const response = await this.client.send( new ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: this.config.prefix === "/" ? "" : this.config.prefix, ContinuationToken: continuationToken, }), ); if (response.Contents) { for (const object of response.Contents) { totalSize += object.Size || 0; } } continuationToken = response.NextContinuationToken; } while (continuationToken); return totalSize; } // Implementações temporárias para compilar - S3 será implementado no futuro async canModify(path) { return true; } async canCreateIn(parentPath) { return true; } }; export { S3FileSystem }; //# sourceMappingURL=index.js.map