@jsvfs/adapter-azure-blob
Version:
An adapter for Azure Storage Blobs.
231 lines • 10.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AzureBlobAdapter = void 0;
const extras_1 = require("@jsvfs/extras");
const storage_blob_1 = require("@azure/storage-blob");
const helpers_1 = require("./helpers");
/** An adapter for Azure Storage Blobs. */
class AzureBlobAdapter {
/** Creates an instance of Azure blob adapter. */
constructor(opts) {
const { access } = opts;
if (typeof access === 'undefined')
throw new Error("Option 'access' cannot be undefined.");
if ('connectionString' in access) {
this.blobService = storage_blob_1.BlobServiceClient.fromConnectionString(access.connectionString);
}
else if ('storageAccount' in access &&
'storageKey' in access) {
this.blobService = new storage_blob_1.BlobServiceClient(`https://${access.storageAccount}.blob.core.windows.net`, new storage_blob_1.StorageSharedKeyCredential(access.storageAccount, access.storageKey));
}
else {
throw new Error("Option 'access' does not contain either a connection string or an account and key.");
}
if (helpers_1.isContainerName(opts.container)) {
this.root = opts.container;
}
else {
this.root = '/';
}
this.containerCache = new Map();
this.include = new extras_1.Matcher(Array.isArray(opts.include) ? opts.include : []);
this.flushEnabled = opts.flushEnabled ?? false;
this.createIfNotExist = opts.createIfNotExist ?? false;
this.handle = 'azure-blob';
this.journal = new extras_1.Journal();
}
/** Returns true if root is the storage account. */
get isGlobal() {
return this.root === '/';
}
/** Snapshot of the underlying file system; an asynchronous iterable which returns an entry of path and data.
* @returns {AsyncGenerator<[string, SnapshotEntry]>} The asynchronous iterable to get the snapshot.
*/
async *snapshot() {
for await (const [name, client] of this.listContainers('snapshot')) {
try {
for await (const blobItem of client.listBlobsFlat()) {
const contents = await this.readBlob(client, blobItem.name, 'snapshot');
const snapshotName = this.isGlobal
? '/' + name + '/' + blobItem.name
: '/' + blobItem.name;
if (this.include.match(snapshotName)) {
yield [snapshotName, { type: 'file', contents }];
}
}
}
catch (error) {
this.journal.push({
level: 'error',
message: `Could not list blobs in container '${name}'.`,
op: 'snapshot',
error
});
}
}
}
/** Read a file from persistent storage. */
async read(path) {
const parsed = helpers_1.parse(path, this.root);
try {
return await this.readBlob(parsed.container, parsed.blobName, 'read');
}
catch (error) {
this.journal.push({
level: 'error',
message: `Could not get contents of blob '${parsed.blobName}' in container '${parsed.container}'.`,
op: 'read',
error
});
return Buffer.alloc(0);
}
}
/** Create a file or write the contents of a file to persistent storage. */
async write(path, contents = Buffer.alloc(0)) {
const parsed = helpers_1.parse(path, this.root);
const container = await this.getContainer(parsed.container, 'write');
const blobClient = container.getBlockBlobClient(parsed.blobName);
try {
await blobClient.uploadData(contents);
}
catch (error) {
this.journal.push({
level: 'error',
message: `Could not upload blob '${parsed.blobName}' to container '${parsed.container}'.`,
op: 'write',
error
});
}
}
/** Make a directory or directory tree in persistent storage. Technically unsupported by Microsoft, as 'directories' are virtual. */
async mkdir(path) { }
/** Create a link in persistent storage. Definitely unsupported by Microsoft, so we copy the file contents from an existing blob. */
async link(linkPath, linkTarget, type) {
const parsedPath = helpers_1.parse(linkPath, this.root);
const parsedTarget = helpers_1.parse(linkTarget, this.root);
const containerFrom = await this.getContainer(parsedTarget.container, 'link');
const containerTo = await this.getContainer(parsedPath.container, 'link');
const blobFrom = containerFrom.getBlockBlobClient(parsedTarget.blobName);
const blobTo = containerTo.getBlockBlobClient(parsedPath.blobName);
try {
await blobTo.syncCopyFromURL(blobFrom.url);
}
catch (error) {
this.journal.push({
level: 'error',
message: `Could not copy blob to '${parsedPath.blobName}' in container '${parsedPath.container}' from '${parsedTarget.blobName}' in container '${parsedTarget.container}'.`,
op: 'link',
error
});
}
}
/** Remove items from persistent storage. */
async remove(path, type) {
const parsed = helpers_1.parse(path, this.root);
const container = await this.getContainer(parsed.container, 'remove');
const blobClient = container.getBlockBlobClient(parsed.blobName);
switch (type) {
case 'file':
case 'hardlink':
case 'softlink':
try {
await blobClient.deleteIfExists();
}
catch (error) {
this.journal.push({
level: 'error',
message: `Could not delete blob '${parsed.blobName}' from container '${parsed.container}'.`,
op: 'remove',
error
});
}
}
}
/** Flush the underlying file system to prepare for a commit. */
async flush() {
if (this.flushEnabled) {
for await (const [name, client] of this.listContainers('flush')) {
for await (const blobItem of client.listBlobsFlat()) {
const snapshotName = this.isGlobal
? '/' + name + '/' + blobItem.name
: '/' + blobItem.name;
if (this.include.match(snapshotName)) {
const blobClient = client.getBlockBlobClient(blobItem.name);
try {
await blobClient.deleteIfExists();
}
catch (error) {
this.journal.push({
level: 'error',
message: `Could not delete blob '${blobItem.name}' from container '${name}'.`,
op: 'flush',
error
});
}
}
}
}
}
}
/** Reads a blob from blob storage. */
async readBlob(container, blobName, op) {
const containerClient = typeof container === 'string' ? await this.getContainer(container, 'snapshot') : container;
const blobClient = containerClient.getBlockBlobClient(blobName);
try {
return await blobClient.downloadToBuffer();
}
catch (error) {
this.journal.push({
level: 'error',
message: `Could not read blob '${blobName}' from container '${containerClient.containerName}'.`,
op,
error
});
return Buffer.alloc(0);
}
}
/** Get or initialize the given container by name. */
async getContainer(name, op, exists = false) {
let containerClient = this.containerCache.get(name);
if (typeof containerClient === 'undefined') {
containerClient = this.blobService.getContainerClient(name);
if (!exists && this.createIfNotExist) {
try {
await containerClient.createIfNotExists();
}
catch (error) {
this.journal.push({
level: 'error',
message: `Could not create blob container '${name}'.`,
op,
error
});
}
}
this.containerCache.set(name, containerClient);
}
return containerClient;
}
/** List the containers for this instance and optionally cache them. */
async *listContainers(op) {
if (this.containerCache.size === 0) {
if (this.isGlobal) {
for await (const containerItem of this.blobService.listContainers()) {
if (typeof containerItem.deleted === 'undefined' || !containerItem.deleted) {
yield [containerItem.name, await this.getContainer(containerItem.name, op, true)];
}
}
}
else {
yield [this.root, await this.getContainer(this.root, op)];
}
}
else {
for (const item of this.containerCache.entries()) {
yield item;
}
}
}
}
exports.AzureBlobAdapter = AzureBlobAdapter;
//# sourceMappingURL=AzureBlobAdapter.js.map