@azure/storage-blob
Version:
Microsoft Azure Storage SDK for JavaScript - Blob
172 lines • 8.37 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { stringToUint8Array } from "@azure/core-util";
import { readResponseBodyToBytes } from "./utils.common.js";
/**
* Reads the raw body of an Apache Arrow List Blobs response (a Node.js readable
* stream or a browser Blob) and parses it into blob items and prefixes.
*
* @param response - The raw stream response returned by the Apache Arrow list operation.
*/
export async function parseBlobListArrowResponse(response) {
// Load apache-arrow lazily so it is only pulled in when the Arrow format is requested.
const { tableFromIPC } = await import("apache-arrow");
// Node streams the body into apache-arrow so record batches are read incrementally;
// the browser reads the whole Blob.
const table = response.readableStreamBody
? await tableFromIPC(response.readableStreamBody)
: tableFromIPC(await readResponseBodyToBytes(response));
return projectArrowTable(table);
}
/**
* Reconstructs blob items and prefixes from a decoded Apache Arrow table.
*
* The Arrow data is columnar: each field (e.g. `Name`, `Content-Length`) is a
* separate column. We reconstruct one {@link BlobItemInternal} row per row by reading
* each column at that row index; the caller projects those to public models. The
* continuation token travels in the schema metadata (a page-level value, not a column).
*/
function projectArrowTable(table) {
const nextMarker = table.schema.metadata?.get("NextMarker") ?? undefined;
const getColumn = (columnName) => table.getChild(columnName);
const cell = (rowIndex, columnName) => getColumn(columnName)?.get(rowIndex);
const asString = (rowIndex, columnName) => {
const value = cell(rowIndex, columnName);
return value === undefined || value === null ? undefined : String(value);
};
const asBoolean = (rowIndex, columnName) => {
const value = cell(rowIndex, columnName);
return value === undefined || value === null ? undefined : Boolean(value);
};
const asNumber = (rowIndex, columnName) => {
const value = cell(rowIndex, columnName);
return value === undefined || value === null ? undefined : Number(value);
};
const asDate = (rowIndex, columnName) => {
const value = cell(rowIndex, columnName);
if (value === undefined || value === null) {
return undefined;
}
// apache-arrow normalizes every Timestamp unit to epoch milliseconds when a
// cell is read (SECOND x1000, MICROSECOND /1000, NANOSECOND /1e6), so the
// value is already in the milliseconds a Date expects and must not be scaled
// again. Fall back to string parsing if a column ever arrives non-numeric.
const millis = Number(value);
if (Number.isNaN(millis)) {
const parsed = Date.parse(String(value));
return Number.isNaN(parsed) ? undefined : new Date(parsed);
}
return new Date(millis);
};
const asBytesFromBase64 = (rowIndex, columnName) => {
const value = asString(rowIndex, columnName);
return value === undefined ? undefined : stringToUint8Array(value, "base64");
};
const asMap = (rowIndex, columnName) => toRecord(cell(rowIndex, columnName));
const blobItems = [];
const blobPrefixes = [];
for (let i = 0; i < table.numRows; i++) {
// BlobPrefix rows only populate the `Name` column; all others are null.
const resourceType = asString(i, "ResourceType");
if (resourceType !== undefined && resourceType.toLowerCase() === "blobprefix") {
blobPrefixes.push({ name: { content: asString(i, "Name") ?? "" } });
continue;
}
const properties = {
createdOn: asDate(i, "Creation-Time"),
lastModified: asDate(i, "Last-Modified") ?? new Date(0),
etag: asString(i, "Etag") ?? "",
contentLength: asNumber(i, "Content-Length"),
contentType: asString(i, "Content-Type"),
contentEncoding: asString(i, "Content-Encoding"),
contentLanguage: asString(i, "Content-Language"),
contentMD5: asBytesFromBase64(i, "Content-MD5"),
contentDisposition: asString(i, "Content-Disposition"),
cacheControl: asString(i, "Cache-Control"),
blobSequenceNumber: asNumber(i, "x-ms-blob-sequence-number"),
blobType: asString(i, "BlobType"),
leaseStatus: asString(i, "LeaseStatus"),
leaseState: asString(i, "LeaseState"),
leaseDuration: asString(i, "LeaseDuration"),
copyId: asString(i, "CopyId"),
copyStatus: asString(i, "CopyStatus"),
copySource: asString(i, "CopySource"),
copyProgress: asString(i, "CopyProgress"),
copyCompletedOn: asDate(i, "CopyCompletionTime"),
copyStatusDescription: asString(i, "CopyStatusDescription"),
serverEncrypted: asBoolean(i, "ServerEncrypted"),
incrementalCopy: asBoolean(i, "IncrementalCopy"),
destinationSnapshot: asString(i, "CopyDestinationSnapshot"),
deletedOn: asDate(i, "DeletedTime"),
remainingRetentionDays: asNumber(i, "RemainingRetentionDays"),
accessTier: asString(i, "AccessTier"),
accessTierInferred: asBoolean(i, "AccessTierInferred"),
archiveStatus: asString(i, "ArchiveStatus"),
smartAccessTier: asString(i, "SmartAccessTier"),
customerProvidedKeySha256: asString(i, "CustomerProvidedKeySha256"),
encryptionScope: asString(i, "EncryptionScope"),
accessTierChangedOn: asDate(i, "AccessTierChangeTime"),
tagCount: asNumber(i, "TagCount"),
expiresOn: asDate(i, "Expiry-Time"),
isSealed: asBoolean(i, "Sealed"),
rehydratePriority: asString(i, "RehydratePriority"),
lastAccessedOn: asDate(i, "LastAccessTime"),
immutabilityPolicyExpiresOn: asDate(i, "ImmutabilityPolicyUntilDate"),
immutabilityPolicyMode: asString(i, "ImmutabilityPolicyMode"),
legalHold: asBoolean(i, "LegalHold"),
};
blobItems.push({
name: { content: asString(i, "Name") ?? "" },
deleted: asBoolean(i, "Deleted") ?? false,
snapshot: asString(i, "Snapshot") ?? "",
versionId: asString(i, "VersionId"),
isCurrentVersion: asBoolean(i, "IsCurrentVersion"),
properties,
metadata: asMap(i, "Metadata"),
blobTags: toBlobTags(asMap(i, "Tags")),
objectReplicationMetadata: asMap(i, "OrMetadata"),
hasVersionsOnly: asBoolean(i, "HasVersionsOnly"),
});
}
return { nextMarker, blobItems, blobPrefixes };
}
/**
* Converts an Apache Arrow map cell (a plain string dictionary) into the generated
* {@link BlobTags} shape, so the shared projection layer can flatten it exactly like
* the XML path does.
*/
function toBlobTags(map) {
return map === undefined
? undefined
: { blobTagSet: Object.entries(map).map(([key, value]) => ({ key, value })) };
}
/**
* Converts an Apache Arrow map cell into a plain string dictionary. Handles the
* possible shapes an arrow map value can take (iterable of `[key, value]` pairs,
* iterable of `{ key, value }` structs, or a plain object).
*/
function toRecord(value) {
if (value === undefined || value === null) {
return undefined;
}
const entries = [];
const asText = (v) => (v === undefined || v === null ? "" : String(v));
if (typeof value[Symbol.iterator] === "function") {
for (const entry of value) {
if (Array.isArray(entry)) {
entries.push([String(entry[0]), asText(entry[1])]);
}
else if (entry && typeof entry === "object" && "key" in entry) {
const { key, value: entryValue } = entry;
entries.push([String(key), asText(entryValue)]);
}
}
}
else if (typeof value === "object") {
for (const [key, entryValue] of Object.entries(value)) {
entries.push([key, asText(entryValue)]);
}
}
return Object.fromEntries(entries);
}
//# sourceMappingURL=blobListArrowParser.js.map