@itwin/core-backend
Version:
iTwin.js backend components
635 lines • 30 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Workspace
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.workspaceManifestProperty = exports.workspaceDbFileExt = void 0;
exports.constructWorkspaceDb = constructWorkspaceDb;
exports.constructWorkspace = constructWorkspace;
exports.constructWorkspaceEditor = constructWorkspaceEditor;
exports.validateWorkspaceContainerId = validateWorkspaceContainerId;
exports.throwWorkspaceDbLoadErrors = throwWorkspaceDbLoadErrors;
const crypto_1 = require("crypto");
const fs = require("fs-extra");
const path_1 = require("path");
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const CloudSqlite_1 = require("../../CloudSqlite");
const IModelHost_1 = require("../../IModelHost");
const IModelJsFs_1 = require("../../IModelJsFs");
const SQLiteDb_1 = require("../../SQLiteDb");
const Settings_1 = require("../../workspace/Settings");
const Workspace_1 = require("../../workspace/Workspace");
const WorkspaceEditor_1 = require("../../workspace/WorkspaceEditor");
const WorkspaceSqliteDb_1 = require("./WorkspaceSqliteDb");
const SettingsImpl_1 = require("./SettingsImpl");
const Symbols_1 = require("../Symbols");
function workspaceDbNameWithDefault(dbName) {
return dbName ?? "workspace-db";
}
/** file extension for local WorkspaceDbs */
exports.workspaceDbFileExt = "itwin-workspace";
function makeWorkspaceCloudCache(arg) {
const cache = CloudSqlite_1.CloudSqlite.CloudCaches.getCache(arg);
if (undefined === cache.workspaceContainers) // if we just created this container, add the map.
CloudSqlite_1.CloudSqlite.addHiddenProperty(cache, "workspaceContainers", new Map());
return cache;
}
function getContainerFullId(props) {
return `${props.baseUri}/${props.containerId}`;
}
function getWorkspaceCloudContainer(props, cache) {
const id = getContainerFullId(props);
let cloudContainer = cache.workspaceContainers.get(id);
if (undefined !== cloudContainer)
return cloudContainer;
cloudContainer = CloudSqlite_1.CloudSqlite.createCloudContainer(props);
cache.workspaceContainers.set(id, cloudContainer);
cloudContainer.connectCount = 0;
CloudSqlite_1.CloudSqlite.addHiddenProperty(cloudContainer, "sharedConnect", function () {
if (this.connectCount++ === 0) {
this.connect(cache);
return true;
}
return false;
});
CloudSqlite_1.CloudSqlite.addHiddenProperty(cloudContainer, "sharedDisconnect", function () {
if (--this.connectCount <= 0) {
this.disconnect();
cache.workspaceContainers.delete(id);
this.connectCount = 0;
}
});
return cloudContainer;
}
class WorkspaceContainerImpl {
[Symbols_1._implementationProhibited] = undefined;
workspace;
filesDir;
id;
fromProps;
_cloudContainer;
get cloudContainer() {
return this._cloudContainer;
}
_wsDbs = new Map();
get dirName() { return (0, path_1.join)(this.workspace.containerDir, this.id); }
constructor(workspace, props) {
validateWorkspaceContainerId(props.containerId);
this.workspace = workspace;
this.id = props.containerId;
this.fromProps = props;
if (props.baseUri !== "")
this._cloudContainer = getWorkspaceCloudContainer(props, this.workspace.getCloudCache());
workspace.addContainer(this);
this.filesDir = (0, path_1.join)(this.dirName, "Files");
const cloudContainer = this.cloudContainer;
if (undefined === cloudContainer)
return;
// sharedConnect returns true if we just connected (if the container is shared, it may have already been connected)
if (cloudContainer.sharedConnect() && false !== props.syncOnConnect) {
try {
cloudContainer.checkForChanges();
}
catch {
// must be offline
}
}
}
resolveDbFileName(props) {
const container = this.cloudContainer;
if (undefined === container)
return (0, path_1.join)(this.dirName, `${props.dbName}.${exports.workspaceDbFileExt}`); // local file, versions not allowed
return CloudSqlite_1.CloudSqlite.querySemverMatch({ ...props, container, dbName: workspaceDbNameWithDefault(props.dbName) });
}
addWorkspaceDb(toAdd) {
if (undefined !== this._wsDbs.get(toAdd.dbName))
core_common_1.WorkspaceError.throwError("already-exists", { message: `workspaceDb '${toAdd.dbName}' already exists in workspace` });
this._wsDbs.set(toAdd.dbName, toAdd);
}
getWorkspaceDb(props) {
return this._wsDbs.get(workspaceDbNameWithDefault(props?.dbName)) ?? new WorkspaceDbImpl(props ?? {}, this);
}
closeWorkspaceDb(toDrop) {
const name = toDrop.dbName;
const wsDb = this._wsDbs.get(name);
if (wsDb === toDrop) {
this._wsDbs.delete(name);
wsDb.close();
}
}
close() {
for (const [_name, db] of this._wsDbs)
db.close();
this._wsDbs.clear();
this.cloudContainer?.sharedDisconnect();
}
}
/** Implementation of WorkspaceDb */
class WorkspaceDbImpl {
[Symbols_1._implementationProhibited] = undefined;
sqliteDb = new WorkspaceSqliteDb_1.WorkspaceSqliteDb();
dbName;
_container;
onClose = new core_bentley_1.BeEvent();
dbFileName;
_manifest;
/** true if this WorkspaceDb is currently open */
get isOpen() { return this.sqliteDb.isOpen; }
get container() { return this._container; }
queryFileResource(rscName) {
const info = this.sqliteDb[Symbols_1._nativeDb].queryEmbeddedFile(rscName);
if (undefined === info)
return undefined;
// since resource names can contain illegal characters, path separators, etc., we make the local file name from its hash, in hex.
let localFileName = (0, path_1.join)(this._container.filesDir, (0, crypto_1.createHash)("sha1").update(this.dbFileName).update(rscName).digest("hex"));
if (info.fileExt !== "") // since some applications may expect to see the extension, append it here if it was supplied.
localFileName = `${localFileName}.${info.fileExt}`;
return { localFileName, info };
}
constructor(props, container) {
this.dbName = workspaceDbNameWithDefault(props.dbName);
CloudSqlite_1.CloudSqlite.validateDbName(this.dbName);
this._container = container;
this.dbFileName = container.resolveDbFileName(props);
container.addWorkspaceDb(this);
if (true === props.prefetch)
this.prefetch();
}
open() {
this.sqliteDb.openDb(this.dbFileName, core_bentley_1.OpenMode.Readonly, this._container.cloudContainer);
}
close() {
if (this.isOpen) {
this.onClose.raiseEvent();
this.sqliteDb.closeDb();
this._container.closeWorkspaceDb(this);
}
}
get version() {
const cloudContainer = this.container.cloudContainer;
if (undefined === cloudContainer)
return "1.0.0"; // local file, no versioning. return default
return CloudSqlite_1.CloudSqlite.parseDbFileName(this.dbFileName).version;
}
get manifest() {
return this._manifest ??= this.withOpenDb((db) => {
const manifestJson = db[Symbols_1._nativeDb].queryFileProperty(exports.workspaceManifestProperty, true);
return manifestJson ? JSON.parse(manifestJson) : { workspaceName: this.dbName };
});
}
withOpenDb(operation) {
const done = this.isOpen ? () => { } : (this.open(), () => this.close());
try {
return operation(this.sqliteDb);
}
finally {
done();
}
}
getString(rscName) {
return this.withOpenDb((db) => {
return db.withSqliteStatement("SELECT value from strings WHERE id=?", (stmt) => {
stmt.bindString(1, rscName);
return core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step() ? stmt.getValueString(0) : undefined;
});
});
}
getBlobReader(rscName) {
return this.sqliteDb.withSqliteStatement("SELECT rowid from blobs WHERE id=?", (stmt) => {
stmt.bindString(1, rscName);
const blobReader = SQLiteDb_1.SQLiteDb.createBlobIO();
blobReader.open(this.sqliteDb[Symbols_1._nativeDb], { tableName: "blobs", columnName: "value", row: stmt.getValueInteger(0) });
return blobReader;
});
}
getBlob(rscName) {
return this.withOpenDb((db) => {
return db.withSqliteStatement("SELECT value from blobs WHERE id=?", (stmt) => {
stmt.bindString(1, rscName);
return core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step() ? stmt.getValueBlob(0) : undefined;
});
});
}
getFile(rscName, targetFileName) {
return this.withOpenDb((db) => {
const file = this.queryFileResource(rscName);
if (!file)
return undefined;
const info = file.info;
const localFileName = targetFileName ?? file.localFileName;
// check whether the file is already up to date.
const stat = fs.existsSync(localFileName) && fs.statSync(localFileName);
if (stat && Math.round(stat.mtimeMs) === info.date && stat.size === info.size)
return localFileName; // yes, we're done
// extractEmbeddedFile fails if the file exists or if the directory does not exist
if (stat)
fs.removeSync(localFileName);
else
IModelJsFs_1.IModelJsFs.recursiveMkDirSync((0, path_1.dirname)(localFileName));
db[Symbols_1._nativeDb].extractEmbeddedFile({ name: rscName, localFileName });
const date = new Date(info.date);
fs.utimesSync(localFileName, date, date); // set the last-modified date of the file to match date in container
fs.chmodSync(localFileName, "0444"); // set file readonly
return localFileName;
});
}
prefetch(opts) {
const cloudContainer = this._container.cloudContainer;
if (cloudContainer === undefined)
core_common_1.WorkspaceError.throwError("no-cloud-container", { message: "no cloud container to prefetch" });
return CloudSqlite_1.CloudSqlite.startCloudPrefetch(cloudContainer, this.dbFileName, opts);
}
queryResources(args) {
const table = "blob" !== args.type ? "strings" : "blobs";
this.withOpenDb((db) => {
const where = undefined !== args.namePattern ? ` WHERE id ${args.nameCompare ?? "="} ?` : "";
db.withSqliteStatement(`SELECT id from ${table}${where}`, (stmt) => {
function* makeIterable() {
while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
yield stmt.getValueString(0);
}
}
if (undefined !== args.namePattern) {
stmt.bindString(1, args.namePattern);
}
args.callback(makeIterable());
});
});
}
}
/** Implementation of Workspace */
class WorkspaceImpl {
[Symbols_1._implementationProhibited] = undefined;
_containers = new Map();
containerDir;
settings;
_cloudCache;
getCloudCache() {
return this._cloudCache ??= makeWorkspaceCloudCache({ cacheName: "Workspace", cacheSize: "20G" });
}
constructor(settings, opts) {
this.settings = settings;
this.containerDir = opts?.containerDir ?? (0, path_1.join)(IModelHost_1.IModelHost.cacheDir, "Workspace");
let settingsFiles = opts?.settingsFiles;
if (settingsFiles) {
if (typeof settingsFiles === "string")
settingsFiles = [settingsFiles];
settingsFiles.forEach((file) => settings.addFile(file, Settings_1.SettingsPriority.application));
}
}
addContainer(toAdd) {
if (undefined !== this._containers.get(toAdd.id))
core_common_1.WorkspaceError.throwError("container-exists", { message: `container ${toAdd.id} already exists in workspace` });
this._containers.set(toAdd.id, toAdd);
}
findContainer(containerId) {
return this._containers.get(containerId);
}
getContainer(props) {
return this.findContainer(props.containerId) ?? new WorkspaceContainerImpl(this, props);
}
async getContainerAsync(props) {
const accessToken = props.accessToken ?? ((props.baseUri === "") || props.isPublic) ? "" : await CloudSqlite_1.CloudSqlite.requestToken({ ...props, accessLevel: "read" });
return this.getContainer({ ...props, accessToken });
}
async getWorkspaceDb(props) {
let container = this.findContainer(props.containerId);
if (undefined === container) {
const accessToken = props.isPublic ? "" : await CloudSqlite_1.CloudSqlite.requestToken({ accessLevel: "read", ...props });
container = new WorkspaceContainerImpl(this, { ...props, accessToken });
}
return container.getWorkspaceDb(props);
}
async loadSettingsDictionary(props, problems) {
if (!Array.isArray(props))
props = [props];
for (const prop of props) {
const db = await this.getWorkspaceDb(prop);
db.open();
try {
const manifest = db.manifest;
const dictProps = { name: prop.resourceName, workspaceDb: db, priority: prop.priority };
// don't load if we already have this dictionary. Happens if the same WorkspaceDb is in more than one list
if (undefined === this.settings.getDictionary(dictProps)) {
const settingsJson = db.getString(prop.resourceName);
if (undefined === settingsJson)
throwWorkspaceDbLoadError(`could not load setting dictionary resource '${prop.resourceName}' from: '${manifest.workspaceName}'`, prop, db);
db.close(); // don't leave this db open in case we're going to find another dictionary in it recursively.
this.settings.addJson(dictProps, settingsJson);
const dict = this.settings.getDictionary(dictProps);
if (dict) {
Workspace_1.Workspace.onSettingsDictionaryLoadedFn({ dict, from: db });
// if the dictionary we just loaded has a "settingsWorkspaces" entry, load them too, recursively
const nested = dict.getSetting(Workspace_1.WorkspaceSettingNames.settingsWorkspaces);
if (nested !== undefined) {
IModelHost_1.IModelHost.settingsSchemas.validateSetting(nested, Workspace_1.WorkspaceSettingNames.settingsWorkspaces);
await this.loadSettingsDictionary(nested, problems);
}
}
}
}
catch (e) {
db.close();
problems?.push(e);
}
}
}
close() {
this.settings.close();
for (const [_id, container] of this._containers)
container.close();
this._containers.clear();
}
resolveWorkspaceDbSetting(settingName, filter) {
const settingDef = IModelHost_1.IModelHost.settingsSchemas.settingDefs.get(settingName);
const combine = settingDef?.combineArray === true;
filter = filter ?? (() => true);
const result = [];
for (const entry of this.settings.getSettingEntries(settingName)) {
for (const dbProp of entry.value) {
if (filter(dbProp, entry.dictionary)) {
result.push(dbProp);
}
}
if (!combine) {
break;
}
}
return result;
}
async getWorkspaceDbs(args) {
const dbList = (args.settingName !== undefined) ? this.resolveWorkspaceDbSetting(args.settingName, args.filter) : args.dbs;
const result = [];
const pushUnique = (wsDb) => {
for (const db of result) {
// if we already have this db, skip it. The test below also has to consider that we create a separate WorkspaceDb object for the same
// database from more than one Workspace (though then they must use a "shared" CloudContainer).
if (db === wsDb || ((db.container.cloudContainer === wsDb.container.cloudContainer) && (db.dbFileName === wsDb.dbFileName)))
return; // this db is redundant
}
result.push(wsDb);
};
for (const dbProps of dbList) {
try {
pushUnique(await this.getWorkspaceDb(dbProps));
}
catch (e) {
const loadErr = e;
loadErr.wsDbProps = dbProps;
args.problems?.push(loadErr);
}
}
return result;
}
}
const workspaceEditorName = "WorkspaceEditor"; // name of the cache for the editor workspace
class EditorWorkspaceImpl extends WorkspaceImpl {
getCloudCache() {
return this._cloudCache ??= makeWorkspaceCloudCache({ cacheName: workspaceEditorName, cacheSize: "20G" });
}
}
class EditorImpl {
[Symbols_1._implementationProhibited] = undefined;
workspace = new EditorWorkspaceImpl(new SettingsImpl_1.SettingsImpl(), { containerDir: (0, path_1.join)(IModelHost_1.IModelHost.cacheDir, workspaceEditorName) });
async initializeContainer(args) {
class CloudAccess extends CloudSqlite_1.CloudSqlite.DbAccess {
static _cacheName = workspaceEditorName;
static async initializeWorkspace(args) {
const props = await this.createBlobContainer({ scope: args.scope, metadata: { ...args.metadata, containerType: "workspace" } });
const dbFullName = CloudSqlite_1.CloudSqlite.makeSemverName(workspaceDbNameWithDefault(args.dbName), "0.0.0");
await super._initializeDb({ ...args, props, dbName: dbFullName, dbType: WorkspaceSqliteDb_1.WorkspaceSqliteDb, blockSize: "4M" });
return props;
}
}
return CloudAccess.initializeWorkspace(args);
}
async createNewCloudContainer(args) {
const cloudContainer = await this.initializeContainer(args);
const userToken = await IModelHost_1.IModelHost.authorizationClient?.getAccessToken();
const accessToken = await CloudSqlite_1.CloudSqlite.requestToken({ ...cloudContainer, accessLevel: "write", userToken });
return this.getContainer({ accessToken, ...cloudContainer, writeable: true, description: args.metadata.description });
}
getContainer(props) {
return this.workspace.findContainer(props.containerId) ?? new EditorContainerImpl(this.workspace, props);
}
async getContainerAsync(props) {
const accessToken = props.accessToken ?? (props.baseUri === "") ? "" : await CloudSqlite_1.CloudSqlite.requestToken({ ...props, accessLevel: "write" });
return this.getContainer({ ...props, accessToken });
}
close() {
this.workspace.close();
}
}
class EditorContainerImpl extends WorkspaceContainerImpl {
get cloudContainer() {
return super.cloudContainer;
}
get cloudProps() {
const cloudContainer = this.cloudContainer;
if (undefined === cloudContainer)
return undefined;
return {
baseUri: cloudContainer.baseUri,
containerId: cloudContainer.containerId,
storageType: cloudContainer.storageType,
isPublic: cloudContainer.isPublic,
};
}
async createNewWorkspaceDbVersion(args) {
const container = this.cloudContainer;
if (undefined === container)
core_common_1.WorkspaceError.throwError("no-cloud-container", { message: "versions require cloud containers" });
const fromDb = { ...args.fromProps, dbName: workspaceDbNameWithDefault(args.fromProps?.dbName) };
return CloudSqlite_1.CloudSqlite.createNewDbVersion(container, { ...args, fromDb });
}
getWorkspaceDb(props) {
return this.getEditableDb(props);
}
getEditableDb(props) {
const db = this._wsDbs.get(workspaceDbNameWithDefault(props.dbName)) ?? new EditableDbImpl(props, this);
if (this.cloudContainer && !CloudSqlite_1.CloudSqlite.isSemverEditable(db.dbFileName, this.cloudContainer)) {
this._wsDbs.delete(workspaceDbNameWithDefault(props.dbName));
core_common_1.CloudSqliteError.throwError("already-published", { message: `${db.dbFileName} has been published and is not editable. Make a new version first.` });
}
return db;
}
acquireWriteLock(user) {
if (this.cloudContainer) {
this.cloudContainer.acquireWriteLock(user);
this.cloudContainer.writeLockHeldBy = user;
}
}
releaseWriteLock() {
if (this.cloudContainer) {
this.cloudContainer.releaseWriteLock();
this.cloudContainer.writeLockHeldBy = undefined;
}
}
abandonChanges() {
if (this.cloudContainer) {
this.cloudContainer.abandonChanges();
this.cloudContainer.writeLockHeldBy = undefined;
}
}
async createDb(args) {
if (!this.cloudContainer) {
WorkspaceEditor_1.WorkspaceEditor.createEmptyDb({ localFileName: this.resolveDbFileName(args), manifest: args.manifest });
}
else {
// currently the only way to create a workspaceDb in a cloud container is to create a temporary workspaceDb and upload it.
const tempDbFile = (0, path_1.join)(IModelHost_1.KnownLocations.tmpdir, `empty.${exports.workspaceDbFileExt}`);
if (fs.existsSync(tempDbFile))
IModelJsFs_1.IModelJsFs.removeSync(tempDbFile);
WorkspaceEditor_1.WorkspaceEditor.createEmptyDb({ localFileName: tempDbFile, manifest: args.manifest });
await CloudSqlite_1.CloudSqlite.uploadDb(this.cloudContainer, { localFileName: tempDbFile, dbName: CloudSqlite_1.CloudSqlite.makeSemverName(workspaceDbNameWithDefault(args.dbName)) });
IModelJsFs_1.IModelJsFs.removeSync(tempDbFile);
}
return this.getWorkspaceDb(args);
}
}
class EditableDbImpl extends WorkspaceDbImpl {
get container() {
(0, core_bentley_1.assert)(this._container instanceof EditorContainerImpl);
return this._container;
}
static validateResourceName(name) {
if (name.trim() !== name)
core_common_1.WorkspaceError.throwError("invalid-name", { message: "resource name may not have leading or trailing spaces" });
if (name.length > 1024) {
core_common_1.WorkspaceError.throwError("invalid-name", { message: "resource name too long" });
}
}
validateResourceSize(val) {
const len = typeof val === "string" ? val.length : val.byteLength;
if (len > (1024 * 1024 * 1024)) // one gigabyte
core_common_1.WorkspaceError.throwError("too-large", { message: "value is too large" });
}
get cloudProps() {
const props = this._container.cloudProps;
if (props === undefined)
return undefined;
const parsed = CloudSqlite_1.CloudSqlite.parseDbFileName(this.dbFileName);
return { ...props, dbName: parsed.dbName, version: parsed.version };
}
open() {
this.sqliteDb.openDb(this.dbFileName, core_bentley_1.OpenMode.ReadWrite, this._container.cloudContainer);
}
close() {
if (this.isOpen) {
// whenever we close an EditableDb, update the name of the last editor in the manifest
const lastEditedBy = this._container.cloudContainer?.writeLockHeldBy;
if (lastEditedBy !== undefined)
this.updateManifest({ ...this.manifest, lastEditedBy });
// make sure all changes were saved before we close
this.sqliteDb.saveChanges();
}
super.close();
}
getFileModifiedTime(localFileName) {
return Math.round(fs.statSync(localFileName).mtimeMs);
}
performWriteSql(rscName, sql, bind) {
this.sqliteDb.withSqliteStatement(sql, (stmt) => {
stmt.bindString(1, rscName);
bind?.(stmt);
const rc = stmt.step();
if (core_bentley_1.DbResult.BE_SQLITE_DONE !== rc) {
if (core_bentley_1.DbResult.BE_SQLITE_CONSTRAINT_PRIMARYKEY === rc)
core_common_1.WorkspaceError.throwError("resource-exists", { message: `resource "${rscName}" already exists` });
core_common_1.WorkspaceError.throwError("write-error", { message: `workspace [${sql}], rc=${rc}` });
}
});
this.sqliteDb.saveChanges();
}
updateManifest(manifest) {
this.sqliteDb[Symbols_1._nativeDb].saveFileProperty(exports.workspaceManifestProperty, JSON.stringify(manifest));
this._manifest = undefined;
}
updateSettingsResource(settings, rscName) {
this.updateString(rscName ?? "settingsDictionary", JSON.stringify(settings));
}
addString(rscName, val) {
EditableDbImpl.validateResourceName(rscName);
this.validateResourceSize(val);
this.performWriteSql(rscName, "INSERT INTO strings(id,value) VALUES(?,?)", (stmt) => stmt.bindString(2, val));
}
updateString(rscName, val) {
this.validateResourceSize(val);
this.performWriteSql(rscName, "INSERT INTO strings(id,value) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value WHERE value!=excluded.value", (stmt) => stmt.bindString(2, val));
}
removeString(rscName) {
this.performWriteSql(rscName, "DELETE FROM strings WHERE id=?");
}
addBlob(rscName, val) {
EditableDbImpl.validateResourceName(rscName);
this.validateResourceSize(val);
this.performWriteSql(rscName, "INSERT INTO blobs(id,value) VALUES(?,?)", (stmt) => stmt.bindBlob(2, val));
}
updateBlob(rscName, val) {
this.validateResourceSize(val);
this.performWriteSql(rscName, "INSERT INTO blobs(id,value) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value WHERE value!=excluded.value", (stmt) => stmt.bindBlob(2, val));
}
getBlobWriter(rscName) {
return this.sqliteDb.withSqliteStatement("SELECT rowid from blobs WHERE id=?", (stmt) => {
stmt.bindString(1, rscName);
const blobWriter = SQLiteDb_1.SQLiteDb.createBlobIO();
blobWriter.open(this.sqliteDb[Symbols_1._nativeDb], { tableName: "blobs", columnName: "value", row: stmt.getValueInteger(0), writeable: true });
return blobWriter;
});
}
removeBlob(rscName) {
this.performWriteSql(rscName, "DELETE FROM blobs WHERE id=?");
}
addFile(rscName, localFileName, fileExt) {
EditableDbImpl.validateResourceName(rscName);
fileExt = fileExt ?? (0, path_1.extname)(localFileName);
if (fileExt?.[0] === ".")
fileExt = fileExt.slice(1);
this.sqliteDb[Symbols_1._nativeDb].embedFile({ name: rscName, localFileName, date: this.getFileModifiedTime(localFileName), fileExt });
}
updateFile(rscName, localFileName) {
this.queryFileResource(rscName); // throws if not present
this.sqliteDb[Symbols_1._nativeDb].replaceEmbeddedFile({ name: rscName, localFileName, date: this.getFileModifiedTime(localFileName) });
}
removeFile(rscName) {
const file = this.queryFileResource(rscName);
if (undefined === file)
core_common_1.WorkspaceError.throwError("does-not-exist", { message: `file resource "${rscName}" does not exist` });
if (file && fs.existsSync(file.localFileName))
fs.unlinkSync(file.localFileName);
this.sqliteDb[Symbols_1._nativeDb].removeEmbeddedFile(rscName);
}
}
function constructWorkspaceDb(props, container) {
return new WorkspaceDbImpl(props, container);
}
function constructWorkspace(settings, opts) {
return new WorkspaceImpl(settings, opts);
}
function constructWorkspaceEditor() {
return new EditorImpl();
}
/**
* Validate that a WorkspaceContainer.Id is valid.
* The rules for ContainerIds (from Azure, see https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata):
* - may only contain lower case letters, numbers or dashes
* - may not start or end with with a dash nor have more than one dash in a row
* - may not be shorter than 3 or longer than 63 characters
*/
function validateWorkspaceContainerId(id) {
if (!/^(?=.{3,63}$)[a-z0-9]+(-[a-z0-9]+)*$/g.test(id))
core_common_1.WorkspaceError.throwError("invalid-name", { message: `invalid containerId: [${id}]` });
}
exports.workspaceManifestProperty = { namespace: "workspace", name: "manifest" };
function throwWorkspaceDbLoadError(message, wsDbProps, wsDb) {
core_common_1.WorkspaceError.throwError("load-error", { message, wsDb, wsDbProps });
}
function throwWorkspaceDbLoadErrors(message, wsLoadErrors) {
core_common_1.WorkspaceError.throwError("load-errors", { message, wsLoadErrors });
}
//# sourceMappingURL=WorkspaceImpl.js.map