@le-space/orbitdb-storage-bridge 0.10.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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/components/StorachaAuth.svelte +801 -0
- package/dist/components/StorachaIntegration.svelte +965 -0
- package/dist/components/WebAuthnDIDProvider.js +562 -0
- package/dist/components/storacha-backup.js +325 -0
- package/dist/components/theme.js +79 -0
- package/lib/backends/aleph-pin.js +161 -0
- package/lib/backends/aleph.js +197 -0
- package/lib/backends/lighthouse.js +325 -0
- package/lib/backends/memory.js +140 -0
- package/lib/backends/pinata.js +330 -0
- package/lib/backends/resolve.js +72 -0
- package/lib/backends/storacha.js +178 -0
- package/lib/backends/types.js +187 -0
- package/lib/backup-car.js +1286 -0
- package/lib/backup-helpers.js +87 -0
- package/lib/backup-metadata.js +40 -0
- package/lib/backup.js +249 -0
- package/lib/block-bytes.js +39 -0
- package/lib/car-storage.js +342 -0
- package/lib/courier-sync.js +886 -0
- package/lib/dehydrate.js +146 -0
- package/lib/extract-blocks.js +258 -0
- package/lib/gateway-fetch.js +116 -0
- package/lib/ipns-helpers.js +324 -0
- package/lib/logger.js +128 -0
- package/lib/memory-courier.js +105 -0
- package/lib/orbitdb-storacha-bridge.js +2789 -0
- package/lib/pointer-ipns.js +234 -0
- package/lib/restore-cid.js +302 -0
- package/lib/ucan-bridge.js +921 -0
- package/lib/utils.js +316 -0
- package/package.json +172 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module CARStorage
|
|
3
|
+
* @description
|
|
4
|
+
* A custom storage implementation that uses CAR (Content Addressable Archive) files
|
|
5
|
+
* for persistent storage in OrbitDB. This storage can be used with ComposedStorage
|
|
6
|
+
* for hybrid memory/CAR file storage solutions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CarWriter, CarReader } from "@ipld/car";
|
|
10
|
+
import { CID } from "multiformats/cid";
|
|
11
|
+
import { createWriteStream, createReadStream } from "fs";
|
|
12
|
+
import { promises as fs } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { Readable } from "stream";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates an instance of CARStorage.
|
|
18
|
+
* @function
|
|
19
|
+
* @param {Object} options Configuration options for CAR storage
|
|
20
|
+
* @param {string} options.path The directory path where CAR files will be stored
|
|
21
|
+
* @param {string} [options.name='orbitdb-data'] The base name for CAR files
|
|
22
|
+
* @param {boolean} [options.autoFlush=true] Whether to automatically flush to CAR files
|
|
23
|
+
* @param {number} [options.flushThreshold=100] Number of operations before auto-flush
|
|
24
|
+
* @return {module:Storage} An instance of CARStorage
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { logger } from "./logger.js";
|
|
28
|
+
const CARStorage = async ({
|
|
29
|
+
path,
|
|
30
|
+
name = "orbitdb-data",
|
|
31
|
+
autoFlush = true,
|
|
32
|
+
flushThreshold = 100,
|
|
33
|
+
} = {}) => {
|
|
34
|
+
if (!path) {
|
|
35
|
+
throw new Error("CARStorage requires a path parameter");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ensure the directory exists
|
|
39
|
+
await fs.mkdir(path, { recursive: true });
|
|
40
|
+
|
|
41
|
+
// In-memory cache for fast access
|
|
42
|
+
const cache = new Map();
|
|
43
|
+
|
|
44
|
+
// Track modifications for auto-flush
|
|
45
|
+
let operationCount = 0;
|
|
46
|
+
let isModified = false;
|
|
47
|
+
|
|
48
|
+
// CAR file paths
|
|
49
|
+
const getCarPath = (suffix = "") => join(path, `${name}${suffix}.car`);
|
|
50
|
+
const primaryCarPath = getCarPath();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Loads existing data from CAR file into memory cache
|
|
54
|
+
*/
|
|
55
|
+
const loadFromCAR = async () => {
|
|
56
|
+
try {
|
|
57
|
+
const stats = await fs.stat(primaryCarPath);
|
|
58
|
+
if (stats.isFile()) {
|
|
59
|
+
logger.info(`Loading existing CAR file: ${primaryCarPath}`);
|
|
60
|
+
|
|
61
|
+
const inStream = createReadStream(primaryCarPath);
|
|
62
|
+
const reader = await CarReader.fromIterable(inStream);
|
|
63
|
+
|
|
64
|
+
const { decode } = await import("@ipld/dag-cbor");
|
|
65
|
+
|
|
66
|
+
for await (const { cid, bytes } of reader.blocks()) {
|
|
67
|
+
try {
|
|
68
|
+
// Decode the wrapped data
|
|
69
|
+
const decoded = decode(bytes);
|
|
70
|
+
if (decoded && decoded.originalKey && decoded.value) {
|
|
71
|
+
// Restore the original key-value mapping
|
|
72
|
+
cache.set(decoded.originalKey, decoded.value);
|
|
73
|
+
} else {
|
|
74
|
+
// Fallback: use CID as key and raw value
|
|
75
|
+
cache.set(cid.toString(), bytes);
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Fallback: use CID as key and raw value
|
|
79
|
+
cache.set(cid.toString(), bytes);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
logger.info(`Loaded ${cache.size} entries from CAR file`);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code !== "ENOENT") {
|
|
87
|
+
logger.warn(`Error loading CAR file: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Flushes in-memory cache to CAR file
|
|
94
|
+
*/
|
|
95
|
+
const flushToCAR = async () => {
|
|
96
|
+
if (!isModified || cache.size === 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logger.info(`Flushing ${cache.size} entries to CAR file`);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Create a temporary CAR file
|
|
104
|
+
const tempCarPath = getCarPath(".tmp");
|
|
105
|
+
const outStream = createWriteStream(tempCarPath);
|
|
106
|
+
|
|
107
|
+
// Collect all blocks for the CAR file
|
|
108
|
+
const blocks = [];
|
|
109
|
+
const roots = [];
|
|
110
|
+
const keyToCidMap = new Map();
|
|
111
|
+
|
|
112
|
+
// Import SHA-256 hasher
|
|
113
|
+
const { sha256 } = await import("multiformats/hashes/sha2");
|
|
114
|
+
const { encode } = await import("@ipld/dag-cbor");
|
|
115
|
+
|
|
116
|
+
for (const [key, value] of cache.entries()) {
|
|
117
|
+
try {
|
|
118
|
+
let cid;
|
|
119
|
+
|
|
120
|
+
if (typeof key === "string" && key.startsWith("zdpu")) {
|
|
121
|
+
// Try to convert OrbitDB hash format to CID
|
|
122
|
+
try {
|
|
123
|
+
const hash = key.slice(4); // Remove 'zdpu' prefix
|
|
124
|
+
cid = CID.parse(`bafyre${hash.slice(1)}`); // Convert to IPFS CID format
|
|
125
|
+
} catch {
|
|
126
|
+
// If conversion fails, generate a CID from the key
|
|
127
|
+
const keyData = new TextEncoder().encode(key);
|
|
128
|
+
const hash = await sha256.digest(keyData);
|
|
129
|
+
cid = CID.create(1, 0x55, hash); // version 1, raw codec, sha256 hash
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
try {
|
|
133
|
+
cid = CID.parse(key);
|
|
134
|
+
} catch {
|
|
135
|
+
// Generate a CID from the key for arbitrary strings
|
|
136
|
+
const keyData = new TextEncoder().encode(key);
|
|
137
|
+
const hash = await sha256.digest(keyData);
|
|
138
|
+
cid = CID.create(1, 0x55, hash); // version 1, raw codec, sha256 hash
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Store the mapping for later retrieval
|
|
143
|
+
keyToCidMap.set(key, cid.toString());
|
|
144
|
+
|
|
145
|
+
// Create a wrapper object that includes both key and value
|
|
146
|
+
const blockData = {
|
|
147
|
+
originalKey: key,
|
|
148
|
+
value: value,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const encodedBlock = encode(blockData);
|
|
152
|
+
blocks.push({ cid, bytes: encodedBlock });
|
|
153
|
+
|
|
154
|
+
// Add first block as root for CAR structure
|
|
155
|
+
if (roots.length === 0) {
|
|
156
|
+
roots.push(cid);
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.warn(`Error processing entry ${key}:`, error.message);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (blocks.length > 0) {
|
|
164
|
+
const { writer, out } = CarWriter.create(roots);
|
|
165
|
+
|
|
166
|
+
// Pipe to file
|
|
167
|
+
Readable.from(out).pipe(outStream);
|
|
168
|
+
|
|
169
|
+
// Add all blocks
|
|
170
|
+
for (const { cid, bytes } of blocks) {
|
|
171
|
+
await writer.put({ cid, bytes });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await writer.close();
|
|
175
|
+
|
|
176
|
+
// Wait for file write to complete
|
|
177
|
+
await new Promise((resolve, reject) => {
|
|
178
|
+
outStream.on("finish", resolve);
|
|
179
|
+
outStream.on("error", reject);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Replace old CAR file with new one
|
|
183
|
+
try {
|
|
184
|
+
await fs.unlink(primaryCarPath);
|
|
185
|
+
} catch {
|
|
186
|
+
// Ignore if file doesn't exist
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await fs.rename(tempCarPath, primaryCarPath);
|
|
190
|
+
|
|
191
|
+
isModified = false;
|
|
192
|
+
operationCount = 0;
|
|
193
|
+
|
|
194
|
+
logger.info(`Successfully flushed to CAR file: ${primaryCarPath}`);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.error(`Error flushing to CAR file: ${error.message}`);
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Auto-flush if threshold is reached
|
|
204
|
+
*/
|
|
205
|
+
const checkAutoFlush = async () => {
|
|
206
|
+
operationCount++;
|
|
207
|
+
if (autoFlush && operationCount >= flushThreshold) {
|
|
208
|
+
await flushToCAR();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Load existing data on initialization
|
|
213
|
+
await loadFromCAR();
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Puts data to the CAR storage.
|
|
217
|
+
* @function
|
|
218
|
+
* @param {string} hash The hash of the data to put.
|
|
219
|
+
* @param {Uint8Array} data The data to store.
|
|
220
|
+
*/
|
|
221
|
+
const put = async (hash, data) => {
|
|
222
|
+
if (!hash) {
|
|
223
|
+
throw new Error("Hash is required for put operation");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Store data exactly as provided - no conversion
|
|
227
|
+
cache.set(hash, data);
|
|
228
|
+
isModified = true;
|
|
229
|
+
|
|
230
|
+
await checkAutoFlush();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Gets data from the CAR storage.
|
|
235
|
+
* @function
|
|
236
|
+
* @param {string} hash The hash of the data to get.
|
|
237
|
+
* @return {Uint8Array|undefined} The stored data or undefined if not found.
|
|
238
|
+
*/
|
|
239
|
+
const get = async (hash) => {
|
|
240
|
+
// Return data exactly as stored - no conversion
|
|
241
|
+
return cache.get(hash);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Deletes data from the CAR storage.
|
|
246
|
+
* @function
|
|
247
|
+
* @param {string} hash The hash of the data to delete.
|
|
248
|
+
*/
|
|
249
|
+
const del = async (hash) => {
|
|
250
|
+
const existed = cache.delete(hash);
|
|
251
|
+
if (existed) {
|
|
252
|
+
isModified = true;
|
|
253
|
+
await checkAutoFlush();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Iterates over records in the CAR storage.
|
|
259
|
+
* @function
|
|
260
|
+
* @param {Object} options Iterator options
|
|
261
|
+
* @param {number} [options.amount=-1] Number of items to return (-1 for all)
|
|
262
|
+
* @param {boolean} [options.reverse=false] Whether to reverse the iteration order
|
|
263
|
+
* @yields {Array} [key, value] pairs
|
|
264
|
+
*/
|
|
265
|
+
const iterator = async function* ({ amount = -1, reverse = false } = {}) {
|
|
266
|
+
const entries = Array.from(cache.entries());
|
|
267
|
+
|
|
268
|
+
if (reverse) {
|
|
269
|
+
entries.reverse();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let count = 0;
|
|
273
|
+
for (const [key, value] of entries) {
|
|
274
|
+
if (amount > 0 && count >= amount) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
yield [key, value];
|
|
278
|
+
count++;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Merges data from another storage into the CAR storage.
|
|
284
|
+
* @function
|
|
285
|
+
* @param {module:Storage} other Another storage instance.
|
|
286
|
+
*/
|
|
287
|
+
const merge = async (other) => {
|
|
288
|
+
if (!other || typeof other.iterator !== "function") {
|
|
289
|
+
throw new Error("Other storage must implement iterator method");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for await (const [key, value] of other.iterator()) {
|
|
293
|
+
await put(key, value);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Clears all data from the CAR storage.
|
|
299
|
+
* @function
|
|
300
|
+
*/
|
|
301
|
+
const clear = async () => {
|
|
302
|
+
cache.clear();
|
|
303
|
+
isModified = true;
|
|
304
|
+
|
|
305
|
+
// Remove CAR file
|
|
306
|
+
try {
|
|
307
|
+
await fs.unlink(primaryCarPath);
|
|
308
|
+
} catch {
|
|
309
|
+
// Ignore if file doesn't exist
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Forces a flush to the CAR file (persist operation).
|
|
315
|
+
* @function
|
|
316
|
+
* @param {string} [hash] Optional specific hash to persist (currently ignored)
|
|
317
|
+
*/
|
|
318
|
+
const persist = async (_hash) => {
|
|
319
|
+
await flushToCAR();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Closes the CAR storage and ensures all data is persisted.
|
|
324
|
+
* @function
|
|
325
|
+
*/
|
|
326
|
+
const close = async () => {
|
|
327
|
+
await flushToCAR();
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
put,
|
|
332
|
+
get,
|
|
333
|
+
del,
|
|
334
|
+
iterator,
|
|
335
|
+
merge,
|
|
336
|
+
clear,
|
|
337
|
+
persist,
|
|
338
|
+
close,
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export default CARStorage;
|