@ocfl/ocfl 0.2.2 → 0.3.0

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/lib/inventory.js CHANGED
@@ -1,53 +1,194 @@
1
+ 'use strict';
2
+
1
3
  //@ts-check
4
+ /** @typedef {import('./types').Inventory} Inventory */
5
+ /** @typedef {import('./types').InventoryDigests} InventoryDigests */
2
6
 
3
- const path = require('path');
4
- const { digestSync } = require('./digest.js').OcflDigest;
5
- //const {createHash} = require('crypto');
6
-
7
- function nextVersion(currentVersion) {
8
- let zeroPaddingLength = 0;
9
- let currentVer = currentVersion.replace('v', '');
10
- if (currentVer.startsWith('0')) zeroPaddingLength = currentVer.length;
11
- let newVer = '' + (parseInt(currentVer) + 1);
12
- newVer = 'v' + (zeroPaddingLength ? newVer.padStart(zeroPaddingLength, '0') : newVer);
13
- return newVer;
14
- }
7
+ class VersionNumber {
8
+ value;
9
+ zeroPaddingLength;
10
+ /**
11
+ *
12
+ * @param {number} num
13
+ * @param {number} [zeroPaddingLength]
14
+ */
15
+ constructor(num, zeroPaddingLength) {
16
+ this.value = num;
17
+ this.zeroPaddingLength = zeroPaddingLength;
18
+ }
19
+ valueOf() {
20
+ return this.value;
21
+ }
22
+ toString() {
23
+ return 'v' + (this.zeroPaddingLength ? (this.value + '').padStart(this.zeroPaddingLength, '0') : this.value);
24
+ }
25
+ next() {
26
+ return new VersionNumber(this.value + 1, this.zeroPaddingLength);
27
+ }
28
+ previous() {
29
+ const n = this.value - 1;
30
+ if (n > 0) return new VersionNumber(n, this.zeroPaddingLength);
31
+ }
32
+ /**
33
+ *
34
+ * @param {number} num
35
+ * @param {number} zeroPaddingLength
36
+ */
37
+ static fromInt(num, zeroPaddingLength) {
38
+ return new VersionNumber(num, zeroPaddingLength);
39
+ }
40
+ /**
41
+ *
42
+ * @param {string} versionLabel
43
+ */
44
+ static fromString(versionLabel) {
45
+ const label = versionLabel.replace('v', '');
46
+ let zeroPaddingLength;
47
+ if (label.startsWith('0')) zeroPaddingLength = label.length;
48
+ return new VersionNumber(parseInt(label), zeroPaddingLength);
49
+ }
15
50
 
16
- function prevVersion(currentVersion) {
17
- let zeroPaddingLength = 0;
18
- let currentVer = currentVersion.replace('v', '');
19
- if (currentVer.startsWith('0')) zeroPaddingLength = currentVer.length;
20
- let prevVer = '' + (parseInt(currentVer) - 1);
21
- prevVer = 'v' + (zeroPaddingLength ? prevVer.padStart(zeroPaddingLength, '0') : prevVer);
22
- return prevVer;
51
+ static next(versionLabel) {
52
+ return VersionNumber.fromString(versionLabel).next();
53
+ }
54
+
55
+ static previous(versionLabel) {
56
+ return VersionNumber.fromString(versionLabel).previous();
57
+ }
23
58
  }
24
59
 
25
60
  /**
26
61
  * @typedef {{logicalPath?: string; digest?: string; contentPath?: string;}} FileRef
27
62
  */
28
63
 
64
+ function findKey(file, obj) {
65
+ for (let digest in obj) {
66
+ const arr = obj[digest];
67
+ if (arr.includes(file)) return digest;
68
+ }
69
+ }
70
+
29
71
  /**
30
72
  * Wrapper class for inventory data
31
73
  */
32
74
  class OcflObjectInventory {
33
75
  /** @type {Inventory} */
34
76
  #data;
35
- /** @type {InventoryVersion} */
36
- #version;
77
+ /** @type {{[key: string]: {[key:string]: {[key:string]: string}}}} */
78
+ #fileIndex = {};
79
+
37
80
  metadata;
81
+
82
+ /**
83
+ * Helper function to find the digest by logical file path and cached it by indexing
84
+ * @param {string} logicalPath
85
+ * @param {string} version
86
+ * @param {boolean} [onlyUsecache]
87
+ */
88
+ _indexGet(logicalPath, version, onlyUsecache) {
89
+ let digests = this.#fileIndex[version]?.[logicalPath];
90
+ if (!digests && !onlyUsecache) {
91
+ // search and then index
92
+ const fixity = this.fixity;
93
+ const algo = this.digestAlgorithm;
94
+ const digestArr = [];
95
+ const mainDigest = findKey(logicalPath, this.versionState(version));
96
+ if (mainDigest) {
97
+ digestArr.push([algo, mainDigest]);
98
+ if (fixity) {
99
+ const cp = this.getContentPath(mainDigest);
100
+ for (const fixityDigest in fixity) {
101
+ const digest = findKey(cp, fixity[fixityDigest]);
102
+ if (digest) digestArr.push([fixityDigest, digest]);
103
+ }
104
+ }
105
+ }
106
+ if (digestArr.length > 0) {
107
+ const verIndex = this.#fileIndex[version] = this.#fileIndex[version] || {};
108
+ digests = verIndex[logicalPath] = Object.fromEntries(digestArr);
109
+ }
110
+ }
111
+ return digests;
112
+ }
113
+
114
+ _indexAdd(logicalPath, version, digests) {
115
+ this.#fileIndex[version][logicalPath] = digests;
116
+ }
117
+
118
+ _indexRemove(logicalPath, version = this.head) {
119
+ const val = this.#fileIndex[version][logicalPath];
120
+ delete this.#fileIndex[version][logicalPath];
121
+ return val;
122
+ }
123
+
124
+ _indexBuild() {
125
+ // index fixity contentPaths
126
+ const fixity = this.fixity;
127
+ const fixityIndex = {};
128
+ if (fixity) {
129
+ for (const fixityAlgo in fixity) {
130
+ const digestBlock = fixity[fixityAlgo];
131
+ const fixityIndexDigest = fixityIndex[fixityAlgo] = {};
132
+ for (const digest in digestBlock) {
133
+ const cps = digestBlock[digest];
134
+ for (const cp of cps) {
135
+ fixityIndexDigest[cp] = digest;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ // index logicalPaths
141
+ // { v1: { 'dir/file.txt': { sha512: 'abcdef' } } }
142
+ const index = this.#fileIndex = {};
143
+ const algo = this.digestAlgorithm;
144
+ for (const verName in this.versions) {
145
+ const ver = this.versions[verName];
146
+ const indexVer = index[verName] = {};
147
+ for (const digest in ver.state) {
148
+ for (const lp of ver.state[digest]) {
149
+ const file = indexVer[lp] = { [algo]: digest };
150
+ if (fixity) {
151
+ for (const fixityAlgo in fixityIndex) {
152
+ const fixityIndexDigest = fixityIndex[fixityAlgo];
153
+ const cps = this.manifest[digest] || [];
154
+ for (const cp of cps) {
155
+ if (fixityIndexDigest[cp]) file[fixityAlgo] = fixityIndexDigest[cp];
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ _indexFindPaths(prefix, version = this.head) {
165
+ const res = [];
166
+ const index = this.#fileIndex[version];
167
+ const prefixWithSlash = prefix.endsWith('/') ? prefix : prefix + '/';
168
+ for (const lp in index) {
169
+ if (index[lp] && (lp === prefix || lp.startsWith(prefixWithSlash))) res.push(lp);
170
+ }
171
+ return res;
172
+ }
173
+
174
+
38
175
  /**
39
- * Create a mutable inventory and increase the inventory version number
40
- * @param {Inventory} data
176
+ * Create a new mutable OcflObjectInventory from the object config or clone an existing inventory,
177
+ * and increase the inventory version number. If data.head is undefined, starts from v1.
178
+ * @param {Partial<Inventory> | OcflObjectInventory} data
41
179
  * @param {boolean} [cleanState]
42
180
  */
43
181
  static newVersion(data, cleanState) {
182
+ if (data instanceof OcflObjectInventory) {
183
+ data = data.toJSON();
184
+ }
44
185
  if (!data.id || !data.digestAlgorithm || !data.type) throw new Error('Inventory must have an id, digestAlgorithm and type.');
45
186
  data.manifest = data.manifest || {};
46
187
  data.versions = data.versions || {};
47
188
  let prevVer;
48
189
  if (data.head) {
49
190
  prevVer = data.versions[data.head];
50
- data.head = nextVersion(data.head);
191
+ data.head = VersionNumber.next(data.head).toString();
51
192
  } else {
52
193
  data.head = 'v1';
53
194
  cleanState = true;
@@ -66,31 +207,39 @@ class OcflObjectInventory {
66
207
  */
67
208
  constructor(data, version) {
68
209
  this.#data = data;
69
- this.#version = data.versions[version || data.head];
210
+ this._indexBuild();
70
211
  }
71
212
 
72
213
  get id() { return this.#data.id }
73
214
  get type() { return this.#data.type }
74
215
  get digestAlgorithm() { return this.#data.digestAlgorithm }
75
216
  get head() { return this.#data.head }
217
+ /** The version number of the current head */
218
+ get versionNumber() { return VersionNumber.fromString(this.head); }
76
219
  get contentDirectory() { return this.#data.contentDirectory || 'content' }
77
220
  get manifest() { return this.#data.manifest }
78
221
  get versions() { return this.#data.versions }
222
+ get version() { return this.versions[this.head] }
79
223
  get fixity() { return this.#data.fixity }
80
- get state() { return this.#version.state }
81
- get created() { return this.#version.created }
82
- get message() { return this.#version.message }
83
- get user() { return this.#version.user }
84
- get prevVersion() { return prevVersion(this.head); }
224
+ set fixity(val) { this.#data.fixity = val; }
225
+ /** The state of the current version */
226
+ get state() { return this.version.state }
227
+ get created() { return this.version.created }
228
+ get message() { return this.version.message }
229
+ get user() { return this.version.user }
230
+ //get nextVersion() { return this.versionNumber.next().toString(); }
231
+ //get prevVersion() { return this.versionNumber.previous()?.toString(); }
85
232
 
86
233
  /** Return true if there has been actual change to the state of current version compared to the previous version */
87
234
  get isChanged() {
88
- let diff = this.diffState(this.head, prevVersion(this.head));
235
+ const prev = this.versionNumber.previous()?.toString();
236
+ if (!prev) return true;
237
+ let diff = this.diffState(this.head, prev);
89
238
  return Object.keys(diff[0]).length > 0 || Object.keys(diff[1]).length > 0;
90
239
  }
91
- set created(val) { this.#version.created = val; }
92
- set message(val) { this.#version.message = val; }
93
- set user(val) { this.#version.user = val; }
240
+ set created(val) { this.version.created = val; }
241
+ set message(val) { this.version.message = val; }
242
+ set user(val) { this.version.user = val; }
94
243
 
95
244
  /**
96
245
  * Return the state of the specified version
@@ -105,11 +254,6 @@ class OcflObjectInventory {
105
254
  return JSON.stringify(this.#data, null, 2);
106
255
  }
107
256
 
108
- /** Compute digest from current state of this inventory object*/
109
- digest() {
110
- return digestSync(this.#data.digestAlgorithm, this.toString());
111
- }
112
-
113
257
  /**
114
258
  * @return {Inventory}
115
259
  */
@@ -138,6 +282,7 @@ class OcflObjectInventory {
138
282
  version,
139
283
  digest,
140
284
  contentPath,
285
+ fixity: this.getFixity(logicalPath, version),
141
286
  ...this.metadata?.[contentPath]
142
287
  };
143
288
  }
@@ -166,14 +311,57 @@ class OcflObjectInventory {
166
311
  }
167
312
 
168
313
  /**
169
- * Get the content digest of the specified logicalPath
314
+ * Get the digest of a content represented by the logicalPath of the specified version
315
+ * @param {string} logicalPath
316
+ * @param {string} [versionName]
317
+ */
318
+ getDigest(logicalPath, versionName = this.head) {
319
+ return this._indexGet(logicalPath, versionName, true)?.[this.digestAlgorithm];
320
+ }
321
+
322
+ /**
323
+ * Get the file information by the logicalPath of the specified version
324
+ * @param {string} logicalPath
325
+ * @param {string} [versionName]
326
+ */
327
+ getFile(logicalPath, versionName = this.head) {
328
+ const digest = this.getDigest(logicalPath, versionName);
329
+ const contentPath = this.getContentPath(digest);
330
+ const version = versionName;
331
+ if (digest) {
332
+ return {
333
+ logicalPath,
334
+ version,
335
+ digest,
336
+ contentPath,
337
+ fixity: this.getFixity(logicalPath, version),
338
+ ...this.metadata?.[contentPath]
339
+ }
340
+ }
341
+ }
342
+
343
+ getFileByDigest(digest, versionName) {
344
+ if (versionName) return this.getFile(this.versions[versionName].state[digest]?.[0], versionName);
345
+ for (const version in this.versions) {
346
+ const logicalPath = this.versions[version].state[digest]?.[0];
347
+ if (logicalPath) return this.getFile(logicalPath, version);
348
+ }
349
+ }
350
+
351
+ getFileByContentPath(contentPath, versionName) {
352
+ for (const digest in this.manifest) {
353
+ if (this.manifest[digest].includes(contentPath)) return this.getFileByDigest(digest, versionName);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Get the fixity of a content represented by the logicalPath of the specified version
170
359
  * @param {string} logicalPath
171
360
  * @param {string} [versionName]
172
- * @returns
173
361
  */
174
- getDigest(logicalPath, versionName) {
175
- let state = this.versionState(versionName);
176
- return Object.keys(state).find(d => state[d].includes(logicalPath));
362
+ getFixity(logicalPath, versionName = this.head) {
363
+ const { [this.digestAlgorithm]: digest, ...fixity } = this._indexGet(logicalPath, versionName, true) || {};
364
+ return fixity;
177
365
  }
178
366
 
179
367
  /**
@@ -214,41 +402,27 @@ class OcflObjectInventory {
214
402
  * Wrapper class with mutable methods for inventory data
215
403
  */
216
404
  class OcflObjectInventoryMut extends OcflObjectInventory {
217
- /** @type {Object.<string,string>} */
218
- #byPath;
219
405
 
220
406
  constructor(data) {
221
407
  super(data);
222
- //index logicalPaths
223
- let byPath = this.#byPath = {};
224
- let state = this.state;
225
- for (let d in state) {
226
- let files = state[d];
227
- for (let lp of files) {
228
- byPath[lp] = d;
229
- }
230
- }
231
- }
232
-
233
- /**
234
- * Get the digest of a content represented by the logicalPath of the specified version
235
- * @param {string} logicalPath
236
- * @param {string} [versionName]
237
- * @return {string}
238
- */
239
- getDigest(logicalPath, versionName) {
240
- if (versionName) return super.getDigest(logicalPath, versionName);
241
- else return this.#byPath[logicalPath];
242
408
  }
243
409
 
244
410
  /**
245
411
  * Add a new file to the inventory or update the digest of an existing file
246
412
  * @param {string} digest
247
413
  * @param {string} logicalPath
414
+ * @param {{[x: string]: string}} [fixity]
248
415
  * @return {boolean} - true if the state is changed
249
416
  */
250
- add(logicalPath, digest) {
251
- let state = this.state;
417
+ add(logicalPath, digest, fixity) {
418
+ const state = this.state;
419
+ const contentPath = this.head + '/' + this.contentDirectory + '/' + logicalPath;
420
+ // add fixity info
421
+ if (fixity) {
422
+ for (const digestName in fixity) {
423
+ this.addFixity(contentPath, digestName, fixity[digestName]);
424
+ }
425
+ }
252
426
  if (state[digest]) {
253
427
  // this is a file with identical content but different name
254
428
  if (state[digest].indexOf(logicalPath) < 0) state[digest].push(logicalPath);
@@ -258,19 +432,38 @@ class OcflObjectInventoryMut extends OcflObjectInventory {
258
432
  if (mf && mf.length > 0) {
259
433
  // this is a reinstatement
260
434
  } else {
261
- // normal addition
262
- this.manifest[digest] = [path.join(this.head, this.contentDirectory, logicalPath)];
435
+ // normal addition of a new file
436
+ this.manifest[digest] = [contentPath];
263
437
  }
264
- if (this.#byPath[logicalPath]) {
438
+ if (this.getDigest(logicalPath)) {
265
439
  // this is an update, remove old digest
266
440
  this.delete(logicalPath);
267
441
  }
268
442
  state[digest] = [logicalPath];
269
443
  }
270
- this.#byPath[logicalPath] = digest;
444
+ this._indexAdd(logicalPath, this.head, { [this.digestAlgorithm]: digest, ...fixity });
271
445
  return true;
272
446
  }
273
447
 
448
+ /**
449
+ * Low-level method to add a set of digest entries associated to file into the fixity block
450
+ * @param {string} contentPath
451
+ * @param {string} digestName
452
+ * @param {string} digestValue
453
+ */
454
+ addFixity(contentPath, digestName, digestValue) {
455
+ if (!digestValue || !digestName) return;
456
+ const fixityBlock = this.fixity = this.fixity || {};
457
+ const digestBlock = fixityBlock[digestName] = fixityBlock[digestName] || {};
458
+ if (digestBlock[digestValue]) {
459
+ if (!digestBlock[digestValue].includes(contentPath)) {
460
+ digestBlock[digestValue].push(contentPath);
461
+ }
462
+ } else {
463
+ digestBlock[digestValue] = [contentPath];
464
+ }
465
+ }
466
+
274
467
  /**
275
468
  * Rename a logical path. This will overwrite dest if exists
276
469
  * @param {string} srcLogicalPath
@@ -279,18 +472,17 @@ class OcflObjectInventoryMut extends OcflObjectInventory {
279
472
  rename(srcLogicalPath, destLogicalPath) {
280
473
  let srcPrefix = srcLogicalPath.endsWith('/') ? srcLogicalPath : srcLogicalPath + '/';
281
474
  let destPrefix = destLogicalPath.endsWith('/') ? destLogicalPath : destLogicalPath + '/';
282
- let srcList = Object.keys(this.#byPath).filter(p => p.startsWith(srcPrefix));
475
+ let srcList = this._indexFindPaths(srcPrefix);
283
476
  let pairs = srcList.map(src => [src, destPrefix + src.slice(srcPrefix.length)]);
284
477
  if (pairs.length === 0) pairs.push([srcLogicalPath, destLogicalPath]);
285
478
  for (let [src, dest] of pairs) {
286
- let digest = this.#byPath[src];
479
+ let digest = this.getDigest(src);
287
480
  let files = this.state[digest];
288
481
  let index = files?.indexOf(src);
289
482
  if (index >= 0) {
290
483
  this.delete(dest); //overwrite
291
484
  files[index] = dest;
292
- this.#byPath[dest] = digest;
293
- this.#byPath[src] = null;
485
+ this._indexAdd(dest, this.head, this._indexRemove(src, this.head));
294
486
  } else {
295
487
  /** @todo decide: throw error or silently ignore non-existant file? */
296
488
  }
@@ -305,21 +497,21 @@ class OcflObjectInventoryMut extends OcflObjectInventory {
305
497
  copy(srcLogicalPath, destLogicalPath) {
306
498
  let srcPrefix = srcLogicalPath.endsWith('/') ? srcLogicalPath : srcLogicalPath + '/';
307
499
  let destPrefix = destLogicalPath.endsWith('/') ? destLogicalPath : destLogicalPath + '/';
308
- let srcList = Object.keys(this.#byPath).filter(p => p.startsWith(srcPrefix));
500
+ let srcList = this._indexFindPaths(srcPrefix);
309
501
  let pairs = srcList.map(src => [src, destPrefix + src.slice(srcPrefix.length)]);
310
502
  if (pairs.length === 0) pairs.push([srcLogicalPath, destLogicalPath]);
311
503
  for (let [src, dest] of pairs) {
312
504
  if (!dest) throw new Error(`Target logical path cannot be empty`);
313
- let digest = this.#byPath[src];
505
+ let digest = this._indexGet(src, this.head, true);
314
506
  if (!digest) throw new Error(`Source logical path "${src}" does not exist in object [${this.id}]`);
315
507
  //console.log(this.state);
316
- let files = this.state[digest];
508
+ let files = this.state[digest[this.digestAlgorithm]];
317
509
  let index = files?.indexOf(dest);
318
510
  if (index >= 0) {
319
511
  /** @todo decide: throw error or silently ignore existing dest file? */
320
512
  } else {
321
513
  files.push(dest);
322
- this.#byPath[dest] = digest;
514
+ this._indexAdd(dest, this.head, digest);
323
515
  }
324
516
  }
325
517
  }
@@ -327,20 +519,23 @@ class OcflObjectInventoryMut extends OcflObjectInventory {
327
519
  /**
328
520
  * Delete a file
329
521
  * @param {string} logicalPath
522
+ * @param {string} [version]
330
523
  */
331
- delete(logicalPath) {
332
- let prefix = logicalPath.endsWith('/') ? logicalPath : logicalPath + '/';
333
- let names = this.getDigest(logicalPath) ? [logicalPath] :
334
- Object.keys(this.#byPath).filter(p => p.startsWith(prefix));
524
+ delete(logicalPath, version = this.head) {
525
+ const names = this._indexFindPaths(logicalPath, version);
335
526
  let count = 0;
527
+ const state = this.versionState(version);
336
528
  for (let lp of names) {
337
- let digest = this.#byPath[lp];
338
- let files = this.state[digest];
339
- let index = files?.indexOf(lp);
529
+ const digest = this.getDigest(lp, version);
530
+ const files = state[digest];
531
+ const index = files?.indexOf(lp);
340
532
  if (index >= 0) {
341
- files.splice(index, 1);
342
- if (files.length === 0) delete this.state[digest];
343
- this.#byPath[lp] = null;
533
+ if (files.length === 1) {
534
+ delete state[digest];
535
+ } else {
536
+ files.splice(index, 1);
537
+ }
538
+ this._indexRemove(lp, version);
344
539
  count++;
345
540
  }
346
541
  }
@@ -348,19 +543,17 @@ class OcflObjectInventoryMut extends OcflObjectInventory {
348
543
  }
349
544
 
350
545
  reinstate(logicalPath, versionName) {
351
- let prefix = logicalPath.endsWith('/') ? logicalPath : logicalPath + '/';
352
- let digest = this.getDigest(logicalPath, versionName);
353
- /** @type {FileRef[]} */
354
- let files = digest ? [{ logicalPath, digest }] :
355
- [...this.find(f => f.logicalPath.startsWith(prefix), versionName)];
546
+ let names = this._indexFindPaths(logicalPath, versionName);
547
+ //let digest = this.getDigest(logicalPath, versionName);
356
548
  let state = this.state;
357
549
  let count = 0;
358
- for (let f of files) {
359
- if (this.getDigest(f.logicalPath)) {
360
- let c = this.delete(f.logicalPath);
550
+ for (let lp of names) {
551
+ const digests = this._indexGet(lp, versionName);
552
+ if (digests) {
553
+ let c = this.delete(lp);
361
554
  }
362
- state[f.digest] = [f.logicalPath];
363
- this.#byPath[f.logicalPath] = f.digest;
555
+ state[digests[this.digestAlgorithm]] = [lp];
556
+ this._indexAdd(lp, this.head, digests);
364
557
  count++;
365
558
  }
366
559
  return count;
@@ -368,4 +561,4 @@ class OcflObjectInventoryMut extends OcflObjectInventory {
368
561
 
369
562
  }
370
563
 
371
- module.exports = { OcflObjectInventory, OcflObjectInventoryMut };
564
+ module.exports = { OcflObjectInventory, OcflObjectInventoryMut, VersionNumber };