@itwin/imodels-client-authoring
Version:
iModels API client wrapper for applications that author iModels.
274 lines • 14.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangesetOperations = void 0;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
const path = __importStar(require("path"));
const imodels_client_management_1 = require("@itwin/imodels-client-management");
const FileDownload_1 = require("../FileDownload");
const LimitedParallelQueue_1 = require("./LimitedParallelQueue");
class ChangesetOperations extends imodels_client_management_1.ChangesetOperations {
/**
* Creates a Changeset. Wraps the
* {@link https://developer.bentley.com/apis/imodels-v2/operations/create-imodel-changeset/ Create iModel Changeset}
* operation from iModels API. Internally it creates a Changeset instance, uploads the Changeset
* file and confirms Changeset file upload. The execution of this method depends on the Changeset file size - the larger
* the file, the longer the upload will take.
* @param {CreateChangesetParams} params parameters for this operation. See {@link CreateChangesetParams}.
* @returns newly created Changeset. See {@link Changeset}.
*/
async create(params) {
const changesetFileSize = await this._options.localFileSystem.getFileSize(params.changesetProperties.filePath);
const createChangesetBody = this.getCreateChangesetRequestBody(params.changesetProperties, changesetFileSize);
const createChangesetResponse = await this.sendPostRequest({
authorization: params.authorization,
url: this._options.urlFormatter.getChangesetListUrl({
iModelId: params.iModelId,
}),
body: createChangesetBody,
headers: params.headers,
});
const uploadLink = createChangesetResponse.body.changeset._links.upload;
(0, imodels_client_management_1.assertLink)(uploadLink);
await this._options.cloudStorage.upload({
url: uploadLink.href,
data: params.changesetProperties.filePath,
storageType: uploadLink.storageType,
});
const completeLink = createChangesetResponse.body.changeset._links.complete;
(0, imodels_client_management_1.assertLink)(completeLink);
const confirmUploadBody = this.getConfirmUploadRequestBody(params.changesetProperties);
const confirmUploadResponse = await this.sendPatchRequest({
authorization: params.authorization,
url: completeLink.href,
body: confirmUploadBody,
headers: params.headers,
});
const result = this.appendRelatedEntityCallbacks(params.authorization, confirmUploadResponse.body.changeset, params.headers);
return result;
}
/**
* Downloads a single Changeset identified by either index or id. If an error occurs when downloading a Changeset
* this operation queries the failed Changeset by id and retries the download once. If the Changeset file with
* the expected name already exists in the target directory and the file size matches the one expected the Changeset
* is not downloaded again.
* @param {DownloadSingleChangesetParams} params parameters for this operation. See {@link DownloadSingleChangesetParams}.
* @returns downloaded Changeset. See {@link DownloadedChangeset}.
*/
async downloadSingle(params) {
await this._options.localFileSystem.createDirectory(params.targetDirectoryPath);
const changeset = await this.querySingleInternal(params);
return this.downloadSingleChangeset({ ...params, changeset });
}
/**
* Downloads Changeset list. Internally the method uses {@link ChangesetOperations.getRepresentationList} to query the
* Changeset collection so this operation supports most of the the same url parameters to specify what Changesets to
* download. One of the most common properties used are `afterIndex` and `lastIndex` to download Changeset range. This
* operation downloads Changesets in parallel. If an error occurs when downloading a Changeset this operation queries
* the failed Changeset by id and retries the download once. If the Changeset file with the expected name already
* exists in the target directory and the file size matches the one expected the Changeset is not downloaded again.
* @param {DownloadChangesetListParams} params parameters for this operation. See {@link DownloadChangesetListParams}.
* @returns downloaded Changeset metadata along with the downloaded file path. See {@link DownloadedChangeset}.
*/
async downloadList(params) {
await this._options.localFileSystem.createDirectory(params.targetDirectoryPath);
const [downloadCallback, downloadFailedCallback] = (await this.provideDownloadCallbacks(params)) ?? [];
let result = [];
for await (const changesetPage of this.getRepresentationList(params).byPage()) {
const changesetsWithFilePath = changesetPage.map((changeset) => ({
...changeset,
filePath: path.join(params.targetDirectoryPath, this.createFileName(changeset.id)),
}));
result = result.concat(changesetsWithFilePath);
// We sort the changesets by fileSize in descending order to download small
// changesets first because their SAS tokens have a shorter lifespan.
changesetsWithFilePath.sort((changeset1, changeset2) => changeset1.fileSize - changeset2.fileSize);
const queue = new LimitedParallelQueue_1.LimitedParallelQueue({ maxParallelPromises: 10 });
for (const changeset of changesetsWithFilePath)
queue.push(async () => this.downloadChangesetFileWithRetry({
authorization: params.authorization,
iModelId: params.iModelId,
changeset,
abortSignal: params.abortSignal,
downloadCallback,
firstDownloadFailedCallback: downloadFailedCallback,
headers: params.headers,
}));
await queue.waitAll();
}
return result;
}
getCreateChangesetRequestBody(changesetProperties, changesetFileSize) {
return {
id: changesetProperties.id,
description: changesetProperties.description,
parentId: changesetProperties.parentId,
briefcaseId: changesetProperties.briefcaseId,
containingChanges: changesetProperties.containingChanges,
fileSize: changesetFileSize,
synchronizationInfo: changesetProperties.synchronizationInfo,
groupId: changesetProperties.groupId,
};
}
getConfirmUploadRequestBody(changesetProperties) {
return {
state: imodels_client_management_1.ChangesetState.FileUploaded,
briefcaseId: changesetProperties.briefcaseId,
};
}
async downloadSingleChangeset(params) {
const changesetWithPath = {
...params.changeset,
filePath: path.join(params.targetDirectoryPath, this.createFileName(params.changeset.id)),
};
const downloadCallback = params.progressCallback
? (bytes) => params.progressCallback?.(bytes, changesetWithPath.fileSize)
: undefined;
await this.downloadChangesetFileWithRetry({
authorization: params.authorization,
iModelId: params.iModelId,
changeset: changesetWithPath,
abortSignal: params.abortSignal,
downloadCallback,
headers: params.headers,
});
return changesetWithPath;
}
async downloadChangesetFileWithRetry(params) {
const targetFilePath = params.changeset.filePath;
if (await this.isChangesetAlreadyDownloaded(targetFilePath, params.changeset.fileSize))
return;
const downloadParams = {
storage: this._options.cloudStorage,
localPath: targetFilePath,
abortSignal: params.abortSignal,
};
let bytesDownloaded = 0;
if (params.downloadCallback) {
downloadParams.latestDownloadedChunkSizeCallback = (downloaded) => {
bytesDownloaded += downloaded;
params.downloadCallback?.(downloaded);
};
}
try {
const downloadLink = params.changeset._links.download;
(0, imodels_client_management_1.assertLink)(downloadLink);
await (0, FileDownload_1.downloadFile)({
...downloadParams,
url: downloadLink.href,
storageType: downloadLink.storageType,
});
}
catch (error) {
this.throwIfAbortError(error, params.changeset);
params.firstDownloadFailedCallback?.(bytesDownloaded);
const changeset = await this.querySingleInternal({
authorization: params.authorization,
iModelId: params.iModelId,
changesetId: params.changeset.id,
headers: params.headers,
});
try {
const newDownloadLink = changeset._links.download;
(0, imodels_client_management_1.assertLink)(newDownloadLink);
await (0, FileDownload_1.downloadFile)({
...downloadParams,
url: newDownloadLink.href,
storageType: newDownloadLink.storageType,
});
}
catch (errorAfterRetry) {
this.throwIfAbortError(error, params.changeset);
let originalError;
if (errorAfterRetry instanceof Error)
originalError = errorAfterRetry;
throw new imodels_client_management_1.IModelsErrorImpl({
code: imodels_client_management_1.IModelsErrorCode.ChangesetDownloadFailed,
message: `Failed to download changeset. Changeset id: ${params.changeset.id}, changeset index: ${params.changeset.index}, error: ${JSON.stringify(errorAfterRetry)}.`,
originalError,
statusCode: undefined,
details: undefined,
});
}
}
}
async isChangesetAlreadyDownloaded(targetFilePath, expectedFileSize) {
const fileExists = await this._options.localFileSystem.fileExists(targetFilePath);
if (!fileExists)
return false;
const existingFileSize = await this._options.localFileSystem.getFileSize(targetFilePath);
if (existingFileSize === expectedFileSize)
return true;
await this._options.localFileSystem.deleteFile(targetFilePath);
return false;
}
createFileName(changesetId) {
return `${changesetId}.cs`;
}
async provideDownloadCallbacks(params) {
if (!params.progressCallback)
return;
let totalSize = 0;
let totalDownloaded = 0;
for await (const changesetPage of this.getMinimalList(params).byPage()) {
for (const changeset of changesetPage) {
totalSize += changeset.fileSize;
const filePath = path.join(params.targetDirectoryPath, this.createFileName(changeset.id));
if (await this.isChangesetAlreadyDownloaded(filePath, changeset.fileSize))
totalDownloaded += changeset.fileSize;
}
}
const progressCallback = (downloaded) => {
totalDownloaded += downloaded;
params.progressCallback?.(totalDownloaded, totalSize);
};
// We increase total size to prevent cases where downloaded size is larger than total size at the end of the download.
const downloadFailedCallback = (downloadedBeforeFailure) => (totalSize += downloadedBeforeFailure);
return [progressCallback, downloadFailedCallback];
}
throwIfAbortError(error, changeset) {
if (!(0, imodels_client_management_1.isIModelsApiError)(error) ||
error.code !== imodels_client_management_1.IModelsErrorCode.DownloadAborted)
return;
error.originalError = new Error(error.message);
error.message = `Changeset(s) download was aborted. Changeset id: ${changeset.id}}.`;
throw error;
}
}
exports.ChangesetOperations = ChangesetOperations;
//# sourceMappingURL=ChangesetOperations.js.map