feat: implement Azure Blob storage (getBlobClient, upload, get, delete, ping, copy)

This commit is contained in:
jingi
2026-06-19 13:51:12 -07:00
parent 187d100230
commit 6ede27667f
+185
View File
@@ -0,0 +1,185 @@
import {BlobSASPermissions, BlobServiceClient, StorageSharedKeyCredential} from "@azure/storage-blob";
import {
StorageCopyInput,
StorageDeleteInput,
StorageGetInput,
StorageMetaData,
StorageResult,
StorageUploadInput
} from '@/features/storages/storages.types';
import {Readable} from "node:stream";
type BlobConfig = {
accountName: string;
accountKey?: string;
connectionString?: string;
containerName: string;
endpointUrl?: string;
};
async function getBlobClient(config: BlobConfig) {
if (config.connectionString) {
return BlobServiceClient.fromConnectionString(co
nfig.connectionString);
}
const url = config.endpointUrl ?? `https://${config.
accountName}.blob.core.windows.net`;
const credential = new StorageSharedKeyCredential(co
nfig.accountName, config.accountKey!);
return new BlobServiceClient(url, credential);
}
const BASE_DIR = "";
async function ensureContainer(config: BlobConfig) {
const client = await getBlobClient(config);
const containerClient = client.getContainerClient(config.containerName);
await containerClient.createIfNotExists();
}
export async function uploadBlob(
config: BlobConfig,
input: { data: StorageUploadInput, metadata?: StorageMetaData }
): Promise<StorageResult> {
const client = await getBlobClient(config);
await ensureContainer(config);
const key = `${BASE_DIR}${input.data.path}`;
const file = input.data.file;
let uploadStream: Readable;
if (Buffer.isBuffer(file) || file instanceof Uint8Array) {
uploadStream = Readable.from(file);
} else if ((file as any).pipe) {
uploadStream = file;
} else {
return {success: false, provider: "Blob", error: "Unsupported file type for streaming upload"};
}
try {
const containerClient = client.getContainerClient(config.containerName);
const blockBlobClient = containerClient.getBlockBlobClient(key);
await blockBlobClient.uploadStream(uploadStream);
} catch (err: any) {
return {success: false, provider: "Blob", error: err.message};
}
return {success: true, provider: "Blob"};
}
export async function getBlob(
config: BlobConfig,
input: { data: StorageGetInput, metadata: StorageMetaData }
): Promise<StorageResult> {
const client = await getBlobClient(config);
const key = `${BASE_DIR}${input.data.path}`;
const containerClient = client.getContainerClient(config.containerName);
const blockBlobClient = containerClient.getBlockBlobClient(key);
if (!(await blockBlobClient.exists())) {
return {success: false, provider: "Blob", error: "File not found"};
}
const downloadResponse = await blockBlobClient.download();
const fileStream = downloadResponse.readableStreamBody as unknown as Readable;
let presignedUrl: string | undefined;
if (input.data.signedUrl) {
presignedUrl = await blockBlobClient.generateSasUrl({
expiresOn: new Date(Date.now() + (input.data.expiresInSeconds ?? 60) * 1000),
permissions: BlobSASPermissions.parse("r"),
});
}
return {
success: true,
provider: "Blob",
file: fileStream,
url: presignedUrl,
};
}
export async function deleteBlob(config: BlobConfig, input: {
data: StorageDeleteInput,
metadata?: StorageMetaData
}): Promise<StorageResult> {
const client = await getBlobClient(config);
const key = `${BASE_DIR}${input.data.path}`;
try {
const containerClient = client.getContainerClient(config.containerName);
const blockBlobClient = containerClient.getBlockBlobClient(key);
await blockBlobClient.delete();
return {success: true, provider: "Blob"};
} catch (err: any) {
return {success: false, provider: "Blob", error: err.message};
}
}
export async function pingBlob(config: BlobConfig): Promise<StorageResult> {
try {
const client = await getBlobClient(config);
const containerClient = client.getContainerClient(config.containerName);
const exists = await containerClient.exists();
if (!exists) return {
success: false,
provider: "Blob",
response: "Container does not exist"
};
const key = `${BASE_DIR}ping.txt`;
const blockBlobClient = containerClient.getBlockBlobClient(key);
await blockBlobClient.upload(Buffer.from("ping"), 4);
await blockBlobClient.download();
await blockBlobClient.delete();
return {
success: true,
provider: "Blob",
response: "Blob storage OK"
};
} catch (err: any) {
return {
success: false,
provider: "Blob",
response: err.message
};
}
}
export async function copyBlob(
config: BlobConfig,
input: {
data: StorageCopyInput,
},
): Promise<StorageResult> {
const client = await getBlobClient(config);
await ensureContainer(config);
const sourceKey = `${BASE_DIR}${input.data.from}`;
const destinationKey = `${BASE_DIR}${input.data.to}`;
try {
const containerClient = client.getContainerClient(config.containerName);
const sourceBlob = containerClient.getBlockBlobClient(sourceKey);
const destBlob = containerClient.getBlockBlobClient(destinationKey);
const poller = await destBlob.beginCopyFromURL(sourceBlob.url);
await poller.pollUntilDone();
return {
success: true,
provider: "Blob",
};
} catch (err: any) {
return {
success: false,
provider: "Blob",
error: err.message,
};
}
}