@atomist/sdm-core
Version:
Atomist Software Delivery Machine - Implementation
148 lines (133 loc) • 4.77 kB
text/typescript
/*
* Copyright © 2019 Atomist, Inc.
*
* 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.
*/
import {
HandlerContext,
logger,
} from "@atomist/automation-client";
import { PreferenceStoreFactory } from "@atomist/sdm";
import * as fs from "fs-extra";
import * as _ from "lodash";
import * as os from "os";
import * as path from "path";
import {
lock,
unlock,
} from "proper-lockfile";
import {
AbstractPreferenceStore,
Preference,
} from "./AbstractPreferenceStore";
type PreferenceFile = Record<string, { name: string, value: string, ttl?: number }>;
type WithPreferenceFile<V> = (p: PreferenceFile) => Promise<{ value?: V, save: boolean }>;
/**
* Factory to create a new FilePreferenceStore instance
*/
export const FilePreferenceStoreFactory: PreferenceStoreFactory = ctx => new FilePreferenceStore(ctx);
/**
* PreferenceStore implementation that stores preferences in a shared file.
* Note: this implementation attempts to lock the preference file before reading or writing to it
* but it is not intended for production usage.
*/
export class FilePreferenceStore extends AbstractPreferenceStore {
constructor(context: HandlerContext,
private readonly filePath: string = path.join(os.homedir(), ".atomist", "prefs", "client.prefs.json")) {
super(context);
this.init();
}
protected async doGet(name: string, namespace: string): Promise<Preference | undefined> {
const key = this.scopeKey(name, namespace);
return this.doWithPreferenceFile<Preference | undefined>(async prefs => {
if (!!prefs[key]) {
return {
save: false,
value: {
name,
namespace,
value: prefs[key].value,
ttl: prefs[key].ttl,
},
};
} else {
return {
save: false,
value: undefined,
};
}
});
}
protected async doPut(pref: Preference): Promise<void> {
return this.doWithPreferenceFile<void>(async prefs => {
const key = this.scopeKey(pref.name, pref.namespace);
prefs[key] = {
name: pref.name,
value: pref.value,
ttl: typeof pref.ttl === "number" ? Date.now() + pref.ttl : undefined,
};
return {
save: true,
};
});
}
protected doList(namespace: string): Promise<Preference[]> {
return this.doWithPreferenceFile<Preference[]>(async prefs => {
const values: Preference[] = [];
_.forEach(prefs, (v, k) => {
if (!namespace || k.startsWith(`${namespace}_$_`)) {
values.push(v as Preference);
}
});
return {
save: false,
value: values,
};
});
}
protected doDelete(pref: string, namespace: string): Promise<void> {
return this.doWithPreferenceFile<void>(async prefs => {
const key = this.scopeKey(pref, namespace);
delete prefs[key];
return {
save: true,
};
});
}
private async read(): Promise<PreferenceFile> {
return (await fs.readJson(this.filePath)) as PreferenceFile;
}
private async doWithPreferenceFile<V>(withPreferenceFile: WithPreferenceFile<V>): Promise<V> {
await lock(this.filePath, { retries: 5 });
const prefs = await this.read();
let result;
try {
result = await withPreferenceFile(prefs);
if (result.save) {
await fs.writeJson(this.filePath, prefs);
}
} catch (e) {
logger.error(`Operation on preference file failed: ${e.message}`);
}
await unlock(this.filePath);
return result.value as V;
}
private init(): void {
fs.ensureDirSync(path.dirname(this.filePath));
try {
fs.readFileSync(this.filePath);
} catch (e) {
fs.writeJsonSync(this.filePath, {});
}
}
}