@ocfl/ocfl 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # ocfl-js - Common modules
2
2
  This is JavaScript/Node.js library to create and interact with Oxford Common File Layout (OCFL) storage and objects within it.
3
3
  This package contains abstract classes definition. Do not use this package directly. To work with an actual OCFL storage and object, use the following concrete implementation:
4
- * ocfl-fs - File system storage backend
4
+
5
+ * ocfl-fs - File system backend
5
6
 
6
7
  ## Extensions
7
8
  This library by default includes all the community extensions defined in the [OCFL Community Extensions page](https://github.com/OCFL/extensions). The implementation is organised in the ocfl-extensions package.
@@ -14,3 +15,7 @@ To implement a local extension in this library, first follow the description in
14
15
 
15
16
  }
16
17
 
18
+
19
+ ## Implementation
20
+
21
+ A new data store backend can be implemented by creating a new package with a store class that extends the `OcflStore` class and implements its methods. Please see the source code of ocfl-fs as an example implementation
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
 
@@ -73,7 +74,7 @@ class Ocfl {
73
74
  get StorageLayout() { return OcflStorageLayout.layout }
74
75
 
75
76
  /**
76
- * Create a storage layout from extension name or config
77
+ * Instantiate a storage layout from extension name or config
77
78
  * @param {string|OcflExtensionConfig} config - Layout name or config
78
79
  */
79
80
  storageLayout(config) {
@@ -90,7 +91,7 @@ class Ocfl {
90
91
  }
91
92
 
92
93
  /**
93
- * Create an OCFL Object
94
+ * Instantiate an OCFL Object object
94
95
  * @param {OcflObjectConfig} config
95
96
  * @param {SO} [storeOptions]
96
97
  * @return {OcflObject}
@@ -100,7 +101,7 @@ class Ocfl {
100
101
  }
101
102
 
102
103
  /**
103
- * Create an OCFL Storage
104
+ * Instantiate an OCFL Storage object
104
105
  * @param {OcflStorageConfig} config
105
106
  * @param {SO} [storeOptions]
106
107
  * @return {OcflStorage}
@@ -109,16 +110,27 @@ class Ocfl {
109
110
  return new OcflStorageImpl(config, this.#store.getInstance(storeOptions));
110
111
  }
111
112
 
112
- }
113
-
114
- class Storage {
115
- async create(config) {
116
-
113
+ /**
114
+ * Create a new OCFL Storage
115
+ * @param {OcflStorageConfig} config
116
+ * @param {SO} [storeOptions]
117
+ * @return {Promise<OcflStorage>}
118
+ */
119
+ async createStorage(config, storeOptions = this.#defaultOptions) {
120
+ return (new OcflStorageImpl(config, this.#store.getInstance(storeOptions))).create();
117
121
  }
118
- async load(config) {
119
122
 
123
+ /**
124
+ * Load an existing OCFL Storage
125
+ * @param {OcflStorageConfig} config
126
+ * @param {SO} [storeOptions]
127
+ * @return {Promise<OcflStorage>}
128
+ */
129
+ async loadStorage(config, storeOptions = this.#defaultOptions) {
130
+ return (new OcflStorageImpl(config, this.#store.getInstance(storeOptions))).load();
120
131
  }
121
- }
132
+ }
133
+
122
134
 
123
135
 
124
136
  module.exports = {
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 */
@@ -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
  /**
@@ -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/object.js CHANGED
@@ -123,13 +123,19 @@ class OcflObject {
123
123
  * Identifier of the OCFL Object
124
124
  * @return {string}
125
125
  */
126
- get id() { throw new NotImplementedError() }
126
+ get id() { throw new NotImplementedError(); }
127
127
 
128
128
  /**
129
- * The non-storage specific absolute path to the root of this OCFL Object
129
+ * The absolute path to the root of this OCFL Object
130
130
  * @return {string}
131
131
  */
132
- get root() { throw new NotImplementedError() }
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(); }
133
139
 
134
140
  toString() { return `OcflObject { root: ${this.root}, id: ${this.id} }`; }
135
141
 
@@ -140,7 +146,7 @@ class OcflObject {
140
146
  * @param {string} [version] - Full name of the version, eg: v1, v2, v3
141
147
  * @returns {Promise<OcflObjectInventory>}
142
148
  */
143
- async getInventory(version = 'latest') { throw new NotImplementedError() }
149
+ async getInventory(version = 'latest') { throw new NotImplementedError(); }
144
150
 
145
151
  /**
146
152
  * Update the content files or directories as one transaction and commit the changes as a new version.
@@ -150,7 +156,7 @@ class OcflObject {
150
156
  * @param {ObjectUpdateMode} [mode=UPDATE_MODE.MERGE]
151
157
  * @return {Promise<?OcflObjectTransaction>}
152
158
  */
153
- async update(updater, mode = UPDATE_MODE.MERGE) { throw new NotImplementedError() }
159
+ async update(updater, mode) { throw new NotImplementedError(); }
154
160
 
155
161
  /**
156
162
  * Import one or more content files or directories to the object
@@ -199,7 +205,8 @@ class OcflObject {
199
205
 
200
206
  /**
201
207
  * Iterate through the content files contained in the specified version.
202
- * Returns an array of FileRef which consists of logical path, content path, and digest, in no particular order
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.
203
210
  * @param {string} [version]
204
211
  * @return {Promise<Generator<OcflObjectFile, void, unknown>>}
205
212
  */
@@ -224,7 +231,7 @@ class OcflObject {
224
231
  * @type { GetFileByLogical & GetFileByOpt }
225
232
  */
226
233
  // @ts-ignore
227
- getFile = function (opt, version) { }
234
+ getFile = function (opt, version) { };
228
235
 
229
236
  /**
230
237
  * Count the number of files contained in this object
@@ -258,6 +265,8 @@ class OcflObjectImpl extends OcflObject {
258
265
  /** @type {string} */
259
266
  #root;
260
267
  /** @type {string} */
268
+ #workspace;
269
+ /** @type {string} */
261
270
  #id;
262
271
  /** @type {OcflExtension[]} */
263
272
  #extensions;
@@ -283,13 +292,14 @@ class OcflObjectImpl extends OcflObject {
283
292
  this.digestAlgorithm = DIGEST.of(config.digestAlgorithm || 'sha512');
284
293
  if (!this.digestAlgorithm) throw new Error('Invalid digest algorithm. Must be one of `sha256` or `sha512`.');
285
294
  //this.fixityDigest = config.fixityDigest ?? this.digestAlgorithm;
286
- this.workspace = config.workspace ? path.resolve(config.workspace) : undefined;
295
+ this.#workspace = config.workspace ? path.resolve(config.workspace) : undefined;
287
296
  this.contentDirectory = config.contentDirectory ?? 'content';
288
297
  /** @type Object.<string, OcflObjectInventory> */
289
298
  this._inventory = {};
290
299
  }
291
300
  get id() { return this._inventory?.latest?.id || this.#id; }
292
301
  get root() { return this.#root; }
302
+ get workspace() { return this.#workspace; }
293
303
 
294
304
  async getInventory(version = 'latest') {
295
305
  if (!this._inventory[version]) {
@@ -324,7 +334,7 @@ class OcflObjectImpl extends OcflObject {
324
334
  OcflDigest.digestAsync(data.digestAlgorithm, datastr)
325
335
  ]));
326
336
  digest = digest.match(/[^\s]+/)?.[0];
327
- if (digest !== actualDigest) throw new Error('corrupted file');
337
+ if (digest !== actualDigest) throw new Error(`Inventory file ${this.root}/${invPath} digest mismatch: recorded=${digest} actual=${actualDigest}`);
328
338
  //return new OcflObjectInventory({ data, digest });
329
339
  return new OcflObjectInventory(data);
330
340
  }
@@ -373,13 +383,13 @@ class OcflObjectImpl extends OcflObject {
373
383
  */
374
384
  readFile = async function (relPath, options) {
375
385
  return this.#store.readFile(path.join(this.root, relPath), options);
376
- }
386
+ };
377
387
 
378
388
  getFile = function (opt, version) {
379
389
  var obj = this;
380
390
  var opt_ = typeof opt === 'string' ? { logicalPath: opt, version: version } : opt;
381
391
  return new OcflObjectFile(this, opt_);
382
- }
392
+ };
383
393
 
384
394
  // async _resolveContentPath({ logicalPath = '', contentPath = '', digest = '', version = '' }) {
385
395
  // if (!contentPath) {
@@ -477,6 +487,7 @@ class OcflObjectImpl extends OcflObject {
477
487
  // workspace dir exists, abort update
478
488
  throw new Error('Uncommitted changes detected. Object is being updated by other process or there has been a failed update attempt');
479
489
  }
490
+ await this._validateObjectPath();
480
491
  let createdDir = await this.#store.mkdir(workspacePath);
481
492
  if (!this.workspace) await this._ensureNamaste();
482
493
  return createTransactionProxy(new OcflObjectTransactionImpl(this, this.#store, inventory, workspaceVersionPath, createdDir));
@@ -491,18 +502,28 @@ class OcflObjectImpl extends OcflObject {
491
502
  return;
492
503
  }
493
504
  // namaste not found, check if object root is not empty
505
+ let files;
494
506
  try {
495
- let files = await this.#store.readdir(this.root);
496
- if (files.length > 0) throw new Error('Cannot create an OCFL Object in a non-empty directory');
507
+ files = await this.#store.readdir(this.root);
497
508
  } catch (error) {
498
509
  }
510
+ if (files && files.length > 0) throw new Error('Cannot create an OCFL Object in a non-empty directory');
499
511
  //create the namaste file
500
512
  let filePath = path.join(this.root, NAMASTE_T + prefix + this.ocflVersion);
501
513
  await this.#store.writeFile(filePath, prefix + this.ocflVersion + '\n', 'utf8');
502
514
  }
503
515
 
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
+
504
526
  async isObject(aPath) { }
505
- async importDir(id, sourceDir) { }
506
527
  getVersionString(i) { }
507
528
  async determineVersion() { }
508
529
  nameVersion(version) {
@@ -540,7 +561,7 @@ const proxyHandler = {
540
561
  }
541
562
  }
542
563
  }
543
- }
564
+ };
544
565
 
545
566
  /**
546
567
  * Return any OcflObject implementation as OcflObject.
package/lib/storage.js CHANGED
@@ -7,8 +7,8 @@ const { OcflStorageLayout } = require("./extension");
7
7
  const { HashedNTupleStorageLayout } = require("./extensions/0004-hashed-n-tuple-storage-layout");
8
8
  const { OcflObject, OcflObjectImpl } = require("./object");
9
9
  const { isDirEmpty, findNamasteVersion } = require("./utils.js");
10
- const { OCFL_VERSION, OCFL_VERSIONS, OCFL_LAYOUT,
11
- EXTENSIONS_DIR, EXTENSION_CONFIG,
10
+ const { OCFL_VERSION, OCFL_VERSIONS, OCFL_LAYOUT,
11
+ EXTENSIONS_DIR, EXTENSION_CONFIG,
12
12
  NAMASTE_PREFIX_STORAGE, NAMASTE_PREFIX_OBJECT, NAMASTE_T } = require('./constants').OcflConstants;
13
13
  const DIGEST = require('./digest').OcflDigest.CONTENT;
14
14
 
@@ -66,42 +66,44 @@ class OcflStorage {
66
66
  * Create the storage root in the underlying storage backend
67
67
  * @param {Object} [options] - Additional options for creating storage root
68
68
  * @param {boolean} [options.ocflSpecText=false] - If true, create a copy of the OCFL specification text in the storage root
69
+ * @return {Promise<OcflStorage>}
69
70
  */
70
71
  async create(options) { throw new NotImplementedError(); }
71
72
 
72
- /**
73
- * Check if the storage root path points to an existing file or non-empty directory in the underlying backend store.
74
- * The existing directory may or may not be a valid OCFL Storage.
75
- * @return {Promise<boolean>}
76
- */
77
- async exists() { throw new NotImplementedError(); }
78
-
79
73
  /**
80
74
  * Check namaste, retrieve storage layout extension, and read its config file.
81
- * @return {Promise<boolean>}
75
+ * @return {Promise<OcflStorage>}
82
76
  */
83
77
  async load() { throw new NotImplementedError(); }
84
78
 
79
+ /**
80
+ * Check if the storage root path points to an existing file or non-empty directory in the underlying backend store.
81
+ * The existing directory may or may not be a valid OCFL Storage.
82
+ * @return {Promise<boolean>}
83
+ */
84
+ async exists() { throw new NotImplementedError(); }
85
+
85
86
  /**
86
87
  *
87
- * @return {AsyncIterator<OcflObject>}
88
+ * @return {AsyncIterableIterator<OcflObject>}
88
89
  */
89
90
  [Symbol.asyncIterator]() { throw new NotImplementedError(); }
90
91
 
91
92
  /**
92
- * Return all OCFL objects under this storage as array
93
- * @return {Promise<OcflObject[]>}
93
+ * Return a new AsyncIterator that contains all the OCFL objects in this Storage.
94
+ * @return {AsyncIterableIterator<OcflObject>}
94
95
  */
95
- async objects() {
96
- const objects = [];
97
- for await(const o of this) objects.push(o);
98
- return objects;
96
+ objects() {
97
+ return this[Symbol.asyncIterator]();
98
+ // const objects = [];
99
+ // for await (const o of this) objects.push(o);
100
+ // return objects;
99
101
  }
100
102
 
101
103
  async delete(id) {
102
104
  return this.remove(id);
103
105
  }
104
-
106
+
105
107
  async purge(id) {
106
108
  return this.remove(id);
107
109
  }
@@ -188,7 +190,7 @@ class OcflStorageImpl extends OcflStorage {
188
190
  object(opt) {
189
191
  let id, root;
190
192
  if (typeof opt === 'string') id = opt;
191
- else ({id, root} = opt);
193
+ else ({ id, root } = opt);
192
194
  let relObjectRoot = root || this.objectRoot(id);
193
195
  return new OcflObjectImpl({
194
196
  id,
@@ -230,7 +232,7 @@ class OcflStorageImpl extends OcflStorage {
230
232
 
231
233
  //@todo: load storage level extensions
232
234
 
233
- return true;
235
+ return this;
234
236
  }
235
237
 
236
238
  /**
@@ -264,11 +266,9 @@ class OcflStorageImpl extends OcflStorage {
264
266
  if (options?.ocflSpecText) {
265
267
 
266
268
  }
269
+ return this;
267
270
  }
268
271
 
269
- /**
270
- * @return {AsyncIterator<OcflObject, OcflObject>}
271
- */
272
272
  [Symbol.asyncIterator]() {
273
273
  let store = this.#store;
274
274
  let workspace = this.#workspace;
@@ -276,6 +276,7 @@ class OcflStorageImpl extends OcflStorage {
276
276
  let root = this.root;
277
277
  let stack = [this.#store.opendir(this.root)];
278
278
  return {
279
+ [Symbol.asyncIterator]() { return this; },
279
280
  async next() {
280
281
  let dirp;
281
282
  while ((dirp = stack.pop())) {
package/lib/types.d.ts CHANGED
@@ -42,3 +42,12 @@ 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ocfl/ocfl",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Oxford Common File Layout (OCFL) JS library",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -33,11 +33,13 @@
33
33
  "Peter Sefton (https://orcid.org/0000-0002-3545-944X)"
34
34
  ],
35
35
  "license": "GPL-3.0-or-later",
36
+ "dependencies": {
37
+ "blake2": "^4.1.1"
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",
42
+ "typedoc": "^0.23.15",
41
43
  "mocha": "^10.0.0"
42
44
  },
43
45
  "mocha": {
@@ -48,9 +50,6 @@
48
50
  ],
49
51
  "recursive": true
50
52
  },
51
- "dependencies": {
52
- "blake2": "^4.1.1"
53
- },
54
53
  "bugs": {
55
54
  "url": "https://github.com/Arkisto-Platform/ocfl-js/issues"
56
55
  },
@@ -59,5 +58,10 @@
59
58
  "lib": "lib",
60
59
  "test": "test"
61
60
  },
62
- "files": [ "index.js", "index.mjs", "lib", "resources" ]
61
+ "files": [
62
+ "index.js",
63
+ "index.mjs",
64
+ "lib",
65
+ "resources"
66
+ ]
63
67
  }