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