@ocfl/ocfl 0.1.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.
@@ -0,0 +1,46 @@
1
+ const path = require("path");
2
+ const { OcflStorageLayout } = require("../extension");
3
+ const { EXTENSIONS_DIR } = require("../constants").OcflConstants;
4
+
5
+ /** @type {FlatDirectStorageLayout} */
6
+ var instance;
7
+
8
+ /**
9
+ * OCFL object identifiers are mapped directly to directory names that are direct children of the OCFL storage root directory.
10
+ */
11
+ // @ts-ignore
12
+ class FlatDirectStorageLayout extends OcflStorageLayout {
13
+ static get NAME() { return '0002-flat-direct-storage-layout' }
14
+ static get DESCRIPTION() {
15
+ return "OCFL object identifiers are mapped directly to directory names" +
16
+ " that are direct children of the OCFL storage root.";
17
+ }
18
+
19
+ static create() {
20
+ if (!instance) instance = new FlatDirectStorageLayout();
21
+ return instance;
22
+ }
23
+
24
+ constructor() {
25
+ super()
26
+ }
27
+
28
+ /**
29
+ * @param {string} id
30
+ * @return {string}
31
+ */
32
+ map(id) {
33
+ if (path.basename(id) !== id) {
34
+ throw new Error(`The object id <${id}> is incompatible with layout extension ${this.name} because it contains the path separator character.`);
35
+ } else if (id === EXTENSIONS_DIR) {
36
+ throw new Error(`The object id <${id}> is incompatible with layout extension ${this.name} because it conflicts with the extensions directory.`);
37
+ }
38
+ if (id.length > 255) {
39
+ throw new Error(`The object id <${id}> is incompatible with layout extension ${this.name} because it has more than 255 characters.`);
40
+ }
41
+
42
+ return id;
43
+ }
44
+ }
45
+
46
+ module.exports = { FlatDirectStorageLayout };
@@ -0,0 +1,58 @@
1
+ const { OcflStorageLayout } = require("../extension");
2
+ const { OcflDigest } = require("../digest");
3
+ const path = require("path");
4
+
5
+ function encodeIdentifier(id) {
6
+ return encodeURIComponent(id).toLowerCase().replace(/\./g, '%2e');
7
+ }
8
+
9
+ const DefaultConfig = {
10
+ extensionName: '0003-hash-and-id-n-tuple-storage-layout',
11
+ digestAlgorithm: 'sha256',
12
+ tupleSize: 3,
13
+ numberOfTuples: 3,
14
+ };
15
+
16
+ /**
17
+ * @extends {OcflStorageLayout<typeof DefaultConfig>}
18
+ */
19
+ class HashAndIdNTupleStorageLayout extends OcflStorageLayout {
20
+ static get NAME() { return DefaultConfig.extensionName }
21
+ static get DESCRIPTION() {
22
+ return "OCFL object identifiers are hashed and encoded as lowercase hex strings." +
23
+ " These digests are then divided into N n-tuple segments," +
24
+ " which are used to create nested paths under the OCFL storage root." +
25
+ " Finally, the OCFL object identifier is percent-encoded to create a directory name for the OCFL object root.";
26
+ }
27
+
28
+ /**
29
+ * @param {Partial<typeof DefaultConfig>} [config]
30
+ */
31
+ constructor(config) {
32
+ super(config);
33
+ let c = this.parameters;
34
+ if (!OcflDigest.FIXITY.of(c.digestAlgorithm)) throw new Error('Invalid digestAlgorithm');
35
+ let digest = OcflDigest.digestSync(c.digestAlgorithm, 'test');
36
+ let p = c.numberOfTuples * c.tupleSize
37
+ if (p > digest.length) throw new Error('Product of numberOfTuples and tupleSize is greater than the number of characters in the hex encoded digest.');
38
+ }
39
+
40
+ /**
41
+ * @param {string} id
42
+ * @return {string}
43
+ */
44
+ map(id) {
45
+ let digest = OcflDigest.digestSync(this.config.digestAlgorithm, id);
46
+ let segments = [];
47
+ let s = this.config.tupleSize;
48
+ let n = this.config.numberOfTuples;
49
+ let i;
50
+ for (i = 0; i < n; ++i) {
51
+ segments.push(digest.slice(s * i, s * i + s));
52
+ }
53
+ segments.push(encodeIdentifier(id));
54
+ return segments.join(path.sep);
55
+ }
56
+ }
57
+
58
+ module.exports = { HashAndIdNTupleStorageLayout };
@@ -0,0 +1,61 @@
1
+ const { OcflStorageLayout } = require("../extension");
2
+ const { OcflDigest } = require("../digest");
3
+ const path = require("path");
4
+
5
+ const DefaultConfig = {
6
+ extensionName: '0004-hashed-n-tuple-storage-layout',
7
+ digestAlgorithm: 'sha256',
8
+ tupleSize: 3,
9
+ numberOfTuples: 3,
10
+ shortObjectRoot: false
11
+ };
12
+ /** @typedef {Partial<typeof DefaultConfig>} HashedNTupleStorageLayoutConfig */
13
+ /**
14
+ * @extends {OcflStorageLayout<typeof DefaultConfig>}
15
+ */
16
+ class HashedNTupleStorageLayout extends OcflStorageLayout {
17
+ static get NAME() { return DefaultConfig.extensionName; }
18
+ static get DESCRIPTION() {
19
+ return "OCFL object identifiers are hashed and encoded as lowercase hex strings." +
20
+ " These digests are then divided into N n-tuple segments," +
21
+ " which are used to create nested paths under the OCFL storage root.";
22
+ }
23
+
24
+ /**
25
+ *
26
+ * @param {HashedNTupleStorageLayoutConfig} [config]
27
+ */
28
+ constructor(config) {
29
+ super(config, DefaultConfig);
30
+ let c = this.parameters;
31
+ if (!OcflDigest.FIXITY.of(c.digestAlgorithm)) throw new Error('Invalid digestAlgorithm');
32
+ let digest = OcflDigest.digestSync(c.digestAlgorithm, 'test');
33
+ let p = c.numberOfTuples * c.tupleSize
34
+ if (p > digest.length) throw new Error('Product of numberOfTuples and tupleSize is greater than the number of characters in the hex encoded digest.');
35
+ else if (p === digest.length && c.shortObjectRoot) throw new Error('shortObjectRoot cannot be set to true');
36
+ }
37
+
38
+ /**
39
+ * @param {string} id
40
+ * @return {string}
41
+ */
42
+ map(id) {
43
+ let digest = OcflDigest.digestSync(this.parameters.digestAlgorithm, id);
44
+ let segments = [];
45
+ let s = this.parameters.tupleSize;
46
+ let n = this.parameters.numberOfTuples;
47
+ let i;
48
+ for (i = 0; i < n; ++i) {
49
+ segments.push(digest.slice(s * i, s * i + s));
50
+ }
51
+ if (this.parameters.shortObjectRoot) {
52
+ segments.push(digest.slice(s * i));
53
+ } else {
54
+ segments.push(digest);
55
+ }
56
+ return segments.join(path.sep);
57
+ }
58
+
59
+ }
60
+
61
+ module.exports = { HashedNTupleStorageLayout };
File without changes
@@ -0,0 +1,7 @@
1
+ const {OcflStorageLayout} = require("../extension");
2
+
3
+ class FlatOmitPrefixStorageLayout extends OcflStorageLayout {
4
+ static get NAME() { return '0006-flat-omit-prefix-storage-layout' }
5
+ }
6
+
7
+ module.exports = { FlatOmitPrefixStorageLayout };
@@ -0,0 +1,7 @@
1
+ const {OcflStorageLayout} = require("../extension");
2
+
3
+ class NTupleOmitPrefixStorageLayout extends OcflStorageLayout {
4
+ static get NAME() { return '0007-n-tuple-omit-prefix-storage-layout' }
5
+ }
6
+
7
+ module.exports = { NTupleOmitPrefixStorageLayout };
File without changes
@@ -0,0 +1,12 @@
1
+
2
+ module.exports = {
3
+ ...require('./0001-digest-algorithms'),
4
+ ...require('./0002-flat-direct-storage-layout'),
5
+ ...require('./0003-hash-and-id-n-tuple-storage-layout'),
6
+ ...require('./0004-hashed-n-tuple-storage-layout'),
7
+ // ...require('./0005-mutable-head'),
8
+ ...require('./0006-flat-omit-prefix-storage-layout'),
9
+ ...require('./0007-n-tuple-omit-prefix-storage-layout')
10
+ //...require('./0008-schema-registry')
11
+ };
12
+ //const { DigestAlgorithm } = require('./0001-digest-algorithms.js');
package/lib/index.js ADDED
@@ -0,0 +1,25 @@
1
+ // this is an internal index.js to avoid circular dependencies problem
2
+ // const { OcflStorage, OcflStorageImpl } = require('./storage.js');
3
+ // const { OcflObject,
4
+ // OcflObjectImpl,
5
+ // OcflObjectInventory,
6
+ // OcflObjectTransactionImpl,
7
+ // createObject,
8
+ // createObjectProxy,
9
+ // createTransactionProxy } = require('./object.js');
10
+ // const { OcflConstants } = require('./constants.js');
11
+ // const { OcflDigest } = require('./digest.js');
12
+ // const { OcflExtension } = require('./extension.js');
13
+
14
+ module.exports = {
15
+ ...require('./constants.js'),
16
+ ...require('./digest.js'),
17
+ ...require('./transaction.js'),
18
+ ...require('./extension.js'),
19
+ ...require('./store.js'),
20
+ ...require('./object.js'),
21
+ ...require('./storage.js'),
22
+ extensions: {
23
+ ...require('./extensions/index.js')
24
+ }
25
+ };
@@ -0,0 +1,393 @@
1
+ //@ts-check
2
+
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
+ }
15
+
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;
23
+ }
24
+
25
+ /**
26
+ * @typedef {{logicalPath?: string; digest?: string; contentPath?: string;}} FileRef
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object.<string, string[]>} InventoryState
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object} InventoryVersion
35
+ * @property {string} [created]
36
+ * @property {InventoryState} state
37
+ * @property {string} [message]
38
+ * @property {{name:string, address:string}} [user]
39
+ */
40
+
41
+ /**
42
+ * @typedef {Object} Inventory
43
+ * @property {string} id - A unique identifier for the OCFL Object.
44
+ * @property {string} type - The URI of the inventory section of the specification version matching the object conformance declaration.
45
+ * @property {string} digestAlgorithm - The digest algorithm used for calculating digests for content-addressing within the OCFL Object and for the Inventory Digest.
46
+ * @property {string} head - The version directory name of the most recent version of the object.
47
+ * @property {string} [contentDirectory='content'] - The name of the designated content directory within the version directories.
48
+ * @property {InventoryState} manifest
49
+ * @property {Object.<string, InventoryVersion>} versions
50
+ * @property {Object} [fixity]
51
+ */
52
+
53
+
54
+ /**
55
+ * Wrapper class for inventory data
56
+ */
57
+ class OcflObjectInventory {
58
+ /** @type {Inventory} */
59
+ #data;
60
+ /** @type {InventoryVersion} */
61
+ #version;
62
+
63
+ /**
64
+ * Create a mutable inventory and increase the inventory version number
65
+ * @param {Object} data
66
+ * @param {string} data.id
67
+ * @param {string} data.type
68
+ * @param {string} data.digestAlgorithm
69
+ * @param {string} [data.head]
70
+ * @param {string} [data.contentDirectory]
71
+ * @param {Object.<string, string[]>} [data.manifest]
72
+ * @param {Object.<string, InventoryVersion>} [data.versions]
73
+ * @param {Object} [data.fixity]
74
+ * @param {boolean} [cleanState]
75
+ */
76
+ static newVersion(data, cleanState) {
77
+ if (!data.id || !data.digestAlgorithm || !data.type) throw new Error('Inventory must have an id, digestAlgorithm and type.');
78
+ data.manifest = data.manifest || {};
79
+ data.versions = data.versions || {};
80
+ let prevVer;
81
+ if (data.head) {
82
+ prevVer = data.versions[data.head];
83
+ data.head = nextVersion(data.head);
84
+ } else {
85
+ data.head = 'v1';
86
+ cleanState = true;
87
+ }
88
+ data.versions[data.head] = {
89
+ created: (new Date()).toISOString(),
90
+ state: cleanState ? {} : JSON.parse(JSON.stringify(prevVer.state))
91
+ };
92
+ return new OcflObjectInventoryMut(data);
93
+ }
94
+
95
+ /**
96
+ * Create a new inventory model
97
+ * @param {Inventory} data - Initial data of the inventory
98
+ * @param {string} [version] - Set the version, default to data.head
99
+ */
100
+ constructor(data, version) {
101
+ this.#data = data;
102
+ this.#version = data.versions[version || data.head];
103
+ }
104
+
105
+ get id() { return this.#data.id }
106
+ get type() { return this.#data.type }
107
+ get digestAlgorithm() { return this.#data.digestAlgorithm }
108
+ get head() { return this.#data.head }
109
+ get contentDirectory() { return this.#data.contentDirectory || 'content' }
110
+ get manifest() { return this.#data.manifest }
111
+ get versions() { return this.#data.versions }
112
+ get fixity() { return this.#data.fixity }
113
+ get state() { return this.#version.state }
114
+ get created() { return this.#version.created }
115
+ get message() { return this.#version.message }
116
+ get user() { return this.#version.user }
117
+ get prevVersion() { return prevVersion(this.head); }
118
+
119
+ /** Return true if there has been actual change to the state of current version compared to the previous version */
120
+ get isChanged() {
121
+ let diff = this.diffState(this.head, prevVersion(this.head));
122
+ return Object.keys(diff[0]).length > 0 || Object.keys(diff[1]).length > 0;
123
+ }
124
+ set created(val) { this.#version.created = val; }
125
+ set message(val) { this.#version.message = val; }
126
+ set user(val) { this.#version.user = val; }
127
+
128
+ /**
129
+ * Return the state of the specified version
130
+ * @param {string} [version ] - The inventory version, default to the latest
131
+ */
132
+ versionState(version) {
133
+ if (!version || version === 'latest') version = this.head;
134
+ return this.versions[version]?.state;
135
+ }
136
+
137
+ toString() {
138
+ return JSON.stringify(this.#data, null, 2);
139
+ }
140
+
141
+ /** Compute digest from current state of this inventory object*/
142
+ digest() {
143
+ return digestSync(this.#data.digestAlgorithm, this.toString());
144
+ }
145
+
146
+ /**
147
+ * @return {Inventory}
148
+ */
149
+ toJSON() {
150
+ return JSON.parse(this.toString());
151
+ }
152
+
153
+ /**
154
+ *
155
+ * @param {string} [version] - The version name
156
+ */
157
+ *files(version) {
158
+ let state = (!version || version === 'latest') ? this.state : this.versions[version]?.state;
159
+ for (let digest in state) {
160
+ let files = state[digest];
161
+ let contentPath = this.getContentPath(digest);
162
+ for (let logicalPath of files) {
163
+ yield {
164
+ logicalPath,
165
+ digest,
166
+ contentPath
167
+ };
168
+ }
169
+ }
170
+ }
171
+
172
+ *find(fn, version) {
173
+ for (let f of this.files(version)) {
174
+ if (fn(f)) yield f;
175
+ }
176
+ }
177
+
178
+ clone() {
179
+ return new OcflObjectInventory(this.toJSON());
180
+ }
181
+
182
+ /**
183
+ * Get the first content path found with the given digest from the manifest.
184
+ * Also can be used to check the existance of an entry with the specified digest in the manifest.
185
+ * @param {string} digest
186
+ * @return {string} - The first content path found in the array
187
+ */
188
+ getContentPath(digest) {
189
+ let cp = this.manifest[digest];
190
+ if (cp && cp.length > 0) return cp[0];
191
+ }
192
+
193
+ /**
194
+ * Get the content digest of the specified logicalPath
195
+ * @param {string} logicalPath
196
+ * @param {string} [versionName]
197
+ * @returns
198
+ */
199
+ getDigest(logicalPath, versionName) {
200
+ let state = this.versionState(versionName);
201
+ return Object.keys(state).find(d => state[d].includes(logicalPath));
202
+ }
203
+
204
+ /**
205
+ * Compare the differences between the state of two versions
206
+ * @param {string} version0
207
+ * @param {string} version1
208
+ * @return {[InventoryState, InventoryState]} - A tuple of [A, B] where A contains files
209
+ * that are in `version0` but not in `version1` and B contains files
210
+ * that are in `version1` but not in `version0`
211
+ */
212
+ diffState(version0, version1) {
213
+ var state1 = this.versionState(version0) ?? {};
214
+ var state2 = this.versionState(version1) ?? {};
215
+
216
+ var result = [[state1, state2], [state2, state1]].map(([s1, s2]) => {
217
+ /** @type {Map<string, string[]>} */
218
+ let r = new Map();
219
+ for (let d in s1) {
220
+ let d1 = s1[d];
221
+ let d2 = s2[d];
222
+ let diff;
223
+ if (d2 && d2.length) {
224
+ diff = d1.filter(f => !d2.includes(f));
225
+ } else {
226
+ diff = d1.slice();
227
+ }
228
+ if (diff.length > 0) r.set(d, diff);
229
+ }
230
+ return Object.fromEntries(r);
231
+ });
232
+
233
+ return /** @type {[InventoryState, InventoryState]} */(result);
234
+ }
235
+ }
236
+
237
+
238
+ /**
239
+ * Wrapper class with mutable methods for inventory data
240
+ */
241
+ class OcflObjectInventoryMut extends OcflObjectInventory {
242
+ /** @type {Object.<string,string>} */
243
+ #byPath;
244
+
245
+ constructor(data) {
246
+ super(data);
247
+ //index logicalPaths
248
+ let byPath = this.#byPath = {};
249
+ let state = this.state;
250
+ for (let d in state) {
251
+ let files = state[d];
252
+ for (let lp of files) {
253
+ byPath[lp] = d;
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Get the digest of a content represented by the logicalPath of the specified version
260
+ * @param {string} logicalPath
261
+ * @param {string} [versionName]
262
+ * @return {string}
263
+ */
264
+ getDigest(logicalPath, versionName) {
265
+ if (versionName) return super.getDigest(logicalPath, versionName);
266
+ else return this.#byPath[logicalPath];
267
+ }
268
+
269
+ /**
270
+ * Add a new file to the inventory or update the digest of an existing file
271
+ * @param {string} digest
272
+ * @param {string} logicalPath
273
+ * @return {boolean} - true if the state is changed
274
+ */
275
+ add(logicalPath, digest) {
276
+ let state = this.state;
277
+ if (state[digest]) {
278
+ // this is a file with identical content but different name
279
+ if (state[digest].indexOf(logicalPath) < 0) state[digest].push(logicalPath);
280
+ else return false; // else, same digest and path, ignore
281
+ } else {
282
+ let mf = this.manifest[digest];
283
+ if (mf && mf.length > 0) {
284
+ // this is a reinstatement
285
+ } else {
286
+ if (this.#byPath[logicalPath]) {
287
+ // this is an update, remove old digest
288
+ this.delete(logicalPath);
289
+ } else {
290
+ // normal addition
291
+ }
292
+ this.manifest[digest] = [path.join(this.head, this.contentDirectory, logicalPath)];
293
+ }
294
+ state[digest] = [logicalPath];
295
+ }
296
+ this.#byPath[logicalPath] = digest;
297
+ return true;
298
+ }
299
+
300
+ /**
301
+ * Rename a logical path. This will overwrite dest if exists
302
+ * @param {string} srcLogicalPath
303
+ * @param {string} destLogicalPath
304
+ */
305
+ rename(srcLogicalPath, destLogicalPath) {
306
+ let srcPrefix = srcLogicalPath.endsWith('/') ? srcLogicalPath : srcLogicalPath + '/';
307
+ let destPrefix = destLogicalPath.endsWith('/') ? destLogicalPath : destLogicalPath + '/';
308
+ let srcList = Object.keys(this.#byPath).filter(p => p.startsWith(srcPrefix));
309
+ let pairs = srcList.map(src => [src, destPrefix + src.slice(srcPrefix.length)]);
310
+ if (pairs.length === 0) pairs.push([srcLogicalPath, destLogicalPath]);
311
+ for (let [src, dest] of pairs) {
312
+ let digest = this.#byPath[src];
313
+ let files = this.state[digest];
314
+ let index = files?.indexOf(src);
315
+ if (index >= 0) {
316
+ this.delete(dest); //overwrite
317
+ files[index] = dest;
318
+ this.#byPath[dest] = digest;
319
+ this.#byPath[src] = null;
320
+ } else {
321
+ /** @todo decide: throw error or silently ignore non-existant file? */
322
+ }
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Create an alias or reference of a logical path
328
+ * @param {string} srcLogicalPath
329
+ * @param {string} destLogicalPath
330
+ */
331
+ copy(srcLogicalPath, destLogicalPath) {
332
+ let srcPrefix = srcLogicalPath.endsWith('/') ? srcLogicalPath : srcLogicalPath + '/';
333
+ let destPrefix = destLogicalPath.endsWith('/') ? destLogicalPath : destLogicalPath + '/';
334
+ let srcList = Object.keys(this.#byPath).filter(p => p.startsWith(srcPrefix));
335
+ let pairs = srcList.map(src => [src, destPrefix + src.slice(srcPrefix.length)]);
336
+ if (pairs.length === 0) pairs.push([srcLogicalPath, destLogicalPath]);
337
+ for (let [src, dest] of pairs) {
338
+ if (!dest) throw new Error(`Target logical path cannot be empty`);
339
+ let digest = this.#byPath[src];
340
+ if (!digest) throw new Error(`Source logical path "${src}" does not exist in object [${this.id}]`);
341
+ //console.log(this.state);
342
+ let files = this.state[digest];
343
+ let index = files?.indexOf(dest);
344
+ if (index >= 0) {
345
+ /** @todo decide: throw error or silently ignore existing dest file? */
346
+ } else {
347
+ files.push(dest);
348
+ this.#byPath[dest] = digest;
349
+ }
350
+ }
351
+ }
352
+
353
+ delete(logicalPath) {
354
+ let prefix = logicalPath.endsWith('/') ? logicalPath : logicalPath + '/';
355
+ let names = this.getDigest(logicalPath) ? [logicalPath] :
356
+ Object.keys(this.#byPath).filter(p => p.startsWith(prefix));
357
+ let count = 0;
358
+ for (let lp of names) {
359
+ let digest = this.#byPath[lp];
360
+ let files = this.state[digest];
361
+ let index = files?.indexOf(lp);
362
+ if (index >= 0) {
363
+ files.splice(index, 1);
364
+ if (files.length === 0) delete this.state[digest];
365
+ this.#byPath[lp] = null;
366
+ count++;
367
+ }
368
+ }
369
+ return count;
370
+ }
371
+
372
+ reinstate(logicalPath, versionName) {
373
+ let prefix = logicalPath.endsWith('/') ? logicalPath : logicalPath + '/';
374
+ let digest = this.getDigest(logicalPath, versionName);
375
+ /** @type {FileRef[]} */
376
+ let files = digest ? [{ logicalPath, digest }] :
377
+ [...this.find(f => f.logicalPath.startsWith(prefix), versionName)];
378
+ let state = this.state;
379
+ let count = 0;
380
+ for (let f of files) {
381
+ if (this.getDigest(f.logicalPath)) {
382
+ let c = this.delete(f.logicalPath);
383
+ }
384
+ state[f.digest] = [f.logicalPath];
385
+ this.#byPath[f.logicalPath] = f.digest;
386
+ count++;
387
+ }
388
+ return count;
389
+ }
390
+
391
+ }
392
+
393
+ module.exports = { OcflObjectInventory, OcflObjectInventoryMut };