@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/index.js +72 -97
- package/index.mjs +3 -3
- package/lib/digest.js +245 -76
- package/lib/enum.js +73 -75
- package/lib/extension.js +4 -16
- package/lib/extensions/0003-hash-and-id-n-tuple-storage-layout.js +5 -3
- package/lib/extensions/0004-hashed-n-tuple-storage-layout.js +6 -4
- package/lib/extensions/0009-digest-algorithms.js +18 -0
- package/lib/extensions/index.js +1 -1
- package/lib/file.js +129 -0
- package/lib/index.js +0 -12
- package/lib/inventory.js +297 -136
- package/lib/object.js +293 -434
- package/lib/storage.js +26 -15
- package/lib/store.js +54 -16
- package/lib/transaction.js +177 -142
- package/lib/types.d.ts +110 -9
- package/lib/utils.js +52 -21
- package/lib/validation.js +1 -1
- package/package.json +16 -10
- package/lib/extensions/0001-digest-algorithms.js +0 -41
- package/lib/temp.js +0 -264
package/lib/transaction.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
//@ts-check
|
|
2
2
|
/** Models a set of ocfl object modifying operations as a transaction that is as close as atomic as possible */
|
|
3
|
-
|
|
4
|
-
const { stat, readdir } = require('fs/promises');
|
|
3
|
+
//todo: replace path module with a more generic path module that works in both node and browser
|
|
5
4
|
const path = require('path');
|
|
6
|
-
const { parallelize
|
|
5
|
+
const { parallelize } = require('./utils');
|
|
7
6
|
const { OcflDigest } = require('./digest');
|
|
8
7
|
const { INVENTORY_NAME } = require('./constants').OcflConstants;
|
|
9
8
|
|
|
10
|
-
//const { pipeline } = require('stream/promises');
|
|
11
|
-
//const { OcflObjectImpl, OcflObjectInventory } = require('./object');
|
|
12
|
-
|
|
13
9
|
/**
|
|
14
|
-
* @typedef {import('./object').
|
|
10
|
+
* @typedef {import('./object').OcflObject} OcflObject
|
|
15
11
|
* @typedef {import('./store').OcflStore} OcflStore
|
|
16
12
|
* @typedef {import('./inventory').OcflObjectInventoryMut} OcflObjectInventoryMut
|
|
17
13
|
* @typedef {import('stream').Writable} Writable
|
|
@@ -39,6 +35,7 @@ class OcflObjectTransaction {
|
|
|
39
35
|
* @param {Object} [options.user] - Identify the user or agent that created the current Version
|
|
40
36
|
* @param {string} options.user.name - Any readable name of the user, e.g., a proper name, user ID, agent ID.
|
|
41
37
|
* @param {string} [options.user.address] - A URI: either a mailto URI [RFC6068] with the e-mail address of the user or a URL to a personal identifier, e.g., an ORCID iD.
|
|
38
|
+
* @param {boolean} [options.force=false] - If true, force to commit. Use it after a purge.
|
|
42
39
|
*/
|
|
43
40
|
async commit(options) { throw new Error('Not Implemented'); }
|
|
44
41
|
|
|
@@ -51,22 +48,24 @@ class OcflObjectTransaction {
|
|
|
51
48
|
return await this.remove(logicalPath, { purge: true });
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
|
|
55
51
|
/**
|
|
56
52
|
* Write data to a file at `logicalPath`
|
|
57
53
|
* @abstract
|
|
58
54
|
* @param {string} logicalPath - A path relative to the object content directory
|
|
59
|
-
* @param {
|
|
55
|
+
* @param {string|Uint8Array|DataView|ArrayBuffer|ReadableStream} data - Data to be written to the file
|
|
60
56
|
* @param {Object|string} [options] - The same options as https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback
|
|
61
57
|
*/
|
|
62
58
|
async write(logicalPath, data, options) { throw new Error('Not Implemented'); }
|
|
63
59
|
|
|
64
60
|
/**
|
|
65
61
|
* Import content by recursively copying a file or directory from local file system to the
|
|
66
|
-
* OCFL Object content. For example, `
|
|
62
|
+
* OCFL Object content. For example, `object.import('/home/john/data/b','data/b')` will
|
|
67
63
|
* copy `/home/john/data/b` from the local filesystem to the content `data/b` in the OFCL Object.
|
|
68
64
|
* Set target to empty string ('') to copy a directory content to the object's content root.
|
|
69
65
|
* If target is not specified, the base name of the source will be used.
|
|
66
|
+
* For examples:
|
|
67
|
+
* To copy '/src/data' directory as 'data' use import('/src/data', 'data').
|
|
68
|
+
* To copy all files inside '/src/data' directory to the logical root, use import('/src/data', '').
|
|
70
69
|
* @abstract
|
|
71
70
|
* @param {string} source - A path to a file or directory in the local filesystem to be copied
|
|
72
71
|
* @param {string} [target] - A logical path in the OCFL Object content.
|
|
@@ -84,7 +83,7 @@ class OcflObjectTransaction {
|
|
|
84
83
|
|
|
85
84
|
/**
|
|
86
85
|
* Changes the logical path of existing content. This is similar to move operation in file systems
|
|
87
|
-
*
|
|
86
|
+
* Unless renaming files that are not yet commited, this operation will NOT rename the actual file name referenced by the manifest.
|
|
88
87
|
* @abstract
|
|
89
88
|
* @param {*} source
|
|
90
89
|
* @param {*} target
|
|
@@ -92,7 +91,7 @@ class OcflObjectTransaction {
|
|
|
92
91
|
async rename(source, target) { throw new Error('Not Implemented'); }
|
|
93
92
|
|
|
94
93
|
/**
|
|
95
|
-
*
|
|
94
|
+
* Alias of {@link rename()}
|
|
96
95
|
* @param {*} source
|
|
97
96
|
* @param {*} target
|
|
98
97
|
*/
|
|
@@ -103,11 +102,12 @@ class OcflObjectTransaction {
|
|
|
103
102
|
* @abstract
|
|
104
103
|
* @param {string} logicalPath - A path relative to the object content directory
|
|
105
104
|
* @param {Object} [options]
|
|
105
|
+
* @param {boolean} [options.purge=false] - If true, completely remove the content from all versions
|
|
106
106
|
*/
|
|
107
107
|
async remove(logicalPath, options) { throw new Error('Not Implemented'); }
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Remove a file or directory
|
|
110
|
+
* Remove a file or directory. Alias of {@link remove()}
|
|
111
111
|
* @param {string} logicalPath - A path relative to the object content directory
|
|
112
112
|
* @param {Object} [options]
|
|
113
113
|
*/
|
|
@@ -123,14 +123,23 @@ class OcflObjectTransaction {
|
|
|
123
123
|
async reinstate(logicalPath, versionName) { throw new Error('Not Implemented'); }
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* Create a writable stream
|
|
126
|
+
* Create a NodeJS writable stream
|
|
127
127
|
* @abstract
|
|
128
128
|
* @param {string} logicalPath
|
|
129
129
|
* @param {BufferEncoding|StreamOptions} [options]
|
|
130
130
|
* @return {Promise<import('stream').Writable>}
|
|
131
|
+
* @deprecated Use {@link createWritable()} instead
|
|
131
132
|
*/
|
|
132
133
|
async createWriteStream(logicalPath, options) { throw new Error('Not Implemented'); }
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Create a writable stream
|
|
137
|
+
* @abstract
|
|
138
|
+
* @param {string} logicalPath
|
|
139
|
+
* @param {*} [options]
|
|
140
|
+
* @return {Promise<WritableStream>}
|
|
141
|
+
*/
|
|
142
|
+
async createWritable(logicalPath, options) { throw new Error('Not Implemented'); }
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
|
|
@@ -142,12 +151,10 @@ class OcflObjectTransactionImpl extends OcflObjectTransaction {
|
|
|
142
151
|
_object;
|
|
143
152
|
_inventory;
|
|
144
153
|
_committed = false;
|
|
145
|
-
_unfinished = 0;
|
|
146
|
-
|
|
147
154
|
|
|
148
155
|
/**
|
|
149
156
|
*
|
|
150
|
-
* @param {
|
|
157
|
+
* @param {OcflObject} ocflObject
|
|
151
158
|
* @param {OcflStore} ocflStore
|
|
152
159
|
* @param {OcflObjectInventoryMut} inventory
|
|
153
160
|
* @param {string} workspaceVersionPath
|
|
@@ -161,9 +168,29 @@ class OcflObjectTransactionImpl extends OcflObjectTransaction {
|
|
|
161
168
|
this._store = ocflStore;
|
|
162
169
|
this._inventory = inventory;
|
|
163
170
|
this._createdDir = createdDir;
|
|
171
|
+
/** @type {(Promise<any> & { done?: boolean})[]} */
|
|
164
172
|
this._queue = [];
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Create a new transaction instance.
|
|
177
|
+
* @param {OcflObject} ocflObject
|
|
178
|
+
* @param {OcflStore} ocflStore
|
|
179
|
+
* @param {OcflObjectInventoryMut} inventory
|
|
180
|
+
* @param {string} workspacePath
|
|
181
|
+
*/
|
|
182
|
+
static async create(ocflObject, ocflStore, inventory, workspacePath) {
|
|
183
|
+
// @todo: validate existing object
|
|
184
|
+
// check existing
|
|
185
|
+
let workspaceVersionPath = path.join(workspacePath, inventory.head);
|
|
186
|
+
if (await ocflStore.exists(workspaceVersionPath)) {
|
|
187
|
+
// workspace dir exists, abort update
|
|
188
|
+
throw new Error('Uncommitted changes detected. Object is being updated by other process or there has been a failed update attempt');
|
|
189
|
+
}
|
|
190
|
+
await ocflStore.mkdir(workspacePath);
|
|
191
|
+
return new OcflObjectTransactionImpl(ocflObject, ocflStore, inventory, workspaceVersionPath, workspacePath);
|
|
192
|
+
}
|
|
193
|
+
|
|
167
194
|
_getRealPath(logicalPath) {
|
|
168
195
|
let p = path.normalize(logicalPath);
|
|
169
196
|
if (p.startsWith('/') || p.startsWith('..')) throw new Error('logicalPath must be a relative path without `..`');
|
|
@@ -182,13 +209,13 @@ class OcflObjectTransactionImpl extends OcflObjectTransaction {
|
|
|
182
209
|
// console.log('commit');
|
|
183
210
|
// console.log(this._inventory.toString());
|
|
184
211
|
if (!this._committed) {
|
|
185
|
-
let count = this.
|
|
212
|
+
let count = this._queue.filter(p => !p.done).length;
|
|
186
213
|
if (count > 0) {
|
|
187
214
|
await this.rollback();
|
|
188
215
|
throw new Error(`Unfinished operations detected (${count}). Make sure that updater callback is async function and there are no floating promises inside.`);
|
|
189
216
|
}
|
|
190
217
|
// if there is no changes, abort commit
|
|
191
|
-
if (!this._inventory.isChanged) {
|
|
218
|
+
if (!options?.force && !this._inventory.isChanged) {
|
|
192
219
|
return await this.rollback();
|
|
193
220
|
}
|
|
194
221
|
await this._commit(options);
|
|
@@ -199,22 +226,26 @@ class OcflObjectTransactionImpl extends OcflObjectTransaction {
|
|
|
199
226
|
}
|
|
200
227
|
|
|
201
228
|
async _commit(options = {}) {
|
|
229
|
+
const inventory = this._inventory;
|
|
230
|
+
const digestAlgorithm = inventory.digestAlgorithm;
|
|
202
231
|
// set commit info
|
|
203
|
-
|
|
204
|
-
if (options.message)
|
|
205
|
-
if (options.user)
|
|
232
|
+
inventory.created = (new Date()).toISOString();
|
|
233
|
+
if (options.message) inventory.message = options.message;
|
|
234
|
+
if (options.user) inventory.user = options.user;
|
|
206
235
|
|
|
207
236
|
let workspaceVersionPath = this._workspaceVersionPath;
|
|
208
|
-
let objectVersionPath = path.join(this._object.root,
|
|
237
|
+
let objectVersionPath = path.join(this._object.root, inventory.head);
|
|
209
238
|
/* eg: /workspace/aa/bb/cc/object-1/v1/inventory.json */
|
|
210
239
|
let invPath = path.join(workspaceVersionPath, INVENTORY_NAME);
|
|
211
240
|
/* eg: /workspace/aa/bb/cc/object-1/v1/inventory.json.sha512 */
|
|
212
|
-
let invDigestPath = invPath + '.' +
|
|
241
|
+
let invDigestPath = invPath + '.' + digestAlgorithm;
|
|
213
242
|
|
|
214
243
|
// calculate digest
|
|
215
|
-
|
|
244
|
+
const inventoryContent = inventory.toString();
|
|
245
|
+
let digest = (await OcflDigest.digest(digestAlgorithm, inventoryContent)) + ' ' + INVENTORY_NAME;
|
|
246
|
+
|
|
216
247
|
// write inventory.json to workspace
|
|
217
|
-
await this._store.writeFile(invPath,
|
|
248
|
+
await this._store.writeFile(invPath, inventoryContent);
|
|
218
249
|
await this._store.writeFile(invDigestPath, digest);
|
|
219
250
|
if (workspaceVersionPath !== objectVersionPath) {
|
|
220
251
|
try {
|
|
@@ -228,11 +259,11 @@ class OcflObjectTransactionImpl extends OcflObjectTransaction {
|
|
|
228
259
|
}
|
|
229
260
|
// replace root inventory
|
|
230
261
|
/* eg: /data/aa/bb/cc/object-1/inventory.json */
|
|
231
|
-
|
|
262
|
+
const rootInvPath = path.join(this._object.root, INVENTORY_NAME);
|
|
232
263
|
/* eg: /data/aa/bb/cc/object-1/v1/inventory.json */
|
|
233
264
|
invPath = path.join(objectVersionPath, INVENTORY_NAME);
|
|
234
|
-
invDigestPath = invPath + '.' +
|
|
235
|
-
let rootInvDigestPath = rootInvPath + '.' +
|
|
265
|
+
invDigestPath = invPath + '.' + digestAlgorithm;
|
|
266
|
+
let rootInvDigestPath = rootInvPath + '.' + digestAlgorithm;
|
|
236
267
|
await this._store.copyFile(invPath, rootInvPath + '.tmp');
|
|
237
268
|
await Promise.all([
|
|
238
269
|
this._store.move(rootInvPath + '.tmp', rootInvPath),
|
|
@@ -244,88 +275,124 @@ class OcflObjectTransactionImpl extends OcflObjectTransaction {
|
|
|
244
275
|
*
|
|
245
276
|
* @param {string} logicalPath
|
|
246
277
|
* @param {Object} options
|
|
247
|
-
* @return {Promise<
|
|
278
|
+
* @return {Promise<WritableStream>}
|
|
248
279
|
*/
|
|
249
|
-
async
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
ws
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
280
|
+
async createWritable(logicalPath, options) {
|
|
281
|
+
if (this._committed) throw new Error('Transaction already commited');
|
|
282
|
+
const digestAlgo = this._inventory.digestAlgorithm;
|
|
283
|
+
const digestAlgos = [digestAlgo, ...(this._object.fixityAlgorithms || [])];
|
|
284
|
+
const hs = await OcflDigest.createStream(digestAlgos);
|
|
285
|
+
const realPath = this._getRealPath(logicalPath);
|
|
286
|
+
const writer = (await this._store.createWritable(realPath, options)).getWriter();
|
|
287
|
+
//const hs = await OcflDigest.createStream(this._inventory.digestAlgorithm);
|
|
288
|
+
const inv = this._inventory;
|
|
289
|
+
const store = this._store;
|
|
290
|
+
const ws = new WritableStream({
|
|
291
|
+
write(chunk, controller) {
|
|
292
|
+
hs.update(chunk);
|
|
293
|
+
return writer.write(chunk);
|
|
294
|
+
},
|
|
295
|
+
async close() {
|
|
296
|
+
await writer.close();
|
|
297
|
+
//const digest = hs.digest('hex');
|
|
298
|
+
const {[digestAlgo]: digest, ...fixity} = hs.digest();
|
|
299
|
+
if (inv.getContentPath(digest)) await store.remove(realPath);
|
|
300
|
+
inv.add(logicalPath, digest, fixity);
|
|
301
|
+
}
|
|
263
302
|
});
|
|
303
|
+
/** @type {Promise<any> & { done?: boolean }} */
|
|
304
|
+
const p = writer.closed;
|
|
305
|
+
p.finally(() => { p.done = true; });
|
|
306
|
+
this._queue.push(p);
|
|
307
|
+
|
|
264
308
|
return ws;
|
|
265
309
|
}
|
|
266
310
|
|
|
267
311
|
async write(logicalPath, data, options) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
312
|
+
return this._transact(async () => {
|
|
313
|
+
let realPath = this._getRealPath(logicalPath);
|
|
314
|
+
const digestAlgo = this._inventory.digestAlgorithm;
|
|
315
|
+
const digestAlgos = [digestAlgo, ...(this._object.fixityAlgorithms || [])];
|
|
316
|
+
let digest, fixity;
|
|
317
|
+
if (typeof data === 'string' || ArrayBuffer.isView(data)) {
|
|
318
|
+
// if data is not stream, check digest first
|
|
319
|
+
if (ArrayBuffer.isView(data) && !(data instanceof Uint8Array || data instanceof Uint16Array || data instanceof Uint32Array)) {
|
|
320
|
+
data = new Uint8Array(data.buffer)
|
|
321
|
+
}
|
|
322
|
+
const hashes = await OcflDigest.digest(digestAlgos, data);
|
|
323
|
+
({[digestAlgo]: digest, ...fixity} = hashes);
|
|
324
|
+
if (!this._inventory.getContentPath(digest)) {
|
|
325
|
+
// no digest yet, write the file to storage backend
|
|
326
|
+
await this._store.writeFile(realPath, data, options);
|
|
327
|
+
}
|
|
328
|
+
} else if (data instanceof ReadableStream) {
|
|
329
|
+
// save the stream first
|
|
330
|
+
const hs = await OcflDigest.createStreamThrough(digestAlgos);
|
|
331
|
+
const rs = data.pipeThrough(hs);
|
|
332
|
+
await this._store.writeFile(realPath, rs, options);
|
|
333
|
+
({[digestAlgo]: digest, ...fixity} = hs.digest());
|
|
334
|
+
// already exists, delete temp file
|
|
335
|
+
if (this._inventory.getContentPath(digest)) await this._store.remove(realPath);
|
|
336
|
+
} else {
|
|
337
|
+
throw new TypeError('Unsupported data type ', data);
|
|
277
338
|
}
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
data.pipe(hs);
|
|
282
|
-
await this._store.writeFile(realPath, hs, options);
|
|
283
|
-
digest = hs.digest();
|
|
284
|
-
// already exists, delete temp file
|
|
285
|
-
if (this._inventory.getContentPath(digest)) await this._store.remove(realPath);
|
|
286
|
-
}
|
|
287
|
-
this._inventory.add(logicalPath, digest);
|
|
288
|
-
// console.log(this._inventory.toString());
|
|
339
|
+
this._inventory.add(logicalPath, digest, fixity);
|
|
340
|
+
// console.log(this._inventory.toString());
|
|
341
|
+
});
|
|
289
342
|
}
|
|
290
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Import a single file from source. This is a helper method for {@link OcflObjectTransaction.import | import()} method.
|
|
346
|
+
* @param {*} source
|
|
347
|
+
* @param {*} target
|
|
348
|
+
*/
|
|
291
349
|
async importFile(source, target) {
|
|
292
350
|
if (!target) throw new TypeError('Target logical path must not be empty if source is a file.');
|
|
293
|
-
|
|
294
|
-
|
|
351
|
+
const realPath = this._getRealPath(target);
|
|
352
|
+
const rs = await this._store.createReadable(source);
|
|
353
|
+
const digestAlgo = this._inventory.digestAlgorithm;
|
|
354
|
+
const digestAlgos = [digestAlgo, ...(this._object.fixityAlgorithms || [])];
|
|
355
|
+
const {[digestAlgo]: digest, ...fixity} = await OcflDigest.digest(digestAlgos, rs);
|
|
295
356
|
|
|
296
357
|
if (!this._inventory.getContentPath(digest)) {
|
|
297
358
|
await this._store.copyFile(source, realPath);
|
|
298
359
|
}
|
|
299
|
-
this._inventory.add(target, digest);
|
|
360
|
+
this._inventory.add(target, digest, fixity);
|
|
300
361
|
}
|
|
301
362
|
|
|
302
363
|
async import(source, target) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
await
|
|
311
|
-
|
|
312
|
-
|
|
364
|
+
return this._transact(async () => {
|
|
365
|
+
target = target ?? path.basename(source);
|
|
366
|
+
let srcStat = await this._store.stat(source);
|
|
367
|
+
if (srcStat.isFile()) {
|
|
368
|
+
await this.importFile(source, target);
|
|
369
|
+
} else if (srcStat.isDirectory()) {
|
|
370
|
+
const files = await this._store.readdir(source);
|
|
371
|
+
await parallelize(files, async (filename) => {
|
|
372
|
+
await this.import(path.join(source, filename), path.join(target, filename));
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
});
|
|
313
376
|
}
|
|
314
377
|
|
|
315
378
|
async copy(source, target) {
|
|
316
|
-
return this.
|
|
379
|
+
return this._transact(async () => {
|
|
380
|
+
return this._inventory.copy(source, target);
|
|
381
|
+
});
|
|
317
382
|
}
|
|
318
383
|
|
|
319
384
|
async rename(source, target) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
385
|
+
return this._transact(async () => {
|
|
386
|
+
// if file already exists in the workspace, rename the actual file
|
|
387
|
+
try {
|
|
388
|
+
let realSource = this._getRealPath(source);
|
|
389
|
+
let realTarget = this._getRealPath(target);
|
|
390
|
+
await this._store.move(realSource, realTarget);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
if (error.code !== 'ENOENT') throw error;
|
|
393
|
+
}
|
|
394
|
+
return this._inventory.rename(source, target);
|
|
395
|
+
});
|
|
329
396
|
}
|
|
330
397
|
|
|
331
398
|
async reinstate(logicalPath, versionName) {
|
|
@@ -343,71 +410,39 @@ class OcflObjectTransactionImpl extends OcflObjectTransaction {
|
|
|
343
410
|
}
|
|
344
411
|
|
|
345
412
|
async remove(logicalPath, options) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
413
|
+
return this._transact(async () => {
|
|
414
|
+
if (options?.purge) {
|
|
415
|
+
// completely remove content from all versions
|
|
416
|
+
let ver = this._inventory.versionNumber.previous();
|
|
417
|
+
while (ver) {
|
|
418
|
+
this._inventory.delete(logicalPath, ver.toString());
|
|
419
|
+
ver = ver.previous();
|
|
420
|
+
};
|
|
350
421
|
}
|
|
351
|
-
|
|
422
|
+
if (this._inventory.delete(logicalPath)) {
|
|
423
|
+
return this._store.remove(this._getRealPath(logicalPath));
|
|
424
|
+
}
|
|
425
|
+
});
|
|
352
426
|
}
|
|
353
427
|
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const transactionInterface = new OcflObjectTransaction();
|
|
358
|
-
const methodWrapper = {
|
|
359
|
-
createWriteStream: async function (logicalPath, options) {
|
|
360
|
-
if (this._committed) throw new Error('Transaction already commited');
|
|
361
|
-
this._unfinished++;
|
|
362
|
-
let result = await this.createWriteStream(logicalPath, options);
|
|
363
|
-
result.on('close', () => --this._unfinished);
|
|
364
|
-
return result;
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
['write', 'import', 'copy', 'rename', 'remove', 'reinstate'].forEach(name => {
|
|
368
|
-
methodWrapper[name] = async function () {
|
|
369
|
-
if (this._committed) throw new Error('Transaction already commited');
|
|
370
|
-
this._unfinished++;
|
|
371
|
-
let p = this[name](...arguments);
|
|
372
|
-
this._queue.push(p);
|
|
373
|
-
try {
|
|
374
|
-
let result = await p;
|
|
375
|
-
this._unfinished--;
|
|
376
|
-
return result;
|
|
377
|
-
} catch (error) {
|
|
378
|
-
this._unfinished--;
|
|
379
|
-
throw error;
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
});
|
|
383
|
-
const proxyHandler = {
|
|
384
428
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
429
|
+
* A function wrapper to provide safe mutating operation that
|
|
430
|
+
* will check if a transaction is already committed and track any unfinished promises.
|
|
431
|
+
* @template T
|
|
432
|
+
* @param {() => Promise<T>} op
|
|
387
433
|
*/
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
}
|
|
434
|
+
async _transact(op) {
|
|
435
|
+
if (this._committed) throw new Error('Transaction already commited');
|
|
436
|
+
/** @type {Promise<any> & { done?: boolean }} */
|
|
437
|
+
const p = op();
|
|
438
|
+
p.finally(() => { p.done = true; });
|
|
439
|
+
this._queue.push(p);
|
|
440
|
+
return p;
|
|
397
441
|
}
|
|
398
|
-
}
|
|
399
442
|
|
|
400
|
-
/**
|
|
401
|
-
* Return any OcflObjectTransactionImpl implementation as OcflObjectTransaction.
|
|
402
|
-
* @param {OcflObjectTransactionImpl} impl
|
|
403
|
-
* @return {OcflObjectTransaction}
|
|
404
|
-
*/
|
|
405
|
-
function createTransactionProxy(impl) {
|
|
406
|
-
return new Proxy(impl, proxyHandler);
|
|
407
443
|
}
|
|
408
444
|
|
|
409
445
|
module.exports = {
|
|
410
|
-
createTransactionProxy,
|
|
411
446
|
OcflObjectTransaction,
|
|
412
447
|
OcflObjectTransactionImpl
|
|
413
448
|
};
|
package/lib/types.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
type OcflExtensionCreate = <T extends typeof import('./extension').OcflExtension>(this: T, config?: OcflExtensionConfig) => InstanceType<T>;
|
|
3
|
-
|
|
2
|
+
//type OcflExtensionCreate = <T extends typeof import('./extension').OcflExtension>(this: T, config?: OcflExtensionConfig) => InstanceType<T>;
|
|
4
3
|
|
|
5
4
|
// import { Abortable } from "events";
|
|
6
5
|
// import { ObjectEncodingOptions, OpenMode, PathLike } from "fs";
|
|
@@ -43,11 +42,113 @@ interface ReadFile {
|
|
|
43
42
|
): Promise<string>;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
export interface InventoryDigests {
|
|
46
|
+
[key: string]: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface InventoryVersion {
|
|
50
|
+
created: string;
|
|
51
|
+
message?: string;
|
|
52
|
+
state: InventoryDigests;
|
|
53
|
+
user?: {
|
|
54
|
+
name: string;
|
|
55
|
+
address?: string;
|
|
56
|
+
};
|
|
53
57
|
}
|
|
58
|
+
|
|
59
|
+
export interface Inventory {
|
|
60
|
+
/** A unique identifier for the OCFL Object. */
|
|
61
|
+
id: string;
|
|
62
|
+
/** The URI of the inventory section of the specification version matching the object conformance declaration. */
|
|
63
|
+
type: 'https://ocfl.io/1.0/spec/#inventory' | 'https://ocfl.io/1.1/spec/#inventory';
|
|
64
|
+
/**
|
|
65
|
+
* The algorithm used for calculating digests for content-addressing
|
|
66
|
+
* within the OCFL Object and for the Inventory Digest.
|
|
67
|
+
* This MUST be the algorithm used in the manifest and state blocks
|
|
68
|
+
*/
|
|
69
|
+
digestAlgorithm: 'sha256' | 'sha512';
|
|
70
|
+
/** The version directory name of the most recent version of the object. */
|
|
71
|
+
head: string;
|
|
72
|
+
manifest: InventoryDigests;
|
|
73
|
+
versions: {
|
|
74
|
+
[key: string]: InventoryVersion;
|
|
75
|
+
};
|
|
76
|
+
fixity?: {
|
|
77
|
+
[key: string]: {
|
|
78
|
+
[key: string]: string[];
|
|
79
|
+
};
|
|
80
|
+
// [key in 'md5'|'sha1'|'sha256'|'sha512'|'blake2b-512'|'blake2b-160'|'blake2b-256'|'blake2b-384'|'sha512-256'|'size'|'crc32']: {};
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The name of the designated content directory within the version directories.
|
|
84
|
+
* @default content
|
|
85
|
+
*/
|
|
86
|
+
contentDirectory?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//type Enum<T extends readonly string[]> = {[key in T[number]]: ReturnType<typeof enumeration<T>>};
|
|
90
|
+
//declare function enumeration<T extends readonly string[]>(arr: T): Enum<T>;
|
|
91
|
+
export type IDataType = string | ArrayBufferLike | Uint8Array | Uint16Array | Uint32Array;
|
|
92
|
+
|
|
93
|
+
export interface CommonHasher {
|
|
94
|
+
init: () => CommonHasher;
|
|
95
|
+
update: (data: IDataType) => CommonHasher;
|
|
96
|
+
digest: {
|
|
97
|
+
(outputType: "binary"): Uint8Array;
|
|
98
|
+
(outputType?: "hex"): string;
|
|
99
|
+
};
|
|
100
|
+
digestSize: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface MultiHasher {
|
|
104
|
+
multi: true;
|
|
105
|
+
update: (data: IDataType) => MultiHasher;
|
|
106
|
+
digest: {
|
|
107
|
+
(outputType: "binary"): { [key: string]: Uint8Array };
|
|
108
|
+
(outputType?: "hex"): { [key: string]: string };
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type OcflVersion = '1.0' | '1.1';
|
|
113
|
+
|
|
114
|
+
export type OcflExtensionConfig = { extensionName?: string;[key: string]: any };
|
|
115
|
+
type OcflStorageLayout = import('./extension').OcflStorageLayout<OcflExtensionConfig>;
|
|
116
|
+
|
|
117
|
+
/** Configuration options for an OCFL Object. */
|
|
118
|
+
export type OcflStorageConfig = {
|
|
119
|
+
/** Absolute path to the ocfl storage root. */
|
|
120
|
+
root: string;
|
|
121
|
+
/** Absolute path to storage workspace directory. */
|
|
122
|
+
workspace?: string;
|
|
123
|
+
/** A layout that identifies an arrangement of directories and OCFL objects under the storage root */
|
|
124
|
+
layout?: OcflStorageLayout | OcflExtensionConfig | string;
|
|
125
|
+
/** A default list of digest algorithm names to be added to the fixity block of objects under this storage. */
|
|
126
|
+
fixityAlgorithms?: string[];
|
|
127
|
+
/** Digest algorithm for content-addressing, must use either sha512 or sha256. Defaults to 'sha512'. */
|
|
128
|
+
digestAlgorithm?: 'sha256' | 'sha512';
|
|
129
|
+
/** Content directory name. Only applies to a newly created object. @defaultValue `"content"` */
|
|
130
|
+
contentDirectory?: string;
|
|
131
|
+
/** Ocfl version. Only applies to a newly created object. @defaultValue {@link OcflConstant.OCFL_VERSION} */
|
|
132
|
+
ocflVersion?: OcflVersion;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Configuration options for an OCFL Object. */
|
|
136
|
+
export type OcflObjectConfig = {
|
|
137
|
+
/** Absolute path to the ocfl object root. */
|
|
138
|
+
root: string;
|
|
139
|
+
/** Absolute path to object workspace directory. */
|
|
140
|
+
workspace?: string;
|
|
141
|
+
/** Identifier for the object. Only be used in a newly created object. */
|
|
142
|
+
id?: string;
|
|
143
|
+
/** Reference to existing extensions defined outside of the object, such as in the storage root. */
|
|
144
|
+
extensions?: import('./extension').OcflExtension<{}>[];
|
|
145
|
+
/** Additional digest algorithms to be calculated for each file and added to the fixity block. */
|
|
146
|
+
fixityAlgorithms?: string[];
|
|
147
|
+
/** Digest algorithm for content-addressing, must use either sha512 or sha256. Defaults to 'sha512'. */
|
|
148
|
+
digestAlgorithm?: 'sha256' | 'sha512';
|
|
149
|
+
/** Content directory name. Only applies to a newly created object. @defaultValue `"content"` */
|
|
150
|
+
contentDirectory?: string;
|
|
151
|
+
/** Ocfl version. Only applies to a newly created object. @defaultValue {@link OcflConstant.OCFL_VERSION} */
|
|
152
|
+
ocflVersion?: OcflVersion;
|
|
153
|
+
};
|
|
154
|
+
|