@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.
- package/README.md +16 -0
- package/index.js +131 -0
- package/index.mjs +18 -0
- package/lib/constants.js +24 -0
- package/lib/digest.js +138 -0
- package/lib/enum.js +92 -0
- package/lib/error.js +19 -0
- package/lib/extension.js +206 -0
- package/lib/extensions/0001-digest-algorithms.js +41 -0
- package/lib/extensions/0002-flat-direct-storage-layout.js +46 -0
- package/lib/extensions/0003-hash-and-id-n-tuple-storage-layout.js +58 -0
- package/lib/extensions/0004-hashed-n-tuple-storage-layout.js +61 -0
- package/lib/extensions/0005-mutable-head.js +0 -0
- package/lib/extensions/0006-flat-omit-prefix-storage-layout.js +7 -0
- package/lib/extensions/0007-n-tuple-omit-prefix-storage-layout.js +7 -0
- package/lib/extensions/0008-schema-registry.js +0 -0
- package/lib/extensions/index.js +12 -0
- package/lib/index.js +25 -0
- package/lib/inventory.js +393 -0
- package/lib/object.js +530 -0
- package/lib/storage.js +324 -0
- package/lib/store.js +153 -0
- package/lib/temp.js +264 -0
- package/lib/transaction.js +413 -0
- package/lib/types.d.ts +44 -0
- package/lib/utils.js +92 -0
- package/lib/validation.js +174 -0
- package/package.json +63 -0
- package/resources/ocfl_1.0.txt +936 -0
|
@@ -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
|
|
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
|
+
};
|
package/lib/inventory.js
ADDED
|
@@ -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 };
|