@ocfl/ocfl 0.4.0 → 1.0.0-beta.1

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 +83 -0
  16. package/dist/extension.d.ts.map +1 -0
  17. package/dist/extension.js +170 -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 +395 -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 +206 -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 +456 -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
package/src/object.ts ADDED
@@ -0,0 +1,473 @@
1
+ import path from 'node:path';
2
+ import { OcflConstants } from './constants.ts';
3
+ import { type ContentAlgorithm, OcflDigest } from './digest.ts';
4
+ import type { OcflExtension } from './extension.ts';
5
+ import type { FileRef } from './file.ts';
6
+ import { OcflObjectFile } from './file.ts';
7
+ import { type InventoryFile, OcflObjectInventory, type OcflObjectInventoryMut, VersionNumber } from './inventory.ts';
8
+ import type { OcflStore } from './store.ts';
9
+ import { type OcflObjectTransaction, OcflObjectTransactionImpl } from './transaction.ts';
10
+ import type { Inventory, OcflObjectConfig, OcflVersion } from './types.ts';
11
+ import { findNamasteVersion, isDirEmpty, isErrnoException, parallelize } from './utils.ts';
12
+ import { createError } from './validation.ts';
13
+
14
+ const { OCFL_VERSION, INVENTORY_NAME, NAMASTE_PREFIX_OBJECT, NAMASTE_T } = OcflConstants;
15
+
16
+ export type UpdateMode = 'MERGE' | 'REPLACE';
17
+ type LogicalPath = string;
18
+ type FileRefLogical = { logicalPath: string; version?: string | undefined };
19
+ type FileRefDigest = { digest: string; version?: string | undefined };
20
+ type FileRefContent = { contentPath: string; version?: string | undefined };
21
+ function contentDigestAlgorithm(algorithm: string | undefined): ContentAlgorithm {
22
+ const name = algorithm || 'sha512';
23
+ if (name !== 'sha256' && name !== 'sha512') {
24
+ throw new Error('Invalid digest algorithm. Must be one of `sha256` or `sha512`.');
25
+ }
26
+ return name;
27
+ }
28
+
29
+ /**
30
+ * A class representing an OCFL Object allowing to read and write the content of an OCFL Object in the underlying store.
31
+ */
32
+ class OcflObject {
33
+ #ocflVersion: OcflVersion;
34
+ #root: string;
35
+ #workspace: string | undefined;
36
+ #id: string | undefined;
37
+ #digestAlgorithm: ContentAlgorithm;
38
+ #extensions: OcflExtension[] | undefined;
39
+ #store: OcflStore;
40
+ /**
41
+ * A default list of digest algorithms to be calculated for each file in this object and added to the fixity block.
42
+ */
43
+ fixityAlgorithms: string[] | undefined;
44
+ contentDirectory: string;
45
+ _inventory: { [version: string]: OcflObjectInventory } = {};
46
+
47
+ /**
48
+ * Create a new OCFL Object
49
+ */
50
+ constructor(config: OcflObjectConfig, store: OcflStore) {
51
+ if (!store) {
52
+ throw new TypeError('[OcflObject] store is required.');
53
+ }
54
+ this.#store = store;
55
+ if (!config.root) {
56
+ throw new TypeError('[OcflObject] config.root is required.');
57
+ }
58
+ if (config.root === config.workspace) {
59
+ throw new Error('[OcflObject] config.root and config.workspace must be different.');
60
+ }
61
+ this.#root = path.resolve(config.root);
62
+ this.#ocflVersion = config.ocflVersion || (OCFL_VERSION as OcflVersion);
63
+ //this.contentVersion = null; // No content yet
64
+ this.#id = config.id;
65
+ this.#extensions = config.extensions;
66
+ //this.fixityDigest = config.fixityDigest ?? this.digestAlgorithm;
67
+ this.#digestAlgorithm = contentDigestAlgorithm(config.digestAlgorithm);
68
+ this.#workspace = config.workspace ? path.resolve(config.workspace) : undefined;
69
+ this.contentDirectory = config.contentDirectory ?? 'content';
70
+ this._inventory = {};
71
+ this.fixityAlgorithms = config.fixityAlgorithms;
72
+ }
73
+
74
+ /** the digest algorithm used to identify files within the OCFL object */
75
+ get digestAlgorithm(): ContentAlgorithm {
76
+ return this.#digestAlgorithm;
77
+ }
78
+ set digestAlgorithm(algorithm: string) {
79
+ this.#digestAlgorithm = contentDigestAlgorithm(algorithm);
80
+ }
81
+
82
+ /**
83
+ * Identifier of the OCFL Object
84
+ */
85
+ get id(): string | undefined {
86
+ return this.inventory?.id || this.#id;
87
+ }
88
+
89
+ /**
90
+ * Latest version number of the OCFL Object
91
+ */
92
+ get headVersion(): VersionNumber | undefined {
93
+ const head = this.inventory?.head;
94
+ return head ? VersionNumber.fromString(head) : undefined;
95
+ }
96
+
97
+ /**
98
+ * All available version numbers in the OCFL Object
99
+ */
100
+ get versions(): VersionNumber[] {
101
+ return Object.keys(this.inventory?.versions || {}).map((v) => VersionNumber.fromString(v));
102
+ }
103
+
104
+ /**
105
+ * The absolute path to the root of this OCFL Object
106
+ */
107
+ get root(): string {
108
+ return this.#root;
109
+ }
110
+
111
+ /**
112
+ * The absolute path to the temporary workspace of this OCFL Object
113
+ */
114
+ get workspace(): string | undefined {
115
+ return this.#workspace;
116
+ }
117
+
118
+ /** The raw OCFL version number the object adheres to , eg 1.0, 1.1) */
119
+ get ocflVersion() {
120
+ return this.#ocflVersion;
121
+ }
122
+
123
+ /**
124
+ * Create a new backend-specific update transaction
125
+ */
126
+ async #createTransaction(inventory: OcflObjectInventoryMut): Promise<OcflObjectTransaction> {
127
+ await this._validateObjectPath();
128
+ if (!this.workspace) {
129
+ await this._ensureNamaste();
130
+ }
131
+ const workspacePath = this.workspace || this.root;
132
+ return OcflObjectTransactionImpl.create(this, this.#store, inventory, workspacePath);
133
+ }
134
+
135
+ async _ensureNamaste() {
136
+ const prefix = NAMASTE_PREFIX_OBJECT;
137
+ const nv = await findNamasteVersion(this.#store, prefix, this.root);
138
+ if (nv === '') {
139
+ throw createError('E007', this.root);
140
+ }
141
+ if (nv) {
142
+ if (!this.ocflVersion) {
143
+ this.#ocflVersion = nv as OcflVersion;
144
+ }
145
+ return;
146
+ }
147
+ // namaste not found, check if object root is not empty
148
+ let files: string[] | undefined;
149
+ try {
150
+ files = await this.#store.readdir(this.root);
151
+ } catch (_error) {}
152
+ if (files && files.length > 0) {
153
+ throw new Error('Cannot create an OCFL Object in a non-empty directory');
154
+ }
155
+ //create the namaste file
156
+ const filePath = path.join(this.root, NAMASTE_T + prefix + this.ocflVersion);
157
+ await this.#store.writeFile(filePath, `${prefix + this.ocflVersion}\n`, 'utf8');
158
+ }
159
+
160
+ /**
161
+ * Read and parse the inventory.json file
162
+ * @param version - Full name of the version, eg: v1, v2, v3
163
+ */
164
+ async _readInventory(version: string = 'latest'): Promise<OcflObjectInventory | undefined> {
165
+ const invPath = version === 'latest' ? INVENTORY_NAME : path.join(version, INVENTORY_NAME);
166
+ let datastr: string;
167
+ try {
168
+ datastr = await this.readFile(invPath, 'utf8');
169
+ } catch (err) {
170
+ if (!isErrnoException(err) || err.code !== 'ENOENT') {
171
+ throw err;
172
+ }
173
+ //inventory does not exist yet
174
+ return;
175
+ }
176
+ const data = JSON.parse(datastr) as Inventory;
177
+ const [sidecar, actualDigest] = await Promise.all([
178
+ this.readFile(`${invPath}.${data.digestAlgorithm}`, 'utf8'),
179
+ OcflDigest.digest(data.digestAlgorithm, datastr),
180
+ ]);
181
+ const digest = sidecar.match(/[^\s]+/)?.[0];
182
+ if (digest !== actualDigest) {
183
+ throw createError('E060', path.join(this.root, invPath), `recorded=${digest} actual=${actualDigest}`);
184
+ }
185
+ if (data.head === undefined) {
186
+ throw createError('E036', path.join(this.root, invPath), 'missing head');
187
+ }
188
+ if (data.head === '') {
189
+ throw createError('E040', path.join(this.root, invPath), 'empty head');
190
+ }
191
+ return new OcflObjectInventory(data);
192
+ }
193
+
194
+ /**
195
+ * Set the latest version of inventory in the cache
196
+ */
197
+ _setInventory(inventory: OcflObjectInventory, version = 'latest') {
198
+ //this._inventory['latest'] = this._inventory[inventory.head] = inventory;
199
+ this._inventory[version] = inventory;
200
+ if (version === 'latest') {
201
+ this._inventory[inventory.head] = inventory;
202
+ }
203
+ }
204
+
205
+ // all the parent directories cannot be an ocfl object
206
+ async _validateObjectPath() {
207
+ var p = this.root;
208
+ while (p !== '.' && p !== '/') {
209
+ p = path.dirname(p);
210
+ const nv = await findNamasteVersion(this.#store, NAMASTE_PREFIX_OBJECT, p);
211
+ if (nv) {
212
+ throw createError('E082', this.root, `nested under the object at ${p}`);
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * If a cache exists, return the cached inventory. Otherwise read it from the storage and cache it.
219
+ * If the inventory file does not exist, eg in a newly created object, returns undefined.
220
+ * @param version - Full name of the version, eg: v1, v2, v3
221
+ */
222
+ async getInventory(version: string = 'latest'): Promise<OcflObjectInventory | undefined> {
223
+ if (!this._inventory[version]) {
224
+ const inv = await this._readInventory(version);
225
+ if (inv) {
226
+ this._setInventory(inv, version);
227
+ }
228
+ }
229
+ return this._inventory[version];
230
+ }
231
+
232
+ async _requireInventory(): Promise<OcflObjectInventory> {
233
+ const inv = await this.getInventory();
234
+ if (!inv) {
235
+ throw new Error(`OCFL Object "${this.id}" does not exist`);
236
+ }
237
+ return inv;
238
+ }
239
+
240
+ _baseInventory() {
241
+ return {
242
+ ...(this.id === undefined ? {} : { id: this.id }),
243
+ digestAlgorithm: this.digestAlgorithm || 'sha512',
244
+ type: `https://ocfl.io/${this.ocflVersion}/spec/#inventory` as Inventory['type'],
245
+ contentDirectory: this.contentDirectory || 'content',
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Update the content files or directories as one transaction and commit the changes as a new version.
251
+ * After obtaining the OcflObjectTransaction instance, make sure to either call the
252
+ * {@link OcflObjectTransaction#commit} or {@link OcflObjectTransaction#rollback} method to complete the transaction.
253
+ * If callback function `cb` is provided, all update operations can be done in the callback function
254
+ * and all changes are automatically commited at the end of the function.
255
+ */
256
+ async update(updater?: (transaction: OcflObjectTransaction) => Promise<unknown>, mode_: UpdateMode = 'MERGE'): Promise<OcflObjectTransaction | undefined> {
257
+ if (mode_ !== 'MERGE' && mode_ !== 'REPLACE') {
258
+ throw new TypeError(`Invalid mode '${mode_}'`);
259
+ }
260
+ const inv = (await this.getInventory()) ?? this._baseInventory();
261
+ const newInv = await OcflObjectInventory.newVersion(inv, mode_ === 'REPLACE');
262
+ const t = await this.#createTransaction(newInv);
263
+ if (typeof updater === 'function') {
264
+ try {
265
+ await updater(t);
266
+ } catch (error) {
267
+ // console.log(error);
268
+ await t.rollback();
269
+ throw error;
270
+ }
271
+ await t.commit();
272
+ return;
273
+ }
274
+ return t;
275
+ }
276
+
277
+ /**
278
+ * Import one or more content files or directories to the object
279
+ * in one transaction and commit the changes as a new version.
280
+ * Use this method to simplify copying all contents of a directory
281
+ * in local filesystem to the OCFL Object content directory
282
+ * @param content - The source path(s) or an array of a tuple [source, logical path]
283
+ * @param mode - Update mode. Defaults to `'MERGE'`.
284
+ */
285
+ async import(content: string | string[] | string[][], mode: UpdateMode = 'MERGE') {
286
+ const items = typeof content === 'string' ? [[content, '']] : content.map((p) => (typeof p === 'string' ? [p, ''] : p));
287
+
288
+ await this.update(async (t) => {
289
+ const res = await parallelize([...items], async ([source = '', target]) => t.import(source, target));
290
+ const errors = items.filter((_, i) => res[i] instanceof Error).map(([source]) => source);
291
+ if (errors.length) {
292
+ const msg = `Cannot add the files: ${errors.join(', ')}`;
293
+ throw new Error(msg);
294
+ }
295
+ }, mode);
296
+ }
297
+
298
+ /**
299
+ * Alias for import
300
+ * @see {@link OcflObject.import}
301
+ */
302
+ async add(sourceDir: string | string[] | string[][], mode: UpdateMode = 'MERGE') {
303
+ await this.import(sourceDir, mode);
304
+ }
305
+
306
+ /**
307
+ * Count the number of files contained in this object
308
+ */
309
+ async count(version?: string) {
310
+ const inv = await this._requireInventory();
311
+ return [...inv.files(version)].length;
312
+ }
313
+
314
+ async createReadable(filePath: string, options?: unknown): Promise<ReadableStream> {
315
+ return this.#store.createReadable(path.join(this.root, filePath), options);
316
+ }
317
+
318
+ /**
319
+ * Check if the object root path points to an existing file or non-empty directory
320
+ * in the underlying backend store.
321
+ * The existing directory may or may not be a valid OCFL Object.
322
+ */
323
+ async exists(): Promise<boolean> {
324
+ return !(await isDirEmpty(this.#store, this.root));
325
+ }
326
+
327
+ /**
328
+ * Copy all content of this Object to a directory in local filesystem
329
+ */
330
+ async export(_targetDir: string, _version: string) {
331
+ throw new Error('Not Implemented');
332
+ }
333
+
334
+ /**
335
+ * Iterate through the content files contained in the specified version.
336
+ * Returns an Iterator that contains FileRef which consists of logical path, content path, and digest, in no particular order.
337
+ * The iterator is implemented as a generator.
338
+ */
339
+ async files(version?: string): Promise<Generator<OcflObjectFile, void, unknown>> {
340
+ const inv = await this._requireInventory();
341
+ const o = this;
342
+ function* files() {
343
+ for (const f of inv.files(version)) {
344
+ yield new OcflObjectFile(o, f);
345
+ }
346
+ }
347
+ return files();
348
+ }
349
+
350
+ /**
351
+ * Return a file representation by the content path and version,
352
+ * @param version - Defaults to `'latest'`.
353
+ */
354
+ async fileByContentPath(contentPath: string, version?: string) {
355
+ const inv = await this.getInventory();
356
+ const fr = inv?.getFileByContentPath(contentPath, version);
357
+ if (fr) {
358
+ return new OcflObjectFile(this, fr);
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Return a file representation by the digest and version,
364
+ * @param version - Defaults to `'latest'`.
365
+ */
366
+ async fileByDigest(digest: string, version?: string) {
367
+ const inv = await this.getInventory();
368
+ const fr = inv?.getFileByDigest(digest, version);
369
+ if (fr) {
370
+ return new OcflObjectFile(this, fr);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Return a file representation by the logical path and version,
376
+ * @param version - Defaults to `'latest'`.
377
+ */
378
+ async fileByLogicalPath(logicalPath: string, version?: string) {
379
+ const inv = await this.getInventory();
380
+ const fr = inv?.getFile(logicalPath, version);
381
+ if (fr) {
382
+ return new OcflObjectFile(this, fr);
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Return a file representation by either the [logical path and version], digest, or content path.
388
+ * The returned file object can be used to retrieve the content of a file either as string, buffer, or stream.
389
+ * If the object inventory file is not loaded, this file object will not include the full file metadata information.
390
+ * If version is omitted, it defaults to the last version.
391
+ * @param opt - A choice of logical path (eg: 'test') and version (eg: 'v1'),
392
+ * digest of the file content, or content path (eg: 'v1/content/test')
393
+ * @param version - The object version name, eg: 'v1', 'v2'. Default to 'latest'. Only used if opt is a LogicalPath. Defaults to `'latest'`.
394
+ * @param lazy - If true, always return a file instance even if the object inventory file is not loaded.
395
+ */
396
+ getFile(opt: LogicalPath | FileRefLogical | FileRefDigest | FileRefContent, version?: string, lazy?: boolean): OcflObjectFile | undefined {
397
+ const inv = this.inventory;
398
+ const file: FileRef = typeof opt === 'string' ? { logicalPath: opt, version: version } : { ...opt };
399
+ if (inv) {
400
+ let fr: InventoryFile | undefined;
401
+ if (file.logicalPath) {
402
+ fr = inv?.getFile(file.logicalPath, file.version);
403
+ } else if (file.digest) {
404
+ fr = inv?.getFileByDigest(file.digest, file.version);
405
+ } else if (file.contentPath) {
406
+ fr = inv?.getFileByContentPath(file.contentPath, file.version);
407
+ }
408
+ if (fr) {
409
+ return new OcflObjectFile(this, fr);
410
+ }
411
+ } else if (lazy) {
412
+ return new OcflObjectFile(this, file);
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Return the latest version of the cached data from inventory.json which can be read using {@link getInventory()}
418
+ */
419
+ get inventory() {
420
+ return this._inventory.latest;
421
+ }
422
+
423
+ /**
424
+ * Reads the object inventory file and cache it in the memory.
425
+ * Optionally, preloads and reads all files metadata from the underlying storage.
426
+ */
427
+ async load({ filesMetadata }: { filesMetadata?: boolean | undefined } = {}) {
428
+ this._inventory = {}; // reset inventory cache
429
+ const inv = await this.getInventory();
430
+ //await this.loadExtensions();
431
+ if (inv && filesMetadata) {
432
+ //load file metadata from underlying storage
433
+ const metadata: OcflObjectInventory['metadata'] = {};
434
+ inv.metadata = metadata;
435
+ for await (const file of await this.#store.list(this.root, { recursive: true })) {
436
+ const contentPath = file.path;
437
+ metadata[contentPath] = {
438
+ size: file.size,
439
+ lastModified: file.lastModified.getTime(),
440
+ };
441
+ }
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Read the content of a file inside an object.
447
+ * @param relPath - The file path relative to the object root.
448
+ * @param options - Options to be passed to the underlying method
449
+ */
450
+ async readFile(relPath: string, options: BufferEncoding | { encoding: BufferEncoding }): Promise<string>;
451
+ async readFile(relPath: string, options?: unknown): Promise<Uint8Array>;
452
+ async readFile(relPath: string, options?: unknown): Promise<string | Uint8Array> {
453
+ return this.#store.readFile(path.join(this.root, relPath), options);
454
+ }
455
+
456
+ /**
457
+ * Get the file metadata such as size and modification time.
458
+ * @param relPath - The file path relative to the object root.
459
+ */
460
+ async stat(relPath: string) {
461
+ return this.#store.stat(path.join(this.root, relPath));
462
+ }
463
+
464
+ toString() {
465
+ return `OcflObject { root: ${this.root}, id: ${this.id} }`;
466
+ }
467
+
468
+ toJSON() {
469
+ return { root: this.root, id: this.id };
470
+ }
471
+ }
472
+
473
+ export { OcflObject };