packageDocumentation module import { join } from "path" ; import { UserCancelledError } from "./itwin-client/FileHandler" ; import { ProgressCallback , ProgressInfo } from "./itwin-client/Request" ; import { AcquireNewBriefcaseIdArg , BackendHubAccess , BriefcaseDbArg , BriefcaseIdArg , BriefcaseLocalValue , BriefcaseManager , ChangesetArg , ChangesetRangeArg , CheckpointArg , CheckpointProps , CreateNewIModelProps , IModelDb , IModelHost , IModelIdArg , IModelJsFs , IModelNameArg , ITwinIdArg , LockMap , LockProps , LockState , SnapshotDb , TokenArg , V2CheckpointAccessProps , } from "@itwin/core-backend" ; import { BentleyError , BriefcaseStatus , GuidString , Id64String , IModelHubStatus , IModelStatus , Logger , OpenMode } from "@itwin/core-bentley" ; import { BriefcaseIdValue , ChangesetFileProps , ChangesetId , ChangesetIndex , ChangesetIndexAndId , ChangesetProps , CodeProps , IModelError , IModelVersion , LocalDirName , } from "@itwin/core-common" ; import { IModelBankClient } from "./imodelbank/IModelBankClient" ; import { IModelClient } from "./IModelClient" ; import { BriefcaseQuery } from "./imodelhub/Briefcases" ; import { ChangeSet , ChangeSetQuery , ChangesType } from "./imodelhub/ChangeSets" ; import { CheckpointQuery } from "./imodelhub/Checkpoints" ; import { CheckpointV2 , CheckpointV2Query } from "./imodelhub/CheckpointsV2" ; import { IModelHubClient } from "./imodelhub/Client" ; import { CodeQuery } from "./imodelhub/Codes" ; import { IModelQuery } from "./imodelhub/iModels" ; import { Lock , LockLevel , LockQuery , LockType } from "./imodelhub/Locks" ; import { VersionQuery } from "./imodelhub/Versions" ; import { AzureFileHandler } from "./itwin-client/AzureFileHandler" ; const changeSet0 = { id : "" , changesType : 0 , description : "version0" , parentId : "" , briefcaseId : 0 , pushDate : "" , userCreated : "" , index : 0 } ; internal export class IModelHubBackend implements BackendHubAccess { private _imodelClient ? : IModelClient ; private _isIModelBankClient = false ; public constructor ( client ? ) { this . _imodelClient = client ; this . _isIModelBankClient = client instanceof IModelBankClient ; } public get isUsingIModelBankClient ( ) : boolean { return this . _isIModelBankClient ; } public get iModelClient ( ) : IModelClient { if ( ! this . _imodelClient ) this . _imodelClient = new IModelHubClient ( new AzureFileHandler ( ) ) ; return this . _imodelClient ; } private async getAccessToken ( arg ) { return arg . accessToken ?? await IModelHost . getAccessToken ( ) ; } public async getLatestChangeset ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; const changeSets = await this . iModelClient . changeSets . get ( accessToken , arg . iModelId , new ChangeSetQuery ( ) . top ( 1 ) . latest ( ) ) ; return ( changeSets . length === 0 ) ? changeSet0 : this . toChangeSetProps ( changeSets [ changeSets . length - 1 ] ) ; } public async getChangesetFromNamedVersion ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; const versions = await this . iModelClient . versions . get ( accessToken , arg . iModelId , new VersionQuery ( ) . select ( "ChangeSetId" ) . byName ( arg . versionName ) ) ; if ( ! versions [ 0 ] || ! versions [ 0 ] . changeSetId ) throw new IModelError ( IModelStatus . NotFound , `Named version ${ arg . versionName } not found` ) ; return this . queryChangeset ( { ... arg , changeset : { id : versions [ 0 ] . changeSetId } } ) ; } public async getChangesetFromVersion ( arg ) { const version = arg . version ; if ( version . isFirst ) return this . queryChangeset ( { ... arg , changeset : { index : 0 } } ) ; const asOf = version . getAsOfChangeSet ( ) ; if ( asOf ) return this . queryChangeset ( { ... arg , changeset : { id : asOf } } ) ; const versionName = version . getName ( ) ; if ( versionName ) return this . getChangesetFromNamedVersion ( { ... arg , versionName } ) ; return this . getLatestChangeset ( arg ) ; } public async createNewIModel ( arg ) { if ( this . isUsingIModelBankClient ) throw new IModelError ( IModelStatus . BadRequest , "This is a iModelHub only operation" ) ; const version0 = join ( IModelHost . cacheDir , "temp-version0.bim" ) ; IModelJsFs . removeSync ( version0 ) ; if ( ! arg . version0 ) { const blank = SnapshotDb . createEmpty ( version0 , { rootSubject : { name : arg . description ?? arg . iModelName } } ) ; blank . saveChanges ( ) ; blank . close ( ) ; } else { IModelJsFs . copySync ( arg . version0 , version0 ) ; } const nativeDb = IModelDb . openDgnDb ( { path : version0 } , OpenMode . ReadWrite ) ; try { nativeDb . setITwinId ( arg . iTwinId ) ; nativeDb . saveChanges ( ) ; nativeDb . deleteAllTxns ( ) ; nativeDb . resetBriefcaseId ( BriefcaseIdValue . Unassigned ) ; nativeDb . saveLocalValue ( BriefcaseLocalValue . NoLocking , arg . noLocks ? "true" : undefined ) ; nativeDb . saveChanges ( ) ; } finally { nativeDb . closeIModel ( ) ; } const accessToken = await this . getAccessToken ( arg ) ; const hubIModel = await this . iModelClient . iModels . create ( accessToken , arg . iTwinId , arg . iModelName , { path : version0 , description : arg . description } ) ; IModelJsFs . removeSync ( version0 ) ; return hubIModel . wsgId ; } public async deleteIModel ( arg ) { const dirName = BriefcaseManager . getIModelPath ( arg . iModelId ) ; if ( IModelJsFs . existsSync ( dirName ) ) { IModelJsFs . purgeDirSync ( dirName ) ; IModelJsFs . rmdirSync ( dirName ) ; } const accessToken = await this . getAccessToken ( arg ) ; return this . iModelClient . iModels . delete ( accessToken , arg . iTwinId , arg . iModelId ) ; } public async queryIModelByName ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; const iModels = await this . iModelClient . iModels . get ( accessToken , arg . iTwinId , new IModelQuery ( ) . byName ( arg . iModelName ) ) ; return iModels . length === 0 ? undefined : iModels [ 0 ] . id ! ; } public async pushChangeset ( arg ) { const changeset = new ChangeSet ( ) ; const changesetProps = arg . changesetProps ; changeset . id = changesetProps . id ; changeset . parentId = changesetProps . parentId ; changeset . changesType = changesetProps . changesType as number ; changeset . fileSize = changesetProps . size ! . toString ( ) ; changeset . description = changesetProps . description ; changeset . briefcaseId = changesetProps . briefcaseId ; if ( changeset . description . length >= 255 ) { Logger . logWarning ( "imodelhub-access" , `pushChanges - Truncating description to 255 characters. ${ changeset . description }` ) ; changeset . description = changeset . description . slice ( 0 , 254 ) ; } const accessToken = await this . getAccessToken ( arg ) ; return + ( await this . iModelClient . changeSets . create ( accessToken , arg . iModelId , changeset , changesetProps . pathname ) ) . index ! ; } note see deleteBriefcaseFiles public async releaseBriefcase ( arg ) { const { briefcaseId , iModelId } = arg ; const accessToken = await this . getAccessToken ( arg ) ; try { await this . iModelClient . briefcases . get ( accessToken , iModelId , new BriefcaseQuery ( ) . byId ( briefcaseId ) ) ; } catch ( error ) { throw error ; } await this . iModelClient . briefcases . delete ( accessToken , iModelId , briefcaseId ) ; } public async getMyBriefcaseIds ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; const myHubBriefcases = await this . iModelClient . briefcases . get ( accessToken , arg . iModelId , new BriefcaseQuery ( ) . ownedByMe ( ) . selectDownloadUrl ( ) ) ; const myBriefcaseIds = [ ] ; for ( const hubBc of myHubBriefcases ) myBriefcaseIds . push ( hubBc . briefcaseId ! ) ; return myBriefcaseIds ; } public async acquireNewBriefcaseId ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; const briefcase = await this . iModelClient . briefcases . create ( accessToken , arg . iModelId ) ; if ( ! briefcase ) throw new IModelError ( BriefcaseStatus . CannotAcquire , "Could not acquire briefcase" ) ; return briefcase . briefcaseId ! ; } public toChangeSetProps ( cs ) { return { id : cs . wsgId , parentId : cs . parentId ? cs . parentId : "" , briefcaseId : cs . briefcaseId ! , pushDate : cs . pushDate ! , description : cs . description ?? "" , changesType : ( cs . changesType ?? ChangesType . Regular ) as number , userCreated : cs . userCreated ! , index : + cs . index ! } ; } private toChangeSetFileProps ( cs , basePath ) { const csProps = this . toChangeSetProps ( cs ) as ChangesetFileProps ; csProps . pathname = join ( basePath , cs . fileName ! ) ; return csProps ; } public async downloadChangeset ( arg ) { const changeSetsPath = BriefcaseManager . getChangeSetsPath ( arg . iModelId ) ; const cSet = await this . queryChangeset ( arg ) ; const accessToken = await this . getAccessToken ( arg ) ; const changeSets = await this . iModelClient . changeSets . download ( accessToken , arg . iModelId , new ChangeSetQuery ( ) . byId ( cSet . id ) , changeSetsPath ) ; if ( undefined === changeSets ) throw new IModelError ( IModelStatus . NotFound , `Cannot download changeset` ) ; return this . toChangeSetFileProps ( changeSets [ 0 ] , arg . targetDir ) ; } public async queryChangeset ( arg ) { const changeset = await this . tryQueryChangeset ( arg ) ; if ( ! changeset ) throw new IModelError ( IModelStatus . NotFound , `Changeset not found` ) ; return changeset ; } private async tryQueryChangeset ( arg ) { const hasIndex = ( undefined !== arg . changeset . index ) ; if ( ( hasIndex && arg . changeset . index <= 0 ) || arg . changeset . id === "" ) return changeSet0 ; const query = new ChangeSetQuery ( ) ; if ( hasIndex ) query . filter ( `Index+eq+${ arg . changeset . index }` ) ; else query . byId ( arg . changeset . id ) ; const accessToken = await this . getAccessToken ( arg ) ; const changeSets = await this . iModelClient . changeSets . get ( accessToken , arg . iModelId , query ) ; if ( undefined === changeSets ) return undefined ; return changeSets . length > 0 ? this . toChangeSetProps ( changeSets [ 0 ] ) : undefined ; } private async getParentChangesetId ( arg , index ) { if ( index === 0 ) return "" ; const changeset = await this . tryQueryChangeset ( { ... arg , changeset : { index } } ) ; return changeset ?. parentId ; } private async getQueryFromRange ( arg ) { const query = new ChangeSetQuery ( ) ; if ( ! arg . range ) return query ; const range = arg . range ; const after = await this . getParentChangesetId ( arg , range . first ) ; if ( undefined === after ) return undefined ; if ( range . end === undefined ) { query . fromId ( after ) ; } else { const last = ( await this . queryChangeset ( { ... arg , changeset : { index : range . end } } ) ) . id ; if ( range . end === 0 || after === last ) return undefined ; query . betweenChangeSets ( last , after ) ; } return query ; } /** queries for change sets in the specified range. */ public async queryChangesets ( arg ) { const query = await this . getQueryFromRange ( arg ) ; const val = [ ] ; if ( query ) { const accessToken = await this . getAccessToken ( arg ) ; const changeSets = await this . iModelClient . changeSets . get ( accessToken , arg . iModelId , query ) ; for ( const cs of changeSets ) val . push ( this . toChangeSetProps ( cs ) ) ; } return val ; } /** Downloads change sets in the specified range. */ public async downloadChangesets ( arg ) { const val = [ ] ; const query = await this . getQueryFromRange ( arg ) ; if ( query ) { const accessToken = await this . getAccessToken ( arg ) ; const changeSets = await this . iModelClient . changeSets . download ( accessToken , arg . iModelId , query , arg . targetDir ) ; for ( const cs of changeSets ) val . push ( this . toChangeSetFileProps ( cs , arg . targetDir ) ) ; } return val ; } public async downloadV1Checkpoint ( arg ) { const checkpoint = arg . checkpoint ; let checkpointQuery = new CheckpointQuery ( ) . selectDownloadUrl ( ) ; checkpointQuery = checkpointQuery . precedingCheckpoint ( checkpoint . changeset . id ) ; const accessToken = await this . getAccessToken ( checkpoint ) ; const checkpoints = await this . iModelClient . checkpoints . get ( accessToken , checkpoint . iModelId , checkpointQuery ) ; if ( checkpoints . length !== 1 ) throw new IModelError ( BriefcaseStatus . VersionNotFound , "no checkpoints not found" ) ; const cancelRequest = { } ; const progressCallback = ( progress ) => { if ( arg . onProgress && arg . onProgress ( progress . loaded , progress . total ! ) !== 0 ) cancelRequest . cancel ?. ( ) ; } ; await this . iModelClient . checkpoints . download ( accessToken , checkpoints [ 0 ] , arg . localFile , progressCallback , cancelRequest ) ; return { index : checkpoints [ 0 ] . mergedChangeSetIndex ! , id : checkpoints [ 0 ] . mergedChangeSetId ! } ; } public async queryV2Checkpoint ( arg ) { const checkpointQuery = new CheckpointV2Query ( ) . byChangeSetId ( arg . changeset . id ) . selectContainerAccessKey ( ) ; const accessToken = await this . getAccessToken ( arg ) ; const checkpoints = await this . iModelClient . checkpointsV2 . get ( accessToken , arg . iModelId , checkpointQuery ) ; if ( checkpoints . length < 1 ) return undefined ; const { containerAccessKeyContainer , containerAccessKeySAS , containerAccessKeyAccount , containerAccessKeyDbName } = checkpoints [ 0 ] ; if ( ! containerAccessKeyContainer || ! containerAccessKeySAS || ! containerAccessKeyAccount || ! containerAccessKeyDbName ) throw new Error ( "Invalid V2 checkpoint in iModelHub" ) ; return { containerId : containerAccessKeyContainer , sasToken : containerAccessKeySAS , accountName : containerAccessKeyAccount , dbName : containerAccessKeyDbName , storageType : "azure?sas=1" } ; } public async downloadV2Checkpoint ( arg ) { const checkpoint = arg . checkpoint ; let checkpointQuery = new CheckpointV2Query ( ) ; checkpointQuery = checkpointQuery . precedingCheckpointV2 ( checkpoint . changeset . id ) . selectContainerAccessKey ( ) ; const accessToken = await this . getAccessToken ( checkpoint ) ; let checkpoints = [ ] ; try { checkpoints = await this . iModelClient . checkpointsV2 . get ( accessToken , checkpoint . iModelId , checkpointQuery ) ; } catch ( error ) { if ( error instanceof BentleyError && error . errorNumber === IModelHubStatus . Unknown ) throw new IModelError ( IModelStatus . NotFound , "V2 checkpoints not supported" ) ; throw error ; } if ( checkpoints . length !== 1 ) throw new IModelError ( IModelStatus . NotFound , "V2 checkpoint not found" ) ; const { containerAccessKeyContainer , containerAccessKeySAS , containerAccessKeyAccount , containerAccessKeyDbName } = checkpoints [ 0 ] ; if ( ! containerAccessKeyContainer || ! containerAccessKeySAS || ! containerAccessKeyAccount || ! containerAccessKeyDbName ) throw new IModelError ( IModelStatus . NotFound , "invalid V2 checkpoint" ) ; const transfer = new IModelHost . platform . CloudDbTransfer ( "download" , { containerId : containerAccessKeyContainer , sasToken : containerAccessKeySAS , storageType : "azure?sas=1" , accountName : containerAccessKeyAccount , dbName : containerAccessKeyDbName , writeable : false , localFile : arg . localFile } ) ; let timer ; try { let total = 0 ; const onProgress = arg . onProgress ; if ( onProgress ) { timer = setInterval ( async ( ) => { const progress = transfer . getProgress ( ) ; total = progress . total ; if ( onProgress ( progress . loaded , progress . total ) ) transfer . cancelTransfer ( ) ; } , 250 ) ; } await transfer . promise ; onProgress ?. ( total , total ) ; } catch ( err ) { throw ( err . message === "cancelled" ) ? new UserCancelledError ( BriefcaseStatus . DownloadCancelled , "download cancelled" ) : err ; } finally { if ( timer ) clearInterval ( timer ) ; } return { index : checkpoints [ 0 ] . mergedChangeSetIndex ! , id : checkpoints [ 0 ] . mergedChangeSetId ! } ; } public async releaseAllLocks ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; return this . iModelClient . locks . deleteAll ( accessToken , arg . iModelId , arg . briefcaseId ) ; } public async releaseAllCodes ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; return this . iModelClient . codes . deleteAll ( accessToken , arg . iModelId , arg . briefcaseId ) ; } public async queryAllLocks ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; const heldLocks = await this . iModelClient . locks . get ( accessToken , arg . iModelId , new LockQuery ( ) . byBriefcaseId ( arg . briefcaseId ) ) ; return heldLocks . map ( ( lock ) => ( { id : lock . objectId ! , state : lock . lockLevel ! as number } ) ) ; } public async queryAllCodes ( arg ) { const accessToken = await this . getAccessToken ( arg ) ; const reservedCodes = await this . iModelClient . codes . get ( accessToken , arg . iModelId , new CodeQuery ( ) . byBriefcaseId ( arg . briefcaseId ) ) ; return reservedCodes . map ( ( code ) => ( { spec : code . codeSpecId ! , scope : code . codeScope ! , value : code . value ! } ) ) ; } public toHubLock ( arg , id , state ) { const lock = new Lock ( ) ; lock . briefcaseId = arg . briefcaseId ; lock . lockLevel = state as LockLevel & LockState ; lock . lockType = LockType . Element ; lock . objectId = id ; lock . releasedWithChangeSet = arg . changeset . id ; lock . seedFileId = arg . iModelId ; return lock ; } public toHubLocks ( arg , map ) { const locks = [ ] ; for ( const [ id , state ] of map ) locks . push ( this . toHubLock ( arg , id , state ) ) ; return locks ; } public async acquireLocks ( arg , locks ) { const hubLocks = this . toHubLocks ( arg , locks ) ; const accessToken = await this . getAccessToken ( arg ) ; await this . iModelClient . locks . update ( accessToken , arg . iModelId , hubLocks ) ; } }