-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathJSONFileConnector.js
More file actions
229 lines (202 loc) · 6.27 KB
/
Copy pathJSONFileConnector.js
File metadata and controls
229 lines (202 loc) · 6.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
const logger = require('@aliceo2/web-ui').LogManager
.getLogger(`${process.env.npm_config_log_label ?? 'ilg'}/json`);
const fs = require('fs');
const path = require('path');
/**
* Store user's profiles inside JSON based file with atomic write
*/
class JsonFileConnector {
/**
* Initialize connector by synchronizing DB file and its internal state
* @param {string} pathname - path to JSON DB file
*/
constructor(pathname) {
// Path of the file to store data
this.pathname = path.join(pathname);
// Path for writing file
this.pathnameTmp = `${this.pathname}~tmp`;
// Mirror data from content of JSON file
this.data = { profiles: [] };
// Write lock access
this.lock = new Lock();
this._syncFileAndInternalState();
}
/**
* Synchronize DB file content and `this.data` property
*/
async _syncFileAndInternalState() {
await this._readFromFile();
await this._writeToFile();
logger.info(`Preferences will be saved in ${this.pathname}`);
}
/**
* Read from file private method
* @returns {Promise} - content of the file
*/
async _readFromFile() {
return new Promise((resolve, reject) => {
fs.readFile(this.pathname, (err, data) => {
if (err) {
// file does not exist, it's ok, we will create it
if (err.code === 'ENOENT') {
logger.info('DB file does not exist, will create one');
return resolve();
}
// other errors reading
return reject(err);
}
try {
const dataFromFile = JSON.parse(data);
// check data we just read
if (!dataFromFile || !dataFromFile.profiles || !Array.isArray(dataFromFile.profiles)) {
return reject(new Error(`DB file should have an array of profiles ${this.pathname}`));
}
this.data = dataFromFile;
resolve();
} catch {
return reject(new Error(`Unable to parse DB file ${this.pathname}`));
}
});
});
}
/**
* Write data to disk, atomically, with lock
*/
async _writeToFile() {
await this.lock.acquire();
await new Promise((resolve, reject) => {
const dataToFile = JSON.stringify(this.data, null, 1);
fs.writeFile(this.pathnameTmp, dataToFile, (err) => {
if (err) {
return reject(err);
}
fs.rename(this.pathnameTmp, this.pathname, (err) => {
if (err) {
return reject(err);
}
logger.info('DB file updated');
resolve();
});
});
});
this.lock.release();
}
/**
* Create a new profile for a user with provided content
* Adds created & lastModified timestamps
* @param {string} username - username of the profile
* @param {JSON} content - content of the profile
* @returns {boolean} - true if profile was created, false if it already exists
*/
async createNewProfile(username, content) {
if (username == undefined) {
throw new Error('username for profile is mandatory');
}
const profile = this.data.profiles.find((profile) => profile.username === username);
if (profile) {
throw new Error(`Profile with this username (${username}) already exists`);
}
const dateNow = Date.now();
const profileEntry = {
username: username,
createdTimestamp: dateNow,
lastModifiedTimestamp: dateNow,
content: content,
};
this.data.profiles.push(profileEntry);
await this._writeToFile();
return profileEntry;
}
/**
* Retrieve a profile or undefined if it does not exist
* @param {string} username - username of the profile
* @returns {JSON} - profile content or undefined if it does not exist
*/
async getProfileByUsername(username) {
const profile = this.data.profiles.find((profile) => profile.username === username);
if (!profile) {
return undefined;
}
return profile;
}
/**
* Update a single profile by its username with the provided content
* Updates lastModified timestamp
* @param {string} username - username of the profile
* @param {JSON} content - content of the profile
* @returns {object} updatedProfile
*/
async updateProfile(username, content) {
const profile = await this.getProfileByUsername(username);
if (profile) {
Object.assign(profile.content, content);
profile.lastModifiedTimestamp = Date.now();
this._writeToFile();
return profile;
} else {
throw new Error(`Profile with this username (${
username}) cannot be updated as it does not exist`);
}
}
}
/**
* Simple Lock blocked Promise for exclusive access to resource
* @example
* let lock = new Lock();
* lock.acquire();
* setTimeout(() => lock.release(), 1000);
* await lock.acquire(); // will wait 1000ms
*/
class Lock {
/**
* Initialize lock to released
*/
constructor() {
this._locked = false;
this._queue = []; // callbacks of next owners of the lock
}
/**
* acquires lock if available and returns immediately
* otherwise wait for lock to be released
* @returns {Promise} - resolves when lock is acquired
*/
acquire() {
return new Promise((resolve) => {
// If nobody has the lock, take it and resolve immediately
if (!this._locked) {
this._locked = true;
return resolve();
}
// Otherwise, push as next owner
this._queue.push(resolve);
});
}
/**
* releases lock and give it to next in queue if any
*/
release() {
// Release the lock immediately
setImmediate(() => {
const nextOwner = this._queue.shift();
if (nextOwner) {
this._locked = true;
return nextOwner();
}
this._locked = false;
});
}
}
module.exports = JsonFileConnector;