@flystorage/azure-storage-blob
Version:
<img src="https://raw.githubusercontent.com/duna-oss/flystorage/main/flystorage.svg" width="50px" height="50px" />
239 lines (238 loc) • 8.63 kB
JavaScript
import { ChecksumIsNotAvailable, normalizeExpiryToDate, PathPrefixer, } from '@flystorage/file-storage';
import { BlobSASPermissions, } from '@azure/storage-blob';
import { resolveMimeType } from '@flystorage/stream-mime-type';
import { dirname } from 'node:path';
export class AzureStorageBlobStorageAdapter {
container;
options;
prefixer;
constructor(container, options = {}) {
this.container = container;
this.options = options;
this.prefixer = new PathPrefixer(options.prefix || '');
}
async copyFile(from, to, options) {
const fromUrl = this.blockClient(from).url;
await this.blockClient(to).syncCopyFromURL(fromUrl);
}
async moveFile(from, to, options) {
await this.copyFile(from, to, options);
await this.deleteFile(from);
}
async write(path, contents, options) {
let mimeType = options.mimeType;
let stream = contents;
if (mimeType === undefined) {
[mimeType, stream] = await this.resolveMimetype(path, contents, options);
}
const blob = this.blockClient(path);
await blob.uploadStream(stream, options.size, this.options.uploadMaxConcurrency, {
blobHTTPHeaders: {
blobContentType: mimeType,
blobCacheControl: options.cacheControl
},
});
}
blockClient(path) {
return this.container.getBlockBlobClient(this.prefixer.prefixFilePath(path));
}
async read(path) {
const blob = this.blockClient(path);
const response = await blob.download();
if (!response.readableStreamBody) {
throw new Error('No readable stream body in response.');
}
return response.readableStreamBody;
}
async deleteFile(path) {
const blob = this.blockClient(path);
await blob.deleteIfExists();
}
async createDirectory() {
// no-op, directories do not exist.
}
async stat(path) {
const blob = this.blockClient(path);
const properties = await blob.getProperties();
return this.mapToStatEntry(path, properties);
}
mapToStatEntry(path, properties) {
return {
type: 'file',
isFile: true,
isDirectory: false,
path,
mimeType: properties.contentType,
size: properties.contentLength,
lastModifiedMs: properties.lastModified?.getTime(),
};
}
list(path, options) {
return options.deep
? this.listDeep(path, options)
: this.listShallow(path, options);
}
async *listDeep(path, options) {
const directories = new Set();
const listing = this.container.listBlobsFlat({
prefix: this.prefixer.prefixDirectoryPath(path),
});
const listedPath = path;
for await (const item of listing) {
const path = this.prefixer.stripFilePath(item.name);
let parentDir = dirname(path);
while (!['.', '', listedPath].includes(parentDir)) {
if (directories.has(parentDir)) {
break;
}
yield {
type: 'directory',
isFile: false,
isDirectory: true,
path: parentDir,
};
directories.add(parentDir);
parentDir = dirname(parentDir);
}
yield this.mapToStatEntry(path, item.properties);
}
}
async *listShallow(path, options) {
const listing = this.container.listBlobsByHierarchy('/', {
prefix: this.prefixer.prefixDirectoryPath(path),
});
for await (const item of listing) {
if (item.kind === 'blob') {
yield this.mapToStatEntry(this.prefixer.stripFilePath(item.name), item.properties);
}
else {
yield {
path: this.prefixer.stripDirectoryPath(item.name),
type: 'directory',
isFile: false,
isDirectory: true,
};
}
}
}
async changeVisibility(path, visibility) {
if (this.options.ignoreVisibility !== true) {
throw new Error('Not supported by this adapter');
}
}
async visibility(path) {
if (this.options.ignoreVisibility !== true) {
throw new Error('Not implemented');
}
// default to indicating it ss public because we cannot know if the default is private
return this.options.ignoredVisibilityResponse ?? 'public';
}
async deleteDirectory(path) {
let deletes = [];
const batchSize = this.options.deleteDirBatchSize ?? 10;
for await (const item of this.list(path, { deep: true })) {
if (item.isFile) {
deletes.push(this.deleteFile(item.path));
}
if (deletes.length >= batchSize) {
await Promise.all(deletes);
deletes = [];
}
}
await Promise.all(deletes);
}
async fileExists(path) {
return await this.blockClient(path).exists();
}
async directoryExists(path) {
const listing = this.container.listBlobsFlat({
prefix: this.prefixer.prefixDirectoryPath(path),
}).byPage({
maxPageSize: 1,
});
return (await listing.next()).value.segment.blobItems.length > 0;
}
async publicUrl(path, options) {
return this.blockClient(path).url;
}
async temporaryUrl(path, options) {
return await this.blockClient(path).generateSasUrl({
expiresOn: normalizeExpiryToDate(options.expiresAt),
permissions: BlobSASPermissions.parse('r'),
...(this.options.temporaryUrlOptions ?? {}),
});
}
async prepareUpload(path, options) {
const headers = {};
headers['x-ms-blob-type'] = options['x-ms-blob-type'] ?? 'BlockBlob';
const config = {
expiresOn: normalizeExpiryToDate(options.expiresAt),
permissions: BlobSASPermissions.parse('w'),
...(this.options.temporaryUrlOptions ?? {}),
};
const contentType = options['Content-Type'] ?? options.contentType;
if (typeof contentType === 'string') {
config.contentType = contentType;
headers['Content-Type'] = contentType;
}
const url = await this.blockClient(path).generateSasUrl(config);
return { method: 'PUT', provider: 'azure-storage-blob', url, headers };
}
async checksum(path, options) {
const algo = options.algo ?? 'etag';
if (algo !== 'etag') {
throw ChecksumIsNotAvailable.checksumNotSupported(algo);
}
const blob = this.blockClient(path);
const properties = await blob.getProperties();
const etag = properties.etag;
if (etag === undefined) {
throw new Error('Etag is not defined on blob properties.');
}
return etag;
}
async mimeType(path, options) {
const stat = await this.stat(path);
if (stat.isDirectory) {
throw new Error('Path is not a file. No mimetype available.');
}
if (stat.mimeType === undefined) {
throw new Error('Mime-type not found for file.');
}
return stat.mimeType;
}
async lastModified(path) {
const stat = await this.stat(path);
if (stat.isDirectory) {
throw new Error('Path is not a file. No last modified available.');
}
if (stat.lastModifiedMs === undefined) {
throw new Error('Last modified not found for file.');
}
return stat.lastModifiedMs;
}
async fileSize(path) {
const stat = await this.stat(path);
if (stat.isDirectory) {
throw new Error('Path is not a file. No file size available.');
}
if (stat.size === undefined) {
throw new Error('File size not found for file.');
}
return stat.size;
}
async resolveMimetype(path, contents, options) {
if (options.mimeType) {
return [options.mimeType, contents];
}
const [mimeType, stream] = await resolveMimeType(path, contents);
return [mimeType ?? 'application/octet-stream', stream];
}
}
/**
* BC export
*
* @deprecated
*/
export class AzureStorageBlobFileStorage extends AzureStorageBlobStorageAdapter {
}