@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/temp.js ADDED
@@ -0,0 +1,264 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const hasha = require('hasha');
4
+ const uuidv5 = require('uuid/v5');
5
+ const _ = require('lodash');
6
+ const DIGEST_ALGORITHM = 'sha512';
7
+
8
+ class OcflObject {
9
+
10
+
11
+ constructor(path) {
12
+ this.path = path;
13
+ this.ocflVersion = '1.0';
14
+ this.contentVersion = null; // No content yet
15
+ this.id = null; // Not set yet
16
+ this.DIGEST_ALGORITHM = DIGEST_ALGORITHM;
17
+
18
+ }
19
+
20
+ async writeInventories(inv, version) {
21
+ const main_inv = await fs.writeJson(path.join(this.path, 'inventory.json'), inv, { spaces: 2 });
22
+ const version_inv = await fs.writeJson(path.join(this.path, version, 'inventory.json'), inv, { spaces: 2 });
23
+ const inv_hash = await this.hash_file(path.join(this.path, 'inventory.json'))
24
+ const digest_file_v1 = await fs.writeFile(path.join(this.path, version, 'inventory.json.' + DIGEST_ALGORITHM), inv_hash + " inventory.json");
25
+ const digest_file = await fs.writeFile(path.join(this.path, 'inventory.json.' + DIGEST_ALGORITHM), inv_hash + " inventory.json");
26
+
27
+ }
28
+
29
+ // addContent is passed an id and a callback - this takes one argument, the
30
+ // directory into which content is to be written.
31
+ // This is called for both new and merged versions: the incoming version is
32
+ // written as v1 of a new object in the deposit directory, and if it's not the
33
+ // first version it's then merged with the existing most recent version
34
+
35
+ async addContent(id, writeContent) {
36
+ // Copy files into v1
37
+
38
+ const version = "v1" // Always a fresh start as we're not touching an existing repo object
39
+ const versionPath = path.join(this.path, version, "content");
40
+ await fs.ensureDir(versionPath);
41
+ await writeContent(versionPath);
42
+
43
+ // Make an inventory
44
+ const inv = await this.inventory(id, versionPath);
45
+
46
+ // Put the inventory in the root AND version dir
47
+ await this.writeInventories(inv, version)
48
+ this.contentVersion = await this.determineVersion();
49
+ this.id = id;
50
+ }
51
+
52
+ // preserving the old interface here
53
+
54
+ async importDir(id, sourceDir) {
55
+ await this.addContent(id, async (targetDir) => {
56
+ await fs.copy(sourceDir, targetDir);
57
+ })
58
+ }
59
+
60
+
61
+ async create(path) {
62
+ // Creates a blank object with a content dir but no content at <path>
63
+ if (this.path) {
64
+ throw new Error("This object has already been initialized at: " + this.path)
65
+ }
66
+ this.path = path;
67
+ const stats = await fs.stat(this.path);
68
+ if (await fs.pathExists(this.path) && stats.isDirectory()) {
69
+ const readDir = await fs.readdir(this.path);
70
+ if (readDir.length <= 0) { // empty so initialise an object here
71
+ const generateNamaste = await this.generateNamaste(this.path, this.ocflVersion);
72
+ } else {
73
+ throw new Error('can\'t initialise an object here as there are already files')
74
+ }
75
+ } else {
76
+ //else if it doesnt it dies
77
+ throw new Error('directory does not exist');
78
+ }
79
+ }
80
+
81
+ async load(path) {
82
+ // Tries to load an existing object residing at <path>
83
+ if (this.path) {
84
+ throw new Error("This object has already been initialized at: " + this.path)
85
+ }
86
+ this.path = path;
87
+ const stats = await fs.stat(this.path);
88
+ if (await fs.pathExists(this.path) && stats.isDirectory()) {
89
+
90
+ const ocflVersion = await this.isObject(this.path);
91
+ if (!ocflVersion) {
92
+ throw new Error('can\'t initialise an object here as there are already files')
93
+ }
94
+ } else {
95
+ throw new Error(path + ' does not exist or is not a directory');
96
+ }
97
+ }
98
+
99
+ getVersionString(i) {
100
+ // Make a version name as per the SHOULD in the spec v1..vn
101
+ // TODO have an option for zero padding
102
+ return "v" + i
103
+ }
104
+
105
+ async getInventory() {
106
+ const inventoryPath = path.join(this.path, "inventory.json");
107
+ if (await fs.exists(inventoryPath)) {
108
+ return await JSON.parse(fs.readFileSync(inventoryPath));
109
+ }
110
+ else {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ async determineVersion() {
116
+ const inv = await this.getInventory();
117
+ if (inv) {
118
+ return inv.head;
119
+ }
120
+ else {
121
+ return null;
122
+ }
123
+ // Here's not how to do it:
124
+ /* var version = 0;
125
+ const dirContents = await fs.readdir(this.path);
126
+ for (let f of dirContents.filter(function(d){return d.match(/^v\d+$/)})){
127
+ const v = parseInt(f.replace("v",""));
128
+ if (v > version) {
129
+ version = v;
130
+ }
131
+ }
132
+ return version;10. */
133
+ // Look at each dir that matches v\d+
134
+
135
+ }
136
+
137
+ async isObject(aPath) {
138
+ // TODO: Check if this content root with NAMASTE and returns ocfl version
139
+ // 0=ocfl_object_1.0
140
+ // looks at path and see if the content of the file is
141
+ // TODO: Make this look for a namaste file beginning with 0=ocfl_object_ and extract the version
142
+ const theFile = path.join(aPath, "0=" + this.nameVersion(this.ocflVersion));
143
+ return await fs.pathExists(theFile);
144
+ }
145
+
146
+ nameVersion(version) {
147
+ return 'ocfl_object_' + version;
148
+ }
149
+
150
+ async generateNamaste(aPath, version) {
151
+ const fileName = '0=' + this.nameVersion(version);
152
+ const thePath = path.join(aPath, fileName);
153
+ const writeFile = await fs.writeFile(thePath, this.nameVersion(version));
154
+ const contentDir = await fs.mkdir(path.join(aPath, "v1"));
155
+ }
156
+
157
+
158
+ async digest_dir(dir) {
159
+ var items = {};
160
+ const contents = await fs.readdir(dir);
161
+ items = _.flatten(await Promise.all(contents.map(async (p1) => {
162
+ const p = path.join(dir, p1);
163
+ const stats = await fs.stat(p);
164
+ if (stats.isDirectory()) {
165
+ return await this.digest_dir(p);
166
+ } else {
167
+ const h = await this.hash_file(p);
168
+ return [p, h];
169
+ }
170
+ })));
171
+ return items;
172
+ }
173
+
174
+ async hash_file(p) {
175
+ const hash = await hasha.fromFile(p, { algorithm: DIGEST_ALGORITHM })
176
+ return hash;
177
+ }
178
+
179
+
180
+ async removeEmptyDirectories(folder) {
181
+ // Remove empty directories
182
+ // Adapted (nade async) from: https://gist.github.com/jakub-g/5903dc7e4028133704a4
183
+ if (!folder) {
184
+ folder = this.path;
185
+ }
186
+ const stats = await fs.stat(folder);
187
+ var isDir = await stats.isDirectory();
188
+ if (isDir) {
189
+ var files = await fs.readdir(folder);
190
+ if (files.length > 0) {
191
+ for (let f of files) {
192
+ var fullPath = path.join(folder, f);
193
+ await this.removeEmptyDirectories(fullPath);
194
+ }
195
+ files = await fs.readdir(folder);
196
+ }
197
+ if (files.length == 0) {
198
+ const rm = await fs.rmdir(folder);
199
+ return;
200
+ }
201
+ }
202
+ }
203
+
204
+ async inventory(id, dir) {
205
+ const versionId = "v1";
206
+ const inv = {
207
+ 'id': id,
208
+ 'type': 'https://ocfl.io/1.0/spec/#inventory',
209
+ 'digestAlgorithm': DIGEST_ALGORITHM,
210
+ 'head': versionId,
211
+ 'versions': {
212
+
213
+ }
214
+ };
215
+ inv.versions[versionId] = {
216
+ "created": new Date().toISOString(),
217
+ "state": {}
218
+ }
219
+ // TODO Message and state keys in version
220
+ var hashpairs = await this.digest_dir(dir);
221
+ var versionState = inv.versions[versionId].state;
222
+ inv['manifest'] = {};
223
+ for (let i = 0; i < hashpairs.length; i += 2) {
224
+ const thisHash = hashpairs[i + 1]
225
+ const thisPath = path.relative(this.path, hashpairs[i])
226
+ const versionPath = path.relative(path.join("v1", "content"), thisPath)
227
+ if (!inv['manifest'][thisHash]) {
228
+ //Store only ONE path
229
+ inv['manifest'][thisHash] = [thisPath]
230
+ }
231
+ else {
232
+ // TODO DELETE THIS FILE FROM THE OBJECT BEFORE WE STORE IT
233
+ }
234
+
235
+ if (versionState[thisHash]) {
236
+ // Store all
237
+ versionState[thisHash].push(versionPath)
238
+ }
239
+ else {
240
+ versionState[thisHash] = [versionPath]
241
+
242
+ }
243
+
244
+ }
245
+ return inv
246
+ }
247
+
248
+ async getFilePath(filePath, version) {
249
+ const inv = await this.getInventory();
250
+ if (!version) {
251
+ version = inv.head;
252
+ }
253
+ const state = inv.versions[version].state;
254
+ for (let hash of Object.keys(state)) {
255
+ const aPath = state[hash];
256
+ const findPath = _.find(aPath, (p) => p === filePath);
257
+ if (findPath) {
258
+ return inv.manifest[hash][0];
259
+ }
260
+ }
261
+ throw new Error('cannot find file');
262
+ }
263
+
264
+ }
@@ -0,0 +1,413 @@
1
+ //@ts-check
2
+ /** Models a set of ocfl object modifying operations as a transaction that is as close as atomic as possible */
3
+ const { createReadStream } = require('fs');
4
+ const { stat, readdir } = require('fs/promises');
5
+ const path = require('path');
6
+ const { parallelize, dataSourceAsIterable } = require('./utils');
7
+ const { OcflDigest } = require('./digest');
8
+ const { INVENTORY_NAME } = require('./constants').OcflConstants;
9
+
10
+ //const { pipeline } = require('stream/promises');
11
+ //const { OcflObjectImpl, OcflObjectInventory } = require('./object');
12
+
13
+ /**
14
+ * @typedef {import('./object').OcflObjectImpl} OcflObjectImpl
15
+ * @typedef {import('./store').OcflStore} OcflStore
16
+ * @typedef {import('./inventory').OcflObjectInventoryMut} OcflObjectInventoryMut
17
+ * @typedef {import('stream').Writable} Writable
18
+ * @typedef {import('stream').StreamOptions} StreamOptions
19
+ */
20
+
21
+ /**
22
+ * A Transaction represent an atomic update on an OCFL Object.
23
+ * When a transaction is commited, all changes are saved as a new version.
24
+ * This is a wrapper class to hide all the storage specific implementation.
25
+ */
26
+ class OcflObjectTransaction {
27
+
28
+ /**
29
+ * Discard all the changes made in this transaction,
30
+ * @abstract
31
+ */
32
+ async rollback() { throw new Error('Not Implemented'); }
33
+
34
+ /**
35
+ * Commit all the changes made in this transaction to the actual OCFL Object.
36
+ * @abstract
37
+ * @param {Object} [options] - Commit options. message and user will be included in the version entry of the inventory.json
38
+ * @param {string} [options.message] - Freeform text used to record the rationale for creating the current version
39
+ * @param {Object} [options.user] - Identify the user or agent that created the current Version
40
+ * @param {string} options.user.name - Any readable name of the user, e.g., a proper name, user ID, agent ID.
41
+ * @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.
42
+ */
43
+ async commit(options) { throw new Error('Not Implemented'); }
44
+
45
+ /**
46
+ * Completely remove a file path and corresponding content from all versions of an OCFL Object.
47
+ * This is a special case that is not supported as part of regular OCFL versioning operations.
48
+ * @param {*} logicalPath
49
+ */
50
+ async purge(logicalPath) {
51
+ return await this.remove(logicalPath, { purge: true });
52
+ }
53
+
54
+
55
+ /**
56
+ * Write data to a file at `logicalPath`
57
+ * @abstract
58
+ * @param {string} logicalPath - A path relative to the object content directory
59
+ * @param {*} data - The same data as https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback
60
+ * @param {Object|string} [options] - The same options as https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback
61
+ */
62
+ async write(logicalPath, data, options) { throw new Error('Not Implemented'); }
63
+
64
+ /**
65
+ * Import content by recursively copying a file or directory from local file system to the
66
+ * OCFL Object content. For example, `copy('/home/john/data/b','data/b')` will
67
+ * copy `/home/john/data/b` from the local filesystem to the content `data/b` in the OFCL Object.
68
+ * Set target to empty string ('') to copy a directory content to the object's content root.
69
+ * If target is not specified, the base name of the source will be used.
70
+ * @abstract
71
+ * @param {string} source - A path to a file or directory in the local filesystem to be copied
72
+ * @param {string} [target] - A logical path in the OCFL Object content.
73
+ */
74
+ async import(source, target) { throw new Error('Not Implemented'); }
75
+
76
+ /**
77
+ * Duplicate the logical path `source` as a different logical path `target`, both paths will then point
78
+ * to the same content path and bitstreams.
79
+ * @abstract
80
+ * @param {string} source - The logical path to be copied
81
+ * @param {string} target - The target logical path
82
+ */
83
+ async copy(source, target) { throw new Error('Not Implemented'); }
84
+
85
+ /**
86
+ * Changes the logical path of existing content. This is similar to move operation in file systems
87
+ * This operation will NOT rename the actual file name referenced by the manifest.
88
+ * @abstract
89
+ * @param {*} source
90
+ * @param {*} target
91
+ */
92
+ async rename(source, target) { throw new Error('Not Implemented'); }
93
+
94
+ /**
95
+ * Changes the file path of existing content. This is similar to move operation in file systems
96
+ * @param {*} source
97
+ * @param {*} target
98
+ */
99
+ async move(source, target) { return this.rename(source, target); }
100
+
101
+ /**
102
+ * Remove a file or directory
103
+ * @abstract
104
+ * @param {string} logicalPath - A path relative to the object content directory
105
+ * @param {Object} [options]
106
+ */
107
+ async remove(logicalPath, options) { throw new Error('Not Implemented'); }
108
+
109
+ /**
110
+ * Remove a file or directory
111
+ * @param {string} logicalPath - A path relative to the object content directory
112
+ * @param {Object} [options]
113
+ */
114
+ async delete(logicalPath, options) { return this.remove(logicalPath, options); }
115
+
116
+ /**
117
+ * Makes content from a version earlier than the previous version available in the current version of an OCFL Object.
118
+ * @abstract
119
+ * @param {string} logicalPath
120
+ * @param {string} versionName
121
+ * @return {Promise<number>}
122
+ */
123
+ async reinstate(logicalPath, versionName) { throw new Error('Not Implemented'); }
124
+
125
+ /**
126
+ * Create a writable stream
127
+ * @abstract
128
+ * @param {string} logicalPath
129
+ * @param {BufferEncoding|StreamOptions} [options]
130
+ * @return {Promise<import('stream').Writable>}
131
+ */
132
+ async createWriteStream(logicalPath, options) { throw new Error('Not Implemented'); }
133
+
134
+ }
135
+
136
+
137
+ /**
138
+ * Base class for any transaction implementation on different storage backend
139
+ */
140
+ class OcflObjectTransactionImpl extends OcflObjectTransaction {
141
+ _contentRoot;
142
+ _object;
143
+ _inventory;
144
+ _committed = false;
145
+ _unfinished = 0;
146
+
147
+
148
+ /**
149
+ *
150
+ * @param {OcflObjectImpl} ocflObject
151
+ * @param {OcflStore} ocflStore
152
+ * @param {OcflObjectInventoryMut} inventory
153
+ * @param {string} workspaceVersionPath
154
+ * @param {string} createdDir
155
+ */
156
+ constructor(ocflObject, ocflStore, inventory, workspaceVersionPath, createdDir) {
157
+ super();
158
+ this._workspaceVersionPath = workspaceVersionPath;
159
+ this._contentRoot = path.join(workspaceVersionPath, inventory.contentDirectory);
160
+ this._object = ocflObject;
161
+ this._store = ocflStore;
162
+ this._inventory = inventory;
163
+ this._createdDir = createdDir;
164
+ this._queue = [];
165
+ }
166
+
167
+ _getRealPath(logicalPath) {
168
+ let p = path.normalize(logicalPath);
169
+ if (p.startsWith('/') || p.startsWith('..')) throw new Error('logicalPath must be a relative path without `..`');
170
+ return path.join(this._contentRoot, p);
171
+ }
172
+
173
+ async rollback() {
174
+ if (!this._committed) {
175
+ await Promise.all(this._queue);
176
+ if (this._createdDir) await this._store.remove(this._createdDir);
177
+ this._committed = true;
178
+ }
179
+ }
180
+
181
+ async commit(options) {
182
+ // console.log('commit');
183
+ // console.log(this._inventory.toString());
184
+ if (!this._committed) {
185
+ let count = this._unfinished;
186
+ if (count > 0) {
187
+ await this.rollback();
188
+ throw new Error(`Unfinished operations detected (${count}). Make sure that updater callback is async function and there are no floating promises inside.`);
189
+ }
190
+ // if there is no changes, abort commit
191
+ if (!this._inventory.isChanged) {
192
+ return await this.rollback();
193
+ }
194
+ await this._commit(options);
195
+ this._object._setInventory(this._inventory);
196
+ this._committed = true;
197
+ }
198
+
199
+ }
200
+
201
+ async _commit(options = {}) {
202
+ // set commit info
203
+ this._inventory.created = (new Date()).toISOString();
204
+ if (options.message) this._inventory.message = options.message;
205
+ if (options.user) this._inventory.user = options.user;
206
+
207
+ let workspaceVersionPath = this._workspaceVersionPath;
208
+ let objectVersionPath = path.join(this._object.root, this._inventory.head);
209
+ /* eg: /workspace/aa/bb/cc/object-1/v1/inventory.json */
210
+ let invPath = path.join(workspaceVersionPath, INVENTORY_NAME);
211
+ /* eg: /workspace/aa/bb/cc/object-1/v1/inventory.json.sha512 */
212
+ let invDigestPath = invPath + '.' + this._inventory.digestAlgorithm;
213
+
214
+ // calculate digest
215
+ let digest = this._inventory.digest() + ' ' + INVENTORY_NAME;
216
+ // write inventory.json to workspace
217
+ await this._store.writeFile(invPath, this._inventory.toString());
218
+ await this._store.writeFile(invDigestPath, digest);
219
+ if (workspaceVersionPath !== objectVersionPath) {
220
+ try {
221
+ await this._object._ensureNamaste();
222
+ await this._store.move(workspaceVersionPath, objectVersionPath);
223
+ await this._store.remove(this._createdDir);
224
+ } catch (error) {
225
+ await this.rollback();
226
+ throw error;
227
+ }
228
+ }
229
+ // replace root inventory
230
+ /* eg: /data/aa/bb/cc/object-1/inventory.json */
231
+ let rootInvPath = path.join(this._object.root, INVENTORY_NAME);
232
+ /* eg: /data/aa/bb/cc/object-1/v1/inventory.json */
233
+ invPath = path.join(objectVersionPath, INVENTORY_NAME);
234
+ invDigestPath = invPath + '.' + this._inventory.digestAlgorithm;
235
+ let rootInvDigestPath = rootInvPath + '.' + this._inventory.digestAlgorithm;
236
+ await this._store.copyFile(invPath, rootInvPath + '.tmp');
237
+ await Promise.all([
238
+ this._store.move(rootInvPath + '.tmp', rootInvPath),
239
+ this._store.copyFile(invDigestPath, rootInvDigestPath)]);
240
+
241
+ }
242
+
243
+ /**
244
+ *
245
+ * @param {string} logicalPath
246
+ * @param {Object} options
247
+ * @return {Promise<import('stream').Writable>}
248
+ */
249
+ async createWriteStream(logicalPath, options) {
250
+ let realPath = this._getRealPath(logicalPath);
251
+ let ws = await this._store.createWriteStream(realPath, options);
252
+ let hs = OcflDigest.createStream(this._inventory.digestAlgorithm);
253
+ const wwrite = ws.write;
254
+ function nwrite(chunk, encoding, cb) {
255
+ hs.update(chunk, encoding);
256
+ return wwrite.apply(ws, arguments);
257
+ };
258
+ ws.write = nwrite;
259
+ ws.on('close', async () => {
260
+ let digest = hs.digest('hex');
261
+ if (this._inventory.getContentPath(digest)) await this._store.remove(realPath);
262
+ this._inventory.add(logicalPath, digest);
263
+ });
264
+ return ws;
265
+ }
266
+
267
+ async write(logicalPath, data, options) {
268
+ let realPath = this._getRealPath(logicalPath);
269
+ let dataSrc = dataSourceAsIterable(data);
270
+ let digest;
271
+ if (Array.isArray(dataSrc)) {
272
+ // if data is not stream, check digest first
273
+ digest = await OcflDigest.digestAsync(this._inventory.digestAlgorithm, dataSrc);
274
+ if (!this._inventory.getContentPath(digest)) {
275
+ // no digest yet, write the file to storage backend
276
+ await this._store.writeFile(realPath, data, options);
277
+ }
278
+ } else {
279
+ // save the stream first
280
+ let hs = OcflDigest.createStreamThrough(this._inventory.digestAlgorithm);
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());
289
+ }
290
+
291
+ async importFile(source, target) {
292
+ if (!target) throw new TypeError('Target logical path must not be empty if source is a file.');
293
+ let realPath = this._getRealPath(target);
294
+ let digest = await OcflDigest.digestFromFile(this._inventory.digestAlgorithm, source);
295
+
296
+ if (!this._inventory.getContentPath(digest)) {
297
+ await this._store.copyFile(source, realPath);
298
+ }
299
+ this._inventory.add(target, digest);
300
+ }
301
+
302
+ async import(source, target) {
303
+ target = target ?? path.basename(source);
304
+ let srcStat = await stat(source);
305
+ if (srcStat.isFile()) {
306
+ await this.importFile(source, target);
307
+ } else if (srcStat.isDirectory()) {
308
+ const files = await readdir(source);
309
+ await parallelize(files, async (filename) => {
310
+ await this.import(path.join(source, filename), path.join(target, filename));
311
+ });
312
+ }
313
+ }
314
+
315
+ async copy(source, target) {
316
+ return this._inventory.copy(source, target);
317
+ }
318
+
319
+ async rename(source, target) {
320
+ // if file already exists in the workspace, rename the actual file
321
+ try {
322
+ let realSource = this._getRealPath(source);
323
+ let realTarget = this._getRealPath(target);
324
+ await this._store.move(realSource, realTarget);
325
+ } catch (error) {
326
+ if (error.code !== 'ENOENT') throw error;
327
+ }
328
+ return this._inventory.rename(source, target);
329
+ }
330
+
331
+ async reinstate(logicalPath, versionName) {
332
+ //let digest = this._inventory.getDigestFromLogical(logicalPath);
333
+ let count = this._inventory.reinstate(logicalPath, versionName);
334
+ // if (count) {
335
+ // // ensure no existing file with the same logical path in the workspace
336
+ // try {
337
+ // await this._store.remove(this._getRealPath(logicalPath));
338
+ // } catch (error) {
339
+ // if (error.code !== 'ENOENT') throw error;
340
+ // }
341
+ // }
342
+ return count;
343
+ }
344
+
345
+ async remove(logicalPath, options) {
346
+ if (this._inventory.delete(logicalPath)) {
347
+ try {
348
+ await this._store.remove(this._getRealPath(logicalPath));
349
+ } catch (error) {
350
+ }
351
+ }
352
+ }
353
+
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
+ /**
385
+ * @param {OcflObjectTransactionImpl} target
386
+ * @param {string} prop
387
+ */
388
+ get(target, prop) {
389
+ if (prop in transactionInterface) {
390
+ let p = target[prop];
391
+ if (typeof p === 'function') {
392
+ return methodWrapper[prop]?.bind(target) ?? p.bind(target);
393
+ } else {
394
+ return p;
395
+ }
396
+ }
397
+ }
398
+ }
399
+
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
+ }
408
+
409
+ module.exports = {
410
+ createTransactionProxy,
411
+ OcflObjectTransaction,
412
+ OcflObjectTransactionImpl
413
+ };