@iobroker/db-objects-file 7.2.3 → 8.0.0-alpha.2-20260923-99da17fcc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/index.js +5 -3
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/lib/objects/objectsInMemFileDB.d.ts +288 -59
- package/build/cjs/lib/objects/objectsInMemFileDB.js +225 -109
- package/build/cjs/lib/objects/objectsInMemFileDB.js.map +3 -3
- package/build/cjs/lib/objects/objectsInMemServerClass.d.ts +41 -8
- package/build/cjs/lib/objects/objectsInMemServerClass.js +27 -2
- package/build/cjs/lib/objects/objectsInMemServerClass.js.map +3 -3
- package/build/cjs/lib/objects/objectsInMemServerRedis.d.ts +50 -30
- package/build/cjs/lib/objects/objectsInMemServerRedis.js +72 -37
- package/build/cjs/lib/objects/objectsInMemServerRedis.js.map +4 -4
- package/build/esm/lib/objects/objectsInMemFileDB.d.ts +288 -59
- package/build/esm/lib/objects/objectsInMemFileDB.d.ts.map +1 -1
- package/build/esm/lib/objects/objectsInMemFileDB.js +228 -126
- package/build/esm/lib/objects/objectsInMemFileDB.js.map +1 -1
- package/build/esm/lib/objects/objectsInMemServerClass.d.ts +41 -8
- package/build/esm/lib/objects/objectsInMemServerClass.d.ts.map +1 -1
- package/build/esm/lib/objects/objectsInMemServerClass.js +25 -1
- package/build/esm/lib/objects/objectsInMemServerClass.js.map +1 -1
- package/build/esm/lib/objects/objectsInMemServerRedis.d.ts +50 -30
- package/build/esm/lib/objects/objectsInMemServerRedis.d.ts.map +1 -1
- package/build/esm/lib/objects/objectsInMemServerRedis.js +89 -70
- package/build/esm/lib/objects/objectsInMemServerRedis.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +7 -7
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Object DB in memory - Server
|
|
3
3
|
*
|
|
4
|
-
* Copyright 2013-
|
|
4
|
+
* Copyright 2013-2026 bluefox <dogafox@gmail.com>
|
|
5
5
|
*
|
|
6
6
|
* MIT License
|
|
7
7
|
*
|
|
8
8
|
*/
|
|
9
|
+
/// <reference types="@iobroker/types-dev" />
|
|
9
10
|
import fs from 'fs-extra';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import { InMemoryFileDB } from '@iobroker/db-base';
|
|
@@ -17,62 +18,70 @@ import deepClone from 'deep-clone';
|
|
|
17
18
|
* including the available methods for use by js-controller directly
|
|
18
19
|
*/
|
|
19
20
|
export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
21
|
+
META_ID = '**META**';
|
|
22
|
+
fileOptions = {};
|
|
23
|
+
files = {};
|
|
24
|
+
writeTimer = null;
|
|
25
|
+
writeIds = [];
|
|
26
|
+
defaultNewAcl;
|
|
27
|
+
writeFileInterval;
|
|
28
|
+
objectsDir;
|
|
29
|
+
// cached meta information for file operations
|
|
30
|
+
existingMetaObjects = {};
|
|
31
|
+
/**
|
|
32
|
+
* @param settings Settings for the objects database
|
|
33
|
+
*/
|
|
20
34
|
constructor(settings) {
|
|
21
|
-
settings
|
|
22
|
-
settings.fileDB = settings.fileDB || {
|
|
35
|
+
settings.fileDB ??= {
|
|
23
36
|
fileName: 'objects.json',
|
|
24
37
|
backupDirName: 'backup-objects',
|
|
25
38
|
};
|
|
26
39
|
super(settings);
|
|
27
|
-
|
|
28
|
-
this.change
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
this.META_ID = '**META**';
|
|
33
|
-
this.fileOptions = {};
|
|
34
|
-
this.files = {};
|
|
35
|
-
this.writeTimer = null;
|
|
36
|
-
this.writeIds = [];
|
|
37
|
-
this.preserveSettings = ['custom'];
|
|
40
|
+
this.change ||= id => {
|
|
41
|
+
this.log.silly(`${this.namespace} objects change: ${id} ${JSON.stringify(this.change)}`);
|
|
42
|
+
};
|
|
38
43
|
this.defaultNewAcl = this.settings.defaultNewAcl || null;
|
|
39
44
|
this.namespace = this.settings.namespace || this.settings.hostname || '';
|
|
40
45
|
this.writeFileInterval =
|
|
41
46
|
this.settings.connection && typeof this.settings.connection.writeFileInterval === 'number'
|
|
42
|
-
?
|
|
47
|
+
? this.settings.connection.writeFileInterval
|
|
43
48
|
: 5_000;
|
|
44
49
|
if (!settings.jsonlDB) {
|
|
45
50
|
this.log.silly(`${this.namespace} Objects DB uses file write interval of ${this.writeFileInterval} ms`);
|
|
46
51
|
}
|
|
47
52
|
this.objectsDir = path.join(this.dataDir, 'files');
|
|
48
|
-
// cached meta information for file operations
|
|
49
|
-
this.existingMetaObjects = {};
|
|
50
|
-
// Handle some < js-controller 2.0 broken objects and correct them
|
|
51
|
-
for (const obj of Object.values(this.dataset)) {
|
|
52
|
-
if (tools.isObject(obj) && obj.acl && obj.acl.permissions && !obj.acl.object) {
|
|
53
|
-
obj.acl.object = obj.acl.permissions;
|
|
54
|
-
delete obj.acl.permissions;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
53
|
// init default new acl
|
|
58
54
|
const configObj = this.dataset['system.config'];
|
|
59
|
-
if (configObj
|
|
55
|
+
if (configObj?.common?.defaultNewAcl) {
|
|
60
56
|
this.defaultNewAcl = deepClone(configObj.common.defaultNewAcl);
|
|
61
57
|
}
|
|
62
58
|
}
|
|
63
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Normalize a file name by collapsing slashes and backslashes into a single forward slash
|
|
61
|
+
*
|
|
62
|
+
* @param name The file name to normalize
|
|
63
|
+
*/
|
|
64
64
|
_normalizeFilename(name) {
|
|
65
65
|
return name ? name.replace(/[/\\]+/g, '/') : name;
|
|
66
66
|
}
|
|
67
67
|
// -------------- FILE FUNCTIONS -------------------------------------------
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Schedule writing the file settings (_data.json) to disk
|
|
70
|
+
*
|
|
71
|
+
* @param id The object ID whose file settings should be saved, or a boolean used as the force flag
|
|
72
|
+
* @param force If true, write the settings immediately instead of debounced
|
|
73
|
+
*/
|
|
69
74
|
_saveFileSettings(id, force) {
|
|
70
75
|
if (typeof id === 'boolean') {
|
|
71
76
|
force = id;
|
|
72
77
|
id = undefined;
|
|
73
78
|
}
|
|
74
|
-
id !== undefined && !this.writeIds.includes(id)
|
|
75
|
-
|
|
79
|
+
if (id !== undefined && !this.writeIds.includes(id)) {
|
|
80
|
+
this.writeIds.push(id);
|
|
81
|
+
}
|
|
82
|
+
if (this.writeTimer) {
|
|
83
|
+
clearTimeout(this.writeTimer);
|
|
84
|
+
}
|
|
76
85
|
// if store immediately
|
|
77
86
|
if (force) {
|
|
78
87
|
this.writeTimer = null;
|
|
@@ -106,7 +115,11 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
106
115
|
}, 1_000);
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Load the file settings (_data.json) for the given object ID into memory
|
|
120
|
+
*
|
|
121
|
+
* @param id The object ID whose file settings should be loaded
|
|
122
|
+
*/
|
|
110
123
|
_loadFileSettings(id) {
|
|
111
124
|
if (!this.fileOptions[id]) {
|
|
112
125
|
const location = path.join(this.objectsDir, id, '_data.json');
|
|
@@ -142,7 +155,11 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
142
155
|
}
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Synchronize the in-memory file metadata with the files actually present on disk (server only)
|
|
160
|
+
*
|
|
161
|
+
* @param limitId Optional object ID to limit the synchronization to
|
|
162
|
+
*/
|
|
146
163
|
syncFileDirectory(limitId) {
|
|
147
164
|
const resNotifies = [];
|
|
148
165
|
let resSynced = 0;
|
|
@@ -152,7 +169,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
152
169
|
list.forEach(file => {
|
|
153
170
|
file = `${dir}/${file}`;
|
|
154
171
|
const stat = fs.statSync(file);
|
|
155
|
-
if (stat
|
|
172
|
+
if (stat?.isDirectory()) {
|
|
156
173
|
/* Recurse into a subdirectory */
|
|
157
174
|
results = results.concat(getAllFiles(file));
|
|
158
175
|
}
|
|
@@ -164,7 +181,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
164
181
|
return results;
|
|
165
182
|
}
|
|
166
183
|
const res = this._getObjectView('system', 'meta', null);
|
|
167
|
-
// collect meta ids to generate warning if non
|
|
184
|
+
// collect meta ids to generate warning if non-existing
|
|
168
185
|
const metaIds = res.rows.map(obj => obj.id).filter(id => !limitId || limitId === id);
|
|
169
186
|
if (!fs.existsSync(this.objectsDir)) {
|
|
170
187
|
return {
|
|
@@ -206,10 +223,9 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
206
223
|
this.fileOptions[dir][localFile] = {
|
|
207
224
|
createdAt: fileStat.ctimeMs,
|
|
208
225
|
acl: {
|
|
209
|
-
owner:
|
|
210
|
-
ownerGroup:
|
|
211
|
-
|
|
212
|
-
permissions: (this.defaultNewAcl && this.defaultNewAcl.file) ||
|
|
226
|
+
owner: this.defaultNewAcl?.owner || utils.CONSTS.SYSTEM_ADMIN_USER,
|
|
227
|
+
ownerGroup: this.defaultNewAcl?.ownerGroup || utils.CONSTS.SYSTEM_ADMIN_GROUP,
|
|
228
|
+
permissions: this.defaultNewAcl?.file ||
|
|
213
229
|
utils.CONSTS.ACCESS_USER_RW |
|
|
214
230
|
utils.CONSTS.ACCESS_GROUP_READ |
|
|
215
231
|
utils.CONSTS.ACCESS_EVERY_READ, // 0x644
|
|
@@ -223,27 +239,36 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
223
239
|
});
|
|
224
240
|
this._saveFileSettings(dir);
|
|
225
241
|
resSynced += dirSynced;
|
|
226
|
-
|
|
242
|
+
if (dirSynced) {
|
|
243
|
+
resNotifies.push(`Added ${dirSynced} Files in Directory "${dir}"`);
|
|
244
|
+
}
|
|
227
245
|
});
|
|
228
246
|
return {
|
|
229
247
|
numberSuccess: resSynced,
|
|
230
248
|
notifications: resNotifies,
|
|
231
249
|
};
|
|
232
250
|
}
|
|
233
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Write a file into an object's file storage (used by the server)
|
|
253
|
+
*
|
|
254
|
+
* @param id The object ID owning the file
|
|
255
|
+
* @param name The file name
|
|
256
|
+
* @param data The file content
|
|
257
|
+
* @param options Optional write options, or the mime type as a string
|
|
258
|
+
*/
|
|
234
259
|
_writeFile(id, name, data, options) {
|
|
235
260
|
if (typeof options === 'string') {
|
|
236
261
|
options = { mimeType: options };
|
|
237
262
|
}
|
|
238
|
-
if (options
|
|
263
|
+
if (options?.acl) {
|
|
239
264
|
options.acl = null;
|
|
240
265
|
}
|
|
241
266
|
const _path = utils.sanitizePath(id, name);
|
|
242
267
|
id = _path.id;
|
|
243
268
|
name = _path.name;
|
|
244
|
-
options
|
|
269
|
+
options ||= {};
|
|
245
270
|
this._loadFileSettings(id);
|
|
246
|
-
this.files[id]
|
|
271
|
+
this.files[id] ||= {};
|
|
247
272
|
try {
|
|
248
273
|
if (!fs.existsSync(this.objectsDir)) {
|
|
249
274
|
fs.mkdirSync(this.objectsDir);
|
|
@@ -261,22 +286,17 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
261
286
|
const mime = utils.getMimeType(ext);
|
|
262
287
|
const _mimeType = mime.mimeType;
|
|
263
288
|
const isBinary = mime.isBinary;
|
|
264
|
-
this.fileOptions[id][name]
|
|
265
|
-
this.fileOptions[id][name].acl
|
|
266
|
-
owner: options.user ||
|
|
267
|
-
ownerGroup: options.group ||
|
|
268
|
-
(this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
|
|
269
|
-
utils.CONSTS.SYSTEM_ADMIN_GROUP,
|
|
289
|
+
this.fileOptions[id][name] ||= { createdAt: Date.now() };
|
|
290
|
+
this.fileOptions[id][name].acl ||= {
|
|
291
|
+
owner: options.user || this.defaultNewAcl?.owner || utils.CONSTS.SYSTEM_ADMIN_USER,
|
|
292
|
+
ownerGroup: options.group || this.defaultNewAcl?.ownerGroup || utils.CONSTS.SYSTEM_ADMIN_GROUP,
|
|
270
293
|
permissions: options.mode ||
|
|
271
|
-
|
|
294
|
+
this.defaultNewAcl?.file ||
|
|
272
295
|
utils.CONSTS.ACCESS_USER_RW | utils.CONSTS.ACCESS_GROUP_READ | utils.CONSTS.ACCESS_EVERY_READ, // 0x644
|
|
273
296
|
};
|
|
274
297
|
this.fileOptions[id][name].mimeType = options.mimeType || _mimeType;
|
|
275
298
|
this.fileOptions[id][name].binary = isBinary;
|
|
276
|
-
this.fileOptions[id][name].acl.ownerGroup
|
|
277
|
-
this.fileOptions[id][name].acl.ownerGroup ||
|
|
278
|
-
(this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
|
|
279
|
-
utils.CONSTS.SYSTEM_ADMIN_GROUP;
|
|
299
|
+
this.fileOptions[id][name].acl.ownerGroup ||= this.defaultNewAcl?.ownerGroup || utils.CONSTS.SYSTEM_ADMIN_GROUP;
|
|
280
300
|
this.fileOptions[id][name].modifiedAt = Date.now();
|
|
281
301
|
try {
|
|
282
302
|
// Create directories if complex structure
|
|
@@ -304,43 +324,48 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
304
324
|
// publish event in states
|
|
305
325
|
this.log.silly(`${this.namespace} memory publish ${id} ${JSON.stringify({ name, file: true, size })}`);
|
|
306
326
|
this.publishAll('files', `${id}$%$${name}`, size);
|
|
307
|
-
}, name,
|
|
327
|
+
}, name, Buffer.byteLength(data));
|
|
308
328
|
}
|
|
309
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Read a file from an object's file storage (used by the server)
|
|
331
|
+
*
|
|
332
|
+
* @param id The object ID owning the file
|
|
333
|
+
* @param name The file name
|
|
334
|
+
* @param options Optional read options
|
|
335
|
+
*/
|
|
310
336
|
_readFile(id, name, options) {
|
|
311
|
-
if (options
|
|
337
|
+
if (options?.acl) {
|
|
312
338
|
options.acl = null;
|
|
313
339
|
}
|
|
314
340
|
const _path = utils.sanitizePath(id, name);
|
|
315
341
|
id = _path.id;
|
|
316
342
|
name = _path.name;
|
|
317
|
-
options
|
|
343
|
+
options ||= {};
|
|
318
344
|
try {
|
|
319
345
|
this._loadFileSettings(id);
|
|
320
|
-
this.files[id]
|
|
346
|
+
this.files[id] ||= {};
|
|
321
347
|
if (!this.files[id][name] || this.settings.connection.noFileCache || options.noFileCache) {
|
|
322
348
|
const location = path.join(this.objectsDir, id, name);
|
|
323
349
|
if (fs.existsSync(location)) {
|
|
324
350
|
// Create description object if not exists
|
|
325
|
-
this.fileOptions[id][name]
|
|
351
|
+
this.fileOptions[id][name] ||= {
|
|
326
352
|
acl: {
|
|
327
|
-
owner:
|
|
328
|
-
ownerGroup:
|
|
329
|
-
|
|
330
|
-
permissions: (this.defaultNewAcl && this.defaultNewAcl.file.permissions) ||
|
|
353
|
+
owner: this.defaultNewAcl?.owner || utils.CONSTS.SYSTEM_ADMIN_USER,
|
|
354
|
+
ownerGroup: this.defaultNewAcl?.ownerGroup || utils.CONSTS.SYSTEM_ADMIN_GROUP,
|
|
355
|
+
permissions: this.defaultNewAcl?.file ||
|
|
331
356
|
utils.CONSTS.ACCESS_USER_ALL |
|
|
332
357
|
utils.CONSTS.ACCESS_GROUP_ALL |
|
|
333
358
|
utils.CONSTS.ACCESS_EVERY_ALL, // 777
|
|
334
359
|
},
|
|
335
360
|
};
|
|
361
|
+
// convert from old format
|
|
336
362
|
if (typeof this.fileOptions[id][name] !== 'object') {
|
|
337
363
|
this.fileOptions[id][name] = {
|
|
338
364
|
mimeType: this.fileOptions[id][name],
|
|
339
365
|
acl: {
|
|
340
|
-
owner:
|
|
341
|
-
ownerGroup:
|
|
342
|
-
|
|
343
|
-
permissions: (this.defaultNewAcl && this.defaultNewAcl.file.permissions) ||
|
|
366
|
+
owner: this.defaultNewAcl?.owner || utils.CONSTS.SYSTEM_ADMIN_USER,
|
|
367
|
+
ownerGroup: this.defaultNewAcl?.ownerGroup || utils.CONSTS.SYSTEM_ADMIN_GROUP,
|
|
368
|
+
permissions: this.defaultNewAcl?.file ||
|
|
344
369
|
utils.CONSTS.ACCESS_USER_ALL |
|
|
345
370
|
utils.CONSTS.ACCESS_GROUP_ALL |
|
|
346
371
|
utils.CONSTS.ACCESS_EVERY_ALL, // 777
|
|
@@ -354,10 +379,8 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
354
379
|
this.fileOptions[id][name].binary = mimeType.isBinary;
|
|
355
380
|
this.fileOptions[id][name].mimeType = mimeType.mimeType;
|
|
356
381
|
}
|
|
357
|
-
if (!this.fileOptions[id][name].binary) {
|
|
358
|
-
|
|
359
|
-
this.files[id][name] = this.files[id][name].toString();
|
|
360
|
-
}
|
|
382
|
+
if (!this.fileOptions[id][name].binary && this.files[id][name]) {
|
|
383
|
+
this.files[id][name] = this.files[id][name].toString();
|
|
361
384
|
}
|
|
362
385
|
}
|
|
363
386
|
else {
|
|
@@ -372,9 +395,9 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
372
395
|
if (this.fileOptions[id][name] && !this.fileOptions[id][name].acl) {
|
|
373
396
|
// all files belong to admin by default, but everyone can edit it
|
|
374
397
|
this.fileOptions[id][name].acl = {
|
|
375
|
-
owner:
|
|
376
|
-
ownerGroup:
|
|
377
|
-
permissions:
|
|
398
|
+
owner: this.defaultNewAcl?.owner || utils.CONSTS.SYSTEM_ADMIN_USER,
|
|
399
|
+
ownerGroup: this.defaultNewAcl?.ownerGroup || utils.CONSTS.SYSTEM_ADMIN_GROUP,
|
|
400
|
+
permissions: this.defaultNewAcl?.file ||
|
|
378
401
|
utils.CONSTS.ACCESS_USER_ALL | utils.CONSTS.ACCESS_GROUP_ALL | utils.CONSTS.ACCESS_EVERY_RW, // 776
|
|
379
402
|
};
|
|
380
403
|
}
|
|
@@ -421,7 +444,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
421
444
|
*
|
|
422
445
|
* @param id id of the namespace
|
|
423
446
|
* @param [name] name of the file
|
|
424
|
-
* @returns
|
|
447
|
+
* @returns true if the file exists
|
|
425
448
|
*/
|
|
426
449
|
// needed by server
|
|
427
450
|
_fileExists(id, name) {
|
|
@@ -446,7 +469,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
446
469
|
*
|
|
447
470
|
* @param id id of the namespace
|
|
448
471
|
* @param [name] name of the directory
|
|
449
|
-
* @returns
|
|
472
|
+
* @returns true if the directory exists
|
|
450
473
|
*/
|
|
451
474
|
// special functionality only for Server (used together with SyncFileDirectory)
|
|
452
475
|
dirExists(id, name) {
|
|
@@ -466,7 +489,12 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
466
489
|
return false;
|
|
467
490
|
}
|
|
468
491
|
}
|
|
469
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Delete a file or directory from an object's file storage (used by the server)
|
|
494
|
+
*
|
|
495
|
+
* @param id The object ID owning the file
|
|
496
|
+
* @param name The file or directory name to delete
|
|
497
|
+
*/
|
|
470
498
|
_unlink(id, name) {
|
|
471
499
|
const _path = utils.sanitizePath(id, name);
|
|
472
500
|
id = _path.id;
|
|
@@ -489,7 +517,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
489
517
|
if (this.fileOptions[id]) {
|
|
490
518
|
delete this.fileOptions[id];
|
|
491
519
|
}
|
|
492
|
-
if (this.files[id]
|
|
520
|
+
if (this.files[id]) {
|
|
493
521
|
delete this.files[id];
|
|
494
522
|
}
|
|
495
523
|
}
|
|
@@ -502,10 +530,10 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
502
530
|
this.log.error(`${this.namespace} Cannot delete file "${path.join(id, name)}": ${e.message}`);
|
|
503
531
|
throw e;
|
|
504
532
|
}
|
|
505
|
-
if (this.fileOptions[id][name]) {
|
|
533
|
+
if (this.fileOptions[id]?.[name]) {
|
|
506
534
|
delete this.fileOptions[id][name];
|
|
507
535
|
}
|
|
508
|
-
if (this.files[id]
|
|
536
|
+
if (this.files[id]?.[name]) {
|
|
509
537
|
delete this.files[id][name];
|
|
510
538
|
}
|
|
511
539
|
// Store dir description
|
|
@@ -518,9 +546,15 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
518
546
|
}, id, name);
|
|
519
547
|
}
|
|
520
548
|
}
|
|
521
|
-
|
|
549
|
+
/**
|
|
550
|
+
* List the contents of a directory in an object's file storage (used by the server)
|
|
551
|
+
*
|
|
552
|
+
* @param id The object ID owning the files
|
|
553
|
+
* @param name The directory name to list
|
|
554
|
+
* @param options Optional read options
|
|
555
|
+
*/
|
|
522
556
|
_readDir(id, name, options) {
|
|
523
|
-
if (options
|
|
557
|
+
if (options?.acl) {
|
|
524
558
|
options.acl = null;
|
|
525
559
|
}
|
|
526
560
|
if ((id === '' || id === '/' || id === '*') && (name === '' || name === '*')) {
|
|
@@ -531,7 +565,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
531
565
|
id = _path.id;
|
|
532
566
|
name = _path.name;
|
|
533
567
|
}
|
|
534
|
-
options
|
|
568
|
+
options ||= {};
|
|
535
569
|
// Find all files and directories starts with name
|
|
536
570
|
const _files = [];
|
|
537
571
|
if (id && id === '*') {
|
|
@@ -544,9 +578,8 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
544
578
|
const len = name ? name.length : 0;
|
|
545
579
|
for (const f of Object.keys(this.fileOptions[id])) {
|
|
546
580
|
if (!name || f.substring(0, len) === name) {
|
|
547
|
-
|
|
548
|
-
rest
|
|
549
|
-
if (rest[0] && _files.indexOf(rest[0]) === -1) {
|
|
581
|
+
const rest = f.substring(len).split('/', 2);
|
|
582
|
+
if (rest[0] && !_files.includes(rest[0])) {
|
|
550
583
|
_files.push(rest[0]);
|
|
551
584
|
}
|
|
552
585
|
}
|
|
@@ -558,7 +591,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
558
591
|
if (dirFiles[i] === '..' || dirFiles[i] === '.') {
|
|
559
592
|
continue;
|
|
560
593
|
}
|
|
561
|
-
if (dirFiles[i] !== '_data.json' && _files.
|
|
594
|
+
if (dirFiles[i] !== '_data.json' && !_files.includes(dirFiles[i])) {
|
|
562
595
|
_files.push(dirFiles[i]);
|
|
563
596
|
}
|
|
564
597
|
}
|
|
@@ -575,16 +608,14 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
575
608
|
if (fs.existsSync(path.join(location, file))) {
|
|
576
609
|
try {
|
|
577
610
|
const stats = fs.statSync(path.join(location, file));
|
|
578
|
-
const acl = this.fileOptions[id][name + file]
|
|
611
|
+
const acl = this.fileOptions[id][name + file]?.acl
|
|
579
612
|
? deepClone(this.fileOptions[id][name + file].acl) // copy settings
|
|
580
613
|
: {
|
|
581
614
|
read: true,
|
|
582
615
|
write: true,
|
|
583
|
-
owner:
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
utils.CONSTS.SYSTEM_ADMIN_GROUP,
|
|
587
|
-
permissions: (this.defaultNewAcl && this.defaultNewAcl.file.permissions) ||
|
|
616
|
+
owner: this.defaultNewAcl?.owner || utils.CONSTS.SYSTEM_ADMIN_USER,
|
|
617
|
+
ownerGroup: this.defaultNewAcl?.ownerGroup || utils.CONSTS.SYSTEM_ADMIN_GROUP,
|
|
618
|
+
permissions: this.defaultNewAcl?.file ||
|
|
588
619
|
utils.CONSTS.ACCESS_USER_RW |
|
|
589
620
|
utils.CONSTS.ACCESS_GROUP_READ |
|
|
590
621
|
utils.CONSTS.ACCESS_EVERY_READ,
|
|
@@ -592,24 +623,24 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
592
623
|
// if filter for user
|
|
593
624
|
if (options.filter && acl) {
|
|
594
625
|
// If user may not write
|
|
595
|
-
if (!options.acl
|
|
626
|
+
if (!options.acl?.file.write) {
|
|
596
627
|
// write
|
|
597
628
|
acl.permissions &= ~(utils.CONSTS.ACCESS_USER_WRITE |
|
|
598
629
|
utils.CONSTS.ACCESS_GROUP_WRITE |
|
|
599
630
|
utils.CONSTS.ACCESS_EVERY_WRITE);
|
|
600
631
|
}
|
|
601
632
|
// If user may not read
|
|
602
|
-
if (!options.acl
|
|
633
|
+
if (!options.acl?.file.read) {
|
|
603
634
|
// read
|
|
604
635
|
acl.permissions &= ~(utils.CONSTS.ACCESS_USER_READ |
|
|
605
636
|
utils.CONSTS.ACCESS_GROUP_READ |
|
|
606
637
|
utils.CONSTS.ACCESS_EVERY_READ);
|
|
607
638
|
}
|
|
608
639
|
if (options.user !== utils.CONSTS.SYSTEM_ADMIN_USER &&
|
|
609
|
-
options.groups
|
|
640
|
+
options.groups?.includes(utils.CONSTS.SYSTEM_ADMIN_GROUP)) {
|
|
610
641
|
if (acl.owner !== options.user) {
|
|
611
642
|
// Check if the user is in the group
|
|
612
|
-
if (options.groups
|
|
643
|
+
if (options.groups?.includes(acl.ownerGroup)) {
|
|
613
644
|
// Check group rights
|
|
614
645
|
if (!(acl.permissions & utils.CONSTS.ACCESS_GROUP_RW)) {
|
|
615
646
|
continue;
|
|
@@ -642,9 +673,9 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
642
673
|
}
|
|
643
674
|
res.push({
|
|
644
675
|
file,
|
|
645
|
-
stats
|
|
676
|
+
stats,
|
|
646
677
|
isDir: stats.isDirectory(),
|
|
647
|
-
acl
|
|
678
|
+
acl,
|
|
648
679
|
modifiedAt: this.fileOptions[id][name + file]
|
|
649
680
|
? this.fileOptions[id][name + file].modifiedAt
|
|
650
681
|
: undefined,
|
|
@@ -660,7 +691,13 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
660
691
|
}
|
|
661
692
|
return res;
|
|
662
693
|
}
|
|
663
|
-
|
|
694
|
+
/**
|
|
695
|
+
* Rename a file or directory in an object's file storage (used by the server)
|
|
696
|
+
*
|
|
697
|
+
* @param id The object ID owning the file
|
|
698
|
+
* @param oldName The current file or directory name
|
|
699
|
+
* @param newName The new file or directory name
|
|
700
|
+
*/
|
|
664
701
|
_rename(id, oldName, newName) {
|
|
665
702
|
const _path = utils.sanitizePath(id, oldName);
|
|
666
703
|
id = _path.id;
|
|
@@ -691,29 +728,57 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
691
728
|
});
|
|
692
729
|
this._saveFileSettings(id, true);
|
|
693
730
|
}
|
|
694
|
-
|
|
731
|
+
/**
|
|
732
|
+
* Create a deep clone of the given object
|
|
733
|
+
*
|
|
734
|
+
* @param obj The object to clone
|
|
735
|
+
*/
|
|
695
736
|
_clone(obj) {
|
|
696
737
|
if (obj === null || obj === undefined || !tools.isObject(obj)) {
|
|
697
738
|
return obj;
|
|
698
739
|
}
|
|
699
740
|
const temp = obj.constructor(); // changed
|
|
700
741
|
for (const key of Object.keys(obj)) {
|
|
742
|
+
// @ts-expect-error known problem
|
|
701
743
|
temp[key] = this._clone(obj[key]);
|
|
702
744
|
}
|
|
703
745
|
return temp;
|
|
704
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Subscribe a client to meta changes
|
|
749
|
+
*
|
|
750
|
+
* @param client The client to subscribe
|
|
751
|
+
* @param pattern The pattern of meta IDs to subscribe to
|
|
752
|
+
*/
|
|
705
753
|
_subscribeMeta(client, pattern) {
|
|
706
754
|
this.handleSubscribe(client, 'meta', pattern);
|
|
707
755
|
}
|
|
708
|
-
|
|
756
|
+
/**
|
|
757
|
+
* Subscribe a client to object changes (used by the server)
|
|
758
|
+
*
|
|
759
|
+
* @param client The client to subscribe
|
|
760
|
+
* @param pattern The pattern of object IDs to subscribe to
|
|
761
|
+
*/
|
|
709
762
|
_subscribeConfigForClient(client, pattern) {
|
|
710
763
|
this.handleSubscribe(client, 'objects', pattern);
|
|
711
764
|
}
|
|
712
|
-
|
|
765
|
+
/**
|
|
766
|
+
* Unsubscribe a client from object changes (used by the server)
|
|
767
|
+
*
|
|
768
|
+
* @param client The client to unsubscribe
|
|
769
|
+
* @param pattern The pattern of object IDs to unsubscribe from
|
|
770
|
+
*/
|
|
713
771
|
_unsubscribeConfigForClient(client, pattern) {
|
|
714
|
-
|
|
772
|
+
// ignore options => unsubscribe may everyone
|
|
773
|
+
this.handleUnsubscribe(client, 'objects', pattern).catch(e => this.log.error(`${this.namespace} Cannot unsubscribe client from objects: ${e.message}`));
|
|
715
774
|
}
|
|
716
|
-
|
|
775
|
+
/**
|
|
776
|
+
* Subscribe a client to file changes of an object (used by the server)
|
|
777
|
+
*
|
|
778
|
+
* @param client The client to subscribe
|
|
779
|
+
* @param id The object ID owning the files
|
|
780
|
+
* @param pattern One or more file name patterns to subscribe to
|
|
781
|
+
*/
|
|
717
782
|
_subscribeFileForClient(client, id, pattern) {
|
|
718
783
|
if (Array.isArray(pattern)) {
|
|
719
784
|
pattern.forEach(pattern => this.handleSubscribe(client, 'files', `${id}$%$${pattern}`));
|
|
@@ -722,33 +787,54 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
722
787
|
this.handleSubscribe(client, 'files', `${id}$%$${pattern}`);
|
|
723
788
|
}
|
|
724
789
|
}
|
|
725
|
-
|
|
790
|
+
/**
|
|
791
|
+
* Unsubscribe a client from file changes of an object (used by the server)
|
|
792
|
+
*
|
|
793
|
+
* @param client The client to unsubscribe
|
|
794
|
+
* @param id The object ID owning the files
|
|
795
|
+
* @param pattern One or more file name patterns to unsubscribe from
|
|
796
|
+
*/
|
|
726
797
|
_unsubscribeFileForClient(client, id, pattern) {
|
|
727
798
|
if (Array.isArray(pattern)) {
|
|
728
799
|
pattern.forEach(pattern => this.handleUnsubscribe(client, 'files', `${id}$%$${pattern}`));
|
|
729
800
|
}
|
|
730
801
|
else {
|
|
731
|
-
this.handleUnsubscribe(client, 'files', `${id}$%$${pattern}`);
|
|
802
|
+
this.handleUnsubscribe(client, 'files', `${id}$%$${pattern}`).catch(e => this.log.error(`${this.namespace} Cannot unsubscribe client from files: ${e.message}`));
|
|
732
803
|
}
|
|
733
804
|
}
|
|
734
|
-
|
|
805
|
+
/**
|
|
806
|
+
* Get a single object by its ID (used by the server)
|
|
807
|
+
*
|
|
808
|
+
* @param id The object ID to read
|
|
809
|
+
*/
|
|
735
810
|
_getObject(id) {
|
|
736
811
|
return this.dataset[id];
|
|
737
812
|
}
|
|
738
|
-
|
|
813
|
+
/**
|
|
814
|
+
* Get all object IDs matching the given pattern, sorted (used by the server)
|
|
815
|
+
*
|
|
816
|
+
* @param pattern The pattern to match object IDs against
|
|
817
|
+
*/
|
|
739
818
|
_getKeys(pattern) {
|
|
740
819
|
const r = new RegExp(tools.pattern2RegEx(pattern));
|
|
741
820
|
const result = Object.keys(this.dataset).filter(id => r.test(id) && id !== this.META_ID);
|
|
742
821
|
result.sort();
|
|
743
822
|
return result;
|
|
744
823
|
}
|
|
745
|
-
|
|
824
|
+
/**
|
|
825
|
+
* Get the values of the given object IDs (used by the server)
|
|
826
|
+
*
|
|
827
|
+
* @param keys The object IDs to read
|
|
828
|
+
*/
|
|
746
829
|
_getObjects(keys) {
|
|
747
830
|
if (!keys) {
|
|
748
831
|
throw new Error('no keys');
|
|
749
832
|
}
|
|
750
|
-
return keys.map(id => this.dataset[id]);
|
|
833
|
+
return keys.map(id => (id ? this.dataset[id] : undefined));
|
|
751
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Get the meta dictionary, creating it if it does not exist yet
|
|
837
|
+
*/
|
|
752
838
|
_ensureMetaDict() {
|
|
753
839
|
let meta = this.dataset[this.META_ID];
|
|
754
840
|
if (!meta) {
|
|
@@ -760,8 +846,8 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
760
846
|
/**
|
|
761
847
|
* Get value of given meta id
|
|
762
848
|
*
|
|
763
|
-
* @param id
|
|
764
|
-
* @returns
|
|
849
|
+
* @param id The meta ID to read
|
|
850
|
+
* @returns the stored meta value
|
|
765
851
|
*/
|
|
766
852
|
getMeta(id) {
|
|
767
853
|
const meta = this._ensureMetaDict();
|
|
@@ -770,8 +856,8 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
770
856
|
/**
|
|
771
857
|
* Sets given value to id in metaNamespace
|
|
772
858
|
*
|
|
773
|
-
* @param id
|
|
774
|
-
* @param value
|
|
859
|
+
* @param id The meta ID to write
|
|
860
|
+
* @param value The value to store
|
|
775
861
|
*/
|
|
776
862
|
setMeta(id, value) {
|
|
777
863
|
const meta = this._ensureMetaDict();
|
|
@@ -783,11 +869,14 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
783
869
|
this.log.silly(`${this.namespace} memory publish meta ${id} ${value}`);
|
|
784
870
|
this.publishAll('meta', id, value);
|
|
785
871
|
});
|
|
786
|
-
|
|
787
|
-
this.stateTimer = setTimeout(() => this.saveState(), this.writeFileInterval);
|
|
788
|
-
}
|
|
872
|
+
this.stateTimer ||= setTimeout(() => this.saveState(), this.writeFileInterval);
|
|
789
873
|
}
|
|
790
|
-
|
|
874
|
+
/**
|
|
875
|
+
* Directly set an object value and publish the change (used by the server)
|
|
876
|
+
*
|
|
877
|
+
* @param id The object ID to set
|
|
878
|
+
* @param obj The object to store
|
|
879
|
+
*/
|
|
791
880
|
_setObjectDirect(id, obj) {
|
|
792
881
|
this.dataset[id] = obj;
|
|
793
882
|
// object updated -> if type changed to meta -> cache
|
|
@@ -795,7 +884,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
795
884
|
this.existingMetaObjects[id] = true;
|
|
796
885
|
}
|
|
797
886
|
setImmediate(() => this.publishAll('objects', id, obj));
|
|
798
|
-
this.stateTimer
|
|
887
|
+
this.stateTimer ||= setTimeout(() => this.saveState(), this.writeFileInterval);
|
|
799
888
|
}
|
|
800
889
|
/**
|
|
801
890
|
* Delete the given object from the dataset
|
|
@@ -821,14 +910,21 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
821
910
|
this.stateTimer = setTimeout(() => this.saveState(), this.writeFileInterval);
|
|
822
911
|
}
|
|
823
912
|
}
|
|
824
|
-
|
|
913
|
+
/**
|
|
914
|
+
* Apply a view's map function over all objects and collect the matching rows
|
|
915
|
+
*
|
|
916
|
+
* @param func The view definition containing the map function
|
|
917
|
+
* @param func.map The map function source code to apply
|
|
918
|
+
* @param func.reduce Optional reduce function (only the built-in '_stats' is supported)
|
|
919
|
+
* @param params Query parameters such as startkey and endkey
|
|
920
|
+
*/
|
|
825
921
|
_applyView(func, params) {
|
|
826
922
|
const result = {
|
|
827
923
|
rows: [],
|
|
828
924
|
};
|
|
829
925
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
830
|
-
function _emit_(id,
|
|
831
|
-
result.rows.push({ id
|
|
926
|
+
function _emit_(id, value) {
|
|
927
|
+
result.rows.push({ id, value });
|
|
832
928
|
}
|
|
833
929
|
const f = eval(`(${func.map.replace(/emit/g, '_emit_')})`);
|
|
834
930
|
for (const [id, obj] of Object.entries(this.dataset)) {
|
|
@@ -858,7 +954,7 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
858
954
|
}
|
|
859
955
|
}
|
|
860
956
|
if (max !== null) {
|
|
861
|
-
result.rows = [{ id: '_stats', value: { max
|
|
957
|
+
result.rows = [{ id: '_stats', value: { max } }];
|
|
862
958
|
}
|
|
863
959
|
else {
|
|
864
960
|
result.rows = [];
|
|
@@ -866,14 +962,20 @@ export class ObjectsInMemoryFileDB extends InMemoryFileDB {
|
|
|
866
962
|
}
|
|
867
963
|
return result;
|
|
868
964
|
}
|
|
869
|
-
|
|
965
|
+
/**
|
|
966
|
+
* Run a predefined object view (design document) and return the matching rows (used by the server)
|
|
967
|
+
*
|
|
968
|
+
* @param design The design document name
|
|
969
|
+
* @param search The view name within the design document
|
|
970
|
+
* @param params Query parameters such as startkey and endkey
|
|
971
|
+
*/
|
|
870
972
|
_getObjectView(design, search, params) {
|
|
871
973
|
const designObj = this.dataset[`_design/${design}`];
|
|
872
974
|
if (!designObj) {
|
|
873
975
|
this.log.error(`${this.namespace} Cannot find view "${design}"`);
|
|
874
976
|
throw new Error(`Cannot find view "${design}"`);
|
|
875
977
|
}
|
|
876
|
-
if (!
|
|
978
|
+
if (!designObj.views?.[search]) {
|
|
877
979
|
this.log.warn(`${this.namespace} Cannot find search "${search}" in "${design}"`);
|
|
878
980
|
throw new Error(`Cannot find search "${search}" in "${design}"`);
|
|
879
981
|
}
|