'use strict'; const fs = require('fs'); const path = require('path'); const {BlobServiceClient, StorageSharedKeyCredential, generateBlobSASQueryParameters, BlobSASPermissions} = require('@azure/storage-blob'); const mime = require('mime'); const config = require('config'); const utils = require('../utils'); const ms = require('ms'); const commonDefines = require('../commondefines'); const cfgExpSessionAbsolute = ms(config.get('services.CoAuthoring.expire.sessionabsolute')); const cfgCacheStorage = config.get('storage'); const MAX_DELETE_OBJECTS = 1000; const blobServiceClients = {}; /** * Gets or creates a BlobServiceClient for the given storage configuration. * * @param {Object} storageCfg - configuration object from default.json * @returns {BlobServiceClient} The Azure Blob Service client */ function getBlobServiceClient(storageCfg) { const configKey = `${storageCfg.accessKeyId}_${storageCfg.bucketName}`; if (!blobServiceClients[configKey]) { const credential = new StorageSharedKeyCredential(storageCfg.accessKeyId, storageCfg.secretAccessKey); if (storageCfg.endpoint.includes(storageCfg.accessKeyId)) { blobServiceClients[configKey] = new BlobServiceClient(storageCfg.endpoint, credential); } else { const endpointUrl = new URL(storageCfg.endpoint.replace(/\/+$/, '')); blobServiceClients[configKey] = new BlobServiceClient(`${endpointUrl.protocol}//${storageCfg.accessKeyId}.${endpointUrl.host}`, credential); } } return blobServiceClients[configKey]; } /** * Gets a ContainerClient for the specified storage configuration. * * @param {Object} storageCfg - configuration object from default.json * @returns {ContainerClient} The Azure Container client */ function getContainerClient(storageCfg) { const blobServiceClient = getBlobServiceClient(storageCfg); return blobServiceClient.getContainerClient(storageCfg.bucketName); } /** * Gets a BlockBlobClient for the specified storage configuration and blob name. * * @param {Object} storageCfg - configuration object from default.json * @param {string} blobName - The name of the blob * @returns {BlockBlobClient} The Azure Block Blob client */ function getBlobClient(storageCfg, blobName) { const containerClient = getContainerClient(storageCfg); return containerClient.getBlockBlobClient(blobName); } /** * Constructs a full file path by combining the storage folder name and the path. * * @param {Object} storageCfg - configuration object from default.json * @param {string} strPath - The relative path of the file * @returns {string} The full file path */ function getFilePath(storageCfg, strPath) { const storageFolderName = storageCfg.storageFolderName; return `${storageFolderName}/${strPath}`; } /** * @param {Object} baseOptions - Base options object * @param {Object} storageCfg - Storage configuration * @param {string} commandType - uploadData, uploadStream, download, etc. * @returns {Object|undefined} Merged options or undefined if empty */ function applyCommandOptions(baseOptions, storageCfg, commandType) { if (storageCfg.commandOptions.az && storageCfg.commandOptions.az[commandType]) { const configOptions = storageCfg.commandOptions.az[commandType]; if (configOptions && Object.keys(configOptions).length > 0) { return {...baseOptions, ...configOptions}; } } return Object.keys(baseOptions).length > 0 ? baseOptions : undefined; } async function listObjectsExec(storageCfg, prefix, output = []) { const containerClient = getContainerClient(storageCfg); const storageFolderName = storageCfg.storageFolderName; const prefixWithFolder = storageFolderName ? `${storageFolderName}/${prefix}` : prefix; const baseOptions = {prefix: prefixWithFolder}; const listOptions = applyCommandOptions(baseOptions, storageCfg, 'listBlobsFlat'); for await (const blob of containerClient.listBlobsFlat(listOptions)) { const relativePath = storageFolderName ? blob.name.substring(storageFolderName.length + 1) : blob.name; output.push(relativePath); } return output; } async function deleteObjectsHelp(storageCfg, aKeys) { const containerClient = getContainerClient(storageCfg); const deleteOptions = applyCommandOptions({}, storageCfg, 'deleteBlob'); await Promise.all( aKeys.map(key => { return containerClient.deleteBlob(key.Key, deleteOptions); }) ); } async function headObject(storageCfg, strPath) { const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath)); const properties = await blobClient.getProperties(); return {ContentLength: properties.contentLength}; } async function getObject(storageCfg, strPath) { const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath)); const options = applyCommandOptions({}, storageCfg, 'download'); const response = await blobClient.download(options); return await utils.stream2Buffer(response.readableStreamBody); } async function createReadStream(storageCfg, strPath) { const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath)); const options = applyCommandOptions({}, storageCfg, 'download'); const response = await blobClient.download(options); return { contentLength: response.contentLength, readStream: response.readableStreamBody }; } async function putObject(storageCfg, strPath, buffer, _contentLength) { const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath)); const baseOptions = { blobHTTPHeaders: { contentType: mime.getType(strPath), contentDisposition: utils.getContentDisposition(path.basename(strPath)) } }; const uploadOptions = applyCommandOptions(baseOptions, storageCfg, 'uploadData'); if (buffer instanceof Buffer) { await blobClient.uploadData(buffer, uploadOptions); } else if (typeof buffer.pipe === 'function') { await blobClient.uploadStream(buffer, undefined, undefined, uploadOptions); } else { throw new TypeError('Input must be Buffer or Readable stream'); } } async function uploadObject(storageCfg, strPath, filePath) { const blockBlobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath)); const uploadStream = fs.createReadStream(filePath); const uploadOptions = { blobHTTPHeaders: { contentType: mime.getType(strPath), contentDisposition: utils.getContentDisposition(path.basename(strPath)) } }; const finalOptions = applyCommandOptions(uploadOptions, storageCfg, 'uploadStream'); await blockBlobClient.uploadStream(uploadStream, undefined, undefined, finalOptions); } async function copyObject(storageCfgSrc, storageCfgDst, sourceKey, destinationKey) { const sourceBlobClient = getBlobClient(storageCfgSrc, getFilePath(storageCfgSrc, sourceKey)); const destBlobClient = getBlobClient(storageCfgDst, getFilePath(storageCfgDst, destinationKey)); const sasToken = generateBlobSASQueryParameters( { containerName: storageCfgSrc.bucketName, blobName: getFilePath(storageCfgSrc, sourceKey), permissions: BlobSASPermissions.parse('r'), startsOn: new Date(), expiresOn: new Date(Date.now() + 3600 * 1000) }, new StorageSharedKeyCredential(storageCfgSrc.accessKeyId, storageCfgSrc.secretAccessKey) ).toString(); const copyOptions = applyCommandOptions({}, storageCfgDst, 'syncCopyFromURL'); await destBlobClient.syncCopyFromURL(`${sourceBlobClient.url}?${sasToken}`, copyOptions); } async function listObjects(storageCfg, strPath) { return await listObjectsExec(storageCfg, strPath); } async function deleteObject(storageCfg, strPath) { const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath)); const options = applyCommandOptions({}, storageCfg, 'deleteBlob'); await blobClient.delete(options); } async function deleteObjects(storageCfg, strPaths) { const aKeys = strPaths.map(path => ({Key: getFilePath(storageCfg, path)})); for (let i = 0; i < aKeys.length; i += MAX_DELETE_OBJECTS) { await deleteObjectsHelp(storageCfg, aKeys.slice(i, i + MAX_DELETE_OBJECTS)); } } async function deletePath(storageCfg, strPath) { const list = await listObjects(storageCfg, strPath); await deleteObjects(storageCfg, list); } async function getDirectSignedUrl(ctx, storageCfg, baseUrl, strPath, urlType, optFilename, _opt_creationDate) { const storageUrlExpires = storageCfg.fs.urlExpires; let expires = (commonDefines.c_oAscUrlTypes.Session === urlType ? cfgExpSessionAbsolute / 1000 : storageUrlExpires) || 31536000; expires = Math.min(expires, 604800); const userFriendlyName = optFilename ? optFilename.replace(/\//g, '%2f') : path.basename(strPath); const contentDisposition = utils.getContentDisposition(userFriendlyName, null, null); const blobClient = getBlobClient(storageCfg, getFilePath(storageCfg, strPath)); const sasOptions = { permissions: BlobSASPermissions.parse('r'), expiresOn: new Date(Date.now() + expires * 1000), contentDisposition, contentType: mime.getType(strPath) }; return await blobClient.generateSasUrl(sasOptions); } function needServeStatic() { return !cfgCacheStorage.useDirectStorageUrls; } module.exports = { headObject, getObject, createReadStream, putObject, uploadObject, copyObject, listObjects, deleteObject, deletePath, getDirectSignedUrl, needServeStatic };