@ocfl/ocfl 0.4.0 → 1.0.0-beta.2

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