UNPKG

@itwin/core-backend

Version:
267 lines • 13.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module iModels */ import * as fs from "fs"; import { join } from "path"; import { CloudSqlite } from "./CloudSqlite"; import { IModelHost, KnownLocations } from "./IModelHost"; import { BriefcaseIdValue, CatalogError, CloudSqliteError } from "@itwin/core-common"; import { IpcHandler } from "./IpcHost"; import { StandaloneDb } from "./IModelDb"; import { Guid, OpenMode } from "@itwin/core-bentley"; import { _nativeDb } from "./internal/Symbols"; import { IModelNative } from "./internal/NativePlatform"; let readonlyCloudCache; let writeableCloudCache; const catalogManifestName = "CatalogManifest"; // we make a readonly CloudCache and a writeable CloudCache. That way the access token authorizations are distinct. function makeCloudCache(arg, writeable) { const cache = CloudSqlite.CloudCaches.getCache(arg); // if the cache was just created, add the "catalog" members as hidden if (undefined === cache.catalogContainers) { CloudSqlite.addHiddenProperty(cache, "catalogContainers", new Map()); CloudSqlite.addHiddenProperty(cache, "writeable", writeable); } return cache; } // find an existing CloudContainer for accessing a CatalogIModel, or make a new one and connect it async function getCatalogContainerObj(cache, containerId) { const cloudContainer = cache.catalogContainers.get(containerId); if (undefined !== cloudContainer) return cloudContainer; const accessLevel = cache.writeable ? "write" : "read"; const tokenProps = await CloudSqlite.getBlobService().requestToken({ containerId, accessLevel, userToken: await IModelHost.getAccessToken() }); const container = CloudSqlite.createCloudContainer({ accessLevel, baseUri: tokenProps.baseUri, containerId, storageType: tokenProps.provider, writeable: cache.writeable, accessToken: tokenProps.token }); cache.catalogContainers.set(containerId, container); // save the container in the map of ContainerIds so we can reuse them container.connect(cache); return container; } function getReadonlyCloudCache() { return readonlyCloudCache ??= makeCloudCache({ cacheName: "catalogs", cacheSize: "10G" }, false); } ; function getWritableCloudCache() { return writeableCloudCache ??= makeCloudCache({ cacheName: "writeableCatalogs", cacheSize: "10G" }, true); } ; async function getReadonlyContainer(containerId) { return getCatalogContainerObj(getReadonlyCloudCache(), containerId); } ; async function getWriteableContainer(containerId) { return getCatalogContainerObj(getWritableCloudCache(), containerId); } ; /** Throw an error if the write lock is not held for the supplied container */ function ensureLocked(container, reason) { if (!container.hasWriteLock) CloudSqliteError.throwError("write-lock-not-held", { message: `Write lock must be held to ${reason}` }); } /** update the manifest in a CatalogIModel (calls `saveChanges`) */ function updateManifest(nativeDb, manifest) { nativeDb.saveLocalValue(catalogManifestName, JSON.stringify(manifest)); nativeDb.saveChanges("update manifest"); } function catalogDbNameWithDefault(dbName) { return dbName ?? "catalog-db"; } /** A StandaloneDb that holds a CatalogIModel */ class CatalogDbImpl extends StandaloneDb { isEditable() { return false; } getManifest() { const manifestString = this[_nativeDb].queryLocalValue(catalogManifestName); if (undefined === manifestString) return undefined; return JSON.parse(manifestString); } getVersion() { return CloudSqlite.parseDbFileName(this[_nativeDb].getFilePath()).version; } getInfo() { return { manifest: this.getManifest(), version: this.getVersion() }; } } /** * A CatalogDb that permits editing. * This class ensures that CatalogIModels never have a Txn table when they are published. * It also automatically updates the `lastEditedBy` field in the CatalogManifest. */ class EditableCatalogDbImpl extends CatalogDbImpl { isEditable() { return true; } updateCatalogManifest(manifest) { updateManifest(this[_nativeDb], manifest); } // Make sure the txn table is deleted and update the manifest every time we close. beforeClose() { try { const manifest = this.getManifest(); const container = this.cloudContainer; if (container && manifest) { manifest.lastEditedBy = CloudSqlite.getWriteLockHeldBy(container); this.updateCatalogManifest(manifest); } // when saved, CatalogIModels should never have any Txns. If we wanted to create a changeset, we'd have to do it here. this[_nativeDb].deleteAllTxns(); } catch { } // ignore errors attempting to update // might also want to vacuum here? super.beforeClose(); } } function findCatalogByKey(key) { return CatalogDbImpl.findByKey(key); } /** @beta */ export var CatalogDb; (function (CatalogDb) { /** Create a new [[BlobContainer]] to hold versions of a [[CatalogDb]]. * @returns The properties of the newly created container. * @note creating new containers requires "admin" authorization. */ async function createNewContainer(args) { const dbName = catalogDbNameWithDefault(args.dbName); CloudSqlite.validateDbName(dbName); CloudSqlite.validateDbVersion(args.version); const tmpName = join(KnownLocations.tmpdir, `temp-${dbName}`); try { // make a copy of the file they supplied so we can modify its contents safely fs.copyFileSync(args.localCatalogFile, tmpName); const nativeDb = new IModelNative.platform.DgnDb(); nativeDb.openIModel(tmpName, OpenMode.ReadWrite); nativeDb.setITwinId(Guid.empty); // catalogs must be a StandaloneDb nativeDb.setIModelId(Guid.createValue()); // make sure its iModelId is unique updateManifest(nativeDb, args.manifest); // store the manifest inside the Catalog nativeDb.deleteAllTxns(); // Catalogs should never have Txns (and, this must be empty before resetting BriefcaseId) nativeDb.resetBriefcaseId(BriefcaseIdValue.Unassigned); // catalogs should always be unassigned nativeDb.saveChanges(); // save change to briefcaseId nativeDb.vacuum(); nativeDb.closeFile(); } catch (e) { CatalogError.throwError("invalid-seed-catalog", { message: "Illegal seed catalog", ...args, cause: e }); } const userToken = await IModelHost.getAccessToken(); // create tne new container from the blob service, requires "admin" authorization const cloudContainerProps = await CloudSqlite.getBlobService().create({ scope: { iTwinId: args.iTwinId }, metadata: { ...args.metadata, containerType: "CatalogIModel" }, userToken }); // now create a CloudSqlite container object to access it const container = CloudSqlite.createCloudContainer({ accessToken: await CloudSqlite.requestToken(cloudContainerProps), accessLevel: "admin", writeable: true, baseUri: cloudContainerProps.baseUri, containerId: cloudContainerProps.containerId, storageType: cloudContainerProps.provider, }); // initialize the container for use by CloudSqlite container.initializeContainer({ blockSize: 4 * 1024 * 1024 }); container.connect(getWritableCloudCache()); // upload the initial version of the Catalog await CloudSqlite.withWriteLock({ user: "initialize", container }, async () => { await CloudSqlite.uploadDb(container, { dbName: CloudSqlite.makeSemverName(dbName, args.version), localFileName: tmpName }); fs.unlinkSync(tmpName); // delete temporary copy of catalog }); container.disconnect(); return cloudContainerProps; } CatalogDb.createNewContainer = createNewContainer; /** Acquire the write lock for a [CatalogIModel]($common) container. Only one person may obtain the write lock at a time. * You must obtain the lock before attempting to write to the container via functions like [[CatalogDb.openEditable]] and [[CatalogDb.createNewVersion]]. * @note This requires "write" authorization to the container */ async function acquireWriteLock(args) { const container = await getWriteableContainer(args.containerId); return CloudSqlite.acquireWriteLock({ container, user: args.username }); } CatalogDb.acquireWriteLock = acquireWriteLock; /** Release the write lock on a [CatalogIModel]($common) container. This uploads all changes made while the lock is held, so they become visible to other users. */ async function releaseWriteLock(args) { const container = await getWriteableContainer(args.containerId); if (args.abandon) container.abandonChanges(); CloudSqlite.releaseWriteLock(container); } CatalogDb.releaseWriteLock = releaseWriteLock; /** Open an [[EditableCatalogDb]] for write access. * @note Once a version of a catalog iModel has been published (i.e. the write lock has been released), it is no longer editable, *unless* it is a prerelease version. * @note The write lock must be held for this operation to succeed */ async function openEditable(args) { const dbName = catalogDbNameWithDefault(args.dbName); if (undefined === args.containerId) // local file? return EditableCatalogDbImpl.openFile(dbName, OpenMode.ReadWrite, args); const container = await getWriteableContainer(args.containerId); ensureLocked(container, "open a Catalog for editing"); // editing Catalogs requires the write lock // look up the full name with version const dbFullName = CloudSqlite.querySemverMatch({ container, dbName, version: args.version ?? "*" }); if (!CloudSqlite.isSemverEditable(dbFullName, container)) CloudSqliteError.throwError("already-published", { message: "Catalog has already been published and is not editable. Make a new version first.", ...args }); if (args.prefetch) CloudSqlite.startCloudPrefetch(container, dbFullName); return EditableCatalogDbImpl.openFile(dbFullName, OpenMode.ReadWrite, { container, ...args }); } CatalogDb.openEditable = openEditable; /** Open a [[CatalogDb]] for read-only access. */ async function openReadonly(args) { const dbName = catalogDbNameWithDefault(args.dbName); if (undefined === args.containerId) // local file? return CatalogDbImpl.openFile(dbName, OpenMode.Readonly, args); const container = await getReadonlyContainer(args.containerId); if (args.syncWithCloud) container.checkForChanges(); const dbFullName = CloudSqlite.querySemverMatch({ container, ...args, dbName }); if (args.prefetch) CloudSqlite.startCloudPrefetch(container, dbFullName); return CatalogDbImpl.openFile(dbFullName, OpenMode.Readonly, { container, ...args }); } CatalogDb.openReadonly = openReadonly; /** * Create a new version of a [CatalogIModel]($common) as a copy of an existing version. Immediately after this operation, the new version will be an exact copy * of the source CatalogIModel. Then, use [[CatalogDb.openEditable]] to modify the new version with new content. * @note The write lock must be held for this operation to succeed */ async function createNewVersion(args) { const container = await getWriteableContainer(args.containerId); ensureLocked(container, "create a new version"); return CloudSqlite.createNewDbVersion(container, { ...args, fromDb: { ...args.fromDb, dbName: catalogDbNameWithDefault(args.fromDb.dbName) } }); } CatalogDb.createNewVersion = createNewVersion; })(CatalogDb || (CatalogDb = {})); /** * Handler for Ipc access to CatalogIModels. Registered by NativeHost. * @internal */ export class CatalogIModelHandler extends IpcHandler { get channelName() { return "catalogIModel/ipc"; } async createNewContainer(args) { return CatalogDb.createNewContainer(args); } async acquireWriteLock(args) { return CatalogDb.acquireWriteLock(args); } async releaseWriteLock(args) { return CatalogDb.releaseWriteLock(args); } async openReadonly(args) { return ((await CatalogDb.openReadonly(args)).getConnectionProps()); } async openEditable(args) { return (await CatalogDb.openEditable(args)).getConnectionProps(); } async createNewVersion(args) { return CatalogDb.createNewVersion(args); } async getInfo(key) { return findCatalogByKey(key).getInfo(); } async updateCatalogManifest(key, manifest) { findCatalogByKey(key).updateCatalogManifest(manifest); } } //# sourceMappingURL=CatalogDb.js.map