@ocfl/ocfl 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/object.js CHANGED
@@ -3,275 +3,58 @@
3
3
 
4
4
  const path = require('path');
5
5
  const validation = require('./validation.js');
6
- const { Enum } = require('./enum.js');
7
- const { OcflObjectInventory, OcflObjectInventoryMut } = require('./inventory.js');
6
+ const { enumeration } = require('./enum.js');
7
+ const { OcflObjectInventory, OcflObjectInventoryMut, VersionNumber } = require('./inventory.js');
8
8
  const { OcflDigest } = require('./digest.js');
9
- const { createTransactionProxy, OcflObjectTransaction, OcflObjectTransactionImpl } = require('./transaction.js');
9
+ const { OcflObjectTransaction, OcflObjectTransactionImpl } = require('./transaction.js');
10
10
  const { OcflStore } = require('./store.js');
11
11
  const { OcflExtension } = require('./extension.js');
12
12
  const { NotImplementedError } = require('./error.js');
13
- const { parallelize, dataSourceAsIterable, isDirEmpty, findNamasteVersion } = require('./utils');
14
- const { format } = require('util');
13
+ const { parallelize, isDirEmpty, findNamasteVersion } = require('./utils');
14
+ const { OcflObjectFile } = require('./file.js');
15
15
  const { OCFL_VERSION, OCFL_VERSIONS, INVENTORY_NAME, NAMASTE_PREFIX_OBJECT, NAMASTE_T } = require('./constants').OcflConstants;
16
16
 
17
- class UPDATE_MODE extends Enum {
18
- /** Merge all changes with the last version during update */
19
- static MERGE = new this();
20
- /** New version will assume that all existing files are removed first before adding new ones */
21
- static REPLACE = new this();
22
- };
17
+ const UPDATE_MODES = /** @type { const } */(['MERGE', 'REPLACE']);
18
+ const UPDATE_MODE = enumeration(UPDATE_MODES);
23
19
  const DIGEST = OcflDigest.CONTENT;
24
20
 
25
21
  /**
26
- * @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
30
- * @property {string} [id] - Identifier for the object. Only be used in a newly created object.
31
- * @property {string} [contentDirectory='content'] - Content directory name. Only applies to a newly created object.
32
- * @property {string} [ocflVersion=c.OCFL_VERSION] - Ocfl version. Only applies to a newly created object.
33
- * @property {OcflExtension[]} [extensions] - Reference to existing extensions defined outside of the object, such as in the storage root.
34
- */
35
- /**
36
- * @typedef {UPDATE_MODE | ('MERGE' | 'REPLACE') } ObjectUpdateMode
37
- *
38
- * @typedef {{logicalPath?: string, digest?: string, contentPath?: string, version?: string}} FileRef
22
+ * @typedef {(typeof UPDATE_MODES)[number]} UpdateModeStr
23
+ * @typedef { InstanceType<typeof UPDATE_MODE> | UpdateModeStr} UpdateMode
24
+ * @typedef {string} LogicalPath
39
25
  * @typedef {{logicalPath: string, version?: string}} FileRefLogical
40
- * @typedef {{digest: string}} FileRefDigest
41
- * @typedef {{contentPath: string}} FileRefContent
26
+ * @typedef {{digest: string, version?: string}} FileRefDigest
27
+ * @typedef {{contentPath: string, version?: string}} FileRefContent
28
+ * @typedef {import('./types').Inventory} Inventory
29
+ * @typedef {import('./types').OcflVersion} OcflVersion
30
+ * @typedef {import('./types').OcflObjectConfig} OcflObjectConfig
31
+ *
42
32
  */
43
33
 
44
- class OcflObjectFile {
45
- #ocflObject;
46
- /**
47
- *
48
- * @param {OcflObjectImpl} ocflObject
49
- * @param {FileRef} fileRef
50
- */
51
- constructor(ocflObject, fileRef) {
52
- this.#ocflObject = ocflObject;
53
- // for (const key in ['logicalPath', 'version', 'digest', 'contentPath']) {
54
- // if (fileRef[key]) this[key] = fileRef[key];
55
- // }
56
- if (fileRef.logicalPath) this.logicalPath = fileRef.logicalPath;
57
- if (fileRef.contentPath) this.contentPath = fileRef.contentPath;
58
- if (fileRef.digest) this.digest = fileRef.digest;
59
- if (fileRef.version) this.version = fileRef.version;
60
- }
61
-
62
- /**
63
- * Resolve contentPath given either the `logicalPath` and `version`, `digest`, or `contentPath`.
64
- */
65
- async #resolveContentPath() {
66
- let contentPath = this.contentPath;
67
- if (!contentPath) {
68
- let inv = await this.#ocflObject.getInventory();
69
- let version = !this.version || this.version === 'latest' ? inv.head : this.version;
70
- let digest = this.digest || inv.getDigest(this.logicalPath, version);
71
- contentPath = inv.getContentPath(digest);
72
- if (!contentPath) throw new Error(`Cannot find content "${this.toString()}" in the OCFL Object "${this.#ocflObject.id}" version "${this.version || 'latest'}"`);
73
- }
74
- return contentPath;
75
- }
76
-
77
- toString() {
78
- let o = {};
79
- for (const key in ['logicalPath', 'version', 'digest', 'contentPath']) {
80
- if (this[key]) o[key] = this[key];
81
- }
82
- return format('%O', o);
83
- }
84
-
85
- /**
86
- * Get the file content as a buffer
87
- * @param {*} [options] Options to be passed to underlying data store specific implementation
88
-
89
- * @return {Promise<Buffer>}
90
- */
91
- async asBuffer(options) {
92
- return this.#ocflObject.readFile(await this.#resolveContentPath(), options);
93
- }
94
-
95
- /**
96
- * Get the file content as a string
97
- * @param {BufferEncoding} [encoding='utf8'] String encoding, default to utf8
98
- * @return {Promise<string>}
99
- */
100
- async asString(encoding = 'utf8') {
101
- // @ts-ignore
102
- return this.asBuffer(encoding);
103
- }
104
-
105
- /**
106
- * Get the file content as a stream
107
- * @param {*} [options] Options to be passed to underlying data store specific implementation
108
- * @return {Promise<NodeJS.ReadableStream>}
109
- */
110
- async asStream(options) {
111
- return this.#ocflObject.createReadStream(await this.#resolveContentPath(), options);
112
- }
113
- }
114
-
115
-
116
34
  /**
117
- * Interface class representing OCFL Object
118
- * @interface
35
+ * A class representing an OCFL Object allowing to read and write the content of an OCFL Object in the underlying store.
119
36
  */
120
37
  class OcflObject {
121
38
 
122
- /**
123
- * Identifier of the OCFL Object
124
- * @return {string}
125
- */
126
- get id() { throw new NotImplementedError(); }
127
-
128
- /**
129
- * The absolute path to the root of this OCFL Object
130
- * @return {string}
131
- */
132
- get root() { throw new NotImplementedError(); }
133
-
134
- /**
135
- * The absolute path to the temporary workspace of this OCFL Object
136
- * @return {string}
137
- */
138
- get workspace() { throw new NotImplementedError(); }
139
-
140
- toString() { return `OcflObject { root: ${this.root}, id: ${this.id} }`; }
141
-
142
- toJSON() { return { root: this.root, id: this.id }; }
143
-
144
- /**
145
- * If a cache exists, return the cached inventory. Otherwise read it from the storage and cache it.
146
- * @param {string} [version] - Full name of the version, eg: v1, v2, v3
147
- * @returns {Promise<OcflObjectInventory>}
148
- */
149
- async getInventory(version = 'latest') { throw new NotImplementedError(); }
150
-
151
- /**
152
- * Update the content files or directories as one transaction and commit the changes as a new version.
153
- * If callback function `cb` is provided, all update operations can be done in the callback function
154
- * and all changes are automatically commited at the end of the function.
155
- * @param {function(OcflObjectTransaction):*} [updater]
156
- * @param {ObjectUpdateMode} [mode=UPDATE_MODE.MERGE]
157
- * @return {Promise<?OcflObjectTransaction>}
158
- */
159
- async update(updater, mode) { throw new NotImplementedError(); }
160
-
161
- /**
162
- * Import one or more content files or directories to the object
163
- * in one transaction and commit the changes as a new version.
164
- * Use this method to simplify copying all contents of a directory
165
- * in local filesystem to the OCFL Object content directory
166
- * @param {string|string[]|string[][]} content - The source path(s) or an array of a tuple [source, logical path]
167
- * @param {ObjectUpdateMode} [mode=UPDATE_MODE.MERGE] - Update mode.
168
- */
169
- async import(content, mode = UPDATE_MODE.MERGE) {
170
- //let entries = /**@type {string[][]}*/(content);
171
- if (typeof content === 'string') content = [[content, '']];
172
- //else if (typeof content[0] === 'string') entries = /**@type {string[]}*/(content).map(src => [src, '']);
173
-
174
- await this.update(async (t) => {
175
- let res = await parallelize(/**@type {string[][]}*/(content), async (p) => {
176
- if (typeof p === 'string') p = [p, ''];
177
- let [source, target] = p;
178
- return t.import(source, target);
179
- });
180
- let errors = [];
181
- for (let i = 0; i < res.length; ++i) {
182
- if (res[i] instanceof Error) errors.push(content[i][0]);
183
- }
184
- if (errors.length) {
185
- let msg = "Cannot add the files: " + errors.join(', ');
186
- throw new Error(msg);
187
- }
188
- }, mode);
189
- }
190
-
191
- /**
192
- * Alias for import
193
- * @see {@link OcflObject.import}
194
- */
195
- async add(sourceDir, mode = UPDATE_MODE.MERGE) {
196
- await this.import(sourceDir, mode);
197
- }
198
-
199
- async load() {
200
- this._inventory = {};
201
- await this.getInventory();
202
- //await this.loadExtensions();
203
- return;
204
- }
205
-
206
- /**
207
- * Iterate through the content files contained in the specified version.
208
- * Returns an Iterator that contains FileRef which consists of logical path, content path, and digest, in no particular order.
209
- * The iterator is implemented as a generator.
210
- * @param {string} [version]
211
- * @return {Promise<Generator<OcflObjectFile, void, unknown>>}
212
- */
213
- async files(version) { throw new Error('Not Implemented'); }
214
-
215
- /**
216
- * Get a file by its logical path and version. If version is omitted, it defaults to the last version.
217
- * @callback GetFileByLogical
218
- * @param {string} logicalPath Logical path of the file, eg: 'test'
219
- * @param {string} [version='latest'] The object version name, eg: 'v1', 'v2'. Default to 'latest'
220
- * @returns {OcflObjectFile}
221
- */
222
- /**
223
- * Get a file by either the [logical path and version], digest, or content path.
224
- * @callback GetFileByOpt
225
- * @param { FileRefLogical | FileRefDigest | FileRefContent } opt A choice of logical path (eg: 'test') and version (eg: 'v1'),
226
- * digest of the file content, or content path (eg: 'v1/content/test')
227
- * @returns {OcflObjectFile}
228
- */
229
- /**
230
- * Retrieve the content of a file either as string, buffer, or stream.
231
- * @type { GetFileByLogical & GetFileByOpt }
232
- */
233
- // @ts-ignore
234
- getFile = function (opt, version) { };
235
-
236
- /**
237
- * Count the number of files contained in this object
238
- * @param {string} [version]
239
- */
240
- async count(version) {
241
- return [...await this.files(version)].length;
242
- }
243
-
244
- /**
245
- * Check if the object root path points to an existing file or non-empty directory
246
- * in the underlying backend store.
247
- * The existing directory may or may not be a valid OCFL Object.
248
- * @return {Promise<boolean>}
249
- */
250
- async exists() { throw new Error('Not Implemented'); }
251
-
252
- //async inventory(version = 'latest') { }
253
-
254
- }
255
-
256
-
257
- /**
258
- * Abstract class implementing {@link OcflObject}.
259
- * This class provides common functionalities for the subclasses and
260
- * at the same provide encapsulation emulating private and protected methods.
261
- * @implements {OcflObject}
262
- */
263
- class OcflObjectImpl extends OcflObject {
264
-
39
+ /** @type {OcflVersion} */
40
+ #ocflVersion;
265
41
  /** @type {string} */
266
42
  #root;
267
43
  /** @type {string} */
268
44
  #workspace;
269
45
  /** @type {string} */
270
46
  #id;
47
+ /** @type {('sha256' | 'sha512')} */
48
+ #digestAlgorithm;
271
49
  /** @type {OcflExtension[]} */
272
50
  #extensions;
273
51
  /** @type {OcflStore} */
274
52
  #store;
53
+ /**
54
+ * A default list of digest algorithms to be calculated for each file in this object and added to the fixity block.
55
+ * @type {string[]}
56
+ */
57
+ fixityAlgorithms;
275
58
 
276
59
  /**
277
60
  * Create a new OCFL Object
@@ -279,38 +62,101 @@ class OcflObjectImpl extends OcflObject {
279
62
  * @param {OcflStore} store
280
63
  */
281
64
  constructor(config, store) {
282
- super();
283
65
  if (!store) throw new TypeError('[OcflObject] store is required.');
284
66
  this.#store = store;
285
67
  if (!config.root) throw new TypeError('[OcflObject] config.root is required.');
286
68
  if (config.root === config.workspace) throw new Error('[OcflObject] config.root and config.workspace must be different.');
287
69
  this.#root = path.resolve(config.root);
288
- this.ocflVersion = config.ocflVersion || OCFL_VERSION;
70
+ this.#ocflVersion = config.ocflVersion || /** @type {OcflVersion} */(OCFL_VERSION);
289
71
  //this.contentVersion = null; // No content yet
290
72
  this.#id = config.id;
291
73
  this.#extensions = config.extensions;
292
- this.digestAlgorithm = DIGEST.of(config.digestAlgorithm || 'sha512');
293
- if (!this.digestAlgorithm) throw new Error('Invalid digest algorithm. Must be one of `sha256` or `sha512`.');
294
74
  //this.fixityDigest = config.fixityDigest ?? this.digestAlgorithm;
75
+ this.digestAlgorithm = config.digestAlgorithm || 'sha512';
295
76
  this.#workspace = config.workspace ? path.resolve(config.workspace) : undefined;
296
77
  this.contentDirectory = config.contentDirectory ?? 'content';
297
78
  /** @type Object.<string, OcflObjectInventory> */
298
79
  this._inventory = {};
80
+ this.fixityAlgorithms = config.fixityAlgorithms;
81
+ }
82
+
83
+ /** the digest algorithm used to identify files within the OCFL object */
84
+ get digestAlgorithm() { return this.#digestAlgorithm; }
85
+ set digestAlgorithm(algorithm) {
86
+ this.#digestAlgorithm = algorithm;
87
+ // @ts-ignore
88
+ this.#digestAlgorithm = enumeration.of(DIGEST, algorithm || 'sha512')?.name;
89
+ if (!this.#digestAlgorithm) throw new Error('Invalid digest algorithm. Must be one of `sha256` or `sha512`.');
90
+ }
91
+
92
+ /**
93
+ * Identifier of the OCFL Object
94
+ * @return {string}
95
+ */
96
+ get id() { return this.inventory?.id || this.#id; }
97
+
98
+ /**
99
+ * Latest version number of the OCFL Object
100
+ * @return {VersionNumber}
101
+ */
102
+ get headVersion() {
103
+ const head = this.inventory?.head;
104
+ if (head) return VersionNumber.fromString(head);
105
+ }
106
+
107
+ /**
108
+ * All available version numbers in the OCFL Object
109
+ * @return {VersionNumber[]}
110
+ */
111
+ get versions() {
112
+ return Object.keys(this.inventory?.versions || {}).map(v => VersionNumber.fromString(v));
299
113
  }
300
- get id() { return this._inventory?.latest?.id || this.#id; }
114
+
115
+ /**
116
+ * The absolute path to the root of this OCFL Object
117
+ * @return {string}
118
+ */
301
119
  get root() { return this.#root; }
120
+
121
+ /**
122
+ * The absolute path to the temporary workspace of this OCFL Object
123
+ * @return {string}
124
+ */
302
125
  get workspace() { return this.#workspace; }
303
126
 
304
- async getInventory(version = 'latest') {
305
- if (!this._inventory[version]) {
306
- let inv = await this._readInventory(version);
307
- if (inv) this._inventory[version] = inv;
308
- }
309
- return this._inventory[version];
127
+ /** The raw OCFL version number the object adheres to , eg 1.0, 1.1) */
128
+ get ocflVersion() { return this.#ocflVersion; }
129
+
130
+ /**
131
+ * Create a new backend-specific update transaction
132
+ * @param {OcflObjectInventoryMut} inventory
133
+ * @return {Promise<OcflObjectTransaction>}
134
+ */
135
+ async #createTransaction(inventory) {
136
+ await this._validateObjectPath();
137
+ if (!this.workspace) await this._ensureNamaste();
138
+ let workspacePath = this.workspace || this.root;
139
+ return OcflObjectTransactionImpl.create(this, this.#store, inventory, workspacePath);
310
140
  }
311
141
 
312
- _setInventory(inventory) {
313
- this._inventory['latest'] = this._inventory[inventory.head] = inventory;
142
+ async _ensureNamaste() {
143
+ let prefix = NAMASTE_PREFIX_OBJECT;
144
+ let nv = await findNamasteVersion(this.#store, prefix, this.root);
145
+ if (nv === '') throw validation.createError(6, this.root);
146
+ if (nv) {
147
+ if (!this.ocflVersion) this.#ocflVersion = /** @type {OcflVersion} */(nv);
148
+ return;
149
+ }
150
+ // namaste not found, check if object root is not empty
151
+ let files;
152
+ try {
153
+ files = await this.#store.readdir(this.root);
154
+ } catch (error) {
155
+ }
156
+ if (files && files.length > 0) throw new Error('Cannot create an OCFL Object in a non-empty directory');
157
+ //create the namaste file
158
+ let filePath = path.join(this.root, NAMASTE_T + prefix + this.ocflVersion);
159
+ await this.#store.writeFile(filePath, prefix + this.ocflVersion + '\n', 'utf8');
314
160
  }
315
161
 
316
162
  /**
@@ -328,10 +174,10 @@ class OcflObjectImpl extends OcflObject {
328
174
  //inventory does not exist yet
329
175
  return;
330
176
  }
331
- let data = /** @type import('./inventory.js').Inventory */(JSON.parse(datastr));
177
+ let data = /** @type Inventory */(JSON.parse(datastr));
332
178
  let [digest, actualDigest] = /** @type [string,string] */ (await Promise.all([
333
179
  this.readFile(invPath + '.' + data.digestAlgorithm, 'utf8'),
334
- OcflDigest.digestAsync(data.digestAlgorithm, datastr)
180
+ OcflDigest.digest(data.digestAlgorithm, datastr)
335
181
  ]));
336
182
  digest = digest.match(/[^\s]+/)?.[0];
337
183
  if (digest !== actualDigest) throw new Error(`Inventory file ${this.root}/${invPath} digest mismatch: recorded=${digest} actual=${actualDigest}`);
@@ -340,15 +186,65 @@ class OcflObjectImpl extends OcflObject {
340
186
  }
341
187
 
342
188
  /**
189
+ * Set the latest version of inventory in the cache
190
+ * @param {*} inventory
191
+ * @param {*} version
192
+ */
193
+ _setInventory(inventory, version='latest') {
194
+ //this._inventory['latest'] = this._inventory[inventory.head] = inventory;
195
+ this._inventory[version] = inventory;
196
+ if (version === 'latest') this._inventory[inventory.head] = inventory;
197
+ }
198
+
199
+ // all the parent directories cannot be an ocfl object
200
+ async _validateObjectPath() {
201
+ var p = this.root;
202
+ while (p !== '.' && p !== '/') {
203
+ p = path.dirname(p);
204
+ let nv = await findNamasteVersion(this.#store, NAMASTE_PREFIX_OBJECT, p);
205
+ if (nv) throw new Error(`Object root cannot be nested under another object ${p}`);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * If a cache exists, return the cached inventory. Otherwise read it from the storage and cache it.
211
+ * If the inventory file does not exist, eg in a newly created object, returns undefined.
212
+ * @param {string} [version] - Full name of the version, eg: v1, v2, v3
213
+ * @returns {Promise<OcflObjectInventory>}
214
+ */
215
+ async getInventory(version = 'latest') {
216
+ if (!this._inventory[version]) {
217
+ let inv = await this._readInventory(version);
218
+ if (inv) this._setInventory(inv, version);
219
+ }
220
+ return this._inventory[version];
221
+ }
222
+
223
+ _baseInventory() {
224
+ return {
225
+ id: this.id,
226
+ digestAlgorithm: this.digestAlgorithm || 'sha512',
227
+ type: /** @type {Inventory["type"]} */(`https://ocfl.io/${this.ocflVersion}/spec/#inventory`),
228
+ contentDirectory: this.contentDirectory || 'content'
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Update the content files or directories as one transaction and commit the changes as a new version.
234
+ * After obtaining the OcflObjectTransaction instance, make sure to either call the
235
+ * {@link OcflObjectTransaction#commit} or {@link OcflObjectTransaction#rollback} method to complete the transaction.
236
+ * If callback function `cb` is provided, all update operations can be done in the callback function
237
+ * and all changes are automatically commited at the end of the function.
343
238
  * @param {function(OcflObjectTransaction):Promise<*>} [updater]
344
- * @param {ObjectUpdateMode} [mode=UPDATE_MODE.MERGE]
239
+ * @param {UpdateMode} [mode_]
240
+ * @return {Promise<?OcflObjectTransaction>}
345
241
  */
346
- async update(updater, mode = UPDATE_MODE.MERGE) {
347
- if (typeof mode === 'string') mode = UPDATE_MODE.of(mode.toUpperCase());
348
- if (!mode || !UPDATE_MODE.has(mode)) throw new TypeError(`Invalid mode '${mode.toString()}'`);
349
- let inv = await this.getInventory();
350
- let newInv = await this._createInventory(inv, mode === UPDATE_MODE.REPLACE);
351
- let t = await this.#createTransaction(newInv);
242
+ async update(updater, mode_ = UPDATE_MODE.MERGE) {
243
+ const mode = enumeration.of(UPDATE_MODE, mode_);
244
+ if (!mode) throw new TypeError(`Invalid mode '${mode.toString()}'`);
245
+ const inv = await this.getInventory() ?? this._baseInventory();
246
+ const newInv = await OcflObjectInventory.newVersion(inv, mode === UPDATE_MODE.REPLACE);
247
+ const t = await this.#createTransaction(newInv);
352
248
  if (typeof updater === 'function') {
353
249
  try {
354
250
  await updater(t);
@@ -363,70 +259,81 @@ class OcflObjectImpl extends OcflObject {
363
259
  return t;
364
260
  }
365
261
 
366
- async createReadStream(filePath, options) {
367
- return this.#store.createReadStream(path.join(this.root, filePath), options);
368
- }
369
262
 
370
- /**
371
- @typedef {import('fs').OpenMode} OpenMode
372
- @typedef {import('events').Abortable} Abortable
373
- @typedef {{
374
- (relPath: string, options?:({encoding?: null | undefined, flag?: OpenMode | undefined} & Abortable) | null): Promise<Buffer>
375
- (relPath: string, options:({encoding: BufferEncoding, flag?: OpenMode | undefined} & Abortable)|BufferEncoding): Promise<string>
376
- }} ReadFileFn
377
- */
378
- /**
379
- * Read a file inside an object.
380
- * @type {ReadFileFn} abc
381
- * @param {string} relPath - The file path relative to the object root.
382
- * @param options - Options to be passed to the underlying method
263
+ /**
264
+ * Import one or more content files or directories to the object
265
+ * in one transaction and commit the changes as a new version.
266
+ * Use this method to simplify copying all contents of a directory
267
+ * in local filesystem to the OCFL Object content directory
268
+ * @param {string|string[]|string[][]} content - The source path(s) or an array of a tuple [source, logical path]
269
+ * @param {UpdateMode} [mode=UPDATE_MODE.MERGE] - Update mode.
383
270
  */
384
- readFile = async function (relPath, options) {
385
- return this.#store.readFile(path.join(this.root, relPath), options);
386
- };
387
-
388
- getFile = function (opt, version) {
389
- var obj = this;
390
- var opt_ = typeof opt === 'string' ? { logicalPath: opt, version: version } : opt;
391
- return new OcflObjectFile(this, opt_);
392
- };
271
+ async import(content, mode = UPDATE_MODE.MERGE) {
272
+ //let entries = /**@type {string[][]}*/(content);
273
+ if (typeof content === 'string') content = [[content, '']];
274
+ //else if (typeof content[0] === 'string') entries = /**@type {string[]}*/(content).map(src => [src, '']);
393
275
 
394
- // async _resolveContentPath({ logicalPath = '', contentPath = '', digest = '', version = '' }) {
395
- // if (!contentPath) {
396
- // let inv = await this.getInventory();
397
- // version = version && version !== 'latest' ? version : inv.head;
398
- // contentPath = inv.getContentPath(digest || inv.getDigest(logicalPath, version));
399
- // }
400
- // return contentPath;
401
- // }
276
+ await this.update(async (t) => {
277
+ let res = await parallelize(/**@type {string[][]}*/(content), async (p) => {
278
+ if (typeof p === 'string') p = [p, ''];
279
+ let [source, target] = p;
280
+ return t.import(source, target);
281
+ });
282
+ let errors = [];
283
+ for (let i = 0; i < res.length; ++i) {
284
+ if (res[i] instanceof Error) errors.push(content[i][0]);
285
+ }
286
+ if (errors.length) {
287
+ let msg = "Cannot add the files: " + errors.join(', ');
288
+ throw new Error(msg);
289
+ }
290
+ }, mode);
291
+ }
402
292
 
403
- // async getAsBuffer(opt) {
404
- // let p = await this._resolveContentPath(opt);
405
- // if (!p) throw new Error(`Cannot find content "${opt.logicalPath || opt.contentPath || opt.digest}" in the OCFL Object "${this.#id}" version "${opt.version || 'latest'}"`);
406
- // return this.readFile(p, opt.options);
407
- // }
408
- // async getAsString(opt) {
409
- // // @ts-ignore
410
- // return this.getAsBuffer({ ...opt, options: opt.encoding || 'utf8' });
411
- // }
412
- // async getAsStream(opt) {
413
- // let p = await this._resolveContentPath(opt);
414
- // if (!p) throw new Error(`Cannot find content "${opt.logicalPath || opt.contentPath || opt.digest}" in the OCFL Object "${this.#id}" version "${opt.version || 'latest'}"`);
415
- // return this.createReadStream(p, opt.options);
416
- // }
293
+ /**
294
+ * Alias for import
295
+ * @see {@link OcflObject.import}
296
+ */
297
+ async add(sourceDir, mode = UPDATE_MODE.MERGE) {
298
+ await this.import(sourceDir, mode);
299
+ }
417
300
 
418
301
  /**
419
- * Copy all content of this Object to a directory in local filesystem
420
- * @param {*} targetDir
302
+ * Count the number of files contained in this object
303
+ * @param {string} [version]
421
304
  */
422
- async export(targetDir, version) { }
305
+ async count(version) {
306
+ let inv = await this.getInventory();
307
+ return [...inv.files(version)].length;
308
+ }
309
+
310
+ async createReadable(filePath, options) {
311
+ return this.#store.createReadable(path.join(this.root, filePath), options);
312
+ }
423
313
 
314
+ /**
315
+ * Check if the object root path points to an existing file or non-empty directory
316
+ * in the underlying backend store.
317
+ * The existing directory may or may not be a valid OCFL Object.
318
+ * @return {Promise<boolean>}
319
+ */
424
320
  async exists() {
425
321
  return !(await isDirEmpty(this.#store, this.root));
426
322
  }
427
323
 
428
324
  /**
325
+ * Copy all content of this Object to a directory in local filesystem
326
+ * @param {string} targetDir
327
+ * @param {string} version
328
+ */
329
+ async export(targetDir, version) { throw new Error('Not Implemented'); }
330
+
331
+ /**
332
+ * Iterate through the content files contained in the specified version.
333
+ * Returns an Iterator that contains FileRef which consists of logical path, content path, and digest, in no particular order.
334
+ * The iterator is implemented as a generator.
429
335
  * @param {string} [version]
336
+ * @return {Promise<Generator<OcflObjectFile, void, unknown>>}
430
337
  */
431
338
  async files(version) {
432
339
  let inv = await this.getInventory();
@@ -439,99 +346,90 @@ class OcflObjectImpl extends OcflObject {
439
346
  return files();
440
347
  }
441
348
 
442
-
443
349
  /**
444
- * Create a new inventory from the object config or clone an existing inventory.
445
- * @param {OcflObjectInventory} [inventory]
446
- * @param {boolean} [cleanState]
447
- * @return {OcflObjectInventoryMut}
350
+ * Get a file by either the [logical path and version], digest, or content path.
351
+ * If version is omitted, it defaults to the last version.
352
+ * @param { LogicalPath | FileRefLogical | FileRefDigest | FileRefContent } opt A choice of logical path (eg: 'test') and version (eg: 'v1'),
353
+ * digest of the file content, or content path (eg: 'v1/content/test')
354
+ * @param {string} [version='latest'] The object version name, eg: 'v1', 'v2'. Default to 'latest'. Only used if opt is a LogicalPath.
355
+ * @returns {OcflObjectFile}
448
356
  */
449
- _createInventory(inventory, cleanState) {
450
- /** @type {Object} */
451
- let data;
452
- if (inventory) {
453
- data = inventory.toJSON();
454
- } else {
455
- data = {
456
- id: this.id,
457
- digestAlgorithm: this.digestAlgorithm.value,
458
- type: `https://ocfl.io/${this.ocflVersion}/spec/#inventory`
459
- };
460
- if (this.contentDirectory !== 'content') data.contentDirectory = this.contentDirectory;
357
+ getFile(opt, version) {
358
+ const inv = this.inventory;
359
+ /** @type {any} */
360
+ let file = typeof opt === 'string' ? { logicalPath: opt, version: version } : { ...opt };
361
+ if (file.logicalPath) {
362
+ file = inv?.getFile(file.logicalPath, file.version);
363
+ } else if (file.digest) {
364
+ file = inv?.getFileByDigest(file.digest, file.version);
365
+ } else if (file.contentPath) {
366
+ file = inv?.getFileByContentPath(file.contentPath, file.version);
461
367
  }
462
- return OcflObjectInventory.newVersion(data, cleanState);
463
- }
368
+ if (file) return new OcflObjectFile(this, file);
369
+ };
464
370
 
465
371
  /**
466
- *
467
- * @param {string} absOrRelPath
468
- * @return {string}
372
+ * Return the latest version of the cached data from inventory.json which can be read using {@link getInventory()}
469
373
  */
470
- toAbsPath(absOrRelPath = '') {
471
- if (!path.isAbsolute(absOrRelPath)) return path.join(this.root, absOrRelPath);
472
- return absOrRelPath;
374
+ get inventory() {
375
+ return this._inventory.latest;
473
376
  }
474
377
 
475
- /**
476
- * Create a new backend-specific update transaction
477
- * @param {OcflObjectInventoryMut} inventory
478
- * @return {Promise<OcflObjectTransaction>}
378
+ /**
379
+ * Reads the object inventory file and cache it in the memory.
380
+ * Optionally, preloads and reads all files metadata from the underlying storage.
381
+ * @param {object} [options]
382
+ * @param {boolean} [options.filesMetadata] Whether to read file metadata from the underlying storage.
479
383
  */
480
- async #createTransaction(inventory) {
481
- // @todo: validate existing object
482
- // check existing
483
- let workspacePath = this.workspace || this.root;
484
- let workspaceVersionPath = path.join(workspacePath, inventory.head);
485
-
486
- if (await this.#store.exists(workspaceVersionPath)) {
487
- // workspace dir exists, abort update
488
- throw new Error('Uncommitted changes detected. Object is being updated by other process or there has been a failed update attempt');
384
+ async load({ filesMetadata } = {}) {
385
+ this._inventory = {}; // reset inventory cache
386
+ const inv = await this.getInventory();
387
+ //await this.loadExtensions();
388
+ if (inv && filesMetadata) {
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
+ }
489
398
  }
490
- await this._validateObjectPath();
491
- let createdDir = await this.#store.mkdir(workspacePath);
492
- if (!this.workspace) await this._ensureNamaste();
493
- return createTransactionProxy(new OcflObjectTransactionImpl(this, this.#store, inventory, workspaceVersionPath, createdDir));
494
399
  }
495
400
 
496
- async _ensureNamaste() {
497
- let prefix = NAMASTE_PREFIX_OBJECT;
498
- let nv = await findNamasteVersion(this.#store, prefix, this.root);
499
- if (nv === '') throw validation.createError(6, this.root);
500
- if (nv) {
501
- if (!this.ocflVersion) this.ocflVersion = nv;
502
- return;
503
- }
504
- // namaste not found, check if object root is not empty
505
- let files;
506
- try {
507
- files = await this.#store.readdir(this.root);
508
- } catch (error) {
509
- }
510
- if (files && files.length > 0) throw new Error('Cannot create an OCFL Object in a non-empty directory');
511
- //create the namaste file
512
- let filePath = path.join(this.root, NAMASTE_T + prefix + this.ocflVersion);
513
- await this.#store.writeFile(filePath, prefix + this.ocflVersion + '\n', 'utf8');
514
- }
401
+ /**
402
+ * Read the content of a file inside an object.
403
+ * @param {string} relPath - The file path relative to the object root.
404
+ * @param {*} options - Options to be passed to the underlying method
405
+ */
406
+ async readFile(relPath, options) {
407
+ return this.#store.readFile(path.join(this.root, relPath), options);
408
+ };
409
+
410
+ toString() { return `OcflObject { root: ${this.root}, id: ${this.id} }`; }
411
+
412
+ toJSON() { return { root: this.root, id: this.id }; }
413
+
414
+
415
+ }
416
+
417
+
418
+ /**
419
+ * @implements {OcflObject}
420
+ */
421
+ class OcflObjectImpl extends OcflObject {
422
+
423
+ // async _resolveContentPath({ logicalPath = '', contentPath = '', digest = '', version = '' }) {
424
+ // if (!contentPath) {
425
+ // let inv = await this.getInventory();
426
+ // version = version && version !== 'latest' ? version : inv.head;
427
+ // contentPath = inv.getContentPath(digest || inv.getDigest(logicalPath, version));
428
+ // }
429
+ // return contentPath;
430
+ // }
515
431
 
516
- // all the parent directories cannot be an ocfl object
517
- async _validateObjectPath() {
518
- var p = this.root;
519
- while (p !== '.' && p !== '/' ) {
520
- p = path.dirname(p);
521
- let nv = await findNamasteVersion(this.#store, NAMASTE_PREFIX_OBJECT, p);
522
- if (nv) throw new Error(`Object root cannot be nested under another object ${p}`);
523
- }
524
- }
525
-
526
432
  async isObject(aPath) { }
527
- getVersionString(i) { }
528
- async determineVersion() { }
529
- nameVersion(version) {
530
- return 'ocfl_object_' + version;
531
- }
532
- async digest_dir(dir) { }
533
- async hash_file(p) { }
534
- async removeEmptyDirectories(folder) { }
535
433
  static status(rootPath) { }
536
434
  static exists(rootPath) { }
537
435
 
@@ -545,48 +443,9 @@ class OcflObjectImpl extends OcflObject {
545
443
  };
546
444
 
547
445
 
548
- const objectInterface = new OcflObject();
549
- const proxyHandler = {
550
- /**
551
- * @param {OcflObject} target
552
- * @param {string} prop
553
- */
554
- get(target, prop, receiver) {
555
- if (prop in objectInterface) {
556
- let p = target[prop];
557
- if (typeof p === 'function') {
558
- return p.bind(target);
559
- } else {
560
- return p;
561
- }
562
- }
563
- }
564
- };
565
-
566
- /**
567
- * Return any OcflObject implementation as OcflObject.
568
- * @param {OcflObject} impl
569
- */
570
- function createObjectProxy(impl) {
571
- return new Proxy(impl, proxyHandler);
572
- }
573
-
574
- /**
575
- * Create an OCFL Object
576
- * @param {OcflObjectConfig} config
577
- * @return {OcflObject}
578
- */
579
- function createObject(config) {
580
- throw new Error('Not Implemented');
581
- }
582
-
583
446
  module.exports = {
584
447
  OcflObject,
585
- OcflObjectImpl,
586
448
  OcflObjectInventory,
587
449
  //OcflObjectTransaction,
588
- OcflObjectTransactionImpl,
589
- createTransactionProxy,
590
- createObjectProxy,
591
- createObject
450
+ OcflObjectTransactionImpl
592
451
  };