@adpt/core
Version:
AdaptJS core library
235 lines • 9.09 kB
JavaScript
;
/*
* Copyright 2018-2019 Unbounded Systems, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs = tslib_1.__importStar(require("fs-extra"));
const lodash_1 = require("lodash");
const moment_1 = tslib_1.__importDefault(require("moment"));
const path = tslib_1.__importStar(require("path"));
const lockfile_1 = require("../utils/lockfile");
const history_1 = require("./history");
// These are exported only for testing
exports.dataDirName = "dataDir";
exports.domFilename = "adapt_dom.xml";
exports.stateFilename = "adapt_state.json";
exports.observationsFilename = "adapt_observations.json";
exports.infoFilename = "adapt_deploy.json";
// Example dirName: 00000-preAct-2018-11-15T22:20:46+00:00
const dirNameRegEx = /^(\d{5})-([^-]+)-(.*)$/;
function dirStatus(dirName) {
const m = dirName.match(dirNameRegEx);
if (m) {
const status = m[2];
if (history_1.isHistoryStatus(status))
return status;
}
throw new Error(`History directory '${dirName}' unrecognized format`);
}
function dirStatusMatches(dirName, expected) {
if (expected === undefined)
return true;
const status = dirStatus(dirName);
if (expected === history_1.HistoryStatus.complete)
return history_1.isStatusComplete(status);
else
return dirStatus(dirName) === expected;
}
class LocalHistoryStore {
constructor(db, dbPath, rootDir) {
this.db = db;
this.dbPath = dbPath;
this.rootDir = rootDir;
this.dataDir = path.join(rootDir, exports.dataDirName + ".uncommitted");
}
async init(create) {
let currentInfo;
try {
currentInfo = this.db.getData(this.dbPath);
}
catch (err) {
if (err.name !== "DataError")
throw err;
if (!create)
throw new Error(`History store does not exist`);
currentInfo = {
stateDirs: [],
};
this.db.push(this.dbPath, currentInfo);
}
await fs.ensureDir(this.rootDir);
}
async destroy() {
await this.releaseDataDir();
try {
this.db.delete(this.dbPath);
}
catch (err) { /**/ }
try {
await fs.remove(this.rootDir);
}
catch (err) { /**/ }
}
async commitEntry(toStore) {
const { domXml, stateJson, observationsJson } = toStore, info = tslib_1.__rest(toStore, ["domXml", "stateJson", "observationsJson"]);
if (toStore.dataDir !== this.dataDir) {
throw new Error(`Internal error: commiting invalid dataDir ` +
`'${toStore.dataDir}'. Should be '${this.dataDir}'`);
}
const dirName = await this.nextDirName(toStore.status);
const dirPath = path.join(this.rootDir, dirName);
info.dataDir = path.join(dirPath, exports.dataDirName);
await fs.outputFile(path.join(dirPath, exports.domFilename), domXml);
await fs.outputFile(path.join(dirPath, exports.stateFilename), stateJson);
await fs.outputFile(path.join(dirPath, exports.observationsFilename), observationsJson);
await fs.outputJson(path.join(dirPath, exports.infoFilename), info);
// If we're committing preAct, the directory remains in place and
// locked; just snapshot it with a copy. Otherwise, we're done with it
// and we can move it and unlock it.
if (toStore.status === history_1.HistoryStatus.preAct) {
await fs.copy(this.dataDir, info.dataDir, {
overwrite: false,
errorOnExist: true,
preserveTimestamps: true,
});
}
else {
await fs.move(this.dataDir, info.dataDir);
await this.releaseDataDir();
}
this.db.push(this.dbPath + "/stateDirs[]", dirName, false /*override*/);
}
async last(withStatus) {
const lastDir = this.lastDir(withStatus);
if (lastDir === undefined)
return undefined;
return this.historyEntry(lastDir);
}
async remove(historyName) {
await fs.remove(historyName);
const info = await this.getInfo();
info.stateDirs = info.stateDirs.filter((d) => d !== historyName);
await this.setInfo(info);
}
async historyEntry(historyName) {
const dirName = path.join(this.rootDir, historyName);
const domXml = await fs.readFile(path.join(dirName, exports.domFilename));
const stateJson = await fs.readFile(path.join(dirName, exports.stateFilename));
const observationsJson = await fs.readFile(path.join(dirName, exports.observationsFilename));
const info = await fs.readJson(path.join(dirName, exports.infoFilename));
return {
domXml: domXml.toString(),
stateJson: stateJson.toString(),
observationsJson: observationsJson.toString(),
fileName: info.fileName,
projectRoot: info.projectRoot,
stackName: info.stackName,
dataDir: info.dataDir,
status: info.status,
};
}
async getDataDir(withStatus) {
if (this.dataDirRelease) {
throw new Error(`Internal error: attempting to lock dataDir ` +
`'${this.dataDir}' twice`);
}
// dataDir must exist in order to lock it.
await fs.ensureDir(this.dataDir);
try {
this.dataDirRelease = await lockfile_1.lock(this.dataDir);
}
catch (e) {
throw new Error(`Unable to get exclusive access to deployment ` +
`directory '${this.dataDir}'. Please retry in a moment. ` +
`[${e.message}]`);
}
// Ensure we start from the last committed state
await fs.remove(this.dataDir);
await fs.ensureDir(this.dataDir);
const last = this.lastDir(withStatus);
if (last) {
await fs.copy(path.join(this.rootDir, last, exports.dataDirName), this.dataDir, {
preserveTimestamps: true,
});
}
return this.dataDir;
}
async releaseDataDir() {
if (!this.dataDirRelease)
return; // We don't hold the lock
await fs.remove(this.dataDir);
await this.dataDirRelease();
this.dataDirRelease = undefined;
}
async nextDirName(status) {
const lastDir = this.lastDir();
let nextSeq;
if (lastDir === undefined) {
nextSeq = 0;
}
else {
const matches = lastDir.match(/^\d+/);
if (matches == null)
throw new Error(`stateDir entry '${lastDir}' is invalid`);
const lastSeq = parseInt(matches[0], 10);
if (status === history_1.HistoryStatus.preAct) {
// Completely new entry
nextSeq = lastSeq + 1;
}
else {
if (dirStatus(lastDir) !== history_1.HistoryStatus.preAct) {
throw new Error(`Unexpected status for last history entry. ` +
`(lastDir: ${lastDir}, status: ${status})`);
}
// Completion of the current entry
nextSeq = lastSeq;
}
}
const seqStr = lodash_1.padStart(nextSeq.toString(10), 5, "0");
const timestamp = moment_1.default().format();
return `${seqStr}-${status}-${timestamp}`;
}
lastDir(withStatus) {
try {
// The array returned by getData is the actual object stored in the
// DB. Make a copy.
const dirList = this.db.getData(this.dbPath + "/stateDirs").slice();
while (true) {
const dir = dirList.pop();
if (dir === undefined || dirStatusMatches(dir, withStatus))
return dir;
}
}
catch (err) {
if (err.name !== "DataError")
throw err;
return undefined;
}
}
async getInfo() {
return this.db.getData(this.dbPath);
}
async setInfo(info) {
return this.db.push(this.dbPath, info, true);
}
}
async function createLocalHistoryStore(db, dbPath, rootDir, create) {
const h = new LocalHistoryStore(db, dbPath, rootDir);
await h.init(create);
return h;
}
exports.createLocalHistoryStore = createLocalHistoryStore;
//# sourceMappingURL=local_history.js.map