162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
/**
|
|
* Shared Azure Blob Storage utilities.
|
|
*
|
|
* Provides singleton BlobServiceClient, container helpers, and SAS token generation.
|
|
* Containers are lazily created on first access (createIfNotExists).
|
|
*
|
|
* Expected env vars:
|
|
* AZURE_BLOB_CONNECTION_STRING — full connection string (preferred)
|
|
* — OR —
|
|
* AZURE_BLOB_ACCOUNT_NAME + AZURE_BLOB_ACCOUNT_KEY
|
|
*/
|
|
|
|
import {
|
|
BlobServiceClient,
|
|
ContainerClient,
|
|
StorageSharedKeyCredential,
|
|
BlobSASPermissions,
|
|
generateBlobSASQueryParameters,
|
|
SASProtocol,
|
|
} from '@azure/storage-blob';
|
|
|
|
let serviceClient: BlobServiceClient | null = null;
|
|
const containerClients = new Map<string, ContainerClient>();
|
|
|
|
/**
|
|
* Known blob containers and their purposes.
|
|
*
|
|
* Note: This is a convenience list (not enforced). Products can add their own
|
|
* containers as needed.
|
|
*/
|
|
export const BLOB_CONTAINERS = {
|
|
audio: 'audio', // Dictation audio recordings
|
|
transcripts: 'transcripts', // Exported transcript files (PDF, DOCX, TXT)
|
|
attachments: 'attachments', // Tracker item attachments (screenshots, docs)
|
|
avatars: 'avatars', // User profile images
|
|
releases: 'releases', // Desktop app update binaries
|
|
backups: 'backups', // Cosmos DB JSON backups
|
|
} as const;
|
|
|
|
export type BlobContainerName = (typeof BLOB_CONTAINERS)[keyof typeof BLOB_CONTAINERS];
|
|
|
|
/**
|
|
* Get or create the BlobServiceClient singleton.
|
|
*/
|
|
export function getBlobServiceClient(): BlobServiceClient {
|
|
if (serviceClient) return serviceClient;
|
|
|
|
const connectionString = process.env.AZURE_BLOB_CONNECTION_STRING;
|
|
if (connectionString) {
|
|
serviceClient = BlobServiceClient.fromConnectionString(connectionString);
|
|
return serviceClient;
|
|
}
|
|
|
|
const accountName = process.env.AZURE_BLOB_ACCOUNT_NAME;
|
|
const accountKey = process.env.AZURE_BLOB_ACCOUNT_KEY;
|
|
if (accountName && accountKey) {
|
|
const credential = new StorageSharedKeyCredential(accountName, accountKey);
|
|
serviceClient = new BlobServiceClient(
|
|
`https://${accountName}.blob.core.windows.net`,
|
|
credential
|
|
);
|
|
return serviceClient;
|
|
}
|
|
|
|
throw new Error(
|
|
'Azure Blob Storage not configured. Set AZURE_BLOB_CONNECTION_STRING or AZURE_BLOB_ACCOUNT_NAME + AZURE_BLOB_ACCOUNT_KEY'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a container client, creating the container if it doesn't exist.
|
|
*/
|
|
export async function getContainerClient(containerName: string): Promise<ContainerClient> {
|
|
const cached = containerClients.get(containerName);
|
|
if (cached) return cached;
|
|
|
|
const client = getBlobServiceClient().getContainerClient(containerName);
|
|
await client.createIfNotExists({ access: undefined }); // private by default
|
|
containerClients.set(containerName, client);
|
|
return client;
|
|
}
|
|
|
|
/**
|
|
* Generate a SAS URL for direct browser upload (or download).
|
|
*
|
|
* @param containerName - Target container
|
|
* @param blobName - Full blob path (e.g., "product/user123/audio/recording.wav")
|
|
* @param permissions - SAS permissions (default: read)
|
|
* @param expiresInMinutes - Token lifetime (default: 60)
|
|
* @returns Full SAS URL for the blob
|
|
*/
|
|
export function generateSasUrl(
|
|
containerName: string,
|
|
blobName: string,
|
|
permissions: 'r' | 'w' | 'rw' | 'rwc' | 'rwd' = 'r',
|
|
expiresInMinutes = 60
|
|
): string {
|
|
const connectionString = process.env.AZURE_BLOB_CONNECTION_STRING;
|
|
const accountName = process.env.AZURE_BLOB_ACCOUNT_NAME;
|
|
const accountKey = process.env.AZURE_BLOB_ACCOUNT_KEY;
|
|
|
|
let credAccountName: string;
|
|
let credential: StorageSharedKeyCredential;
|
|
|
|
if (accountName && accountKey) {
|
|
credAccountName = accountName;
|
|
credential = new StorageSharedKeyCredential(accountName, accountKey);
|
|
} else if (connectionString) {
|
|
// Parse account name and key from connection string
|
|
const nameMatch = connectionString.match(/AccountName=([^;]+)/);
|
|
const keyMatch = connectionString.match(/AccountKey=([^;]+)/);
|
|
if (!nameMatch || !keyMatch) {
|
|
throw new Error('Cannot parse AccountName/AccountKey from connection string');
|
|
}
|
|
credAccountName = nameMatch[1];
|
|
credential = new StorageSharedKeyCredential(nameMatch[1], keyMatch[1]);
|
|
} else {
|
|
throw new Error('Blob storage credentials not configured');
|
|
}
|
|
|
|
const sasPermissions = new BlobSASPermissions();
|
|
if (permissions.includes('r')) sasPermissions.read = true;
|
|
if (permissions.includes('w')) sasPermissions.write = true;
|
|
if (permissions.includes('c')) sasPermissions.create = true;
|
|
if (permissions.includes('d')) sasPermissions.delete = true;
|
|
|
|
const now = new Date();
|
|
const expiresOn = new Date(now.getTime() + expiresInMinutes * 60 * 1000);
|
|
|
|
const sasToken = generateBlobSASQueryParameters(
|
|
{
|
|
containerName,
|
|
blobName,
|
|
permissions: sasPermissions,
|
|
startsOn: now,
|
|
expiresOn,
|
|
protocol: SASProtocol.Https,
|
|
},
|
|
credential
|
|
).toString();
|
|
|
|
return `https://${credAccountName}.blob.core.windows.net/${containerName}/${blobName}?${sasToken}`;
|
|
}
|
|
|
|
/**
|
|
* Check if blob storage is configured.
|
|
*/
|
|
export function isBlobStorageConfigured(): boolean {
|
|
return !!(
|
|
process.env.AZURE_BLOB_CONNECTION_STRING ||
|
|
(process.env.AZURE_BLOB_ACCOUNT_NAME && process.env.AZURE_BLOB_ACCOUNT_KEY)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Test helper: reset module singletons/caches.
|
|
*/
|
|
export function _resetBlobClient(): void {
|
|
serviceClient = null;
|
|
containerClients.clear();
|
|
}
|