UNPKG

@synet/fs-azure

Version:

Azure filesystem abstraction following FileSystem pattern

387 lines (386 loc) 14.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AzureBlobStorageFileSystem = void 0; exports.createAzureBlobStorageFileSystem = createAzureBlobStorageFileSystem; const storage_blob_1 = require("@azure/storage-blob"); /** * Azure Blob Storage-based async filesystem implementation * * Provides filesystem operations on Azure Blob Storage. * Each file operation corresponds to Azure blob operations. * Directories are handled virtually through blob name prefixes. */ class AzureBlobStorageFileSystem { constructor(options) { this.cache = new Map(); // Set defaults this.options = { containerName: options.containerName, prefix: options.prefix || "", connectionString: options.connectionString, accountName: options.accountName, accountKey: options.accountKey, blobServiceEndpoint: options.blobServiceEndpoint, }; // Initialize Azure Blob Service Client if (options.connectionString) { this.blobServiceClient = storage_blob_1.BlobServiceClient.fromConnectionString(options.connectionString); } else if (options.accountName && options.accountKey) { const url = `https://${options.accountName}.blob.core.windows.net`; const sharedKeyCredential = new storage_blob_1.StorageSharedKeyCredential(options.accountName, options.accountKey); this.blobServiceClient = new storage_blob_1.BlobServiceClient(url, sharedKeyCredential); } else { throw new Error("Either connectionString or accountName/accountKey must be provided"); } this.containerClient = this.blobServiceClient.getContainerClient(options.containerName); } /** * Get Azure blob name from filesystem path */ getAzureBlobName(path) { const cleanPath = path.replace(/^\/+/, ""); if (this.options.prefix) { const cleanPrefix = this.options.prefix .replace(/^\/+/, "") .replace(/\/+$/, ""); return cleanPrefix ? `${cleanPrefix}/${cleanPath}` : cleanPath; } return cleanPath; } /** * Get filesystem path from Azure blob name */ getFilesystemPath(blobName) { if (this.options.prefix) { const cleanPrefix = this.options.prefix .replace(/^\/+/, "") .replace(/\/+$/, ""); if (cleanPrefix && blobName.startsWith(`${cleanPrefix}/`)) { return blobName.substring(cleanPrefix.length + 1); } } return blobName; } /** * Check if a file exists in Azure Blob Storage */ async exists(path) { try { const blobName = this.getAzureBlobName(path); // Check cache first if (this.cache.has(blobName)) { return true; } // Check Azure Blob Storage const blobClient = this.containerClient.getBlobClient(blobName); const exists = await blobClient.exists(); if (exists) { // Try to get metadata to cache it try { const properties = await blobClient.getProperties(); this.cache.set(blobName, { size: properties.contentLength || 0, lastModified: properties.lastModified || new Date(), etag: properties.etag || "", }); } catch { // If metadata fails, just return true for existence } return true; } return false; } catch (error) { if (this.isNotFoundError(error)) { return false; } throw new Error(`[AzureBlobStorageFileSystem] Failed to check file existence for ${path}: ${error}`); } } /** * Read a file from Azure Blob Storage */ async readFile(path) { try { const blobName = this.getAzureBlobName(path); // Check cache first const cached = this.cache.get(blobName); if (cached?.content) { return cached.content; } // Download from Azure Blob Storage const blobClient = this.containerClient.getBlobClient(blobName); const downloadResponse = await blobClient.download(); if (!downloadResponse.readableStreamBody) { throw new Error(`[AzureBlobStorageFileSystem] No content found for file: ${path}`); } // Convert stream to string const chunks = []; for await (const chunk of downloadResponse.readableStreamBody) { chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)); } const content = Buffer.concat(chunks).toString("utf-8"); // Cache the content and metadata this.cache.set(blobName, { content, size: content.length, lastModified: downloadResponse.lastModified || new Date(), etag: downloadResponse.etag || "", }); return content; } catch (error) { if (this.isNotFoundError(error)) { throw new Error(`[AzureBlobStorageFileSystem] File not found: ${path}`); } throw new Error(`[AzureBlobStorageFileSystem] Failed to read file ${path}: ${error}`); } } /** * Write a file to Azure Blob Storage */ async writeFile(path, data) { try { const blobName = this.getAzureBlobName(path); const blockBlobClient = this.containerClient.getBlockBlobClient(blobName); const buffer = Buffer.from(data, "utf-8"); await blockBlobClient.upload(buffer, buffer.length, { blobHTTPHeaders: { blobContentType: this.getContentType(path), }, }); // Update cache this.cache.set(blobName, { content: data, size: buffer.length, lastModified: new Date(), etag: "", // Will be updated on next read }); } catch (error) { throw new Error(`[AzureBlobStorageFileSystem] Failed to write file ${path}: ${error}`); } } /** * Delete a file from Azure Blob Storage */ async deleteFile(path) { try { const blobName = this.getAzureBlobName(path); const blobClient = this.containerClient.getBlobClient(blobName); await blobClient.delete(); // Remove from cache this.cache.delete(blobName); } catch (error) { if (this.isNotFoundError(error)) { // File doesn't exist, consider deletion successful return; } throw new Error(`[AzureBlobStorageFileSystem] Failed to delete file ${path}: ${error}`); } } /** * List directory contents */ async readDir(dirPath) { try { const prefix = this.getAzureBlobName(dirPath); const normalizedPrefix = prefix ? prefix.endsWith("/") ? prefix : `${prefix}/` : ""; const items = new Set(); // List all blobs with the prefix for await (const blob of this.containerClient.listBlobsFlat({ prefix: normalizedPrefix, })) { const relativePath = this.getFilesystemPath(blob.name); if (!relativePath || relativePath === dirPath) { continue; } // Remove the directory prefix to get relative path let itemPath = relativePath; if (dirPath && dirPath !== "." && dirPath !== "/") { const dirPrefix = dirPath.endsWith("/") ? dirPath : `${dirPath}/`; if (relativePath.startsWith(dirPrefix)) { itemPath = relativePath.substring(dirPrefix.length); } } if (!itemPath) continue; // Extract the immediate child (file or subdirectory) const pathParts = itemPath.split("/"); if (pathParts.length === 1) { // Direct file items.add(pathParts[0]); } else if (pathParts.length > 1) { // Subdirectory - add directory name with trailing slash items.add(`${pathParts[0]}/`); } } return Array.from(items).sort(); } catch (error) { throw new Error(`[AzureBlobStorageFileSystem] Failed to read directory ${dirPath}: ${error}`); } } /** * Ensure directory exists (no-op for blob storage) */ async ensureDir(dirPath) { // Azure Blob Storage doesn't have directories, so this is a no-op // Directories are created implicitly when files are created } /** * Delete directory and all its contents */ async deleteDir(dirPath) { try { const prefix = this.getAzureBlobName(dirPath); const normalizedPrefix = prefix ? prefix.endsWith("/") ? prefix : `${prefix}/` : ""; const deletePromises = []; for await (const blob of this.containerClient.listBlobsFlat({ prefix: normalizedPrefix, })) { const blobClient = this.containerClient.getBlobClient(blob.name); deletePromises.push(blobClient .delete() .then(() => { }) .catch(() => { })); // Remove from cache this.cache.delete(blob.name); } await Promise.all(deletePromises); } catch (error) { throw new Error(`[AzureBlobStorageFileSystem] Failed to delete directory ${dirPath}: ${error}`); } } /** * Change file permissions (no-op for blob storage) */ async chmod(path, mode) { // Azure Blob Storage doesn't support file permissions // This is a no-op for compatibility } /** * Get file statistics */ async stat(path) { try { const blobName = this.getAzureBlobName(path); // Check cache first const cached = this.cache.get(blobName); if (cached) { return { size: cached.size, mtime: cached.lastModified, ctime: cached.lastModified, atime: cached.lastModified, mode: 0o644, isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false, }; } // Get from Azure Blob Storage const blobClient = this.containerClient.getBlobClient(blobName); const properties = await blobClient.getProperties(); const stats = { size: properties.contentLength || 0, mtime: properties.lastModified || new Date(), ctime: properties.lastModified || new Date(), atime: properties.lastModified || new Date(), mode: 0o644, isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false, }; // Cache the metadata this.cache.set(blobName, { size: stats.size, lastModified: stats.mtime, etag: properties.etag || "", }); return stats; } catch (error) { if (this.isNotFoundError(error)) { throw new Error(`[AzureBlobStorageFileSystem] File not found: ${path}`); } throw new Error(`[AzureBlobStorageFileSystem] Failed to get file stats for ${path}: ${error}`); } } /** * Clear the internal cache */ clearCache() { this.cache.clear(); } /** * Get container information */ getContainerInfo() { return { containerName: this.options.containerName, prefix: this.options.prefix, hasConnectionString: !!this.options.connectionString, hasAccountCredentials: !!(this.options.accountName && this.options.accountKey), blobServiceEndpoint: this.options.blobServiceEndpoint, }; } /** * Determine content type based on file extension */ getContentType(filename) { const ext = filename.split(".").pop()?.toLowerCase(); const contentTypes = { json: "application/json", txt: "text/plain", md: "text/markdown", html: "text/html", css: "text/css", js: "application/javascript", ts: "application/typescript", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", pdf: "application/pdf", zip: "application/zip", }; return contentTypes[ext || ""] || "application/octet-stream"; } /** * Check if error is a "not found" error */ isNotFoundError(error) { if (typeof error === "object" && error !== null) { const err = error; return (err.statusCode === 404 || err.code === "BlobNotFound" || err.code === "ContainerNotFound" || (err.message?.includes("BlobNotFound") ?? false)); } return false; } } exports.AzureBlobStorageFileSystem = AzureBlobStorageFileSystem; /** * Create a new Azure Blob Storage filesystem instance * @param options Azure Blob Storage configuration */ function createAzureBlobStorageFileSystem(options) { return new AzureBlobStorageFileSystem(options); }