@ocfl/ocfl 0.3.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.
- package/LICENSE +674 -0
- package/README.md +24 -9
- package/dist/constants.d.ts +15 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -0
- package/dist/digest.d.ts +58 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +204 -0
- package/dist/digest.js.map +1 -0
- package/dist/error.d.ts +5 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +9 -0
- package/dist/error.js.map +1 -0
- package/dist/extension.d.ts +83 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +170 -0
- package/dist/extension.js.map +1 -0
- package/dist/extensions/0002-flat-direct-storage-layout.d.ts +12 -0
- package/dist/extensions/0002-flat-direct-storage-layout.d.ts.map +1 -0
- package/dist/extensions/0002-flat-direct-storage-layout.js +34 -0
- package/dist/extensions/0002-flat-direct-storage-layout.js.map +1 -0
- package/dist/extensions/0003-hash-and-id-n-tuple-storage-layout.d.ts +15 -0
- package/dist/extensions/0003-hash-and-id-n-tuple-storage-layout.d.ts.map +1 -0
- package/dist/extensions/0003-hash-and-id-n-tuple-storage-layout.js +49 -0
- package/dist/extensions/0003-hash-and-id-n-tuple-storage-layout.js.map +1 -0
- package/dist/extensions/0004-hashed-n-tuple-storage-layout.d.ts +17 -0
- package/dist/extensions/0004-hashed-n-tuple-storage-layout.d.ts.map +1 -0
- package/dist/extensions/0004-hashed-n-tuple-storage-layout.js +54 -0
- package/dist/extensions/0004-hashed-n-tuple-storage-layout.js.map +1 -0
- package/dist/extensions/0006-flat-omit-prefix-storage-layout.d.ts +6 -0
- package/dist/extensions/0006-flat-omit-prefix-storage-layout.d.ts.map +1 -0
- package/dist/extensions/0006-flat-omit-prefix-storage-layout.js +8 -0
- package/dist/extensions/0006-flat-omit-prefix-storage-layout.js.map +1 -0
- package/dist/extensions/0007-n-tuple-omit-prefix-storage-layout.d.ts +6 -0
- package/dist/extensions/0007-n-tuple-omit-prefix-storage-layout.d.ts.map +1 -0
- package/dist/extensions/0007-n-tuple-omit-prefix-storage-layout.js +8 -0
- package/dist/extensions/0007-n-tuple-omit-prefix-storage-layout.js.map +1 -0
- package/dist/extensions/0009-digest-algorithms.d.ts +9 -0
- package/dist/extensions/0009-digest-algorithms.d.ts.map +1 -0
- package/dist/extensions/0009-digest-algorithms.js +11 -0
- package/dist/extensions/0009-digest-algorithms.js.map +1 -0
- package/dist/extensions/000N-path-direct-storage-layout.d.ts +16 -0
- package/dist/extensions/000N-path-direct-storage-layout.d.ts.map +1 -0
- package/dist/extensions/000N-path-direct-storage-layout.js +46 -0
- package/dist/extensions/000N-path-direct-storage-layout.js.map +1 -0
- package/dist/extensions/index.d.ts +23 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +8 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/file.d.ts +87 -0
- package/dist/file.d.ts.map +1 -0
- package/dist/file.js +118 -0
- package/dist/file.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +155 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +591 -0
- package/dist/inventory.js.map +1 -0
- package/dist/object.d.ts +188 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/object.js +425 -0
- package/dist/object.js.map +1 -0
- package/dist/storage.d.ts +94 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +302 -0
- package/dist/storage.js.map +1 -0
- package/dist/store.d.ts +100 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +157 -0
- package/dist/store.js.map +1 -0
- package/dist/transaction.d.ts +155 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +395 -0
- package/dist/transaction.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +85 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation.d.ts +32 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +31 -0
- package/dist/validation.js.map +1 -0
- package/package.json +31 -50
- package/src/constants.ts +16 -0
- package/src/digest.ts +258 -0
- package/src/error.ts +9 -0
- package/src/extension.ts +206 -0
- package/{lib/extensions/0002-flat-direct-storage-layout.js → src/extensions/0002-flat-direct-storage-layout.ts} +15 -22
- package/src/extensions/0003-hash-and-id-n-tuple-storage-layout.ts +56 -0
- package/src/extensions/0004-hashed-n-tuple-storage-layout.ts +59 -0
- package/src/extensions/0006-flat-omit-prefix-storage-layout.ts +9 -0
- package/src/extensions/0007-n-tuple-omit-prefix-storage-layout.ts +9 -0
- package/src/extensions/0009-digest-algorithms.ts +12 -0
- package/src/extensions/000N-path-direct-storage-layout.ts +51 -0
- package/src/extensions/index.ts +24 -0
- package/src/file.ts +161 -0
- package/src/index.ts +92 -0
- package/src/inventory.ts +634 -0
- package/src/object.ts +473 -0
- package/src/storage.ts +342 -0
- package/src/store.ts +199 -0
- package/src/transaction.ts +456 -0
- package/{lib/types.d.ts → src/types.ts} +28 -74
- package/src/utils.ts +94 -0
- package/src/validation.ts +46 -0
- package/index.js +0 -129
- package/index.mjs +0 -18
- package/lib/constants.js +0 -24
- package/lib/digest.js +0 -307
- package/lib/enum.js +0 -90
- package/lib/error.js +0 -19
- package/lib/extension.js +0 -194
- package/lib/extensions/0003-hash-and-id-n-tuple-storage-layout.js +0 -60
- package/lib/extensions/0004-hashed-n-tuple-storage-layout.js +0 -63
- package/lib/extensions/0005-mutable-head.js +0 -0
- package/lib/extensions/0006-flat-omit-prefix-storage-layout.js +0 -7
- package/lib/extensions/0007-n-tuple-omit-prefix-storage-layout.js +0 -7
- package/lib/extensions/0008-schema-registry.js +0 -0
- package/lib/extensions/0009-digest-algorithms.js +0 -18
- package/lib/extensions/000N-path-direct-storage-layout.js +0 -61
- package/lib/extensions/index.js +0 -13
- package/lib/file.js +0 -129
- package/lib/index.js +0 -13
- package/lib/inventory.js +0 -564
- package/lib/object.js +0 -451
- package/lib/storage.js +0 -338
- package/lib/store.js +0 -191
- package/lib/transaction.js +0 -448
- package/lib/utils.js +0 -123
- package/lib/validation.js +0 -174
package/src/digest.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import zlib from 'node:zlib';
|
|
3
|
+
import type { CommonHasher, IDataType, MultiHasher } from './types.ts';
|
|
4
|
+
|
|
5
|
+
const CONTENT = ['sha256', 'sha512'] as const;
|
|
6
|
+
type ContentAlgorithm = (typeof CONTENT)[number];
|
|
7
|
+
|
|
8
|
+
/** OCFL algorithm name to node:crypto name */
|
|
9
|
+
const NATIVE = {
|
|
10
|
+
md5: 'md5',
|
|
11
|
+
sha1: 'sha1',
|
|
12
|
+
sha256: 'sha256',
|
|
13
|
+
sha512: 'sha512',
|
|
14
|
+
'sha512/256': 'sha512-256',
|
|
15
|
+
'blake2b-512': 'blake2b512',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Digest = CommonHasher['digest'];
|
|
19
|
+
type MultiDigest = MultiHasher['digest'];
|
|
20
|
+
type WritableStreamHasher = WritableStream & { update(data: IDataType): void; digest: Digest };
|
|
21
|
+
type WritableStreamMultiHasher = WritableStream & { update(data: IDataType): void; digest: MultiDigest };
|
|
22
|
+
type StreamThroughHash = TransformStream & { digest: MultiDigest };
|
|
23
|
+
type StreamThroughOptions = { writableStrategy?: QueuingStrategy; readableStrategy?: QueuingStrategy };
|
|
24
|
+
|
|
25
|
+
function toBytes(data: IDataType): string | Uint8Array {
|
|
26
|
+
if (typeof data === 'string' || data instanceof Uint8Array) {
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
if (ArrayBuffer.isView(data)) {
|
|
30
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
31
|
+
}
|
|
32
|
+
return new Uint8Array(data);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface HashState {
|
|
36
|
+
update(data: string | Uint8Array): void;
|
|
37
|
+
digest(): Uint8Array;
|
|
38
|
+
hex(): string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasher(digestSize: number, start: () => HashState): CommonHasher {
|
|
42
|
+
let state = start();
|
|
43
|
+
return {
|
|
44
|
+
digestSize,
|
|
45
|
+
init() {
|
|
46
|
+
state = start();
|
|
47
|
+
return this;
|
|
48
|
+
},
|
|
49
|
+
update(data) {
|
|
50
|
+
state.update(toBytes(data));
|
|
51
|
+
return this;
|
|
52
|
+
},
|
|
53
|
+
digest: ((encoding?: 'binary' | 'hex') => (encoding === 'binary' ? state.digest() : state.hex())) as Digest,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hasherFactory: { [key: string]: () => Promise<CommonHasher> } = {};
|
|
58
|
+
const hasherSyncCache: { [key: string]: CommonHasher } = {};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Register a hash algorithm and it's factory function.
|
|
62
|
+
*/
|
|
63
|
+
function registerHasherFactory(name: string, factory: () => Promise<CommonHasher>) {
|
|
64
|
+
if (typeof factory === 'function') {
|
|
65
|
+
hasherFactory[name] = factory;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasAlgorithm(name: string) {
|
|
70
|
+
return name in hasherFactory;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const [algo, nativeName] of Object.entries(NATIVE)) {
|
|
74
|
+
const digestSize = crypto.createHash(nativeName).digest().length;
|
|
75
|
+
registerHasherFactory(algo, async () =>
|
|
76
|
+
hasher(digestSize, () => {
|
|
77
|
+
const h = crypto.createHash(nativeName);
|
|
78
|
+
let result: Buffer | undefined;
|
|
79
|
+
const final = () => {
|
|
80
|
+
result ??= h.digest();
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
return { update: (d) => h.update(d), digest: () => new Uint8Array(final()), hex: () => final().toString('hex') };
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
registerHasherFactory('crc32', async () =>
|
|
89
|
+
hasher(4, () => {
|
|
90
|
+
let crc = 0;
|
|
91
|
+
return {
|
|
92
|
+
update: (d) => {
|
|
93
|
+
crc = zlib.crc32(d, crc);
|
|
94
|
+
},
|
|
95
|
+
digest: () => {
|
|
96
|
+
const b = new Uint8Array(4);
|
|
97
|
+
new DataView(b.buffer).setUint32(0, crc);
|
|
98
|
+
return b;
|
|
99
|
+
},
|
|
100
|
+
hex: () => crc.toString(16).padStart(8, '0'),
|
|
101
|
+
};
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
registerHasherFactory('size', async () =>
|
|
106
|
+
hasher(8, () => {
|
|
107
|
+
let size = 0;
|
|
108
|
+
return {
|
|
109
|
+
update: (d) => {
|
|
110
|
+
size += typeof d === 'string' ? Buffer.byteLength(d) : d.byteLength;
|
|
111
|
+
},
|
|
112
|
+
digest: () => new Uint8Array(new BigUint64Array([BigInt(size)]).buffer),
|
|
113
|
+
hex: () => `${size}`,
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create a hasher instance for the given algorithm.
|
|
120
|
+
*/
|
|
121
|
+
async function createHasher(algorithm: string): Promise<CommonHasher> {
|
|
122
|
+
const factory = hasherFactory[algorithm];
|
|
123
|
+
if (!factory) {
|
|
124
|
+
throw new Error(`Unsupported digest algorithm: ${algorithm}`);
|
|
125
|
+
}
|
|
126
|
+
return (await factory()).init();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a hasher instance for calculating multiple types of digest in one go.
|
|
131
|
+
*/
|
|
132
|
+
async function createMultiHasher(algorithms: string | string[]): Promise<MultiHasher> {
|
|
133
|
+
const algoNames = Array.isArray(algorithms) ? (algorithms.length ? [...new Set(algorithms)] : ['sha512']) : [algorithms];
|
|
134
|
+
const hashers = await Promise.all(algoNames.map(async (name) => [name, await createHasher(name)] as const));
|
|
135
|
+
const multiHasher: MultiHasher = {
|
|
136
|
+
multi: true,
|
|
137
|
+
update(data) {
|
|
138
|
+
for (const [, hash] of hashers) {
|
|
139
|
+
hash.update(data);
|
|
140
|
+
}
|
|
141
|
+
return this;
|
|
142
|
+
},
|
|
143
|
+
digest: ((encoding?: 'binary' | 'hex') => {
|
|
144
|
+
const result: { [key: string]: string | Uint8Array } = {};
|
|
145
|
+
for (const [name, hash] of hashers) {
|
|
146
|
+
result[name] = hash.digest(encoding as 'hex');
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}) as MultiDigest,
|
|
150
|
+
};
|
|
151
|
+
return multiHasher;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createAnyHasher(algorithm: string | string[]): Promise<CommonHasher | MultiHasher> {
|
|
155
|
+
return Array.isArray(algorithm) ? createMultiHasher(algorithm) : createHasher(algorithm);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create a hash instance that implements WritableStream.
|
|
160
|
+
* @param algorithm One or more digest algorithm names
|
|
161
|
+
*/
|
|
162
|
+
async function createStream(algorithm?: string): Promise<WritableStreamHasher>;
|
|
163
|
+
async function createStream(algorithm: string[]): Promise<WritableStreamMultiHasher>;
|
|
164
|
+
async function createStream(algorithm: string | string[] = 'sha512'): Promise<WritableStreamHasher | WritableStreamMultiHasher> {
|
|
165
|
+
const hash = await createAnyHasher(algorithm);
|
|
166
|
+
const ws = new WritableStream({
|
|
167
|
+
write(chunk) {
|
|
168
|
+
hash.update(chunk);
|
|
169
|
+
},
|
|
170
|
+
}) as WritableStreamHasher & WritableStreamMultiHasher;
|
|
171
|
+
ws.update = (data) => {
|
|
172
|
+
hash.update(data);
|
|
173
|
+
};
|
|
174
|
+
ws.digest = ((encoding?: 'binary' | 'hex') => hash.digest(encoding as 'hex')) as Digest & MultiDigest;
|
|
175
|
+
return ws;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a pass through stream that pass data as it is and calculate the digest as data pass through.
|
|
180
|
+
*/
|
|
181
|
+
async function createStreamThrough(algorithm: string | string[] = 'sha512', options?: StreamThroughOptions): Promise<StreamThroughHash> {
|
|
182
|
+
const hash = await createAnyHasher(algorithm);
|
|
183
|
+
const { writableStrategy, readableStrategy } = options || {};
|
|
184
|
+
const ts = new TransformStream(
|
|
185
|
+
{
|
|
186
|
+
transform(chunk, controller) {
|
|
187
|
+
controller.enqueue(chunk);
|
|
188
|
+
hash.update(chunk);
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
writableStrategy,
|
|
192
|
+
readableStrategy,
|
|
193
|
+
) as StreamThroughHash;
|
|
194
|
+
ts.digest = ((encoding?: 'binary' | 'hex') => hash.digest(encoding as 'hex')) as MultiDigest;
|
|
195
|
+
return ts;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function digest(algorithm: string[], input: IDataType | ReadableStream): Promise<{ [key: string]: string }>;
|
|
199
|
+
async function digest(algorithm: string, input: IDataType | ReadableStream): Promise<string>;
|
|
200
|
+
async function digest(algorithm: string | string[], input: IDataType | ReadableStream): Promise<string | { [key: string]: string }> {
|
|
201
|
+
if (input instanceof ReadableStream) {
|
|
202
|
+
const hash = await createStream(algorithm as string);
|
|
203
|
+
await input.pipeTo(hash);
|
|
204
|
+
return hash.digest();
|
|
205
|
+
}
|
|
206
|
+
const hash = await createAnyHasher(algorithm);
|
|
207
|
+
return hash.update(input).digest();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function initSync(algorithm: string) {
|
|
211
|
+
if (!hasherSyncCache[algorithm]) {
|
|
212
|
+
const factory = hasherFactory[algorithm];
|
|
213
|
+
if (!factory) {
|
|
214
|
+
throw new Error(`Unsupported digest algorithm: ${algorithm}`);
|
|
215
|
+
}
|
|
216
|
+
hasherSyncCache[algorithm] = await factory();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* For this sync function to work correctly, it requires a call to initSync() to be awaited for each algorithm before calling this function.
|
|
222
|
+
*/
|
|
223
|
+
function digestSync(algorithm: string, input: IDataType): string {
|
|
224
|
+
const hasher = hasherSyncCache[algorithm];
|
|
225
|
+
if (!hasher) {
|
|
226
|
+
throw new Error(`Hasher for algorithm ${algorithm} is not initialized. Please call and await initSync('${algorithm}') before calling this function.`);
|
|
227
|
+
}
|
|
228
|
+
return hasher.init().update(input).digest();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const HEX_DIGEST_LENGTH: { [algorithm: string]: number } = {
|
|
232
|
+
md5: 32,
|
|
233
|
+
sha1: 40,
|
|
234
|
+
sha256: 64,
|
|
235
|
+
'sha512/256': 64,
|
|
236
|
+
sha512: 128,
|
|
237
|
+
'blake2b-512': 128,
|
|
238
|
+
crc32: 8,
|
|
239
|
+
size: 1,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function getHexDigestLength(algorithm: string) {
|
|
243
|
+
return HEX_DIGEST_LENGTH[algorithm] || 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const OcflDigest = {
|
|
247
|
+
CONTENT,
|
|
248
|
+
createStream,
|
|
249
|
+
createStreamThrough,
|
|
250
|
+
digest,
|
|
251
|
+
registerHasherFactory,
|
|
252
|
+
hasAlgorithm,
|
|
253
|
+
getHexDigestLength,
|
|
254
|
+
digestSync,
|
|
255
|
+
initSync,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export { type ContentAlgorithm, OcflDigest };
|
package/src/error.ts
ADDED
package/src/extension.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { NotImplementedError } from './error.ts';
|
|
2
|
+
import type { StorageLayout } from './extensions/index.ts';
|
|
3
|
+
import type { OcflExtensionConfig } from './types.ts';
|
|
4
|
+
|
|
5
|
+
/** Index by Registered extension name */
|
|
6
|
+
const extensionByName: { [name: string]: typeof OcflExtension } = {};
|
|
7
|
+
/** Index by extension class name */
|
|
8
|
+
const extensionByClassName: { [name: string]: typeof OcflExtension } = {};
|
|
9
|
+
|
|
10
|
+
const storageLayout: { [className: string]: typeof OcflStorageLayout } = {};
|
|
11
|
+
|
|
12
|
+
type ExtensionConstructor<T> = new (config?: OcflExtensionConfig) => T;
|
|
13
|
+
|
|
14
|
+
class OcflExtension<C extends object = object> {
|
|
15
|
+
static #object: { [name: string]: typeof OcflExtension } = {};
|
|
16
|
+
static #storage: { [name: string]: typeof OcflExtension } = {};
|
|
17
|
+
|
|
18
|
+
parameters: C;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get an object-level extension
|
|
22
|
+
* @param name - A registered extension name
|
|
23
|
+
*/
|
|
24
|
+
static object(name: string) {
|
|
25
|
+
return OcflExtension.#object[name];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get an storage-level extension
|
|
30
|
+
* @param name - A registered extension name
|
|
31
|
+
*/
|
|
32
|
+
static storage(name: string) {
|
|
33
|
+
return OcflExtension.#storage[name];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** List all registered extension classes */
|
|
37
|
+
static get classes(): (typeof OcflExtension)[] {
|
|
38
|
+
return Object.values(extensionByName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find a registered extension with the specified name
|
|
43
|
+
* @param name - extension class name or registered extension name
|
|
44
|
+
* @returns the extension class
|
|
45
|
+
*/
|
|
46
|
+
static class(name: string): typeof OcflExtension | undefined {
|
|
47
|
+
return extensionByName[name] || extensionByClassName[name];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register an extension
|
|
52
|
+
*/
|
|
53
|
+
// biome-ignore lint/complexity/noThisInStatic: defaults to the subclass register() is called on
|
|
54
|
+
static register(extensionClass: typeof OcflExtension = this) {
|
|
55
|
+
const name = extensionClass.NAME;
|
|
56
|
+
extensionByName[name] = extensionClass;
|
|
57
|
+
extensionByClassName[extensionClass.name] = extensionClass;
|
|
58
|
+
import('./index.ts').then((ocfl) => extensionClass.setup(ocfl));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create an instance of the extension
|
|
63
|
+
*/
|
|
64
|
+
static create<T extends OcflExtension>(this: ExtensionConstructor<T>, config?: OcflExtensionConfig): T {
|
|
65
|
+
const name = config?.extensionName;
|
|
66
|
+
// biome-ignore lint/complexity/noThisInStatic: must be the subclass create() is called on, or it recurses
|
|
67
|
+
if (!name || (this as unknown as typeof OcflExtension).NAME === name) {
|
|
68
|
+
return new this(config);
|
|
69
|
+
}
|
|
70
|
+
return createByName(OcflExtension, 'Extension', name, config) as T;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static get forObject() {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
static get forStorage() {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
static get NAME() {
|
|
80
|
+
return '0000-example-extension';
|
|
81
|
+
}
|
|
82
|
+
static get DESCRIPTION() {
|
|
83
|
+
return 'Example extension';
|
|
84
|
+
}
|
|
85
|
+
static async setup(_ocfl: typeof import('./index.ts')): Promise<void> {}
|
|
86
|
+
|
|
87
|
+
constructor(config?: OcflExtensionConfig, defaultConfig?: C) {
|
|
88
|
+
this.parameters = typeof defaultConfig === 'object' ? Object.create(defaultConfig) : {};
|
|
89
|
+
if (typeof config === 'object' && defaultConfig) {
|
|
90
|
+
for (const k in config) {
|
|
91
|
+
if (k in defaultConfig) {
|
|
92
|
+
(this.parameters as OcflExtensionConfig)[k] = config[k];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Return all the parameters in which the value has been set to non-default value */
|
|
99
|
+
get config(): OcflExtensionConfig | undefined {
|
|
100
|
+
const paramNames = Object.keys(this.parameters);
|
|
101
|
+
if (paramNames.length === 0) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const params: OcflExtensionConfig = { extensionName: this.name };
|
|
105
|
+
for (const name of paramNames) {
|
|
106
|
+
params[name] = this.get(name);
|
|
107
|
+
}
|
|
108
|
+
return params;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get p() {
|
|
112
|
+
return this.parameters;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Extension registered name */
|
|
116
|
+
get name(): string {
|
|
117
|
+
return (this.constructor as typeof OcflExtension).NAME;
|
|
118
|
+
}
|
|
119
|
+
get description(): string {
|
|
120
|
+
return (this.constructor as typeof OcflExtension).DESCRIPTION;
|
|
121
|
+
}
|
|
122
|
+
get minOcflVersion() {
|
|
123
|
+
return '1.0';
|
|
124
|
+
}
|
|
125
|
+
get version() {
|
|
126
|
+
return '1.0';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the specified parameter value
|
|
131
|
+
* @param param - The extension parameter name
|
|
132
|
+
*/
|
|
133
|
+
// biome-ignore lint/suspicious/noExplicitAny: parameter values are extension specific
|
|
134
|
+
get(param: string): any {
|
|
135
|
+
return (this.parameters as OcflExtensionConfig)[param];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Set the specified parameter value
|
|
140
|
+
* @param param - The extension parameter name
|
|
141
|
+
* @param value - The extension parameter value
|
|
142
|
+
*/
|
|
143
|
+
set(param: string, value: unknown) {
|
|
144
|
+
if (param in this.parameters) {
|
|
145
|
+
(this.parameters as OcflExtensionConfig)[param] = value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
class OcflStorageLayout<C extends object = object> extends OcflExtension<C> {
|
|
151
|
+
static get layout(): StorageLayout {
|
|
152
|
+
return storageLayout as unknown as StorageLayout;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static class(name: string): typeof OcflStorageLayout | undefined {
|
|
156
|
+
const c = OcflExtension.class(name);
|
|
157
|
+
if (c && c.prototype instanceof OcflStorageLayout) {
|
|
158
|
+
return c as typeof OcflStorageLayout;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static get classes(): (typeof OcflStorageLayout)[] {
|
|
163
|
+
return Object.values(storageLayout);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Register a storage layout extension
|
|
168
|
+
*/
|
|
169
|
+
// biome-ignore lint/complexity/noThisInStatic: defaults to the subclass register() is called on
|
|
170
|
+
static register(extensionClass: typeof OcflStorageLayout = this) {
|
|
171
|
+
OcflExtension.register(extensionClass);
|
|
172
|
+
storageLayout[extensionClass.name] = extensionClass;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static get forStorage() {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async validate() {}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Map an object identifier to a path
|
|
183
|
+
* @param id - The identifier of the OCFL Object
|
|
184
|
+
*/
|
|
185
|
+
map(_id: string): string {
|
|
186
|
+
throw new NotImplementedError();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type ExtensionRegistry<T> = { class(name: string): { create(config?: OcflExtensionConfig): T } | undefined };
|
|
191
|
+
|
|
192
|
+
/** Create an instance of the extension registered as `name`, or throw if `registry` has none */
|
|
193
|
+
function createByName<T extends OcflExtension>(
|
|
194
|
+
registry: ExtensionRegistry<T>,
|
|
195
|
+
kind: 'Extension' | 'Layout',
|
|
196
|
+
name: string | undefined,
|
|
197
|
+
config?: OcflExtensionConfig,
|
|
198
|
+
): T {
|
|
199
|
+
const cls = name ? registry.class(name) : undefined;
|
|
200
|
+
if (!cls) {
|
|
201
|
+
throw new Error(`${kind} "${name}" is not supported.`);
|
|
202
|
+
}
|
|
203
|
+
return cls.create(config);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { createByName, OcflExtension, OcflStorageLayout };
|
|
@@ -1,35 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { OcflConstants } from '../constants.ts';
|
|
3
|
+
import { OcflStorageLayout } from '../extension.ts';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const { EXTENSIONS_DIR } = OcflConstants;
|
|
6
|
+
|
|
7
|
+
let instance: FlatDirectStorageLayout;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* OCFL object identifiers are mapped directly to directory names that are direct children of the OCFL storage root directory.
|
|
10
11
|
*/
|
|
11
|
-
// @ts-ignore
|
|
12
12
|
class FlatDirectStorageLayout extends OcflStorageLayout {
|
|
13
|
-
static get NAME() {
|
|
13
|
+
static get NAME() {
|
|
14
|
+
return '0002-flat-direct-storage-layout';
|
|
15
|
+
}
|
|
14
16
|
static get DESCRIPTION() {
|
|
15
|
-
return
|
|
16
|
-
" that are direct children of the OCFL storage root.";
|
|
17
|
+
return 'OCFL object identifiers are mapped directly to directory names' + ' that are direct children of the OCFL storage root.';
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
static create() {
|
|
20
|
-
|
|
21
|
-
return instance;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
constructor() {
|
|
25
|
-
super()
|
|
20
|
+
static create<T>(): T {
|
|
21
|
+
instance ??= new FlatDirectStorageLayout();
|
|
22
|
+
return instance as T;
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
* @param {string} id
|
|
30
|
-
* @return {string}
|
|
31
|
-
*/
|
|
32
|
-
map(id) {
|
|
25
|
+
map(id: string): string {
|
|
33
26
|
if (path.basename(id) !== id) {
|
|
34
27
|
throw new Error(`The object id <${id}> is incompatible with layout extension ${this.name} because it contains the path separator character.`);
|
|
35
28
|
} else if (id === EXTENSIONS_DIR) {
|
|
@@ -43,4 +36,4 @@ class FlatDirectStorageLayout extends OcflStorageLayout {
|
|
|
43
36
|
}
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
export { FlatDirectStorageLayout };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { OcflDigest } from '../digest.ts';
|
|
3
|
+
import { OcflStorageLayout } from '../extension.ts';
|
|
4
|
+
|
|
5
|
+
function encodeIdentifier(id: string) {
|
|
6
|
+
return encodeURIComponent(id).toLowerCase().replace(/\./g, '%2e');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const DefaultConfig = {
|
|
10
|
+
extensionName: '0003-hash-and-id-n-tuple-storage-layout',
|
|
11
|
+
digestAlgorithm: 'sha256',
|
|
12
|
+
tupleSize: 3,
|
|
13
|
+
numberOfTuples: 3,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class HashAndIdNTupleStorageLayout extends OcflStorageLayout<typeof DefaultConfig> {
|
|
17
|
+
static get NAME() {
|
|
18
|
+
return DefaultConfig.extensionName;
|
|
19
|
+
}
|
|
20
|
+
static get DESCRIPTION() {
|
|
21
|
+
return (
|
|
22
|
+
'OCFL object identifiers are hashed and encoded as lowercase hex strings.' +
|
|
23
|
+
' These digests are then divided into N n-tuple segments,' +
|
|
24
|
+
' which are used to create nested paths under the OCFL storage root.' +
|
|
25
|
+
' Finally, the OCFL object identifier is percent-encoded to create a directory name for the OCFL object root.'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
constructor(config?: Partial<typeof DefaultConfig>) {
|
|
30
|
+
super(config, DefaultConfig);
|
|
31
|
+
const c = this.parameters;
|
|
32
|
+
const digestLength = OcflDigest.getHexDigestLength(c.digestAlgorithm);
|
|
33
|
+
if (!digestLength) {
|
|
34
|
+
throw new Error('Invalid digestAlgorithm');
|
|
35
|
+
}
|
|
36
|
+
const p = c.numberOfTuples * c.tupleSize;
|
|
37
|
+
if (p > digestLength) {
|
|
38
|
+
throw new Error('Product of numberOfTuples and tupleSize is greater than the number of characters in the hex encoded digest.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
map(id: string): string {
|
|
43
|
+
const digest = OcflDigest.digestSync(this.parameters.digestAlgorithm, id);
|
|
44
|
+
const segments = [];
|
|
45
|
+
const s = this.parameters.tupleSize;
|
|
46
|
+
const n = this.parameters.numberOfTuples;
|
|
47
|
+
let i: number;
|
|
48
|
+
for (i = 0; i < n; ++i) {
|
|
49
|
+
segments.push(digest.slice(s * i, s * i + s));
|
|
50
|
+
}
|
|
51
|
+
segments.push(encodeIdentifier(id));
|
|
52
|
+
return segments.join(path.sep);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { HashAndIdNTupleStorageLayout };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { OcflDigest } from '../digest.ts';
|
|
3
|
+
import { OcflStorageLayout } from '../extension.ts';
|
|
4
|
+
|
|
5
|
+
const DefaultConfig = {
|
|
6
|
+
extensionName: '0004-hashed-n-tuple-storage-layout',
|
|
7
|
+
digestAlgorithm: 'sha256',
|
|
8
|
+
tupleSize: 3,
|
|
9
|
+
numberOfTuples: 3,
|
|
10
|
+
shortObjectRoot: false,
|
|
11
|
+
};
|
|
12
|
+
export type HashedNTupleStorageLayoutConfig = Partial<typeof DefaultConfig>;
|
|
13
|
+
|
|
14
|
+
class HashedNTupleStorageLayout extends OcflStorageLayout<typeof DefaultConfig> {
|
|
15
|
+
static get NAME() {
|
|
16
|
+
return DefaultConfig.extensionName;
|
|
17
|
+
}
|
|
18
|
+
static get DESCRIPTION() {
|
|
19
|
+
return (
|
|
20
|
+
'OCFL object identifiers are hashed and encoded as lowercase hex strings.' +
|
|
21
|
+
' These digests are then divided into N n-tuple segments,' +
|
|
22
|
+
' which are used to create nested paths under the OCFL storage root.'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
constructor(config?: HashedNTupleStorageLayoutConfig) {
|
|
27
|
+
super(config, DefaultConfig);
|
|
28
|
+
const c = this.parameters;
|
|
29
|
+
const digestLength = OcflDigest.getHexDigestLength(c.digestAlgorithm);
|
|
30
|
+
if (!digestLength) {
|
|
31
|
+
throw new Error('Invalid digestAlgorithm');
|
|
32
|
+
}
|
|
33
|
+
const p = c.numberOfTuples * c.tupleSize;
|
|
34
|
+
if (p > digestLength) {
|
|
35
|
+
throw new Error('Product of numberOfTuples and tupleSize is greater than the number of characters in the hex encoded digest.');
|
|
36
|
+
} else if (p === digestLength && c.shortObjectRoot) {
|
|
37
|
+
throw new Error('shortObjectRoot cannot be set to true');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
map(id: string): string {
|
|
42
|
+
const digest = OcflDigest.digestSync(this.parameters.digestAlgorithm, id);
|
|
43
|
+
const segments = [];
|
|
44
|
+
const s = this.parameters.tupleSize;
|
|
45
|
+
const n = this.parameters.numberOfTuples;
|
|
46
|
+
let i: number;
|
|
47
|
+
for (i = 0; i < n; ++i) {
|
|
48
|
+
segments.push(digest.slice(s * i, s * i + s));
|
|
49
|
+
}
|
|
50
|
+
if (this.parameters.shortObjectRoot) {
|
|
51
|
+
segments.push(digest.slice(s * i));
|
|
52
|
+
} else {
|
|
53
|
+
segments.push(digest);
|
|
54
|
+
}
|
|
55
|
+
return segments.join(path.sep);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { HashedNTupleStorageLayout };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OcflExtension } from '../extension.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Notes: this implementation does nothing because the 0009 extension is implemented directly in the OcflDigest class.
|
|
5
|
+
*/
|
|
6
|
+
class DigestAlgorithm extends OcflExtension {
|
|
7
|
+
static get NAME() {
|
|
8
|
+
return '0009-digest-algorithms';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { DigestAlgorithm };
|