UNPKG

@firesystem/s3

Version:
1,020 lines (1,018 loc) 35.9 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { S3FileSystem: () => S3FileSystem }); module.exports = __toCommonJS(index_exports); // src/S3FileSystem.ts var import_core = require("@firesystem/core"); var import_client_s3 = require("@aws-sdk/client-s3"); var S3FileSystem = class { constructor(config) { this.watchers = []; this.events = new import_core.TypedEventEmitter(); this.config = { ...config, prefix: config.prefix || "/", mode: config.mode || "strict" }; if (this.config.prefix !== "/") { this.config.prefix = (0, import_core.normalizePath)(this.config.prefix); if (!this.config.prefix.endsWith("/")) { this.config.prefix += "/"; } this.config.prefix = this.config.prefix.substring(1); } this.client = new import_client_s3.S3Client({ region: config.region, credentials: config.credentials, ...config.clientOptions }); } async initialize() { const startTime = Date.now(); this.events.emit(import_core.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(import_core.FileSystemEvents.INITIALIZED, { duration: Date.now() - startTime }); } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "initialize", path: "/", error }); throw error; } } toS3Key(path) { const normalized = (0, import_core.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 import_client_s3.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 = (0, import_core.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 = (0, import_core.normalizePath)(path); const startTime = Date.now(); const operationId = `read-${normalized}-${startTime}`; this.events.emit(import_core.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 import_client_s3.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 import_client_s3.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: (0, import_core.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(import_core.FileSystemEvents.FILE_READ, { path: normalized, size: result.size || 0 }); this.events.emit(import_core.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(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "readFile", path: normalized, error: notFoundError }); throw notFoundError; } this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "readFile", path: normalized, error }); throw error; } } async writeFile(path, content, metadata) { const normalized = (0, import_core.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(import_core.FileSystemEvents.OPERATION_START, { operation: "writeFile", path: normalized, id: operationId }); this.events.emit(import_core.FileSystemEvents.FILE_WRITING, { path: normalized, size }); try { if (this.config.mode === "strict") { const parent = (0, import_core.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 import_client_s3.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 import_client_s3.PutObjectCommand({ Bucket: this.config.bucket, Key: s3Key, Body: contentStr, ContentType: "text/plain", Metadata: s3Metadata })); const now = /* @__PURE__ */ new Date(); const result = { path: normalized, name: (0, import_core.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(import_core.FileSystemEvents.FILE_WRITTEN, { path: normalized, size }); this.events.emit(import_core.FileSystemEvents.OPERATION_END, { operation: "writeFile", path: normalized, id: operationId, duration: Date.now() - startTime }); return result; } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "writeFile", path: normalized, error }); throw error; } } async deleteFile(path) { const normalized = (0, import_core.normalizePath)(path); const startTime = Date.now(); const operationId = `delete-${normalized}-${startTime}`; this.events.emit(import_core.FileSystemEvents.OPERATION_START, { operation: "deleteFile", path: normalized, id: operationId }); this.events.emit(import_core.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 import_client_s3.DeleteObjectCommand({ Bucket: this.config.bucket, Key: s3Key })); this.notifyWatchers({ type: "deleted", path: normalized, timestamp: /* @__PURE__ */ new Date() }); this.events.emit(import_core.FileSystemEvents.FILE_DELETED, { path: normalized }); this.events.emit(import_core.FileSystemEvents.OPERATION_END, { operation: "deleteFile", path: normalized, id: operationId, duration: Date.now() - startTime }); } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "deleteFile", path: normalized, error }); throw error; } } async exists(path) { const normalized = (0, import_core.normalizePath)(path); try { const s3Key = this.toS3Key(normalized); try { await this.client.send(new import_client_s3.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 import_client_s3.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 import_client_s3.HeadObjectCommand({ Bucket: this.config.bucket, Key: dirKey })); return true; } catch { return false; } } } catch { return false; } } async readDir(path) { const normalized = (0, import_core.normalizePath)(path); const startTime = Date.now(); const operationId = `readdir-${normalized}-${startTime}`; this.events.emit(import_core.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 import_client_s3.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: (0, import_core.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: (0, import_core.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(import_core.FileSystemEvents.DIR_READ, { path: normalized, count: entries.length }); this.events.emit(import_core.FileSystemEvents.OPERATION_END, { operation: "readDir", path: normalized, id: operationId, duration: Date.now() - startTime }); return entries; } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "readDir", path: normalized, error }); throw error; } } async mkdir(path, recursive) { const normalized = (0, import_core.normalizePath)(path); const startTime = Date.now(); const operationId = `mkdir-${normalized}-${startTime}`; this.events.emit(import_core.FileSystemEvents.OPERATION_START, { operation: "mkdir", path: normalized, id: operationId }); this.events.emit(import_core.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: (0, import_core.basename)(normalized), type: "directory", size: 0, created: now2, modified: now2 }; this.notifyWatchers({ type: "created", path: normalized, timestamp: now2 }); this.events.emit(import_core.FileSystemEvents.DIR_CREATED, { path: normalized }); this.events.emit(import_core.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 = (0, import_core.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: (0, import_core.basename)(normalized), type: "directory", size: 0, created: now, modified: now }; this.notifyWatchers({ type: "created", path: normalized, timestamp: now }); this.events.emit(import_core.FileSystemEvents.DIR_CREATED, { path: normalized }); this.events.emit(import_core.FileSystemEvents.OPERATION_END, { operation: "mkdir", path: normalized, id: operationId, duration: Date.now() - startTime }); return result; } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "mkdir", path: normalized, error }); throw error; } } async rmdir(path, recursive) { const normalized = (0, import_core.normalizePath)(path); const startTime = Date.now(); const operationId = `rmdir-${normalized}-${startTime}`; this.events.emit(import_core.FileSystemEvents.OPERATION_START, { operation: "rmdir", path: normalized, id: operationId }); this.events.emit(import_core.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 import_client_s3.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 import_client_s3.DeleteObjectsCommand({ Bucket: this.config.bucket, Delete: { Objects: batch } })); } } else { const response = await this.client.send(new import_client_s3.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 import_client_s3.DeleteObjectCommand({ Bucket: this.config.bucket, Key: prefix })); } this.notifyWatchers({ type: "deleted", path: normalized, timestamp: /* @__PURE__ */ new Date() }); this.events.emit(import_core.FileSystemEvents.DIR_DELETED, { path: normalized }); this.events.emit(import_core.FileSystemEvents.OPERATION_END, { operation: "rmdir", path: normalized, id: operationId, duration: Date.now() - startTime }); } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "rmdir", path: normalized, error }); throw error; } } async rename(oldPath, newPath) { const normalizedOld = (0, import_core.normalizePath)(oldPath); const normalizedNew = (0, import_core.normalizePath)(newPath); const startTime = Date.now(); const operationId = `rename-${normalizedOld}-${startTime}`; this.events.emit(import_core.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 = (0, import_core.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], (0, import_core.dirname)(normalizedNew)); } const result = await this.readFile(normalizedNew); this.events.emit(import_core.FileSystemEvents.OPERATION_END, { operation: "rename", path: normalizedOld, id: operationId, duration: Date.now() - startTime }); return result; } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "rename", path: normalizedOld, error }); throw error; } } async move(sourcePaths, targetPath) { const normalizedTarget = (0, import_core.normalizePath)(targetPath); const startTime = Date.now(); const operationId = `move-${startTime}`; this.events.emit(import_core.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 = (0, import_core.normalizePath)(sourcePath); const targetFilePath = (0, import_core.join)(normalizedTarget, (0, import_core.basename)(normalizedSource)); const sourceEntry = await this.readFile(normalizedSource); await this.writeFile(targetFilePath, sourceEntry.content, sourceEntry.metadata); await this.deleteFile(normalizedSource); } this.events.emit(import_core.FileSystemEvents.OPERATION_END, { operation: "move", path: sourcePaths[0], id: operationId, duration: Date.now() - startTime }); } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "move", path: sourcePaths[0], error }); throw error; } } async copy(sourcePath, targetPath) { const normalizedSource = (0, import_core.normalizePath)(sourcePath); const normalizedTarget = (0, import_core.normalizePath)(targetPath); const startTime = Date.now(); const operationId = `copy-${normalizedSource}-${startTime}`; this.events.emit(import_core.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(import_core.FileSystemEvents.OPERATION_END, { operation: "copy", path: normalizedSource, id: operationId, duration: Date.now() - startTime }); return result; } catch (error) { this.events.emit(import_core.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 = (0, import_core.normalizePath)(path); const s3Key = this.toS3Key(normalized); const dirKey = s3Key.endsWith("/") ? s3Key : s3Key + "/"; try { const response = await this.client.send(new import_client_s3.HeadObjectCommand({ Bucket: this.config.bucket, Key: dirKey })); return response.Metadata?.["x-amz-meta-type"] === "directory"; } catch { const response = await this.client.send(new import_client_s3.ListObjectsV2Command({ Bucket: this.config.bucket, Prefix: dirKey, MaxKeys: 1 })); return (response.Contents?.length || 0) > 0; } } async stat(path) { const normalized = (0, import_core.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 import_client_s3.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 import_client_s3.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(import_core.FileSystemEvents.OPERATION_START, { operation: "clear", path: "/", id: operationId }); try { const objectsToDelete = []; let continuationToken; do { const response = await this.client.send(new import_client_s3.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 import_client_s3.DeleteObjectsCommand({ Bucket: this.config.bucket, Delete: { Objects: batch } })); } this.events.emit(import_core.FileSystemEvents.OPERATION_END, { operation: "clear", path: "/", id: operationId, duration: Date.now() - startTime }); } catch (error) { this.events.emit(import_core.FileSystemEvents.OPERATION_ERROR, { operation: "clear", path: "/", error }); throw error; } } async size() { let totalSize = 0; let continuationToken; do { const response = await this.client.send(new import_client_s3.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; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { S3FileSystem }); //# sourceMappingURL=index.cjs.map