@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.
Files changed (25) hide show
  1. package/build/cjs/index.js +5 -3
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/cjs/lib/objects/objectsInMemFileDB.d.ts +288 -59
  4. package/build/cjs/lib/objects/objectsInMemFileDB.js +225 -109
  5. package/build/cjs/lib/objects/objectsInMemFileDB.js.map +3 -3
  6. package/build/cjs/lib/objects/objectsInMemServerClass.d.ts +41 -8
  7. package/build/cjs/lib/objects/objectsInMemServerClass.js +27 -2
  8. package/build/cjs/lib/objects/objectsInMemServerClass.js.map +3 -3
  9. package/build/cjs/lib/objects/objectsInMemServerRedis.d.ts +50 -30
  10. package/build/cjs/lib/objects/objectsInMemServerRedis.js +72 -37
  11. package/build/cjs/lib/objects/objectsInMemServerRedis.js.map +4 -4
  12. package/build/esm/lib/objects/objectsInMemFileDB.d.ts +288 -59
  13. package/build/esm/lib/objects/objectsInMemFileDB.d.ts.map +1 -1
  14. package/build/esm/lib/objects/objectsInMemFileDB.js +228 -126
  15. package/build/esm/lib/objects/objectsInMemFileDB.js.map +1 -1
  16. package/build/esm/lib/objects/objectsInMemServerClass.d.ts +41 -8
  17. package/build/esm/lib/objects/objectsInMemServerClass.d.ts.map +1 -1
  18. package/build/esm/lib/objects/objectsInMemServerClass.js +25 -1
  19. package/build/esm/lib/objects/objectsInMemServerClass.js.map +1 -1
  20. package/build/esm/lib/objects/objectsInMemServerRedis.d.ts +50 -30
  21. package/build/esm/lib/objects/objectsInMemServerRedis.d.ts.map +1 -1
  22. package/build/esm/lib/objects/objectsInMemServerRedis.js +89 -70
  23. package/build/esm/lib/objects/objectsInMemServerRedis.js.map +1 -1
  24. package/build/tsconfig.build.tsbuildinfo +1 -1
  25. package/package.json +7 -7
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Object DB in memory - Server
3
3
  *
4
- * Copyright 2013-2024 bluefox <dogafox@gmail.com>
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 = 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
- if (!this.change) {
28
- this.change = id => {
29
- this.log.silly(`${this.namespace} objects change: ${id} ${JSON.stringify(this.change)}`);
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
- ? parseInt(this.settings.connection.writeFileInterval)
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 && configObj.common && configObj.common.defaultNewAcl) {
55
+ if (configObj?.common?.defaultNewAcl) {
60
56
  this.defaultNewAcl = deepClone(configObj.common.defaultNewAcl);
61
57
  }
62
58
  }
63
- // internal functionality
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
- // internal functionality
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) && this.writeIds.push(id);
75
- this.writeTimer && clearTimeout(this.writeTimer);
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
- // internal functionality
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
- // server only functionality
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 && stat.isDirectory()) {
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 existing
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: (this.defaultNewAcl && this.defaultNewAcl.owner) || utils.CONSTS.SYSTEM_ADMIN_USER,
210
- ownerGroup: (this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
211
- utils.CONSTS.SYSTEM_ADMIN_GROUP,
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
- dirSynced && resNotifies.push(`Added ${dirSynced} Files in Directory "${dir}"`);
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
- // needed by server
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 && options.acl) {
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 = options || {};
269
+ options ||= {};
245
270
  this._loadFileSettings(id);
246
- this.files[id] = 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] = this.fileOptions[id][name] || { createdAt: Date.now() };
265
- this.fileOptions[id][name].acl = this.fileOptions[id][name].acl || {
266
- owner: options.user || (this.defaultNewAcl && this.defaultNewAcl.owner) || utils.CONSTS.SYSTEM_ADMIN_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
- (this.defaultNewAcl && this.defaultNewAcl.file) ||
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, data.byteLength);
327
+ }, name, Buffer.byteLength(data));
308
328
  }
309
- // needed by server
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 && options.acl) {
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 = options || {};
343
+ options ||= {};
318
344
  try {
319
345
  this._loadFileSettings(id);
320
- this.files[id] = 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] = this.fileOptions[id][name] || {
351
+ this.fileOptions[id][name] ||= {
326
352
  acl: {
327
- owner: (this.defaultNewAcl && this.defaultNewAcl.owner) || utils.CONSTS.SYSTEM_ADMIN_USER,
328
- ownerGroup: (this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
329
- utils.CONSTS.SYSTEM_ADMIN_GROUP,
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: (this.defaultNewAcl && this.defaultNewAcl.owner) || utils.CONSTS.SYSTEM_ADMIN_USER,
341
- ownerGroup: (this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
342
- utils.CONSTS.SYSTEM_ADMIN_GROUP,
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
- if (this.files[id][name]) {
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: (this.defaultNewAcl && this.defaultNewAcl.owner) || utils.CONSTS.SYSTEM_ADMIN_USER,
376
- ownerGroup: (this.defaultNewAcl && this.defaultNewAcl.ownerGroup) || utils.CONSTS.SYSTEM_ADMIN_GROUP,
377
- permissions: (this.defaultNewAcl && this.defaultNewAcl.file.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
- // needed by server
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] && 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] && this.files[id][name]) {
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
- // needed by server
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 && options.acl) {
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 = 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
- let rest = f.substring(len);
548
- rest = rest.split('/', 2);
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.indexOf(dirFiles[i]) === -1) {
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] && this.fileOptions[id][name + file].acl
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: (this.defaultNewAcl && this.defaultNewAcl.owner) ||
584
- utils.CONSTS.SYSTEM_ADMIN_USER,
585
- ownerGroup: (this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
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.file.write) {
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.file.read) {
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.includes(utils.CONSTS.SYSTEM_ADMIN_GROUP)) {
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.includes(acl.ownerGroup)) {
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: stats,
676
+ stats,
646
677
  isDir: stats.isDirectory(),
647
- acl: 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
- // needed by server
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
- // internal functionality
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
- // needed by server
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
- // needed by server
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
- this.handleUnsubscribe(client, 'objects', pattern); // ignore options => unsubscribe may everyone
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
- // needed by server
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
- // needed by server
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
- // needed by server
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
- // needed by server
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
- // needed by server
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
- if (!this.stateTimer) {
787
- this.stateTimer = setTimeout(() => this.saveState(), this.writeFileInterval);
788
- }
872
+ this.stateTimer ||= setTimeout(() => this.saveState(), this.writeFileInterval);
789
873
  }
790
- // needed by server
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 = this.stateTimer || setTimeout(() => this.saveState(), this.writeFileInterval);
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
- // internal functionality
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, obj) {
831
- result.rows.push({ id: id, value: obj });
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: 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
- // needed by server
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 (!(designObj.views && designObj.views[search])) {
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
  }