@ocfl/ocfl 0.1.2 → 0.2.2
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/index.js +6 -5
- package/lib/digest.js +6 -5
- package/lib/extension.js +4 -4
- package/lib/extensions/0001-digest-algorithms.js +10 -5
- package/lib/extensions/000N-path-direct-storage-layout.js +61 -0
- package/lib/extensions/index.js +2 -1
- package/lib/inventory.js +6 -38
- package/lib/object.js +116 -31
- package/lib/storage.js +3 -1
- package/lib/store.js +30 -0
- package/lib/types.d.ts +52 -1
- package/package.json +18 -11
package/index.js
CHANGED
|
@@ -21,11 +21,12 @@ const { OcflConstants, OcflDigest, OcflExtension, OcflStorageLayout, OcflStore,
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* @typedef {Object} StorageLayout
|
|
24
|
-
* @property {typeof extensions.FlatDirectStorageLayout}
|
|
25
|
-
* @property {typeof extensions.FlatOmitPrefixStorageLayout}
|
|
26
|
-
* @property {typeof extensions.HashAndIdNTupleStorageLayout}
|
|
27
|
-
* @property {typeof extensions.HashedNTupleStorageLayout}
|
|
28
|
-
* @property {typeof extensions.NTupleOmitPrefixStorageLayout}
|
|
24
|
+
* @property {typeof extensions.FlatDirectStorageLayout} FlatDirectStorageLayout
|
|
25
|
+
* @property {typeof extensions.FlatOmitPrefixStorageLayout} FlatOmitPrefixStorageLayout
|
|
26
|
+
* @property {typeof extensions.HashAndIdNTupleStorageLayout} HashAndIdNTupleStorageLayout
|
|
27
|
+
* @property {typeof extensions.HashedNTupleStorageLayout} HashedNTupleStorageLayout
|
|
28
|
+
* @property {typeof extensions.NTupleOmitPrefixStorageLayout} NTupleOmitPrefixStorageLayout
|
|
29
|
+
* @property {typeof extensions.PathDirectStorageLayout} PathDirectStorageLayout
|
|
29
30
|
*/
|
|
30
31
|
/** @typedef {import('./lib/extension.js').OcflExtensionConfig} OcflExtensionConfig */
|
|
31
32
|
|
package/lib/digest.js
CHANGED
|
@@ -58,17 +58,18 @@ for (let algo of FIXITY) {
|
|
|
58
58
|
*/
|
|
59
59
|
function createStream(algorithm) {
|
|
60
60
|
const hash = algorithms[algorithm || 'sha512']();
|
|
61
|
-
hash.setEncoding('hex');
|
|
61
|
+
//hash.setEncoding('hex');
|
|
62
62
|
return hash;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
*
|
|
67
|
-
* @param {string} algorithm
|
|
67
|
+
* @param {string} [algorithm]
|
|
68
|
+
* @param {object} [options]
|
|
68
69
|
*/
|
|
69
|
-
function createStreamThrough(algorithm, options) {
|
|
70
|
-
const hash = algorithms[algorithm
|
|
71
|
-
hash.setEncoding('hex');
|
|
70
|
+
function createStreamThrough(algorithm = 'sha512', options) {
|
|
71
|
+
const hash = algorithms[algorithm]();
|
|
72
|
+
//hash.setEncoding('hex');
|
|
72
73
|
return new HashThrough(hash, options);
|
|
73
74
|
}
|
|
74
75
|
|
package/lib/extension.js
CHANGED
|
@@ -7,7 +7,7 @@ const { NotImplementedError } = require("./error");
|
|
|
7
7
|
const extensionByName = {};
|
|
8
8
|
/** Index by extension class name */
|
|
9
9
|
const extensionByClassName = {};
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
const storageLayout = {};
|
|
12
12
|
|
|
13
13
|
/** @typedef {{ extensionName?: string;[key: string]: any; }} OcflExtensionConfig */
|
|
@@ -99,7 +99,7 @@ class OcflExtension {
|
|
|
99
99
|
|
|
100
100
|
static get forObject() { return false; }
|
|
101
101
|
static get forStorage() { return false; }
|
|
102
|
-
static setup(ocfl) { };
|
|
102
|
+
static async setup(ocfl) { };
|
|
103
103
|
static get NAME() { return '0000-example-extension' }
|
|
104
104
|
static get DESCRIPTION() { return 'Example extension' }
|
|
105
105
|
|
|
@@ -110,7 +110,7 @@ class OcflExtension {
|
|
|
110
110
|
*/
|
|
111
111
|
constructor(config, defaultConfig) {
|
|
112
112
|
/** @type {C} */
|
|
113
|
-
this.parameters = defaultConfig ? Object.create(/**@type{object}*/(defaultConfig)) : {};
|
|
113
|
+
this.parameters = typeof defaultConfig === 'object' ? Object.create(/**@type{object}*/(defaultConfig)) : {};
|
|
114
114
|
if (typeof config === 'object') {
|
|
115
115
|
for (let k in config) {
|
|
116
116
|
if (k in defaultConfig) this.parameters[k] = config[k];
|
|
@@ -157,7 +157,7 @@ class OcflExtension {
|
|
|
157
157
|
class OcflStorageLayout extends OcflExtension {
|
|
158
158
|
|
|
159
159
|
static get layout() {
|
|
160
|
-
return storageLayout;
|
|
160
|
+
return /** @type StorageLayout */(storageLayout);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const {OcflExtension} = require("../extension");
|
|
4
4
|
const crypto = require('crypto');
|
|
5
|
-
const blake2 = require('blake2');
|
|
5
|
+
//const blake2 = require('blake2');
|
|
6
|
+
const { createBLAKE2b } = require('hash-wasm');
|
|
6
7
|
|
|
7
8
|
class DigestAlgorithm extends OcflExtension {
|
|
8
9
|
static get NAME() { return '0001-digest-algorithms' }
|
|
@@ -15,7 +16,7 @@ class DigestAlgorithm extends OcflExtension {
|
|
|
15
16
|
*
|
|
16
17
|
* @param {import('ocfl')} ocfl
|
|
17
18
|
*/
|
|
18
|
-
static setup(ocfl) {
|
|
19
|
+
static async setup(ocfl) {
|
|
19
20
|
let {OcflDigest} = ocfl;
|
|
20
21
|
let { FIXITY } = OcflDigest;
|
|
21
22
|
class FIXITY_EX extends FIXITY {
|
|
@@ -26,9 +27,13 @@ class DigestAlgorithm extends OcflExtension {
|
|
|
26
27
|
};
|
|
27
28
|
OcflDigest.FIXITY = FIXITY_EX;
|
|
28
29
|
//console.log(OcflDigest.FIXITY);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const blake2b = Object.fromEntries(await Promise.all([160, 256, 384].map(bits => [bits, createBLAKE2b(bits)])));
|
|
31
|
+
|
|
32
|
+
for (let bits of [160, 256, 384]) {
|
|
33
|
+
OcflDigest.algorithms['blake2b-' + bits] = function() {
|
|
34
|
+
//return blake2.createHash('blake2b', {digestLength: len/8});
|
|
35
|
+
blake2b[bits].init();
|
|
36
|
+
return blake2b[bits];
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
OcflDigest.algorithms['sha512/256'] = function() {
|
|
@@ -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: '000N-path-direct-storage-layout',
|
|
7
|
+
omitScheme: false,
|
|
8
|
+
/** @type {any} */
|
|
9
|
+
replace: [],
|
|
10
|
+
suffix: '/__object__'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** @typedef {Partial<typeof DefaultConfig>} PathDirectStorageLayoutConfig */
|
|
14
|
+
/**
|
|
15
|
+
* @extends {OcflStorageLayout<typeof DefaultConfig>}
|
|
16
|
+
*/
|
|
17
|
+
class PathDirectStorageLayout extends OcflStorageLayout {
|
|
18
|
+
static get NAME() { return DefaultConfig.extensionName; }
|
|
19
|
+
static get DESCRIPTION() {
|
|
20
|
+
return "OCFL object identifiers are used directly as the path.";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* @param {PathDirectStorageLayoutConfig} [config]
|
|
26
|
+
*/
|
|
27
|
+
constructor(config) {
|
|
28
|
+
super(config, DefaultConfig);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} id
|
|
33
|
+
* @return {string}
|
|
34
|
+
*/
|
|
35
|
+
map(id) {
|
|
36
|
+
var suffix = this.parameters.suffix;
|
|
37
|
+
if (suffix && id.endsWith(suffix)) throw new Error(`Identifier cannot end with '${suffix}'`);
|
|
38
|
+
var p = id ?? '';
|
|
39
|
+
for (const r of this.parameters.replace) {
|
|
40
|
+
p = p.replace(r[0], r[1]);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
let u = new URL(p);
|
|
44
|
+
let parts = [];
|
|
45
|
+
if (!this.parameters.omitScheme && u.protocol !== 'file:') {
|
|
46
|
+
parts.push(u.protocol.replace(':', ''));
|
|
47
|
+
}
|
|
48
|
+
if (u.hostname) {
|
|
49
|
+
parts.push(u.hostname.replace(',', '_').replace(';', '/'));
|
|
50
|
+
}
|
|
51
|
+
p = path.join(parts.join('_'), u.pathname + u.search + u.hash);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
}
|
|
54
|
+
p = p.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
55
|
+
return path.join(p, this.parameters.suffix);
|
|
56
|
+
//if (/^\w+:/.test(id)) {
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { PathDirectStorageLayout };
|
package/lib/extensions/index.js
CHANGED
|
@@ -6,7 +6,8 @@ module.exports = {
|
|
|
6
6
|
...require('./0004-hashed-n-tuple-storage-layout'),
|
|
7
7
|
// ...require('./0005-mutable-head'),
|
|
8
8
|
...require('./0006-flat-omit-prefix-storage-layout'),
|
|
9
|
-
...require('./0007-n-tuple-omit-prefix-storage-layout')
|
|
9
|
+
...require('./0007-n-tuple-omit-prefix-storage-layout'),
|
|
10
|
+
...require('./000N-path-direct-storage-layout')
|
|
10
11
|
//...require('./0008-schema-registry')
|
|
11
12
|
};
|
|
12
13
|
//const { DigestAlgorithm } = require('./0001-digest-algorithms.js');
|
package/lib/inventory.js
CHANGED
|
@@ -26,31 +26,6 @@ function prevVersion(currentVersion) {
|
|
|
26
26
|
* @typedef {{logicalPath?: string; digest?: string; contentPath?: string;}} FileRef
|
|
27
27
|
*/
|
|
28
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
29
|
/**
|
|
55
30
|
* Wrapper class for inventory data
|
|
56
31
|
*/
|
|
@@ -59,18 +34,10 @@ class OcflObjectInventory {
|
|
|
59
34
|
#data;
|
|
60
35
|
/** @type {InventoryVersion} */
|
|
61
36
|
#version;
|
|
62
|
-
|
|
37
|
+
metadata;
|
|
63
38
|
/**
|
|
64
39
|
* Create a mutable inventory and increase the inventory version number
|
|
65
|
-
* @param {
|
|
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]
|
|
40
|
+
* @param {Inventory} data
|
|
74
41
|
* @param {boolean} [cleanState]
|
|
75
42
|
*/
|
|
76
43
|
static newVersion(data, cleanState) {
|
|
@@ -170,7 +137,8 @@ class OcflObjectInventory {
|
|
|
170
137
|
logicalPath,
|
|
171
138
|
version,
|
|
172
139
|
digest,
|
|
173
|
-
contentPath
|
|
140
|
+
contentPath,
|
|
141
|
+
...this.metadata?.[contentPath]
|
|
174
142
|
};
|
|
175
143
|
}
|
|
176
144
|
}
|
|
@@ -212,7 +180,7 @@ class OcflObjectInventory {
|
|
|
212
180
|
* Compare the differences between the state of two versions
|
|
213
181
|
* @param {string} version0
|
|
214
182
|
* @param {string} version1
|
|
215
|
-
* @return {[
|
|
183
|
+
* @return {[InventoryDigests, InventoryDigests]} - A tuple of [A, B] where A contains files
|
|
216
184
|
* that are in `version0` but not in `version1` and B contains files
|
|
217
185
|
* that are in `version1` but not in `version0`
|
|
218
186
|
*/
|
|
@@ -237,7 +205,7 @@ class OcflObjectInventory {
|
|
|
237
205
|
return Object.fromEntries(r);
|
|
238
206
|
});
|
|
239
207
|
|
|
240
|
-
return /** @type {[
|
|
208
|
+
return /** @type {[InventoryDigests, InventoryDigests]} */(result);
|
|
241
209
|
}
|
|
242
210
|
}
|
|
243
211
|
|
package/lib/object.js
CHANGED
|
@@ -11,7 +11,6 @@ const { OcflStore } = require('./store.js');
|
|
|
11
11
|
const { OcflExtension } = require('./extension.js');
|
|
12
12
|
const { NotImplementedError } = require('./error.js');
|
|
13
13
|
const { parallelize, dataSourceAsIterable, isDirEmpty, findNamasteVersion } = require('./utils');
|
|
14
|
-
const { format } = require('util');
|
|
15
14
|
const { OCFL_VERSION, OCFL_VERSIONS, INVENTORY_NAME, NAMASTE_PREFIX_OBJECT, NAMASTE_T } = require('./constants').OcflConstants;
|
|
16
15
|
|
|
17
16
|
class UPDATE_MODE extends Enum {
|
|
@@ -24,9 +23,9 @@ const DIGEST = OcflDigest.CONTENT;
|
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
25
|
* @typedef {Object} OcflObjectConfig
|
|
27
|
-
* @property {string} root - Absolute path to the ocfl object root
|
|
28
|
-
* @property {string} [workspace] - Absolute path to object workspace directory
|
|
29
|
-
* @property {('sha256' | 'sha512')} [digestAlgorithm] - Digest algorithm for content-addressing, must use either sha512 or sha256
|
|
26
|
+
* @property {string} root - Absolute path to the ocfl object root.
|
|
27
|
+
* @property {string} [workspace] - Absolute path to object workspace directory.
|
|
28
|
+
* @property {('sha256' | 'sha512')} [digestAlgorithm] - Digest algorithm for content-addressing, must use either sha512 or sha256.
|
|
30
29
|
* @property {string} [id] - Identifier for the object. Only be used in a newly created object.
|
|
31
30
|
* @property {string} [contentDirectory='content'] - Content directory name. Only applies to a newly created object.
|
|
32
31
|
* @property {string} [ocflVersion=c.OCFL_VERSION] - Ocfl version. Only applies to a newly created object.
|
|
@@ -35,14 +34,26 @@ const DIGEST = OcflDigest.CONTENT;
|
|
|
35
34
|
/**
|
|
36
35
|
* @typedef {UPDATE_MODE | ('MERGE' | 'REPLACE') } ObjectUpdateMode
|
|
37
36
|
*
|
|
38
|
-
* @typedef {
|
|
37
|
+
* @typedef {Object} FileRef
|
|
38
|
+
* @property {string} [contentPath] - Actual path to the file location relative to the ocfl object root.
|
|
39
|
+
* @property {string} [logicalPath] - A path that represents a file’s location in the logical state of an object.
|
|
40
|
+
* @property {string} [digest] - An algorithmic characterization of the contents of a file conforming to a standard digest algorithm.
|
|
41
|
+
* @property {string} [version] - Version of the OCFL object.
|
|
42
|
+
* @property {string} [lastModified] - Last modified date of the file.
|
|
43
|
+
* @property {string} [size] - Size of the file in bytes.
|
|
39
44
|
* @typedef {{logicalPath: string, version?: string}} FileRefLogical
|
|
40
45
|
* @typedef {{digest: string}} FileRefDigest
|
|
41
46
|
* @typedef {{contentPath: string}} FileRefContent
|
|
42
47
|
*/
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Represent a file in an OCFL object
|
|
51
|
+
*/
|
|
44
52
|
class OcflObjectFile {
|
|
45
53
|
#ocflObject;
|
|
54
|
+
#lastModified;
|
|
55
|
+
#size;
|
|
56
|
+
|
|
46
57
|
/**
|
|
47
58
|
*
|
|
48
59
|
* @param {OcflObjectImpl} ocflObject
|
|
@@ -57,6 +68,8 @@ class OcflObjectFile {
|
|
|
57
68
|
if (fileRef.contentPath) this.contentPath = fileRef.contentPath;
|
|
58
69
|
if (fileRef.digest) this.digest = fileRef.digest;
|
|
59
70
|
if (fileRef.version) this.version = fileRef.version;
|
|
71
|
+
if (fileRef.lastModified != null) this.#lastModified = fileRef.lastModified;
|
|
72
|
+
if (fileRef.size != null) this.#size = fileRef.size;
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
/**
|
|
@@ -66,6 +79,7 @@ class OcflObjectFile {
|
|
|
66
79
|
let contentPath = this.contentPath;
|
|
67
80
|
if (!contentPath) {
|
|
68
81
|
let inv = await this.#ocflObject.getInventory();
|
|
82
|
+
if (!inv) throw new Error(`OCFL Object "${this.#ocflObject.id}" does not exist`);
|
|
69
83
|
let version = !this.version || this.version === 'latest' ? inv.head : this.version;
|
|
70
84
|
let digest = this.digest || inv.getDigest(this.logicalPath, version);
|
|
71
85
|
contentPath = inv.getContentPath(digest);
|
|
@@ -74,12 +88,22 @@ class OcflObjectFile {
|
|
|
74
88
|
return contentPath;
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Returns the last modified time of the file, in millisecond since the UNIX epoch (January 1st, 1970 at Midnight).
|
|
93
|
+
*/
|
|
94
|
+
get lastModified() { return this.#lastModified }
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns the file size in bytes.
|
|
98
|
+
*/
|
|
99
|
+
get size() { return this.#size }
|
|
100
|
+
|
|
77
101
|
toString() {
|
|
78
|
-
let
|
|
102
|
+
let props = [];
|
|
79
103
|
for (const key in ['logicalPath', 'version', 'digest', 'contentPath']) {
|
|
80
|
-
if (this[key])
|
|
104
|
+
if (this[key]) props.push(key + ": '" + this[key] + "'");
|
|
81
105
|
}
|
|
82
|
-
return
|
|
106
|
+
return `{ ${props.join(', ')} }`;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
/**
|
|
@@ -88,28 +112,51 @@ class OcflObjectFile {
|
|
|
88
112
|
|
|
89
113
|
* @return {Promise<Buffer>}
|
|
90
114
|
*/
|
|
91
|
-
async
|
|
115
|
+
async buffer(options) {
|
|
92
116
|
return this.#ocflObject.readFile(await this.#resolveContentPath(), options);
|
|
93
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* @deprecated
|
|
120
|
+
*/
|
|
121
|
+
async asBuffer(options) {
|
|
122
|
+
return this.buffer(options);
|
|
123
|
+
}
|
|
94
124
|
|
|
95
125
|
/**
|
|
96
126
|
* Get the file content as a string
|
|
97
127
|
* @param {BufferEncoding} [encoding='utf8'] String encoding, default to utf8
|
|
98
128
|
* @return {Promise<string>}
|
|
99
129
|
*/
|
|
130
|
+
async text(encoding = 'utf8') {
|
|
131
|
+
// @ts-ignore
|
|
132
|
+
return this.buffer(encoding);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* @deprecated
|
|
136
|
+
*/
|
|
100
137
|
async asString(encoding = 'utf8') {
|
|
101
138
|
// @ts-ignore
|
|
102
|
-
return this.
|
|
139
|
+
return this.text(encoding);
|
|
103
140
|
}
|
|
104
141
|
|
|
105
142
|
/**
|
|
106
|
-
* Get the file content as a stream
|
|
143
|
+
* Get the file content as a NodeJS stream
|
|
107
144
|
* @param {*} [options] Options to be passed to underlying data store specific implementation
|
|
108
145
|
* @return {Promise<NodeJS.ReadableStream>}
|
|
146
|
+
* @deprecated
|
|
109
147
|
*/
|
|
110
148
|
async asStream(options) {
|
|
111
149
|
return this.#ocflObject.createReadStream(await this.#resolveContentPath(), options);
|
|
112
150
|
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get the file content as a ES Streams standard ReadableStream
|
|
154
|
+
* @param {*} [options] Options to be passed to underlying data store specific implementation
|
|
155
|
+
* @return {Promise<ReadableStream>}
|
|
156
|
+
*/
|
|
157
|
+
async stream(options) {
|
|
158
|
+
return this.#ocflObject.createReadable(await this.#resolveContentPath(), options);
|
|
159
|
+
}
|
|
113
160
|
}
|
|
114
161
|
|
|
115
162
|
|
|
@@ -123,13 +170,19 @@ class OcflObject {
|
|
|
123
170
|
* Identifier of the OCFL Object
|
|
124
171
|
* @return {string}
|
|
125
172
|
*/
|
|
126
|
-
get id() { throw new NotImplementedError() }
|
|
173
|
+
get id() { throw new NotImplementedError(); }
|
|
127
174
|
|
|
128
175
|
/**
|
|
129
|
-
* The
|
|
176
|
+
* The absolute path to the root of this OCFL Object
|
|
130
177
|
* @return {string}
|
|
131
178
|
*/
|
|
132
|
-
get root() { throw new NotImplementedError() }
|
|
179
|
+
get root() { throw new NotImplementedError(); }
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The absolute path to the temporary workspace of this OCFL Object
|
|
183
|
+
* @return {string}
|
|
184
|
+
*/
|
|
185
|
+
get workspace() { throw new NotImplementedError(); }
|
|
133
186
|
|
|
134
187
|
toString() { return `OcflObject { root: ${this.root}, id: ${this.id} }`; }
|
|
135
188
|
|
|
@@ -140,7 +193,7 @@ class OcflObject {
|
|
|
140
193
|
* @param {string} [version] - Full name of the version, eg: v1, v2, v3
|
|
141
194
|
* @returns {Promise<OcflObjectInventory>}
|
|
142
195
|
*/
|
|
143
|
-
async getInventory(version = 'latest') { throw new NotImplementedError() }
|
|
196
|
+
async getInventory(version = 'latest') { throw new NotImplementedError(); }
|
|
144
197
|
|
|
145
198
|
/**
|
|
146
199
|
* Update the content files or directories as one transaction and commit the changes as a new version.
|
|
@@ -150,7 +203,7 @@ class OcflObject {
|
|
|
150
203
|
* @param {ObjectUpdateMode} [mode=UPDATE_MODE.MERGE]
|
|
151
204
|
* @return {Promise<?OcflObjectTransaction>}
|
|
152
205
|
*/
|
|
153
|
-
async update(updater, mode) { throw new NotImplementedError() }
|
|
206
|
+
async update(updater, mode) { throw new NotImplementedError(); }
|
|
154
207
|
|
|
155
208
|
/**
|
|
156
209
|
* Import one or more content files or directories to the object
|
|
@@ -190,12 +243,7 @@ class OcflObject {
|
|
|
190
243
|
await this.import(sourceDir, mode);
|
|
191
244
|
}
|
|
192
245
|
|
|
193
|
-
async load() {
|
|
194
|
-
this._inventory = {};
|
|
195
|
-
await this.getInventory();
|
|
196
|
-
//await this.loadExtensions();
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
246
|
+
async load() { throw new NotImplementedError(); }
|
|
199
247
|
|
|
200
248
|
/**
|
|
201
249
|
* Iterate through the content files contained in the specified version.
|
|
@@ -225,7 +273,7 @@ class OcflObject {
|
|
|
225
273
|
* @type { GetFileByLogical & GetFileByOpt }
|
|
226
274
|
*/
|
|
227
275
|
// @ts-ignore
|
|
228
|
-
getFile = function (opt, version) { }
|
|
276
|
+
getFile = function (opt, version) { };
|
|
229
277
|
|
|
230
278
|
/**
|
|
231
279
|
* Count the number of files contained in this object
|
|
@@ -259,6 +307,8 @@ class OcflObjectImpl extends OcflObject {
|
|
|
259
307
|
/** @type {string} */
|
|
260
308
|
#root;
|
|
261
309
|
/** @type {string} */
|
|
310
|
+
#workspace;
|
|
311
|
+
/** @type {string} */
|
|
262
312
|
#id;
|
|
263
313
|
/** @type {OcflExtension[]} */
|
|
264
314
|
#extensions;
|
|
@@ -284,13 +334,14 @@ class OcflObjectImpl extends OcflObject {
|
|
|
284
334
|
this.digestAlgorithm = DIGEST.of(config.digestAlgorithm || 'sha512');
|
|
285
335
|
if (!this.digestAlgorithm) throw new Error('Invalid digest algorithm. Must be one of `sha256` or `sha512`.');
|
|
286
336
|
//this.fixityDigest = config.fixityDigest ?? this.digestAlgorithm;
|
|
287
|
-
this
|
|
337
|
+
this.#workspace = config.workspace ? path.resolve(config.workspace) : undefined;
|
|
288
338
|
this.contentDirectory = config.contentDirectory ?? 'content';
|
|
289
339
|
/** @type Object.<string, OcflObjectInventory> */
|
|
290
340
|
this._inventory = {};
|
|
291
341
|
}
|
|
292
342
|
get id() { return this._inventory?.latest?.id || this.#id; }
|
|
293
343
|
get root() { return this.#root; }
|
|
344
|
+
get workspace() { return this.#workspace; }
|
|
294
345
|
|
|
295
346
|
async getInventory(version = 'latest') {
|
|
296
347
|
if (!this._inventory[version]) {
|
|
@@ -319,17 +370,34 @@ class OcflObjectImpl extends OcflObject {
|
|
|
319
370
|
//inventory does not exist yet
|
|
320
371
|
return;
|
|
321
372
|
}
|
|
322
|
-
let data = /** @type
|
|
373
|
+
let data = /** @type Inventory */(JSON.parse(datastr));
|
|
323
374
|
let [digest, actualDigest] = /** @type [string,string] */ (await Promise.all([
|
|
324
375
|
this.readFile(invPath + '.' + data.digestAlgorithm, 'utf8'),
|
|
325
376
|
OcflDigest.digestAsync(data.digestAlgorithm, datastr)
|
|
326
377
|
]));
|
|
327
378
|
digest = digest.match(/[^\s]+/)?.[0];
|
|
328
|
-
if (digest !== actualDigest) throw new Error(
|
|
379
|
+
if (digest !== actualDigest) throw new Error(`Inventory file ${this.root}/${invPath} digest mismatch: recorded=${digest} actual=${actualDigest}`);
|
|
329
380
|
//return new OcflObjectInventory({ data, digest });
|
|
330
381
|
return new OcflObjectInventory(data);
|
|
331
382
|
}
|
|
332
383
|
|
|
384
|
+
async load() {
|
|
385
|
+
this._inventory = {};
|
|
386
|
+
const inv = await this.getInventory();
|
|
387
|
+
//await this.loadExtensions();
|
|
388
|
+
|
|
389
|
+
//load file metadata from underlying storage
|
|
390
|
+
const metadata = inv.metadata = {};
|
|
391
|
+
for await (const file of await this.#store.list(this.root, { recursive: true })) {
|
|
392
|
+
const contentPath = file.path;
|
|
393
|
+
metadata[contentPath] = {
|
|
394
|
+
size: file.size,
|
|
395
|
+
lastModified: file.lastModified.getTime()
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
333
401
|
/**
|
|
334
402
|
* @param {function(OcflObjectTransaction):Promise<*>} [updater]
|
|
335
403
|
* @param {ObjectUpdateMode} [mode=UPDATE_MODE.MERGE]
|
|
@@ -354,10 +422,15 @@ class OcflObjectImpl extends OcflObject {
|
|
|
354
422
|
return t;
|
|
355
423
|
}
|
|
356
424
|
|
|
425
|
+
/** @deprecated */
|
|
357
426
|
async createReadStream(filePath, options) {
|
|
358
427
|
return this.#store.createReadStream(path.join(this.root, filePath), options);
|
|
359
428
|
}
|
|
360
429
|
|
|
430
|
+
async createReadable(filePath, options) {
|
|
431
|
+
return this.#store.createReadable(path.join(this.root, filePath), options);
|
|
432
|
+
}
|
|
433
|
+
|
|
361
434
|
/**
|
|
362
435
|
@typedef {import('fs').OpenMode} OpenMode
|
|
363
436
|
@typedef {import('events').Abortable} Abortable
|
|
@@ -374,13 +447,13 @@ class OcflObjectImpl extends OcflObject {
|
|
|
374
447
|
*/
|
|
375
448
|
readFile = async function (relPath, options) {
|
|
376
449
|
return this.#store.readFile(path.join(this.root, relPath), options);
|
|
377
|
-
}
|
|
450
|
+
};
|
|
378
451
|
|
|
379
452
|
getFile = function (opt, version) {
|
|
380
453
|
var obj = this;
|
|
381
454
|
var opt_ = typeof opt === 'string' ? { logicalPath: opt, version: version } : opt;
|
|
382
455
|
return new OcflObjectFile(this, opt_);
|
|
383
|
-
}
|
|
456
|
+
};
|
|
384
457
|
|
|
385
458
|
// async _resolveContentPath({ logicalPath = '', contentPath = '', digest = '', version = '' }) {
|
|
386
459
|
// if (!contentPath) {
|
|
@@ -478,6 +551,7 @@ class OcflObjectImpl extends OcflObject {
|
|
|
478
551
|
// workspace dir exists, abort update
|
|
479
552
|
throw new Error('Uncommitted changes detected. Object is being updated by other process or there has been a failed update attempt');
|
|
480
553
|
}
|
|
554
|
+
await this._validateObjectPath();
|
|
481
555
|
let createdDir = await this.#store.mkdir(workspacePath);
|
|
482
556
|
if (!this.workspace) await this._ensureNamaste();
|
|
483
557
|
return createTransactionProxy(new OcflObjectTransactionImpl(this, this.#store, inventory, workspaceVersionPath, createdDir));
|
|
@@ -492,16 +566,27 @@ class OcflObjectImpl extends OcflObject {
|
|
|
492
566
|
return;
|
|
493
567
|
}
|
|
494
568
|
// namaste not found, check if object root is not empty
|
|
569
|
+
let files;
|
|
495
570
|
try {
|
|
496
|
-
|
|
497
|
-
if (files.length > 0) throw new Error('Cannot create an OCFL Object in a non-empty directory');
|
|
571
|
+
files = await this.#store.readdir(this.root);
|
|
498
572
|
} catch (error) {
|
|
499
573
|
}
|
|
574
|
+
if (files && files.length > 0) throw new Error('Cannot create an OCFL Object in a non-empty directory');
|
|
500
575
|
//create the namaste file
|
|
501
576
|
let filePath = path.join(this.root, NAMASTE_T + prefix + this.ocflVersion);
|
|
502
577
|
await this.#store.writeFile(filePath, prefix + this.ocflVersion + '\n', 'utf8');
|
|
503
578
|
}
|
|
504
579
|
|
|
580
|
+
// all the parent directories cannot be an ocfl object
|
|
581
|
+
async _validateObjectPath() {
|
|
582
|
+
var p = this.root;
|
|
583
|
+
while (p !== '.' && p !== '/' ) {
|
|
584
|
+
p = path.dirname(p);
|
|
585
|
+
let nv = await findNamasteVersion(this.#store, NAMASTE_PREFIX_OBJECT, p);
|
|
586
|
+
if (nv) throw new Error(`Object root cannot be nested under another object ${p}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
505
590
|
async isObject(aPath) { }
|
|
506
591
|
getVersionString(i) { }
|
|
507
592
|
async determineVersion() { }
|
|
@@ -540,7 +625,7 @@ const proxyHandler = {
|
|
|
540
625
|
}
|
|
541
626
|
}
|
|
542
627
|
}
|
|
543
|
-
}
|
|
628
|
+
};
|
|
544
629
|
|
|
545
630
|
/**
|
|
546
631
|
* Return any OcflObject implementation as OcflObject.
|
package/lib/storage.js
CHANGED
|
@@ -15,7 +15,9 @@ const DIGEST = require('./digest').OcflDigest.CONTENT;
|
|
|
15
15
|
|
|
16
16
|
const DEFAULT_LAYOUT = HashedNTupleStorageLayout;
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* An abstraction to represent the OCFL Storage Root, which is
|
|
20
|
+
* a base directory used to store OCFL Objects, identified by a [NAMASTE] file “0=ocfl_1.1”.
|
|
19
21
|
* @typedef {import('./extension').OcflExtensionConfig} OcflExtensionConfig
|
|
20
22
|
* @typedef {import('./store').OcflStore} OcflStore
|
|
21
23
|
*/
|
package/lib/store.js
CHANGED
|
@@ -52,6 +52,7 @@ class OcflStore {
|
|
|
52
52
|
* @param {string} filePath - Absolute path
|
|
53
53
|
* @param {*} options
|
|
54
54
|
* @return {Promise<import('fs').ReadStream>}
|
|
55
|
+
* @deprecated
|
|
55
56
|
*/
|
|
56
57
|
async createReadStream(filePath, options) { throw new Error('Not Implemented'); }
|
|
57
58
|
|
|
@@ -60,9 +61,26 @@ class OcflStore {
|
|
|
60
61
|
* @param {string} relPath
|
|
61
62
|
* @param {*} options
|
|
62
63
|
* @return {Promise<import('fs').WriteStream>}
|
|
64
|
+
* @deprecated
|
|
63
65
|
*/
|
|
64
66
|
async createWriteStream(relPath, options) { throw new Error('Not Implemented'); }
|
|
65
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Create a ReadableStream to get the content of a file
|
|
70
|
+
* @param {string} filePath - Absolute path
|
|
71
|
+
* @param {*} options
|
|
72
|
+
* @return {Promise<ReadableStream>}
|
|
73
|
+
*/
|
|
74
|
+
async createReadable(filePath, options) { throw new Error('Not Implemented'); }
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a WritableStream that writes data to {@link relPath}.
|
|
78
|
+
* @param {string} relPath
|
|
79
|
+
* @param {*} options
|
|
80
|
+
* @return {Promise<WritableStream>}
|
|
81
|
+
*/
|
|
82
|
+
async createWritable(relPath, options) { throw new Error('Not Implemented'); }
|
|
83
|
+
|
|
66
84
|
/**
|
|
67
85
|
* Provide a common interface to read a file inside an object.
|
|
68
86
|
* A concrete subclass MAY implement this method to provide read access to its underlying storage backend.
|
|
@@ -125,6 +143,18 @@ class OcflStore {
|
|
|
125
143
|
*/
|
|
126
144
|
async readdir(filePath, options) { throw new Error('Not Implemented'); }
|
|
127
145
|
|
|
146
|
+
/**
|
|
147
|
+
* List all files contained in a directory including the metadata such as size and last modified date.
|
|
148
|
+
* The returned file.path is a relative path to the dirPath
|
|
149
|
+
* @param {string} dirPath
|
|
150
|
+
* @param {object} [options]
|
|
151
|
+
* @param {BufferEncoding} [options.encoding]
|
|
152
|
+
* @param {number} [options.bufferSize]
|
|
153
|
+
* @param {boolean} [options.recursive]
|
|
154
|
+
* @return {Promise<AsyncIterableIterator<{name: string, path: string, size: number, lastModified: Date}>> }
|
|
155
|
+
*/
|
|
156
|
+
async list(dirPath, { encoding='utf8', bufferSize=32, recursive=false} = {}) { throw new Error('Not Implemented'); }
|
|
157
|
+
|
|
128
158
|
/**
|
|
129
159
|
* Recursively create a directory if the storage backend supports it, otherwise do nothing
|
|
130
160
|
* @param {string} filePath - The directory path
|
package/lib/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
type OcflExtensionCreate = <T extends typeof import('./extension').OcflExtension>(this: T, config?: OcflExtensionConfig) => InstanceType<T>;
|
|
2
|
+
//type OcflExtensionCreate = <T extends typeof import('./extension').OcflExtension>(this: T, config?: OcflExtensionConfig) => InstanceType<T>;
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
// import { Abortable } from "events";
|
|
@@ -42,3 +42,54 @@ interface ReadFile {
|
|
|
42
42
|
| BufferEncoding
|
|
43
43
|
): Promise<string>;
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
type StorageLayout = {
|
|
47
|
+
FlatDirectStorageLayout: typeof import('./extensions/0002-flat-direct-storage-layout').FlatDirectStorageLayout
|
|
48
|
+
HashAndIdNTupleStorageLayout: typeof import('./extensions/0003-hash-and-id-n-tuple-storage-layout').HashAndIdNTupleStorageLayout
|
|
49
|
+
HashedNTupleStorageLayout: typeof import('./extensions/0004-hashed-n-tuple-storage-layout').HashedNTupleStorageLayout
|
|
50
|
+
FlatOmitPrefixStorageLayout: typeof import('./extensions/0006-flat-omit-prefix-storage-layout').FlatOmitPrefixStorageLayout
|
|
51
|
+
NTupleOmitPrefixStorageLayout: typeof import('./extensions/0007-n-tuple-omit-prefix-storage-layout').NTupleOmitPrefixStorageLayout
|
|
52
|
+
PathDirectStorageLayout: typeof import('./extensions/000N-path-direct-storage-layout').PathDirectStorageLayout
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface InventoryDigests {
|
|
56
|
+
[key:string]: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface InventoryVersion {
|
|
60
|
+
created: string;
|
|
61
|
+
message?: string;
|
|
62
|
+
state: InventoryDigests;
|
|
63
|
+
user?: {
|
|
64
|
+
name: string;
|
|
65
|
+
address?: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface Inventory {
|
|
70
|
+
/** A unique identifier for the OCFL Object. */
|
|
71
|
+
id: string;
|
|
72
|
+
/** The URI of the inventory section of the specification version matching the object conformance declaration. */
|
|
73
|
+
type: 'https://ocfl.io/1.0/spec/#inventory' | 'https://ocfl.io/1.1/spec/#inventory';
|
|
74
|
+
/**
|
|
75
|
+
* The algorithm used for calculating digests for content-addressing
|
|
76
|
+
* within the OCFL Object and for the Inventory Digest.
|
|
77
|
+
* This MUST be the algorithm used in the manifest and state blocks
|
|
78
|
+
*/
|
|
79
|
+
digestAlgorithm: 'sha256' | 'sha512';
|
|
80
|
+
/** The version directory name of the most recent version of the object. */
|
|
81
|
+
head: string;
|
|
82
|
+
manifest: InventoryDigests;
|
|
83
|
+
versions: {
|
|
84
|
+
[key:string]: InventoryVersion;
|
|
85
|
+
};
|
|
86
|
+
fixity?: {
|
|
87
|
+
[key in 'md5'|'sha1'|'sha256'|'sha512'|'blake2b-512']: {};
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The name of the designated content directory within the version directories.
|
|
91
|
+
* @default content
|
|
92
|
+
*/
|
|
93
|
+
contentDirectory?: string;
|
|
94
|
+
}
|
|
95
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ocfl/ocfl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Oxford Common File Layout (OCFL) JS library",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/
|
|
28
|
+
"url": "git+https://github.com/Language-Research-Technology/ocfl-js.git"
|
|
29
29
|
},
|
|
30
30
|
"author": "Alvin Sebastian (https://orcid.org/0000-0002-9086-1722)",
|
|
31
31
|
"contributors": [
|
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
"Peter Sefton (https://orcid.org/0000-0002-3545-944X)"
|
|
34
34
|
],
|
|
35
35
|
"license": "GPL-3.0-or-later",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"hash-wasm": "^4.11.0"
|
|
38
|
+
},
|
|
36
39
|
"devDependencies": {
|
|
37
40
|
"@types/mocha": "^9.1.1",
|
|
38
41
|
"@types/node": "^17.0.33",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"mocha": "^10.0.0"
|
|
42
|
+
"mocha": "^10.7.3",
|
|
43
|
+
"typedoc": "^0.23.15"
|
|
42
44
|
},
|
|
43
45
|
"mocha": {
|
|
44
46
|
"extension": [
|
|
@@ -48,16 +50,21 @@
|
|
|
48
50
|
],
|
|
49
51
|
"recursive": true
|
|
50
52
|
},
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"blake2": "^4.1.1"
|
|
53
|
-
},
|
|
54
53
|
"bugs": {
|
|
55
|
-
"url": "https://github.com/
|
|
54
|
+
"url": "https://github.com/Language-Research-Technology/ocfl-js/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/Language-Research-Technology/ocfl-js#readme",
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=17.0.0"
|
|
56
59
|
},
|
|
57
|
-
"homepage": "https://github.com/Arkisto-Platform/ocfl-js#readme",
|
|
58
60
|
"directories": {
|
|
59
61
|
"lib": "lib",
|
|
60
62
|
"test": "test"
|
|
61
63
|
},
|
|
62
|
-
"files": [
|
|
64
|
+
"files": [
|
|
65
|
+
"index.js",
|
|
66
|
+
"index.mjs",
|
|
67
|
+
"lib",
|
|
68
|
+
"resources"
|
|
69
|
+
]
|
|
63
70
|
}
|