UNPKG

@firesystem/s3

Version:
1 lines 63.6 kB
{"version":3,"sources":["../src/index.ts","../src/S3FileSystem.ts"],"sourcesContent":["export { S3FileSystem } from \"./S3FileSystem\";\nexport type { S3FileSystemConfig } from \"./types\";","import type {\n IFileSystem,\n IReactiveFileSystem,\n FileEntry,\n FileStat,\n FileMetadata,\n FSEvent,\n Disposable,\n FileSystemEventPayloads,\n} from \"@firesystem/core\";\nimport {\n normalizePath,\n dirname,\n basename,\n join,\n TypedEventEmitter,\n FileSystemEvents,\n} from \"@firesystem/core\";\nimport {\n S3Client,\n PutObjectCommand,\n GetObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n CopyObjectCommand,\n DeleteObjectsCommand,\n type _Object,\n} from \"@aws-sdk/client-s3\";\nimport type { S3FileSystemConfig } from \"./types\";\n\ninterface WatchListener {\n id: string;\n pattern: string;\n callback: (event: FSEvent) => void;\n}\n\nexport class S3FileSystem implements IReactiveFileSystem {\n private client: S3Client;\n private config: S3FileSystemConfig & { prefix: string; mode: \"strict\" | \"lenient\" };\n private watchers: WatchListener[] = [];\n public readonly events = new TypedEventEmitter<FileSystemEventPayloads>();\n\n constructor(config: S3FileSystemConfig) {\n this.config = {\n ...config,\n prefix: config.prefix || \"/\",\n mode: config.mode || \"strict\",\n };\n\n // Normalize prefix\n if (this.config.prefix !== \"/\") {\n this.config.prefix = normalizePath(this.config.prefix);\n if (!this.config.prefix.endsWith(\"/\")) {\n this.config.prefix += \"/\";\n }\n // Remove leading slash for S3\n this.config.prefix = this.config.prefix.substring(1);\n }\n\n this.client = new S3Client({\n region: config.region,\n credentials: config.credentials,\n ...config.clientOptions,\n });\n }\n\n async initialize(): Promise<void> {\n const startTime = Date.now();\n this.events.emit(FileSystemEvents.INITIALIZING, undefined as any);\n\n try {\n // Only create root directory marker in strict mode\n if (this.config.mode === \"strict\" && \n this.config.prefix !== \"/\" && \n this.config.prefix !== \"\") {\n await this.ensureDirectoryMarker(this.config.prefix);\n }\n\n this.events.emit(FileSystemEvents.INITIALIZED, {\n duration: Date.now() - startTime,\n });\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"initialize\",\n path: \"/\",\n error: error as Error,\n });\n throw error;\n }\n }\n\n private toS3Key(path: string): string {\n const normalized = normalizePath(path);\n if (normalized === \"/\") {\n return this.config.prefix === \"/\" ? \"\" : this.config.prefix;\n }\n \n const withoutLeadingSlash = normalized.substring(1);\n if (this.config.prefix === \"/\" || this.config.prefix === \"\") {\n return withoutLeadingSlash;\n }\n \n return this.config.prefix + withoutLeadingSlash;\n }\n\n private fromS3Key(key: string): string {\n if (this.config.prefix === \"/\" || this.config.prefix === \"\") {\n return \"/\" + key;\n }\n \n if (key.startsWith(this.config.prefix)) {\n const withoutPrefix = key.substring(this.config.prefix.length);\n return \"/\" + withoutPrefix;\n }\n \n return \"/\" + key;\n }\n\n private async streamToString(stream: any): Promise<string> {\n const chunks: any[] = [];\n for await (const chunk of stream) {\n chunks.push(chunk);\n }\n return Buffer.concat(chunks).toString(\"utf-8\");\n }\n\n private async ensureDirectoryMarker(s3Key: string): Promise<void> {\n const key = s3Key.endsWith(\"/\") ? s3Key : s3Key + \"/\";\n \n try {\n await this.client.send(new PutObjectCommand({\n Bucket: this.config.bucket,\n Key: key,\n Body: \"\",\n ContentType: \"application/x-directory\",\n Metadata: {\n \"x-amz-meta-type\": \"directory\",\n \"x-amz-meta-created\": new Date().toISOString(),\n },\n }));\n } catch (error) {\n throw new Error(`Failed to create directory marker: ${error}`);\n }\n }\n\n private async ensureParentExists(path: string): Promise<void> {\n // Only ensure parent exists in strict mode\n if (this.config.mode !== \"strict\") return;\n \n const parent = dirname(path);\n if (parent === \"/\" || parent === \".\") return;\n\n const s3Key = this.toS3Key(parent);\n await this.ensureDirectoryMarker(s3Key);\n }\n\n private parseS3Metadata(metadata?: Record<string, string>): {\n created?: Date;\n modified?: Date;\n fileMetadata?: FileMetadata;\n type?: \"file\" | \"directory\";\n } {\n const result: any = {};\n \n if (metadata) {\n if (metadata[\"x-amz-meta-created\"]) {\n result.created = new Date(metadata[\"x-amz-meta-created\"]);\n }\n if (metadata[\"x-amz-meta-modified\"]) {\n result.modified = new Date(metadata[\"x-amz-meta-modified\"]);\n }\n if (metadata[\"x-amz-meta-type\"]) {\n result.type = metadata[\"x-amz-meta-type\"] as \"file\" | \"directory\";\n }\n if (metadata[\"x-amz-meta-custom\"]) {\n try {\n result.fileMetadata = JSON.parse(metadata[\"x-amz-meta-custom\"]);\n } catch {\n // Ignore parse errors\n }\n }\n }\n \n return result;\n }\n\n private createS3Metadata(\n type: \"file\" | \"directory\",\n metadata?: FileMetadata,\n created?: Date,\n ): Record<string, string> {\n const now = new Date();\n const s3Metadata: Record<string, string> = {\n \"x-amz-meta-type\": type,\n \"x-amz-meta-created\": (created || now).toISOString(),\n \"x-amz-meta-modified\": now.toISOString(),\n };\n\n if (metadata) {\n s3Metadata[\"x-amz-meta-custom\"] = JSON.stringify(metadata);\n }\n\n return s3Metadata;\n }\n\n async readFile(path: string): Promise<FileEntry> {\n const normalized = normalizePath(path);\n const startTime = Date.now();\n const operationId = `read-${normalized}-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"readFile\",\n path: normalized,\n id: operationId,\n });\n\n try {\n const s3Key = this.toS3Key(normalized);\n \n // First check if it's a directory\n const dirKey = s3Key.endsWith(\"/\") ? s3Key : s3Key + \"/\";\n try {\n const dirCheck = await this.client.send(new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: dirKey,\n }));\n if (dirCheck) {\n throw new Error(`EISDIR: illegal operation on a directory, read '${normalized}'`);\n }\n } catch (error: any) {\n if (error.name !== \"NoSuchKey\" && !error.message?.includes(\"EISDIR\")) {\n throw error;\n }\n }\n \n const response = await this.client.send(new GetObjectCommand({\n Bucket: this.config.bucket,\n Key: s3Key,\n }));\n\n if (!response.Body) {\n throw new Error(`File not found: ${normalized}`);\n }\n\n const parsedMetadata = this.parseS3Metadata(response.Metadata);\n\n // In lenient mode, infer type from key\n let type = parsedMetadata.type;\n if (!type && this.config.mode === \"lenient\") {\n type = s3Key.endsWith(\"/\") ? \"directory\" : \"file\";\n }\n\n // Check if trying to read a directory\n if (type === \"directory\" || s3Key.endsWith(\"/\")) {\n throw new Error(`EISDIR: illegal operation on a directory, read '${normalized}'`);\n }\n\n // Read content based on metadata\n let content: any;\n if (parsedMetadata.fileMetadata?.isBinary) {\n // Decode from base64 and return as ArrayBuffer\n const base64Content = await this.streamToString(response.Body);\n const buffer = Buffer.from(base64Content, 'base64');\n content = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);\n } else {\n content = await this.streamToString(response.Body);\n }\n\n const result: FileEntry = {\n path: normalized,\n name: basename(normalized),\n type: type || \"file\",\n size: response.ContentLength || 0,\n created: parsedMetadata.created || new Date(response.LastModified || Date.now()),\n modified: parsedMetadata.modified || new Date(response.LastModified || Date.now()),\n metadata: parsedMetadata.fileMetadata,\n content: content,\n };\n\n this.events.emit(FileSystemEvents.FILE_READ, {\n path: normalized,\n size: result.size || 0,\n });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"readFile\",\n path: normalized,\n id: operationId,\n duration: Date.now() - startTime,\n });\n\n return result;\n } catch (error: any) {\n if (error.name === \"NoSuchKey\") {\n const notFoundError = new Error(`ENOENT: no such file or directory, open '${normalized}'`);\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"readFile\",\n path: normalized,\n error: notFoundError,\n });\n throw notFoundError;\n }\n \n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"readFile\",\n path: normalized,\n error: error as Error,\n });\n throw error;\n }\n }\n\n async writeFile(\n path: string,\n content: any,\n metadata?: FileMetadata,\n ): Promise<FileEntry> {\n const normalized = normalizePath(path);\n const startTime = Date.now();\n const operationId = `write-${normalized}-${startTime}`;\n let contentStr: string;\n let isBinary = false;\n let originalSize: number;\n \n if (content === null || content === undefined) {\n contentStr = \"\";\n originalSize = 0;\n } else if (typeof content === \"string\") {\n contentStr = content;\n originalSize = Buffer.byteLength(contentStr);\n } else if (content instanceof ArrayBuffer) {\n contentStr = Buffer.from(content).toString('base64');\n isBinary = true;\n originalSize = content.byteLength; // Use original ArrayBuffer size\n } else {\n contentStr = JSON.stringify(content);\n originalSize = Buffer.byteLength(contentStr);\n }\n \n const size = originalSize;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"writeFile\",\n path: normalized,\n id: operationId,\n });\n this.events.emit(FileSystemEvents.FILE_WRITING, { path: normalized, size });\n\n try {\n // In strict mode, check if parent exists\n if (this.config.mode === \"strict\") {\n const parent = dirname(normalized);\n if (parent !== \"/\" && parent !== \".\") {\n const parentExists = await this.exists(parent);\n if (!parentExists) {\n throw new Error(`ENOENT: no such file or directory, open '${normalized}'`);\n }\n }\n }\n \n await this.ensureParentExists(normalized);\n\n const s3Key = this.toS3Key(normalized);\n \n // Check if file already exists to preserve created date\n let existingCreated: Date | undefined;\n try {\n const existing = await this.client.send(new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: s3Key,\n }));\n if (existing.Metadata?.[\"x-amz-meta-created\"]) {\n existingCreated = new Date(existing.Metadata[\"x-amz-meta-created\"]);\n }\n } catch {\n // File doesn't exist, which is fine\n }\n \n const finalMetadata = isBinary ? { ...metadata, isBinary: true } : metadata;\n const s3Metadata = this.createS3Metadata(\"file\", finalMetadata, existingCreated);\n\n await this.client.send(new PutObjectCommand({\n Bucket: this.config.bucket,\n Key: s3Key,\n Body: contentStr,\n ContentType: \"text/plain\",\n Metadata: s3Metadata,\n }));\n\n const now = new Date();\n const result: FileEntry = {\n path: normalized,\n name: basename(normalized),\n type: \"file\",\n size,\n created: existingCreated || new Date(s3Metadata[\"x-amz-meta-created\"]),\n modified: now,\n metadata: finalMetadata,\n content,\n };\n\n // Notify watchers\n this.notifyWatchers({\n type: existingCreated ? \"updated\" : \"created\",\n path: normalized,\n timestamp: now,\n });\n\n this.events.emit(FileSystemEvents.FILE_WRITTEN, {\n path: normalized,\n size,\n });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"writeFile\",\n path: normalized,\n id: operationId,\n duration: Date.now() - startTime,\n });\n\n return result;\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"writeFile\",\n path: normalized,\n error: error as Error,\n });\n throw error;\n }\n }\n\n async deleteFile(path: string): Promise<void> {\n const normalized = normalizePath(path);\n const startTime = Date.now();\n const operationId = `delete-${normalized}-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"deleteFile\",\n path: normalized,\n id: operationId,\n });\n this.events.emit(FileSystemEvents.FILE_DELETING, { path: normalized });\n\n try {\n // Check if file exists first\n const exists = await this.exists(normalized);\n if (!exists) {\n throw new Error(`ENOENT: no such file or directory, unlink '${normalized}'`);\n }\n \n const s3Key = this.toS3Key(normalized);\n \n // Check if it's a directory with contents\n if (await this.isDirectory(normalized)) {\n const entries = await this.readDir(normalized);\n if (entries.length > 0) {\n throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);\n }\n }\n \n await this.client.send(new DeleteObjectCommand({\n Bucket: this.config.bucket,\n Key: s3Key,\n }));\n\n // Notify watchers\n this.notifyWatchers({\n type: \"deleted\",\n path: normalized,\n timestamp: new Date(),\n });\n\n this.events.emit(FileSystemEvents.FILE_DELETED, { path: normalized });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"deleteFile\",\n path: normalized,\n id: operationId,\n duration: Date.now() - startTime,\n });\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"deleteFile\",\n path: normalized,\n error: error as Error,\n });\n throw error;\n }\n }\n\n async exists(path: string): Promise<boolean> {\n const normalized = normalizePath(path);\n \n try {\n const s3Key = this.toS3Key(normalized);\n \n // Check as file first\n try {\n await this.client.send(new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: s3Key,\n }));\n return true;\n } catch {\n // In lenient mode, also check if it's a prefix with contents\n if (this.config.mode === \"lenient\") {\n const prefix = s3Key.endsWith(\"/\") ? s3Key : s3Key + \"/\";\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.config.bucket,\n Prefix: prefix,\n MaxKeys: 1,\n }));\n return (response.Contents?.length || 0) > 0;\n }\n \n // In strict mode, check for directory marker\n const dirKey = s3Key.endsWith(\"/\") ? s3Key : s3Key + \"/\";\n try {\n await this.client.send(new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: dirKey,\n }));\n return true;\n } catch {\n return false;\n }\n }\n } catch {\n return false;\n }\n }\n\n async readDir(path: string): Promise<FileEntry[]> {\n const normalized = normalizePath(path);\n const startTime = Date.now();\n const operationId = `readdir-${normalized}-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"readDir\",\n path: normalized,\n id: operationId,\n });\n\n try {\n // Check if directory exists first (except root)\n if (normalized !== \"/\") {\n const exists = await this.exists(normalized);\n if (!exists) {\n throw new Error(`ENOENT: no such file or directory, scandir '${normalized}'`);\n }\n }\n \n const s3Prefix = this.toS3Key(normalized);\n const prefix = s3Prefix === \"\" ? \"\" : (s3Prefix.endsWith(\"/\") ? s3Prefix : s3Prefix + \"/\");\n \n const entries: FileEntry[] = [];\n const seenPaths = new Set<string>();\n let continuationToken: string | undefined;\n\n do {\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.config.bucket,\n Prefix: prefix,\n Delimiter: \"/\",\n ContinuationToken: continuationToken,\n }));\n\n // Add directories (CommonPrefixes)\n if (response.CommonPrefixes) {\n for (const commonPrefix of response.CommonPrefixes) {\n if (commonPrefix.Prefix) {\n const path = this.fromS3Key(commonPrefix.Prefix);\n if (!seenPaths.has(path)) {\n seenPaths.add(path);\n entries.push({\n path,\n name: basename(path.endsWith(\"/\") ? path.slice(0, -1) : path),\n type: \"directory\",\n size: 0,\n created: new Date(),\n modified: new Date(),\n });\n }\n }\n }\n }\n\n // Add files (Contents)\n if (response.Contents) {\n for (const object of response.Contents) {\n if (object.Key) {\n // Skip directory markers\n if (object.Key.endsWith(\"/\")) continue;\n \n // Skip objects not directly in this directory\n const relativePath = object.Key.substring(prefix.length);\n if (relativePath.includes(\"/\")) continue;\n\n const path = this.fromS3Key(object.Key);\n if (!seenPaths.has(path)) {\n seenPaths.add(path);\n entries.push({\n path,\n name: basename(path),\n type: \"file\",\n size: object.Size || 0,\n created: object.LastModified || new Date(),\n modified: object.LastModified || new Date(),\n });\n }\n }\n }\n }\n\n continuationToken = response.NextContinuationToken;\n } while (continuationToken);\n\n this.events.emit(FileSystemEvents.DIR_READ, {\n path: normalized,\n count: entries.length,\n });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"readDir\",\n path: normalized,\n id: operationId,\n duration: Date.now() - startTime,\n });\n\n return entries;\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"readDir\",\n path: normalized,\n error: error as Error,\n });\n throw error;\n }\n }\n\n async mkdir(path: string, recursive?: boolean): Promise<FileEntry> {\n const normalized = normalizePath(path);\n const startTime = Date.now();\n const operationId = `mkdir-${normalized}-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"mkdir\",\n path: normalized,\n id: operationId,\n });\n this.events.emit(FileSystemEvents.DIR_CREATING, { path: normalized, recursive: !!recursive });\n\n try {\n // Check if trying to create root\n if (normalized === \"/\") {\n throw new Error(`EEXIST: file already exists, mkdir '${normalized}'`);\n }\n \n // Check if directory already exists\n if (await this.exists(normalized)) {\n throw new Error(`EEXIST: file already exists, mkdir '${normalized}'`);\n }\n \n // In lenient mode, directories are virtual - just return success\n if (this.config.mode === \"lenient\") {\n const now = new Date();\n const result: FileEntry = {\n path: normalized,\n name: basename(normalized),\n type: \"directory\",\n size: 0,\n created: now,\n modified: now,\n };\n\n this.notifyWatchers({\n type: \"created\",\n path: normalized,\n timestamp: now,\n });\n\n this.events.emit(FileSystemEvents.DIR_CREATED, { path: normalized });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"mkdir\",\n path: normalized,\n id: operationId,\n duration: Date.now() - startTime,\n });\n\n return result;\n }\n\n // Strict mode - create directory markers\n if (recursive) {\n // Create all parent directories\n const parts = normalized.split(\"/\").filter(Boolean);\n let currentPath = \"\";\n \n for (const part of parts) {\n currentPath += \"/\" + part;\n const s3Key = this.toS3Key(currentPath);\n await this.ensureDirectoryMarker(s3Key);\n }\n } else {\n // Check if parent exists\n const parent = dirname(normalized);\n if (parent !== \"/\" && parent !== \".\") {\n const parentExists = await this.exists(parent);\n if (!parentExists) {\n throw new Error(`ENOENT: no such file or directory, mkdir '${normalized}'`);\n }\n }\n \n // Ensure parent exists\n await this.ensureParentExists(normalized);\n \n // Create directory marker\n const s3Key = this.toS3Key(normalized);\n await this.ensureDirectoryMarker(s3Key);\n }\n\n const now = new Date();\n const result: FileEntry = {\n path: normalized,\n name: basename(normalized),\n type: \"directory\",\n size: 0,\n created: now,\n modified: now,\n };\n\n // Notify watchers\n this.notifyWatchers({\n type: \"created\",\n path: normalized,\n timestamp: now,\n });\n\n this.events.emit(FileSystemEvents.DIR_CREATED, { path: normalized });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"mkdir\",\n path: normalized,\n id: operationId,\n duration: Date.now() - startTime,\n });\n\n return result;\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"mkdir\",\n path: normalized,\n error: error as Error,\n });\n throw error;\n }\n }\n\n async rmdir(path: string, recursive?: boolean): Promise<void> {\n const normalized = normalizePath(path);\n const startTime = Date.now();\n const operationId = `rmdir-${normalized}-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"rmdir\",\n path: normalized,\n id: operationId,\n });\n this.events.emit(FileSystemEvents.DIR_DELETING, { path: normalized, recursive: !!recursive });\n\n try {\n // Check if trying to remove root\n if (normalized === \"/\") {\n throw new Error(`EBUSY: resource busy or locked, rmdir '${normalized}'`);\n }\n \n // Check if directory exists\n const exists = await this.exists(normalized);\n if (!exists) {\n throw new Error(`ENOENT: no such file or directory, rmdir '${normalized}'`);\n }\n \n // Check if it's actually a file\n if (!await this.isDirectory(normalized)) {\n throw new Error(`ENOTDIR: not a directory, rmdir '${normalized}'`);\n }\n \n const s3Prefix = this.toS3Key(normalized);\n const prefix = s3Prefix.endsWith(\"/\") ? s3Prefix : s3Prefix + \"/\";\n\n if (recursive) {\n // Delete all objects with this prefix\n const objectsToDelete: { Key: string }[] = [];\n let continuationToken: string | undefined;\n\n do {\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.config.bucket,\n Prefix: prefix,\n ContinuationToken: continuationToken,\n }));\n\n if (response.Contents) {\n for (const object of response.Contents) {\n if (object.Key) {\n objectsToDelete.push({ Key: object.Key });\n }\n }\n }\n\n continuationToken = response.NextContinuationToken;\n } while (continuationToken);\n\n // Delete in batches (S3 allows max 1000 per request)\n const batchSize = 1000;\n for (let i = 0; i < objectsToDelete.length; i += batchSize) {\n const batch = objectsToDelete.slice(i, i + batchSize);\n await this.client.send(new DeleteObjectsCommand({\n Bucket: this.config.bucket,\n Delete: { Objects: batch },\n }));\n }\n } else {\n // Check if directory is empty\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.config.bucket,\n Prefix: prefix,\n MaxKeys: 2, // We only need to know if there's more than the dir marker\n }));\n\n const hasContents = (response.Contents?.length || 0) > 1 ||\n (response.Contents?.length === 1 && !response.Contents[0].Key?.endsWith(\"/\"));\n\n if (hasContents) {\n throw new Error(`ENOTEMPTY: directory not empty, rmdir '${normalized}'`);\n }\n\n // Delete directory marker\n await this.client.send(new DeleteObjectCommand({\n Bucket: this.config.bucket,\n Key: prefix,\n }));\n }\n\n // Notify watchers\n this.notifyWatchers({\n type: \"deleted\",\n path: normalized,\n timestamp: new Date(),\n });\n\n this.events.emit(FileSystemEvents.DIR_DELETED, { path: normalized });\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"rmdir\",\n path: normalized,\n id: operationId,\n duration: Date.now() - startTime,\n });\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"rmdir\",\n path: normalized,\n error: error as Error,\n });\n throw error;\n }\n }\n\n async rename(oldPath: string, newPath: string): Promise<FileEntry> {\n const normalizedOld = normalizePath(oldPath);\n const normalizedNew = normalizePath(newPath);\n const startTime = Date.now();\n const operationId = `rename-${normalizedOld}-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"rename\",\n path: normalizedOld,\n id: operationId,\n });\n\n try {\n // Check if source exists\n if (!await this.exists(normalizedOld)) {\n throw new Error(`ENOENT: no such file or directory, rename '${normalizedOld}' -> '${normalizedNew}'`);\n }\n \n // Check if target already exists\n if (await this.exists(normalizedNew)) {\n throw new Error(`EEXIST: file already exists, rename '${normalizedOld}' -> '${normalizedNew}'`);\n }\n \n // Check if new parent exists\n const newParent = dirname(normalizedNew);\n if (newParent !== \"/\" && newParent !== \".\" && !await this.exists(newParent)) {\n throw new Error(`ENOENT: no such file or directory, rename '${normalizedOld}' -> '${normalizedNew}'`);\n }\n \n // Read the old file/directory\n const oldEntry = await this.readFile(normalizedOld);\n \n if (oldEntry.type === \"file\") {\n // Copy and delete for files\n await this.writeFile(normalizedNew, oldEntry.content, oldEntry.metadata);\n await this.deleteFile(normalizedOld);\n } else {\n // For directories, we need to rename all contents\n await this.move([normalizedOld], dirname(normalizedNew));\n }\n\n const result = await this.readFile(normalizedNew);\n\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"rename\",\n path: normalizedOld,\n id: operationId,\n duration: Date.now() - startTime,\n });\n\n return result;\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"rename\",\n path: normalizedOld,\n error: error as Error,\n });\n throw error;\n }\n }\n\n async move(sourcePaths: string[], targetPath: string): Promise<void> {\n const normalizedTarget = normalizePath(targetPath);\n const startTime = Date.now();\n const operationId = `move-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"move\",\n path: sourcePaths[0],\n id: operationId,\n });\n\n try {\n // Check if target exists and is a directory\n if (!await this.exists(normalizedTarget)) {\n throw new Error(`ENOENT: no such file or directory, open '${normalizedTarget}'`);\n }\n \n if (!await this.isDirectory(normalizedTarget)) {\n throw new Error(`ENOTDIR: not a directory, open '${normalizedTarget}'`);\n }\n \n for (const sourcePath of sourcePaths) {\n const normalizedSource = normalizePath(sourcePath);\n const targetFilePath = join(normalizedTarget, basename(normalizedSource));\n \n // Read source\n const sourceEntry = await this.readFile(normalizedSource);\n \n // Write to target\n await this.writeFile(targetFilePath, sourceEntry.content, sourceEntry.metadata);\n \n // Delete source\n await this.deleteFile(normalizedSource);\n }\n\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"move\",\n path: sourcePaths[0],\n id: operationId,\n duration: Date.now() - startTime,\n });\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"move\",\n path: sourcePaths[0],\n error: error as Error,\n });\n throw error;\n }\n }\n\n async copy(sourcePath: string, targetPath: string): Promise<FileEntry> {\n const normalizedSource = normalizePath(sourcePath);\n const normalizedTarget = normalizePath(targetPath);\n const startTime = Date.now();\n const operationId = `copy-${normalizedSource}-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"copy\",\n path: normalizedSource,\n id: operationId,\n });\n\n try {\n // Check if source is a directory\n if (await this.isDirectory(normalizedSource)) {\n throw new Error(`EISDIR: illegal operation on a directory, open '${normalizedSource}'`);\n }\n \n // Read source\n const sourceEntry = await this.readFile(normalizedSource);\n \n // Write to target\n const result = await this.writeFile(\n normalizedTarget,\n sourceEntry.content,\n sourceEntry.metadata,\n );\n\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"copy\",\n path: normalizedSource,\n id: operationId,\n duration: Date.now() - startTime,\n });\n\n return result;\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"copy\",\n path: normalizedSource,\n error: error as Error,\n });\n throw error;\n }\n }\n\n watch(pattern: string, callback: (event: FSEvent) => void): Disposable {\n const id = Math.random().toString(36).substr(2, 9);\n const listener: WatchListener = { id, pattern, callback };\n this.watchers.push(listener);\n\n return {\n dispose: () => {\n const index = this.watchers.findIndex((w) => w.id === id);\n if (index !== -1) {\n this.watchers.splice(index, 1);\n }\n },\n };\n }\n\n private notifyWatchers(event: FSEvent): void {\n for (const watcher of this.watchers) {\n // Simple pattern matching (could be enhanced with glob)\n if (\n watcher.pattern === \"*\" ||\n event.path.includes(watcher.pattern) ||\n this.matchGlob(event.path, watcher.pattern)\n ) {\n watcher.callback(event);\n }\n }\n }\n\n private matchGlob(path: string, pattern: string): boolean {\n // Enhanced glob matching\n if (pattern === \"*\") {\n // Single * matches files and directories in root\n return path.split(\"/\").length === 2;\n }\n \n if (pattern === \"**\") {\n // ** matches everything\n return true;\n }\n \n // Convert glob to regex\n let regex = pattern\n .replace(/\\*\\*/g, \"___DOUBLE_STAR___\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/___DOUBLE_STAR___/g, \".*\")\n .replace(/\\?/g, \".\")\n .replace(/\\//g, \"\\\\/\");\n \n return new RegExp(\"^\" + regex + \"$\").test(path);\n }\n\n private async isDirectory(path: string): Promise<boolean> {\n const normalized = normalizePath(path);\n const s3Key = this.toS3Key(normalized);\n const dirKey = s3Key.endsWith(\"/\") ? s3Key : s3Key + \"/\";\n \n try {\n const response = await this.client.send(new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: dirKey,\n }));\n return response.Metadata?.[\"x-amz-meta-type\"] === \"directory\";\n } catch {\n // Check if any objects exist with this prefix\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.config.bucket,\n Prefix: dirKey,\n MaxKeys: 1,\n }));\n return (response.Contents?.length || 0) > 0;\n }\n }\n\n async stat(path: string): Promise<FileStat> {\n const normalized = normalizePath(path);\n \n try {\n // Check if it's a directory first\n if (await this.isDirectory(normalized)) {\n const s3Key = this.toS3Key(normalized);\n const dirKey = s3Key.endsWith(\"/\") ? s3Key : s3Key + \"/\";\n \n try {\n const response = await this.client.send(new HeadObjectCommand({\n Bucket: this.config.bucket,\n Key: dirKey,\n }));\n \n const metadata = this.parseS3Metadata(response.Metadata);\n return {\n path: normalized,\n size: 0,\n type: \"directory\",\n created: metadata.created || new Date(response.LastModified || Date.now()),\n modified: metadata.modified || new Date(response.LastModified || Date.now()),\n };\n } catch {\n // Directory exists but no marker\n return {\n path: normalized,\n size: 0,\n type: \"directory\",\n created: new Date(),\n modified: new Date(),\n };\n }\n }\n \n // It's a file\n const entry = await this.readFile(normalized);\n \n return {\n path: entry.path,\n size: entry.size || 0,\n type: entry.type,\n created: entry.created || new Date(),\n modified: entry.modified || new Date(),\n };\n } catch (error: any) {\n if (error.message?.includes(\"EISDIR\")) {\n // Handle the case where readFile throws EISDIR\n return {\n path: normalized,\n size: 0,\n type: \"directory\",\n created: new Date(),\n modified: new Date(),\n };\n }\n throw new Error(`Failed to stat ${normalized}: ${error}`);\n }\n }\n\n async glob(pattern: string): Promise<string[]> {\n const results: string[] = [];\n const seenPaths = new Set<string>();\n \n // Convert glob pattern to prefix for S3\n const prefix = pattern.split(\"*\")[0].replace(/^\\//, \"\");\n const s3Prefix = this.config.prefix === \"/\" ? prefix : this.config.prefix + prefix;\n \n let continuationToken: string | undefined;\n \n do {\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.config.bucket,\n Prefix: s3Prefix,\n ContinuationToken: continuationToken,\n }));\n\n // Add files\n if (response.Contents) {\n for (const object of response.Contents) {\n if (object.Key) {\n const path = this.fromS3Key(object.Key);\n \n // Add the file itself if it matches (skip directory markers)\n if (!object.Key.endsWith(\"/\") && this.matchGlob(path, pattern)) {\n seenPaths.add(path);\n }\n \n // Also check parent directories for patterns like \"*\"\n if (pattern === \"*\" || pattern === \"**\") {\n const parts = path.split(\"/\").filter(Boolean);\n for (let i = 1; i <= parts.length - 1; i++) {\n const dirPath = \"/\" + parts.slice(0, i).join(\"/\");\n if (this.matchGlob(dirPath, pattern)) {\n seenPaths.add(dirPath);\n }\n }\n }\n }\n }\n }\n\n continuationToken = response.NextContinuationToken;\n } while (continuationToken);\n\n return Array.from(seenPaths).sort();\n }\n\n async clear(): Promise<void> {\n const startTime = Date.now();\n const operationId = `clear-${startTime}`;\n\n this.events.emit(FileSystemEvents.OPERATION_START, {\n operation: \"clear\",\n path: \"/\",\n id: operationId,\n });\n\n try {\n // List and delete all objects\n const objectsToDelete: { Key: string }[] = [];\n let continuationToken: string | undefined;\n\n do {\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.config.bucket,\n Prefix: this.config.prefix === \"/\" ? \"\" : this.config.prefix,\n ContinuationToken: continuationToken,\n }));\n\n if (response.Contents) {\n for (const object of response.Contents) {\n if (object.Key) {\n objectsToDelete.push({ Key: object.Key });\n }\n }\n }\n\n continuationToken = response.NextContinuationToken;\n } while (continuationToken);\n\n // Delete in batches\n const batchSize = 1000;\n for (let i = 0; i < objectsToDelete.length; i += batchSize) {\n const batch = objectsToDelete.slice(i, i + batchSize);\n await this.client.send(new DeleteObjectsCommand({\n Bucket: this.config.bucket,\n Delete: { Objects: batch },\n }));\n }\n\n this.events.emit(FileSystemEvents.OPERATION_END, {\n operation: \"clear\",\n path: \"/\",\n id: operationId,\n duration: Date.now() - startTime,\n });\n } catch (error) {\n this.events.emit(FileSystemEvents.OPERATION_ERROR, {\n operation: \"clear\",\n path: \"/\",\n error: error as Error,\n });\n throw error;\n }\n }\n\n async size(): Promise<number> {\n let totalSize = 0;\n let continuationToken: string | undefined;\n\n do {\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.config.bucket,\n Prefix: this.config.prefix === \"/\" ? \"\" : this.config.prefix,\n ContinuationToken: continuationToken,\n }));\n\n if (response.Contents) {\n for (const object of response.Contents) {\n totalSize += object.Size || 0;\n }\n }\n\n continuationToken = response.NextContinuationToken;\n } while (continuationToken);\n\n return totalSize;\n }\n\n // Implementações temporárias para compilar - S3 será implementado no futuro\n async canModify(path: string): Promise<boolean> {\n // TODO: Implementar verificação real de permissões S3\n return true;\n }\n\n async canCreateIn(parentPath: string): Promise<boolean> {\n // TODO: Implementar verificação real de permissões S3\n return true;\n }\n}"],"mappings":"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