Source: storage-blob.ts

/**
 * @author Michael Hasenstein <hasenstein@yahoo.com>
 * @copyright REFINIO GmbH 2017
 * @license CC-BY-NC-SA-2.5; portions MIT License
 * @version 0.0.1
 */

/**
 * This module contains functions that help with BLOBs. Normally BLOBs are written and read
 * using streams, but sometimes you want to get the full BLOB into an ArrayBuffer or a
 * Base64-encoded string.
 * @module
 */

/*
 * Implementation Detail
 *
 * These functions are in an extra module to avoid a circular dependency of imports if we put it
 * into storage-base-common. They do not belong into one of the storage related files in the
 * system folder because they are system-independent, by relying on the in turn system-dependent
 *  storage-stream module(s).
 */

import {createError} from './errors';
import type {BLOB, CLOB, OneObjectTypes} from './recipes';
import type {FileCreation} from './storage-base-common';
import {createCryptoHash} from './system/crypto-helpers';
import {writeUTF8TextFile} from './system/storage-base';
import {createFileReadStream, createFileWriteStream} from './system/storage-streams';
import type {SHA256Hash} from './util/type-checks';

/**
 * Concatenate an array of `ArrayBuffer` into a single `ArrayBuffer`.
 * @static
 * @param {ArrayBuffer[]} buffers
 * @returns {ArrayBuffer} Returns an `ArrayBuffer`
 */
export function concatenateArrayBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
    let totalLength = 0;

    for (const arr of buffers) {
        totalLength += arr.byteLength;
    }

    const result = new Uint8Array(totalLength);

    let offset = 0;

    for (const arr of buffers) {
        result.set(new Uint8Array(arr), offset);
        offset += arr.byteLength;
    }

    return result.buffer;
}

/**
 * Reads a binary file in object storage in its entirety and returns it as `ArrayBuffer`.
 * There is an equivalent write function `storeArrayBufferAsBlob` on {@link WriteStorageApi}
 * objects.
 * @static
 * @param {SHA256Hash} hash
 * @returns {Promise<ArrayBuffer>} Returns an `ArrayBuffer`
 */
export async function readBlobAsArrayBuffer(hash: SHA256Hash<BLOB>): Promise<ArrayBuffer> {
    const stream = createFileReadStream(hash);

    const blobData: ArrayBuffer[] = [];

    stream.onData.addListener(data => {
        blobData.push(data);
    });

    await stream.promise;

    return concatenateArrayBuffers(blobData);
}

/**
 * Reads a binary file in object storage in its entirety and returns it as Base64 encoded string.
 * There is an equivalent write function `storeBase64StringAsBlob` on {@link WriteStorageApi}
 * objects.
 * @static
 * @param {SHA256Hash} hash
 * @returns {Promise<string>} Returns a Base64 encoded string
 */
export async function readBlobAsBase64(hash: SHA256Hash<BLOB>): Promise<string> {
    const stream = createFileReadStream(hash, 'base64');

    const data: string[] = [];

    stream.onData.addListener(chunk => {
        data.push(chunk);
    });

    await stream.promise;

    return data.join('');
}

/**
 * Writes an ArrayBuffer to a file in object storage.
 * @static
 * @param {ArrayBuffer} arrayBuffer
 * @returns {Promise<FileCreation>} Returns a promise that resolves with a {@link FileCreation}
 */
export function storeArrayBufferAsBlob(arrayBuffer: ArrayBuffer): Promise<FileCreation<BLOB>> {
    const stream = createFileWriteStream();
    stream.write(arrayBuffer);
    return stream.end();
}

/**
 * Writes a Base64 string to a file in object storage.
 * @static
 * @param {string} base64Str
 * @returns {Promise<FileCreation>} Returns a promise that resolves with a {@link FileCreation}
 */
export function storeBase64StringAsBlob(base64Str: string): Promise<FileCreation<BLOB>> {
    const stream = createFileWriteStream('base64');
    stream.write(base64Str);
    return stream.end();
}

/**
 * The string will be stored under the name resulting from calculation of a crypto-hash over the
 * string.
 *
 * This function is safe to be used in multistep asynchronous writing operations: It only
 * writes to files using their contents' SHA-256 as filename. That means even if, as part
 * of a parent function, we have a pattern of 1) read file, 2) process, 3) write file it is not
 * possible to lose anything.
 *
 * **This function is not atomic.** Its two steps of creating the SHA-256 hash and of storing
 * the string are independent "await"-ed asynchronous operations, another store operation could
 * take place in between, if scheduled.
 * @static
 * @async
 * @param {string} str - A UTF-8 string
 * @returns {Promise<FileCreation>} A promise with the result of the object creation.
 */
export async function storeUTF8Clob<T extends OneObjectTypes | CLOB = CLOB>(
    str: string
): Promise<FileCreation<T>> {
    if (str === undefined || str.length === 0) {
        throw createError('SB-STORE');
    }

    const hash = await createCryptoHash<T>(str);
    const status = await writeUTF8TextFile(str, hash);

    return {
        hash,
        status
    };
}