@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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for timestamped backup feature
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate backup prefix with timestamp
|
|
8
|
+
*
|
|
9
|
+
* @param {string} spaceName - Name of the Storacha space
|
|
10
|
+
* @returns {string} - Backup prefix including timestamp
|
|
11
|
+
*/
|
|
12
|
+
export function generateBackupPrefix(spaceName) {
|
|
13
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
14
|
+
return `${spaceName}/backup-${timestamp}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// `getBackupFilenames` and `isValidMetadata` live in `backup-metadata.js`,
|
|
18
|
+
// which imports nothing — so a browser consumer can read a backup without
|
|
19
|
+
// pulling a logging framework in behind a predicate (issue #58). Re-exported
|
|
20
|
+
// here so every existing importer keeps working.
|
|
21
|
+
export { getBackupFilenames, isValidMetadata } from "./backup-metadata.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find the latest valid backup in a space
|
|
25
|
+
*
|
|
26
|
+
* @param {Array} spaceFiles - List of files in the space
|
|
27
|
+
* @param {Object} options - Options
|
|
28
|
+
* @param {Function} options.onWarning - Warning callback
|
|
29
|
+
* @returns {Object|null} - Latest valid backup or null
|
|
30
|
+
*/
|
|
31
|
+
export function findLatestBackup(spaceFiles, options = {}) {
|
|
32
|
+
const { onWarning, verbose = false } = options;
|
|
33
|
+
const warn =
|
|
34
|
+
onWarning ||
|
|
35
|
+
(verbose
|
|
36
|
+
? (message) => {
|
|
37
|
+
logger.debug(message);
|
|
38
|
+
}
|
|
39
|
+
: () => {});
|
|
40
|
+
const filenames = spaceFiles.map((f) => f.root.toString());
|
|
41
|
+
|
|
42
|
+
// Group files by full backup path (preserving directory structure)
|
|
43
|
+
const backupGroups = new Map();
|
|
44
|
+
for (const file of filenames) {
|
|
45
|
+
// Match pattern: (optional-prefix/)backup-(timestamp)-(metadata.json|blocks.car)
|
|
46
|
+
const match = file.match(/^(.*)backup-(.*?)-(metadata\.json|blocks\.car)$/);
|
|
47
|
+
if (match) {
|
|
48
|
+
const [, prefix, timestamp, type] = match;
|
|
49
|
+
const backupKey = `${prefix}backup-${timestamp}`;
|
|
50
|
+
|
|
51
|
+
if (!backupGroups.has(backupKey)) {
|
|
52
|
+
backupGroups.set(backupKey, { files: new Set(), timestamp, prefix });
|
|
53
|
+
}
|
|
54
|
+
backupGroups.get(backupKey).files.add(type);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Find complete and valid backups
|
|
59
|
+
const completeBackups = Array.from(backupGroups.entries())
|
|
60
|
+
.filter(([backupKey, { files }]) => {
|
|
61
|
+
const isComplete = files.has("metadata.json") && files.has("blocks.car");
|
|
62
|
+
if (!isComplete) {
|
|
63
|
+
warn(`⚠️ Incomplete backup found: ${backupKey}-*`);
|
|
64
|
+
files.forEach((file) => {
|
|
65
|
+
warn(` Orphaned file: ${backupKey}-${file}`);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return isComplete;
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b[1].timestamp.localeCompare(a[1].timestamp)); // Sort descending by timestamp
|
|
71
|
+
|
|
72
|
+
if (completeBackups.length === 0) {
|
|
73
|
+
if (backupGroups.size > 0) {
|
|
74
|
+
warn(
|
|
75
|
+
"⚠️ No complete backup sets found (missing metadata or blocks files)",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const [backupKey, { timestamp }] = completeBackups[0];
|
|
82
|
+
return {
|
|
83
|
+
metadata: `${backupKey}-metadata.json`,
|
|
84
|
+
blocks: `${backupKey}-blocks.car`,
|
|
85
|
+
timestamp: timestamp,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a backup's metadata is, with no dependencies at all.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `backup-helpers.js` for one reason: that module logs, so
|
|
5
|
+
* importing a *predicate* from it pulls in the logging framework and, through
|
|
6
|
+
* it, `@libp2p/logger`. Fine in Node; measurable in a browser bundle, where
|
|
7
|
+
* `restore-cid.js` exists precisely so that reading a backup costs almost
|
|
8
|
+
* nothing (issue #58).
|
|
9
|
+
*
|
|
10
|
+
* Nothing here imports anything. That is the point, and it is worth keeping
|
|
11
|
+
* true: anything added below that needs a logger belongs in `backup-helpers.js`
|
|
12
|
+
* instead.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Names are `<prefix>-metadata.json` and `<prefix>-blocks.car`. */
|
|
16
|
+
export function getBackupFilenames(prefix) {
|
|
17
|
+
return { metadata: `${prefix}-metadata.json`, blocks: `${prefix}-blocks.car` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether this is metadata we can act on.
|
|
22
|
+
*
|
|
23
|
+
* Accepts both shapes deliberately: the pre-CAR `{ root, path }` databases and
|
|
24
|
+
* the CAR-era `{ address, manifestCID }` ones. A reader that only handles one
|
|
25
|
+
* of them checks for its own fields after this, so that "not a backup" and
|
|
26
|
+
* "a backup of the other kind" stay different answers.
|
|
27
|
+
*/
|
|
28
|
+
export function isValidMetadata(metadata) {
|
|
29
|
+
if (
|
|
30
|
+
!metadata ||
|
|
31
|
+
typeof metadata.version !== "string" ||
|
|
32
|
+
typeof metadata.timestamp !== "number" ||
|
|
33
|
+
!Array.isArray(metadata.databases)
|
|
34
|
+
) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return metadata.databases.every(
|
|
38
|
+
(db) => Boolean(db?.root && db?.path) || Boolean(db?.address && db?.manifestCID),
|
|
39
|
+
);
|
|
40
|
+
}
|
package/lib/backup.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrbitDB database backup with timestamps and error handling
|
|
3
|
+
* NOTE: This file is a placeholder/example and is incomplete
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* eslint-disable no-undef, no-unused-vars */
|
|
7
|
+
import { Readable } from "stream";
|
|
8
|
+
import { CarReader } from "@ipld/car";
|
|
9
|
+
import * as Signer from "@storacha/client/principal/ed25519";
|
|
10
|
+
import { createWritableCarReader } from "@storacha/client/utils";
|
|
11
|
+
import { logger } from "./logger.js";
|
|
12
|
+
import {
|
|
13
|
+
generateBackupPrefix,
|
|
14
|
+
getBackupFilenames,
|
|
15
|
+
isValidMetadata,
|
|
16
|
+
findLatestBackup,
|
|
17
|
+
} from "./backup-helpers.js";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_OPTIONS = {
|
|
20
|
+
verbose: false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Backup an OrbitDB database with timestamps and error handling
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} orbitdb - OrbitDB instance
|
|
27
|
+
* @param {string} databaseAddress - Database address or name
|
|
28
|
+
* @param {Object} options - Backup options
|
|
29
|
+
* @returns {Promise<Object>} - Backup result
|
|
30
|
+
*/
|
|
31
|
+
export async function backupDatabase(orbitdb, databaseAddress, options = {}) {
|
|
32
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
33
|
+
const { verbose } = config;
|
|
34
|
+
const { spaceName } = config;
|
|
35
|
+
const eventEmitter = options.eventEmitter;
|
|
36
|
+
const logDebug = (message) => {
|
|
37
|
+
if (verbose) {
|
|
38
|
+
logger.debug(message);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
logDebug("🚀 Starting OrbitDB Database Backup");
|
|
43
|
+
logDebug(`📍 Database: ${databaseAddress}`);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Open database and extract blocks
|
|
47
|
+
const database = await orbitdb.open(databaseAddress, options.dbConfig);
|
|
48
|
+
const { blocks, blockSources, manifestCID } = await extractDatabaseBlocks(
|
|
49
|
+
database,
|
|
50
|
+
{ logEntriesOnly: false },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Generate timestamped backup paths
|
|
54
|
+
const backupPrefix = generateBackupPrefix(spaceName);
|
|
55
|
+
const backupFiles = getBackupFilenames(backupPrefix);
|
|
56
|
+
|
|
57
|
+
// Create metadata
|
|
58
|
+
const metadata = {
|
|
59
|
+
version: "1.0",
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
databaseCount: 1,
|
|
62
|
+
totalEntries: blocks.size,
|
|
63
|
+
databases: [
|
|
64
|
+
{
|
|
65
|
+
root: database.address.root,
|
|
66
|
+
path: database.address.path,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Validate metadata structure
|
|
72
|
+
if (!isValidMetadata(metadata)) {
|
|
73
|
+
throw new Error("Invalid metadata structure");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Write metadata with error handling
|
|
77
|
+
try {
|
|
78
|
+
await Signer.writeFile(
|
|
79
|
+
backupFiles.metadata,
|
|
80
|
+
JSON.stringify(metadata, null, 2),
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new Error(`Failed to write backup metadata: ${error.message}`, { cause: error });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Write blocks with error handling
|
|
87
|
+
try {
|
|
88
|
+
// Create CAR storage and add blocks
|
|
89
|
+
const storage = await CARStorage({
|
|
90
|
+
path: ".",
|
|
91
|
+
name: "backup-temp",
|
|
92
|
+
autoFlush: false,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
for (const [key, value] of blocks) {
|
|
96
|
+
await storage.put(key, value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await storage.persist();
|
|
100
|
+
const reader = await CarReader.fromIterable(storage.iterator());
|
|
101
|
+
const stream = Readable.from(reader.blocks());
|
|
102
|
+
|
|
103
|
+
await stream.pipe(
|
|
104
|
+
await createWritableCarReader({
|
|
105
|
+
name: backupFiles.blocks,
|
|
106
|
+
comment: `OrbitDB backup blocks (created at ${new Date().toISOString()})`,
|
|
107
|
+
space: spaceName,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Clean up temp storage
|
|
112
|
+
await storage.close();
|
|
113
|
+
await storage.clear();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Try to clean up failed metadata
|
|
116
|
+
try {
|
|
117
|
+
await Signer.rm(backupFiles.metadata);
|
|
118
|
+
} catch (cleanupError) {
|
|
119
|
+
logDebug(
|
|
120
|
+
`Failed to clean up metadata after error: ${cleanupError.message}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Failed to write backup blocks: ${error.message}`, { cause: error });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Return success with file info
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
manifestCID,
|
|
130
|
+
databaseAddress: database.address,
|
|
131
|
+
databaseName: database.name,
|
|
132
|
+
blocksTotal: blocks.size,
|
|
133
|
+
blocksUploaded: blocks.size,
|
|
134
|
+
blockSummary: blockSources,
|
|
135
|
+
backupFiles,
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: error.message,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Restore from the latest valid backup in a space
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} orbitdb - OrbitDB instance
|
|
149
|
+
* @param {Object} options - Restore options
|
|
150
|
+
* @returns {Promise<Object>} - Restore result
|
|
151
|
+
*/
|
|
152
|
+
export async function restoreFromSpace(orbitdb, options = {}) {
|
|
153
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
154
|
+
const { verbose } = config;
|
|
155
|
+
const { spaceName } = config;
|
|
156
|
+
const logDebug = (message) => {
|
|
157
|
+
if (verbose) {
|
|
158
|
+
logger.debug(message);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
logDebug("🔄 Starting OrbitDB Restore");
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Find latest valid backup
|
|
166
|
+
const spaceFiles = await listStorachaSpaceFiles(config);
|
|
167
|
+
const latestBackup = findLatestBackup(spaceFiles, {
|
|
168
|
+
verbose,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!latestBackup) {
|
|
172
|
+
throw new Error("No valid backup found in space");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Read and validate metadata
|
|
176
|
+
let metadata;
|
|
177
|
+
try {
|
|
178
|
+
const metadataJson = await Signer.readFile(
|
|
179
|
+
`${spaceName}/${latestBackup.metadata}`,
|
|
180
|
+
);
|
|
181
|
+
metadata = JSON.parse(metadataJson);
|
|
182
|
+
if (!isValidMetadata(metadata)) {
|
|
183
|
+
throw new Error("Invalid backup metadata structure");
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw new Error(`Failed to read/validate metadata: ${error.message}`, { cause: error });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Create storage and load blocks
|
|
190
|
+
const storage = await CARStorage({
|
|
191
|
+
path: ".",
|
|
192
|
+
name: "restore-temp",
|
|
193
|
+
autoFlush: false,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// Get blocks stream
|
|
198
|
+
const carStream = await Signer.readStream(
|
|
199
|
+
`${spaceName}/${latestBackup.blocks}`,
|
|
200
|
+
);
|
|
201
|
+
const reader = await fromReadableStream(carStream);
|
|
202
|
+
|
|
203
|
+
// Load blocks with validation
|
|
204
|
+
let validBlocks = 0;
|
|
205
|
+
for await (const block of reader.blocks()) {
|
|
206
|
+
try {
|
|
207
|
+
await storage.put(block.cid.toString(), block.bytes);
|
|
208
|
+
validBlocks++;
|
|
209
|
+
} catch (blockError) {
|
|
210
|
+
logDebug(`⚠️ Failed to restore block: ${blockError.message}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (validBlocks === 0) {
|
|
215
|
+
throw new Error("No valid blocks found in backup");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Open database using metadata
|
|
219
|
+
const databaseAddress = `/orbitdb/${metadata.databases[0].root}`;
|
|
220
|
+
const database = await orbitdb.open(databaseAddress);
|
|
221
|
+
|
|
222
|
+
// Replace storage and load entries
|
|
223
|
+
await database._index.replaceStorage(storage);
|
|
224
|
+
await database.load();
|
|
225
|
+
|
|
226
|
+
const entries = await database.all();
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
success: true,
|
|
230
|
+
database,
|
|
231
|
+
databaseAddress,
|
|
232
|
+
entriesRecovered: entries.length,
|
|
233
|
+
blocksRestored: validBlocks,
|
|
234
|
+
backupUsed: latestBackup,
|
|
235
|
+
};
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw new Error(`Failed to restore blocks: ${error.message}`, { cause: error });
|
|
238
|
+
} finally {
|
|
239
|
+
// Clean up temp storage
|
|
240
|
+
await storage.close();
|
|
241
|
+
await storage.clear();
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
error: error.message,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Read a block's bytes from any Helia blockstore.
|
|
3
|
+
*
|
|
4
|
+
* `interface-blockstore` 7 — what Helia 6 and later hand out — streams a block
|
|
5
|
+
* in chunks: `blockstore.get(cid)` returns an async iterable rather than a
|
|
6
|
+
* promise of the bytes. Code written for the earlier interface still runs,
|
|
7
|
+
* `await`s the iterable, and passes an object where bytes belong; the decode
|
|
8
|
+
* after it fails far from the cause. This reads both.
|
|
9
|
+
*
|
|
10
|
+
* Imports nothing, so the light restore path can use it without pulling in a
|
|
11
|
+
* node stack.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ get: (cid: any, options?: object) => any }} blockstore
|
|
16
|
+
* @param {any} cid
|
|
17
|
+
* @param {object} [options] - passed through, e.g. `{ signal }`
|
|
18
|
+
* @returns {Promise<Uint8Array>}
|
|
19
|
+
*/
|
|
20
|
+
export async function readBlockBytes(blockstore, cid, options) {
|
|
21
|
+
const result = blockstore.get(cid, options);
|
|
22
|
+
if (result == null || typeof result[Symbol.asyncIterator] !== "function") {
|
|
23
|
+
return await result;
|
|
24
|
+
}
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let length = 0;
|
|
27
|
+
for await (const chunk of result) {
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
length += chunk.length;
|
|
30
|
+
}
|
|
31
|
+
if (chunks.length === 1) return chunks[0];
|
|
32
|
+
const bytes = new Uint8Array(length);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
for (const chunk of chunks) {
|
|
35
|
+
bytes.set(chunk, offset);
|
|
36
|
+
offset += chunk.length;
|
|
37
|
+
}
|
|
38
|
+
return bytes;
|
|
39
|
+
}
|