@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 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} [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]
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 || 'sha512']();
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
- /** @type {Object.<string, typeof OcflStorageLayout>} */
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
- for (let len of [160, 256, 384]) {
30
- OcflDigest.algorithms['blake2b-' + len] = function() {
31
- return blake2.createHash('blake2b', {digestLength: len/8});
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 };
@@ -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 {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]
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 {[InventoryState, InventoryState]} - A tuple of [A, B] where A contains files
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 {[InventoryState, InventoryState]} */(result);
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 {{logicalPath?: string, digest?: string, contentPath?: string, version?: string}} FileRef
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 o = {};
102
+ let props = [];
79
103
  for (const key in ['logicalPath', 'version', 'digest', 'contentPath']) {
80
- if (this[key]) o[key] = this[key];
104
+ if (this[key]) props.push(key + ": '" + this[key] + "'");
81
105
  }
82
- return format('%O', o);
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 asBuffer(options) {
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.asBuffer(encoding);
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 non-storage specific absolute path to the root of this OCFL Object
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.workspace = config.workspace ? path.resolve(config.workspace) : undefined;
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 import('./inventory.js').Inventory */(JSON.parse(datastr));
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('corrupted file');
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
- let files = await this.#store.readdir(this.root);
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.1.2",
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/Arkisto-Platform/ocfl-js.git"
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
- "clean-jsdoc-theme": "^3.3.4",
40
- "jsdoc": "^3.6.10",
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/Arkisto-Platform/ocfl-js/issues"
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": [ "index.js", "index.mjs", "lib", "resources" ]
64
+ "files": [
65
+ "index.js",
66
+ "index.mjs",
67
+ "lib",
68
+ "resources"
69
+ ]
63
70
  }