@firesystem/s3
Version:
AWS S3 implementation of Virtual File System
1,020 lines (1,018 loc) • 35.9 kB
JavaScript
"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