Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cloud-agnostic/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@itwin/cloud-agnostic-core",
"version": "3.0.5",
"version": "3.1.0",
"description": "Package that allows configuring components loaded by dependency injection",
"keywords": [
"Bentley",
Expand Down
3,362 changes: 1,345 additions & 2,017 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion common/config/rush/version-policies.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"definitionName": "lockStepVersion",
"policyName": "lockStepVersionObjectStorage",
"version": "3.0.5",
"version": "3.1.0",
"nextBump": "prerelease"
}
]
2 changes: 1 addition & 1 deletion storage/azure/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@itwin/object-storage-azure",
"version": "3.0.5",
"version": "3.1.0",
"description": "Object storage implementation using Azure Blob Storage",
"keywords": [
"Bentley",
Expand Down
7 changes: 5 additions & 2 deletions storage/azure/src/server/AzureServerStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
ObjectReference,
EntityPageListIterator,
ServerStorage,
StorageOptions,
TransferData,
TransferType,
} from "@itwin/object-storage-core";
Expand All @@ -42,7 +43,7 @@ import {
} from "./internal";
import { BlobServiceClientWrapper, BlockBlobClientWrapper } from "./wrappers";

export interface AzureServerStorageConfig {
export interface AzureServerStorageConfig extends StorageOptions {
accountName: string;
accountKey: string;
baseUrl: string;
Expand Down Expand Up @@ -144,7 +145,9 @@ export class AzureServerStorage extends ServerStorage {
): EntityPageListIterator<ObjectReference> {
const pageIterator: EntityPageListIterator<ObjectReference> =
new EntityPageListIterator(() =>
this._client.getObjectsNextPage(directory, { maxPageSize: maxPageSize })
this._client.getObjectsNextPage(directory, {
maxPageSize: maxPageSize,
})
);
return pageIterator;
}
Expand Down
30 changes: 14 additions & 16 deletions storage/azure/src/server/AzureServerStorageBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import {
BlobServiceClient,
StorageSharedKeyCredential,
} from "@azure/storage-blob";

import { ConfigError } from "@itwin/cloud-agnostic-core/lib/internal";

import { DependencyConfig, DIContainer } from "@itwin/cloud-agnostic-core";
Expand All @@ -22,7 +17,10 @@ import {
AzureServerStorage,
AzureServerStorageConfig,
} from "./AzureServerStorage";
import { BlobServiceClientWrapper } from "./wrappers/BlobServiceClientWrapper";
import {
BlobServiceClientWrapper,
BlobServiceClientWrapperFactory,
} from "./wrappers";

export type AzureServerStorageBindingsConfig = AzureServerStorageConfig &
DependencyConfig;
Expand Down Expand Up @@ -55,20 +53,20 @@ export class AzureServerStorageBindings extends ServerStorageDependency {
}
);

container.registerFactory(
BlobServiceClientWrapperFactory,
(c: DIContainer) => {
const resolvedConfig = c.resolve<AzureServerStorageBindingsConfig>(
Types.AzureServer.config
);
return new BlobServiceClientWrapperFactory(resolvedConfig.retryOptions);
}
);
container.registerFactory(BlobServiceClientWrapper, (c: DIContainer) => {
return new BlobServiceClientWrapper(c.resolve(BlobServiceClient));
});
container.registerFactory(BlobServiceClient, (c: DIContainer) => {
const resolvedConfig = c.resolve<AzureServerStorageBindingsConfig>(
Types.AzureServer.config
);
return new BlobServiceClient(
resolvedConfig.baseUrl,
new StorageSharedKeyCredential(
resolvedConfig.accountName,
resolvedConfig.accountKey
)
);
return c.resolve(BlobServiceClientWrapperFactory).create(resolvedConfig);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import {
BlobServiceClient,
newPipeline,
StoragePipelineOptions,
StorageSharedKeyCredential,
} from "@azure/storage-blob";

import { RetryOptions } from "@itwin/object-storage-core";

import { BlobServiceClientWrapper } from "./BlobServiceClientWrapper";

export interface AzureBlobServiceConfig {
accountName: string;
accountKey: string;
baseUrl: string;
}

export class BlobServiceClientWrapperFactory {
public constructor(private readonly _retryOptions: RetryOptions = {}) {}

public create(config: AzureBlobServiceConfig): BlobServiceClientWrapper {
const credential = new StorageSharedKeyCredential(
config.accountName,
config.accountKey
);
const pipelineOptions: StoragePipelineOptions = {
retryOptions: {
maxTries:
this._retryOptions.maxRetries != undefined
? this._retryOptions.maxRetries + 1
: undefined,
retryDelayInMs: this._retryOptions.retryDelayMs,
maxRetryDelayInMs: this._retryOptions.maxRetryDelayMs,
},
};
return new BlobServiceClientWrapper(
new BlobServiceClient(
config.baseUrl,
newPipeline(credential, pipelineOptions)
)
);
}
}
1 change: 1 addition & 0 deletions storage/azure/src/server/wrappers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
export * from "./BlobServiceClientWrapper";
export * from "./BlobServiceClientWrapperFactory";
export * from "./BlockBlobClientWrapper";
export * from "./BlockBlobClientWrapperFactory";
2 changes: 1 addition & 1 deletion storage/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@itwin/object-storage-core",
"version": "3.0.5",
"version": "3.1.0",
"description": "Core generic object storage interfaces",
"keywords": [
"Bentley",
Expand Down
8 changes: 6 additions & 2 deletions storage/core/src/client/ClientStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ import {

export abstract class ClientStorage {
public abstract download(
input: (UrlDownloadInput | ConfigDownloadInput) & { transferType: "buffer" }
input: (UrlDownloadInput | ConfigDownloadInput) & {
transferType: "buffer";
}
): Promise<Buffer>;

public abstract download(
input: (UrlDownloadInput | ConfigDownloadInput) & { transferType: "stream" }
input: (UrlDownloadInput | ConfigDownloadInput) & {
transferType: "stream";
}
): Promise<Readable>;

public abstract download(
Expand Down
22 changes: 22 additions & 0 deletions storage/core/src/common/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,25 @@ export interface ConfigTransferInput {
reference: ObjectReference;
transferConfig: TransferConfig;
}

export interface RetryOptions {
/** Maximum number of retry attempts. */
maxRetries?: number;
/**
* Initial delay in milliseconds before the first retry, with exponential
* backoff applied on subsequent attempts.
* Supported by Azure server storage and all URL-based client transfers.
* Ignored by S3, Minio, and Google storage.
*/
retryDelayMs?: number;
/**
* Upper bound in milliseconds for the delay between retries.
* Supported by Azure and Google storage, and all URL-based client transfers.
* Ignored by S3 and Minio server storage.
*/
maxRetryDelayMs?: number;
}

export interface StorageOptions {
retryOptions?: RetryOptions;
}
109 changes: 7 additions & 102 deletions storage/core/src/server/internal/Helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { randomBytes } from "crypto";
import { createReadStream, createWriteStream, promises } from "fs";
import { createWriteStream, promises } from "fs";
import { dirname } from "path";
import { Readable } from "stream";

import axios from "axios";

import {
ConfigTransferInput,
createClientAbortSignal,
UrlTransferInput,
} from "../../common";
import { ConfigTransferInput, UrlTransferInput } from "../../common";
import { defaultExpiresInSeconds } from "../../common/internal";
import {
ConfigDownloadInput,
Expand Down Expand Up @@ -55,7 +49,11 @@ export async function streamToBuffer(stream: Readable): Promise<Buffer> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
chunks.push(data instanceof Buffer ? data : Buffer.from(data))
);
stream.on("end", () => resolve(Buffer.concat(chunks)));
stream.on("end", () => {
const buffer = Buffer.concat(chunks);
stream.destroy();
resolve(buffer);
});
stream.on("error", reject);
});
}
Expand Down Expand Up @@ -122,104 +120,11 @@ export async function streamToTransferType(
}
}

export async function downloadFromUrl(
input: UrlDownloadInput
): Promise<TransferData> {
const { transferType, url, abortSignal } = input;

// There is an issue with Axios type definitions. Casting should be removed
// after upgrading to Axios 1.0
// See: https://github.com/axios/axios/pull/4229
const signal = abortSignal ? createClientAbortSignal(abortSignal) : undefined;

switch (transferType) {
case "buffer":
return downloadFromUrlAsBuffer(url, signal);
case "stream":
return downloadFromUrlAsStream(url, signal);
case "local":
const localPath = input.localPath;
assertLocalFile(localPath);
return downloadFromUrlToLocalFile(url, localPath, signal);
default:
throw new Error(`Type ${input.transferType} is not supported`);
}
}

export async function uploadToUrl(
url: string,
data: TransferData,
headers?: Record<string, string>
): Promise<void> {
let dataToUpload: Readable | Buffer;
if (typeof data === "string") {
await assertFileNotEmpty(data);
dataToUpload = createReadStream(data);
} else {
dataToUpload = data;
}
await axios.put(url, dataToUpload, {
headers,
});
}

// TODO: switch to using crypto.randomUUID function once support for Node 12.x is dropped.
export function getRandomString(): string {
return randomBytes(16).toString("hex");
}

async function downloadFromUrlAsBuffer(
url: string,
signal?: AbortSignal
): Promise<Buffer> {
let promise = axios.get(url, {
responseType: "arraybuffer",
signal,
});
promise = convertAbortErrorName(promise);
return (await promise).data as Buffer;
}

async function downloadFromUrlAsStream(
url: string,
signal?: AbortSignal
): Promise<Readable> {
let promise = axios.get(url, {
responseType: "stream",
signal,
});
promise = convertAbortErrorName(promise);
return (await promise).data as Readable;
}

async function downloadFromUrlToLocalFile(
url: string,
localPath: string,
signal?: AbortSignal
): Promise<string> {
let promise = axios.get(url, {
responseType: "stream",
signal,
});
promise = convertAbortErrorName(promise);

const stream = (await promise).data as Readable;
await streamToLocalFile(stream, localPath);

return localPath;
}

async function convertAbortErrorName<T>(promise: Promise<T>): Promise<T> {
try {
return await promise;
} catch (error: unknown) {
if (error instanceof Error && error.name === "CanceledError")
error.name = "AbortError";

throw error;
}
}

export function getExpiryDate(options?: ExpiryOptions): Date {
if (options?.expiresInSeconds && options?.expiresOn) {
throw new Error(
Expand Down
Loading
Loading