UNPKG

4.77 kBPlain TextView Raw
1/*
2 * Copyright © 2019 Atomist, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18 HandlerContext,
19 logger,
20} from "@atomist/automation-client";
21import { PreferenceStoreFactory } from "@atomist/sdm";
22import * as fs from "fs-extra";
23import * as _ from "lodash";
24import * as os from "os";
25import * as path from "path";
26import {
27 lock,
28 unlock,
29} from "proper-lockfile";
30import {
31 AbstractPreferenceStore,
32 Preference,
33} from "./AbstractPreferenceStore";
34
35type PreferenceFile = Record<string, { name: string, value: string, ttl?: number }>;
36
37type WithPreferenceFile<V> = (p: PreferenceFile) => Promise<{ value?: V, save: boolean }>;
38
39/**
40 * Factory to create a new FilePreferenceStore instance
41 */
42export const FilePreferenceStoreFactory: PreferenceStoreFactory = ctx => new FilePreferenceStore(ctx);
43
44/**
45 * PreferenceStore implementation that stores preferences in a shared file.
46 * Note: this implementation attempts to lock the preference file before reading or writing to it
47 * but it is not intended for production usage.
48 */
49export class FilePreferenceStore extends AbstractPreferenceStore {
50
51 constructor(context: HandlerContext,
52 private readonly filePath: string = path.join(os.homedir(), ".atomist", "prefs", "client.prefs.json")) {
53 super(context);
54 this.init();
55 }
56
57 protected async doGet(name: string, namespace: string): Promise<Preference | undefined> {
58 const key = this.scopeKey(name, namespace);
59 return this.doWithPreferenceFile<Preference | undefined>(async prefs => {
60 if (!!prefs[key]) {
61 return {
62 save: false,
63 value: {
64 name,
65 namespace,
66 value: prefs[key].value,
67 ttl: prefs[key].ttl,
68 },
69 };
70 } else {
71 return {
72 save: false,
73 value: undefined,
74 };
75 }
76 });
77 }
78
79 protected async doPut(pref: Preference): Promise<void> {
80 return this.doWithPreferenceFile<void>(async prefs => {
81 const key = this.scopeKey(pref.name, pref.namespace);
82 prefs[key] = {
83 name: pref.name,
84 value: pref.value,
85 ttl: typeof pref.ttl === "number" ? Date.now() + pref.ttl : undefined,
86 };
87 return {
88 save: true,
89 };
90 });
91 }
92
93 protected doList(namespace: string): Promise<Preference[]> {
94 return this.doWithPreferenceFile<Preference[]>(async prefs => {
95 const values: Preference[] = [];
96 _.forEach(prefs, (v, k) => {
97 if (!namespace || k.startsWith(`${namespace}_$_`)) {
98 values.push(v as Preference);
99 }
100 });
101 return {
102 save: false,
103 value: values,
104 };
105 });
106 }
107
108 protected doDelete(pref: string, namespace: string): Promise<void> {
109 return this.doWithPreferenceFile<void>(async prefs => {
110 const key = this.scopeKey(pref, namespace);
111 delete prefs[key];
112 return {
113 save: true,
114 };
115 });
116 }
117
118 private async read(): Promise<PreferenceFile> {
119 return (await fs.readJson(this.filePath)) as PreferenceFile;
120 }
121
122 private async doWithPreferenceFile<V>(withPreferenceFile: WithPreferenceFile<V>): Promise<V> {
123 await lock(this.filePath, { retries: 5 });
124 const prefs = await this.read();
125 let result;
126 try {
127 result = await withPreferenceFile(prefs);
128 if (result.save) {
129 await fs.writeJson(this.filePath, prefs);
130 }
131 } catch (e) {
132 logger.error(`Operation on preference file failed: ${e.message}`);
133 }
134 await unlock(this.filePath);
135 return result.value as V;
136 }
137
138 private init(): void {
139 fs.ensureDirSync(path.dirname(this.filePath));
140 try {
141 fs.readFileSync(this.filePath);
142 } catch (e) {
143 fs.writeJsonSync(this.filePath, {});
144 }
145 }
146
147}