@ocfl/ocfl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/storage.js ADDED
@@ -0,0 +1,324 @@
1
+ //@ts-check
2
+ //@todo add default layout
3
+ const path = require("path");
4
+ const validation = require('./validation.js');
5
+ const { NotImplementedError } = require("./error");
6
+ const { OcflStorageLayout } = require("./extension");
7
+ const { HashedNTupleStorageLayout } = require("./extensions/0004-hashed-n-tuple-storage-layout");
8
+ const { OcflObject, OcflObjectImpl } = require("./object");
9
+ const { isDirEmpty, findNamasteVersion } = require("./utils.js");
10
+ const { OCFL_VERSION, OCFL_VERSIONS, OCFL_LAYOUT,
11
+ EXTENSIONS_DIR, EXTENSION_CONFIG,
12
+ NAMASTE_PREFIX_STORAGE, NAMASTE_PREFIX_OBJECT, NAMASTE_T } = require('./constants').OcflConstants;
13
+ const DIGEST = require('./digest').OcflDigest.CONTENT;
14
+
15
+
16
+ const DEFAULT_LAYOUT = HashedNTupleStorageLayout;
17
+
18
+ /**
19
+ * @typedef {import('./extension').OcflExtensionConfig} OcflExtensionConfig
20
+ * @typedef {import('./store').OcflStore} OcflStore
21
+ */
22
+
23
+ class OcflStorage {
24
+ static LAYOUT;
25
+
26
+ /**
27
+ * Get the storage root
28
+ * @return {string}
29
+ */
30
+ get root() { throw new NotImplementedError(); }
31
+
32
+ /**
33
+ * Construct the path of the object root relative to the storage root
34
+ * The path is mapped from the object identifier using the algorithm defined in the chosen
35
+ * storage layout extension (specified in {@link OcflStorageConfig}).
36
+ * @param {string} id - Identifier of the object
37
+ * @return {string}
38
+ */
39
+ objectRoot(id) { throw new NotImplementedError(); }
40
+
41
+ /**
42
+ * Absolute path to the object root
43
+ * @param {string} id - Identifier of the object
44
+ */
45
+ objectRootAbs(id) {
46
+ return path.join(this.root, this.objectRoot(id));
47
+ }
48
+
49
+ /**
50
+ * Get an existing object in the Storage or create a new one.
51
+ * @param {string|{id?:string, root?:string}} opt - A unique identifier for the OCFL Object, should be a URI
52
+ * @return {OcflObject}
53
+ */
54
+ object(opt) { throw new NotImplementedError(); }
55
+
56
+ /**
57
+ * Check if the storage has already the object with the given id.
58
+ * @param {*} id
59
+ */
60
+ async has(id) {
61
+ return this.object(id).exists();
62
+ }
63
+
64
+ /**
65
+ * Create the storage root in the underlying storage backend
66
+ * @param {Object} [options] - Additional options for creating storage root
67
+ * @param {boolean} [options.ocflSpecText=false] - If true, create a copy of the OCFL specification text in the storage root
68
+ */
69
+ async create(options) { throw new NotImplementedError(); }
70
+
71
+ /**
72
+ * Check if the storage root path points to an existing file or non-empty directory in the underlying backend store.
73
+ * The existing directory may or may not be a valid OCFL Storage.
74
+ * @return {Promise<boolean>}
75
+ */
76
+ async exists() { throw new NotImplementedError(); }
77
+
78
+ /**
79
+ * Check namaste, retrieve storage layout extension, and read its config file.
80
+ * @return {Promise<boolean>}
81
+ */
82
+ async load() { throw new NotImplementedError(); }
83
+
84
+ /**
85
+ *
86
+ * @return {AsyncIterator<OcflObject>}
87
+ */
88
+ [Symbol.asyncIterator]() { throw new NotImplementedError(); }
89
+
90
+ /**
91
+ * Return all OCFL objects under this storage as array
92
+ * @return {Promise<OcflObject[]>}
93
+ */
94
+ async objects() {
95
+ const objects = [];
96
+ for await(const o of this) objects.push(o);
97
+ return objects;
98
+ }
99
+
100
+ async delete(id) {
101
+ return this.remove(id);
102
+ }
103
+ async purge(id) {
104
+ return this.remove(id);
105
+ }
106
+
107
+ async remove(id) { throw new NotImplementedError(); }
108
+
109
+ };
110
+
111
+ /**
112
+ * @typedef {Object} OcflStorageConfig
113
+ * @property {string} root - Absolute path to the ocfl storage root
114
+ * @property {string} [workspace] - Absolute path to storage workspace directory
115
+ * @property {OcflStorageLayout|OcflExtensionConfig|string} [layout] - A layout that identifies an arrangement of directories and OCFL objects under the storage root
116
+ * @property {('sha256' | 'sha512')} [digestAlgorithm] - Digest algorithm for content-addressing, must use either sha512 or sha256
117
+ * @property {string} [contentDirectory='content'] - Content directory name. Only applies to a newly created object.
118
+ * @property {string} [ocflVersion=c.OCFL_VERSION] - Ocfl version. Only applies to a newly created object.
119
+ */
120
+
121
+ /**
122
+ * General implementation of {@link OcflStorage} that can uses different datastore backends.
123
+ * This class provides common functionalities for the subclasses and
124
+ * at the same provide encapsulation emulating private and protected methods.
125
+ * @implements {OcflStorage}
126
+ */
127
+ class OcflStorageImpl extends OcflStorage {
128
+
129
+ /** @type {string} */
130
+ #root;
131
+ /** @type {string} */
132
+ #workspace;
133
+ /** @type {OcflStorageLayout} */
134
+ #layout;
135
+ /** @type {OcflStore} */
136
+ #store;
137
+ #objectConfig;
138
+
139
+ /**
140
+ *
141
+ * @param {OcflStorageConfig} config
142
+ * @param {OcflStore} store
143
+ */
144
+ constructor(config, store) {
145
+ super();
146
+ if (!store) throw new TypeError('[OcflStorage] store is required.');
147
+ if (!config.root) throw new TypeError('[OcflStorage] config.root is required.');
148
+ this.#store = store;
149
+ const { root, workspace, layout, ocflVersion, ...objectConfig } = config;
150
+ this.#root = path.resolve(root);
151
+ this.#workspace = workspace ? path.resolve(workspace) : path.join(this.#root, 'extensions/workspace');
152
+ if (this.#workspace && this.#workspace.startsWith(this.#root + path.sep) && !this.#workspace.startsWith(this.#root + path.sep + 'extensions' + path.sep)) {
153
+ throw new Error('[OcflStorage] config.workspace cannot be the same as or a subpath of config.root');
154
+ }
155
+ this.#layout = layout instanceof OcflStorageLayout ? layout : this.#createLayout(layout);
156
+ this.ocflVersion = ocflVersion || OCFL_VERSION;
157
+ this.#objectConfig = objectConfig;
158
+ }
159
+
160
+ /**
161
+ *
162
+ * @param {OcflExtensionConfig|string} option
163
+ */
164
+ #createLayout(option) {
165
+ let name = typeof option === 'string' ? option : option?.extensionName;
166
+ let config = typeof option === 'object' ? option : null;
167
+ if (name) {
168
+ return OcflStorageLayout.class(name).create(config);
169
+ }
170
+ // else if (useDefault) {
171
+ // return DEFAULT_LAYOUT.create(config);
172
+ // }
173
+ }
174
+
175
+ get root() { return this.#root; }
176
+
177
+ objectRoot(id) {
178
+ if (!this.#layout) throw new Error('[OcflStorage] No layout defined');
179
+ return this.#layout.map(id);
180
+ }
181
+
182
+ /**
183
+ * The root param is a relative path to the storage root.
184
+ * @param {string|{id?:string, root?:string}} opt
185
+ */
186
+ object(opt) {
187
+ let id, root;
188
+ if (typeof opt === 'string') id = opt;
189
+ else ({id, root} = opt);
190
+ let relObjectRoot = root || this.objectRoot(id);
191
+ return new OcflObjectImpl({
192
+ id,
193
+ root: path.join(this.root, relObjectRoot),
194
+ // @todo:use extension workspace if not defined
195
+ workspace: this.#workspace ? path.join(this.#workspace, relObjectRoot) : undefined,
196
+ ocflVersion: this.ocflVersion,
197
+ ...this.#objectConfig
198
+ },
199
+ this.#store);
200
+ }
201
+
202
+ async exists() {
203
+ return !(await isDirEmpty(this.#store, this.root));
204
+ }
205
+
206
+ async load() {
207
+ let prefix = NAMASTE_PREFIX_STORAGE;
208
+ let tasks = [
209
+ findNamasteVersion(this.#store, prefix, this.root),
210
+ this.#store.readFile(path.join(this.root, OCFL_LAYOUT), 'utf8')
211
+ ];
212
+ // @ts-ignore
213
+ let [ocflVersion, layout] = (await Promise.allSettled(tasks)).map(r => r.value);
214
+ if (!ocflVersion) throw new Error('Invalid storage root');//throw validation.createError(67, this.root);
215
+ let layoutName, layoutConfig;
216
+ // load layout
217
+ try {
218
+ if (layout) layoutName = JSON.parse(layout).extension;
219
+ } catch (error) {
220
+ }
221
+ // load ext config if exists
222
+ try {
223
+ let cfgstr = await this.#store.readFile(path.join(this.root, EXTENSIONS_DIR, layoutName, EXTENSION_CONFIG), 'utf8');
224
+ layoutConfig = JSON.parse(/**@type{string}*/(cfgstr));
225
+ } catch (error) {
226
+ }
227
+ this.#layout = this.#createLayout(layoutConfig || layoutName);
228
+
229
+ //@todo: load storage level extensions
230
+
231
+ return true;
232
+ }
233
+
234
+ /**
235
+ * @param {Object} [options] - Additional options for creating storage root
236
+ * @param {boolean} [options.ocflSpecText=false] - If true, create a copy of the OCFL specification text in the storage root
237
+ */
238
+ async create(options) {
239
+ if (await this.exists()) {
240
+ //cannot create, storage root already exists .
241
+ throw new Error('[OcflStorage] Cannot create storage root in a non-empty directory');
242
+ } else {
243
+ await this.#store.mkdir(this.root);
244
+ }
245
+ // create namaste
246
+ let prefix = NAMASTE_PREFIX_STORAGE;
247
+ let filenamePrefix = NAMASTE_T + prefix;
248
+ let filePath = path.join(this.root, filenamePrefix + this.ocflVersion);
249
+ await this.#store.writeFile(filePath, prefix + this.ocflVersion + '\n', 'utf8');
250
+
251
+ // create layout
252
+ if (!this.#layout) {
253
+ this.#layout = new DEFAULT_LAYOUT();
254
+ }
255
+ let layout = { extension: this.#layout.name, description: this.#layout.description };
256
+ await this.#store.writeFile(path.join(this.root, OCFL_LAYOUT), JSON.stringify(layout, null, 2), 'utf8');
257
+ if (this.#layout.config) {
258
+ await this.#store.writeFile(path.join(this.root, EXTENSIONS_DIR, this.#layout.name, EXTENSION_CONFIG), JSON.stringify(this.#layout.config, null, 2), 'utf8');
259
+ }
260
+
261
+ // todo: create other extensions
262
+ if (options?.ocflSpecText) {
263
+
264
+ }
265
+ }
266
+
267
+ /**
268
+ * @return {AsyncIterator<OcflObject, OcflObject>}
269
+ */
270
+ [Symbol.asyncIterator]() {
271
+ let store = this.#store;
272
+ let workspace = this.#workspace;
273
+ let config = this.#objectConfig;
274
+ let root = this.root;
275
+ let stack = [this.#store.opendir(this.root)];
276
+ return {
277
+ async next() {
278
+ let dirp;
279
+ while ((dirp = stack.pop())) {
280
+ let dir = await dirp;
281
+ let dirent = await dir.read();
282
+ if (!dirent) {
283
+ await dir.close();
284
+ continue;
285
+ }
286
+ stack.push(dirp);
287
+ if (!dirent.isDirectory()) continue;
288
+ let basePath = path.join(dir.path, dirent.name);
289
+ let nv;
290
+ try {
291
+ nv = await findNamasteVersion(store, NAMASTE_PREFIX_OBJECT, basePath);
292
+ } catch (err) {
293
+ }
294
+ if (nv) {
295
+ //ocfl object exists
296
+ // inventory exists
297
+ let object = new OcflObjectImpl({
298
+ root: basePath,
299
+ workspace: workspace ? path.join(workspace, path.relative(root, basePath)) : undefined,
300
+ ocflVersion: nv,
301
+ ...config
302
+ }, store);
303
+ //await object.load();
304
+ return { done: false, value: object };
305
+ } else {
306
+ //ocfl object does not exist, process subdir
307
+ stack.push(store.opendir(basePath));
308
+ }
309
+
310
+ }
311
+ return { done: true, value: null };
312
+ }
313
+ // return() {
314
+ // return { done: true };
315
+ // }
316
+ };
317
+ }
318
+
319
+ }
320
+
321
+ module.exports = {
322
+ OcflStorage,
323
+ OcflStorageImpl
324
+ };
package/lib/store.js ADDED
@@ -0,0 +1,153 @@
1
+ const path = require('path');
2
+ const { pipeline } = require('stream/promises');
3
+ const { parallelize, dataSourceAsIterable } = require('./utils');
4
+
5
+ const emptyOptions = {};
6
+
7
+ /**
8
+ * Abstract class to provide common APIs to access and modify the actual data store backend
9
+ * such as local file system and cloud storage
10
+ */
11
+ class OcflStore {
12
+ static instances;
13
+
14
+ /**
15
+ *
16
+ * @param {Object} [options]
17
+ */
18
+ static getInstance(options = emptyOptions) {
19
+ if (!this.instances) return new this(options);
20
+ let store = this.instances.get(options);
21
+ if (!store) {
22
+ store = new this(options);
23
+ this.instances.set(options, store);
24
+ }
25
+ return store;
26
+ }
27
+
28
+ constructor(options) { }
29
+
30
+ /**
31
+ * Get file information.
32
+ * @param {string} filePath
33
+ * @return {Promise<import('fs').Stats>}
34
+ */
35
+ async stat(filePath) { throw new Error('Not Implemented'); }
36
+
37
+ /**
38
+ * Check if file exists
39
+ * @param {string} filePath
40
+ */
41
+ async exists(filePath) {
42
+ try {
43
+ await this.stat(filePath);
44
+ return true;
45
+ } catch (error) {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Create a readable stream to get the content of a file
52
+ * @param {string} filePath - Absolute path
53
+ * @param {*} options
54
+ * @return {Promise<import('fs').ReadStream>}
55
+ */
56
+ async createReadStream(filePath, options) { throw new Error('Not Implemented'); }
57
+
58
+ /**
59
+ * Create a stream that writes data to {@link relPath}.
60
+ * @param {string} relPath
61
+ * @param {*} options
62
+ * @return {Promise<import('fs').WriteStream>}
63
+ */
64
+ async createWriteStream(relPath, options) { throw new Error('Not Implemented'); }
65
+
66
+ /**
67
+ * Provide a common interface to read a file inside an object.
68
+ * A concrete subclass MAY implement this method to provide read access to its underlying storage backend.
69
+ * The default implementation uses the {@linkcode OcflStore#createReadStream} method.
70
+ * @param {string} filePath
71
+ * @param {*} [options]
72
+ */
73
+ async readFile(filePath, options) {
74
+ let rs = await this.createReadStream(filePath, options);
75
+ let chunks = [];
76
+ for await (const chunk of rs) {
77
+ chunks.push(chunk);
78
+ }
79
+ if (typeof options === 'string' || options.encoding) {
80
+ return Buffer.concat(chunks);
81
+ } else {
82
+ return chunks.join('');
83
+ }
84
+ }
85
+
86
+ /** @typedef {string|Buffer|NodeJS.ArrayBufferView|AsyncIterable|Iterable|import('stream').Readable} WriteFileData */
87
+ /**
88
+ * Provide a common interface to write a file inside an object.
89
+ * A concrete subclass MAY implement this method to provide write access to its underlying storage backend.
90
+ * The default implementation uses the {@link OcflStore.createWriteStream} method.
91
+ * @param {string} filePath - Absolute path to the file.
92
+ * @param {WriteFileData} data - The data to be written to the file.
93
+ * @param {Object|string} options
94
+ */
95
+ async writeFile(filePath, data, options) {
96
+ var source = dataSourceAsIterable(data);
97
+ await this.mkdir(path.dirname(filePath));
98
+ const target = await this.createWriteStream(filePath, options);
99
+ await pipeline(source, target);
100
+ }
101
+
102
+ /**
103
+ * Copy file from `source` to `target`.
104
+ * By default, target is overwritten if it already exists.
105
+ * @param {string} source - source filename to copy
106
+ * @param {string} target - destination filename of the copy operation
107
+ */
108
+ async copyFile(source, target) {
109
+ await this.writeFile(target, await this.createReadStream(source));
110
+ }
111
+
112
+ /**
113
+ * Open a directory
114
+ * @param {string} filePath
115
+ * @param {*} options
116
+ * @return {Promise<import('fs').Dir> }
117
+ */
118
+ async opendir(filePath, options) { throw new Error('Not Implemented'); }
119
+
120
+ /**
121
+ * Read a directory
122
+ * @param {string} filePath
123
+ * @param {*} options
124
+ * @return {Promise<string[]> }
125
+ */
126
+ async readdir(filePath, options) { throw new Error('Not Implemented'); }
127
+
128
+ /**
129
+ * Recursively create a directory if the storage backend supports it, otherwise do nothing
130
+ * @param {string} filePath - The directory path
131
+ * @param {*} [options] - Options to be passed to the underlying method
132
+ * @return {Promise<string>} - The first directory path created
133
+ */
134
+ async mkdir(filePath, options) { throw new Error('Not Implemented'); }
135
+
136
+ /**
137
+ * Rename or move a file or directory
138
+ * @param {string} source
139
+ * @param {string} target
140
+ */
141
+ async move(source, target) { throw new Error('Not Implemented'); }
142
+
143
+ /**
144
+ * Remove a file or directory recursively
145
+ * @param {*} filePath
146
+ */
147
+ async remove(filePath) { throw new Error('Not Implemented'); }
148
+
149
+ }
150
+
151
+ module.exports = {
152
+ OcflStore
153
+ };