@harperfast/rocksdb-js 2.1.0 → 2.3.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/README.md +292 -12
- package/bin/rocksdb-js.mjs +290 -2
- package/dist/index.cjs +257 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +355 -14
- package/dist/index.d.mts +355 -14
- package/dist/index.mjs +257 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -16
package/bin/rocksdb-js.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { RocksDatabase, parseTransactionLog, versions } from '../dist/index.mjs';
|
|
3
|
+
import { RocksDatabase, backups, parseTransactionLog, versions } from '../dist/index.mjs';
|
|
4
4
|
import { existsSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
6
6
|
import { join, resolve } from 'node:path';
|
|
@@ -10,6 +10,7 @@ import repl from 'node:repl';
|
|
|
10
10
|
import { inspect, parseArgs, styleText } from 'node:util';
|
|
11
11
|
|
|
12
12
|
const COMMANDS = {
|
|
13
|
+
backups: backupsCommand,
|
|
13
14
|
clear: clearCommand,
|
|
14
15
|
columns: columnsCommand,
|
|
15
16
|
compact: compactCommand,
|
|
@@ -30,6 +31,11 @@ const COMMANDS = {
|
|
|
30
31
|
use: useCommand,
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
const BACKUP_SUBCOMMANDS = ['backup', 'restore', 'verify', 'delete', 'purge'];
|
|
35
|
+
|
|
36
|
+
// Resolved backup dir -> BackupInfo[]; warmed by list operations, used for tab completion.
|
|
37
|
+
let backupListCache = { dir: null, list: null };
|
|
38
|
+
|
|
33
39
|
const { values: argv, positionals } = parseArgs({
|
|
34
40
|
allowPositionals: true,
|
|
35
41
|
arguments: process.argv.slice(2),
|
|
@@ -88,9 +94,45 @@ function completer(line) {
|
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
if (command === 'backups') {
|
|
98
|
+
if (parts.length === 2) {
|
|
99
|
+
const arg = parts[1];
|
|
100
|
+
return [completePath(arg), arg];
|
|
101
|
+
}
|
|
102
|
+
if (parts.length === 3) {
|
|
103
|
+
const arg = parts[2];
|
|
104
|
+
const hits = BACKUP_SUBCOMMANDS.filter((s) => s.startsWith(arg)).map((s) => `${s} `);
|
|
105
|
+
return [hits.length ? hits : BACKUP_SUBCOMMANDS.map((s) => `${s} `), arg];
|
|
106
|
+
}
|
|
107
|
+
if (parts.length === 4 && backupListCache.dir === resolve(parts[1]) && backupListCache.list) {
|
|
108
|
+
const subcommand = parts[2];
|
|
109
|
+
const arg = parts[3];
|
|
110
|
+
let candidates = [];
|
|
111
|
+
if (['restore', 'verify', 'delete'].includes(subcommand)) {
|
|
112
|
+
candidates = backupListCache.list.map((b) => String(b.backupId));
|
|
113
|
+
} else if (subcommand === 'purge') {
|
|
114
|
+
candidates = backupListCache.list.map((_, i) => String(i + 1));
|
|
115
|
+
}
|
|
116
|
+
return [candidates.filter((c) => c.startsWith(arg)).map((c) => `${c} `), arg];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
91
120
|
return [[], line];
|
|
92
121
|
}
|
|
93
122
|
|
|
123
|
+
function completePath(partial) {
|
|
124
|
+
const sep = partial.lastIndexOf('/');
|
|
125
|
+
const dir = sep === -1 ? '.' : partial.slice(0, sep + 1);
|
|
126
|
+
const prefix = sep === -1 ? partial : partial.slice(sep + 1);
|
|
127
|
+
try {
|
|
128
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
129
|
+
.filter((entry) => entry.name.startsWith(prefix))
|
|
130
|
+
.map((entry) => `${sep === -1 ? '' : dir}${entry.name}${entry.isDirectory() ? '/' : ' '}`);
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
94
136
|
function onSIGINT() {
|
|
95
137
|
if (ctrlC) {
|
|
96
138
|
console.log();
|
|
@@ -200,6 +242,250 @@ async function ask(prompt) {
|
|
|
200
242
|
}
|
|
201
243
|
}
|
|
202
244
|
|
|
245
|
+
function invalidateBackupListCache() {
|
|
246
|
+
backupListCache = { dir: null, list: null };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function fetchBackupList(backupDir) {
|
|
250
|
+
const list = await backups.list(backupDir);
|
|
251
|
+
backupListCache = { dir: backupDir, list };
|
|
252
|
+
return list;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function printBackupList(list) {
|
|
256
|
+
if (list.length === 0) {
|
|
257
|
+
console.log('No backups found');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
for (const backup of list) {
|
|
261
|
+
const ts = new Date(backup.timestamp * 1000).toISOString();
|
|
262
|
+
console.log(
|
|
263
|
+
`${String(backup.backupId).padStart(3)}) ${hl(ts)} ${formatBytes(backup.size)} ${backup.numberFiles} file${backup.numberFiles === 1 ? '' : 's'}`
|
|
264
|
+
);
|
|
265
|
+
if (backup.appMetadata) {
|
|
266
|
+
console.log(` ${note(`metadata: ${backup.appMetadata}`)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function listBackups(backupDir) {
|
|
272
|
+
let list;
|
|
273
|
+
try {
|
|
274
|
+
list = await fetchBackupList(backupDir);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.log(bad(`Unable to list backups: ${error.message ?? error}`));
|
|
277
|
+
console.log(note(`Check the path or create a first backup with "backups <dir> backup"`));
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
printBackupList(list);
|
|
281
|
+
return list;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function requireBackupId(backupDir, idStr, list = null) {
|
|
285
|
+
if (!list) {
|
|
286
|
+
list = await fetchBackupList(backupDir);
|
|
287
|
+
}
|
|
288
|
+
const id = Number(idStr);
|
|
289
|
+
if (!Number.isInteger(id)) {
|
|
290
|
+
console.log(`Invalid backup id: ${hl(idStr)}\n`);
|
|
291
|
+
printBackupList(list);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const backup = list.find((b) => b.backupId === id);
|
|
295
|
+
if (!backup) {
|
|
296
|
+
console.log(`Backup ${hl(id)} does not exist\n`);
|
|
297
|
+
printBackupList(list);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return { id, backup };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function dirSize(dir) {
|
|
304
|
+
let entries;
|
|
305
|
+
try {
|
|
306
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
307
|
+
} catch {
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
let total = 0;
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
const entryPath = join(dir, entry.name);
|
|
313
|
+
if (entry.isDirectory()) {
|
|
314
|
+
total += await dirSize(entryPath);
|
|
315
|
+
} else if (entry.isFile()) {
|
|
316
|
+
try {
|
|
317
|
+
total += (await stat(entryPath)).size;
|
|
318
|
+
} catch {
|
|
319
|
+
// File removed while walking; skip it.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return total;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function closeAllDatabases() {
|
|
327
|
+
for (const [name, db] of Object.entries(dbs)) {
|
|
328
|
+
db?.close();
|
|
329
|
+
delete dbs[name];
|
|
330
|
+
}
|
|
331
|
+
currentDB = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function reopenDatabases(preferredColumn) {
|
|
335
|
+
currentDB = RocksDatabase.open(dbPath, { ...defaultOpenOptions });
|
|
336
|
+
dbs[currentDB.name] = currentDB;
|
|
337
|
+
if (
|
|
338
|
+
preferredColumn &&
|
|
339
|
+
preferredColumn !== currentDB.name &&
|
|
340
|
+
currentDB.columns.includes(preferredColumn)
|
|
341
|
+
) {
|
|
342
|
+
dbs[preferredColumn] = RocksDatabase.open(dbPath, {
|
|
343
|
+
name: preferredColumn,
|
|
344
|
+
...defaultOpenOptions,
|
|
345
|
+
});
|
|
346
|
+
currentDB = dbs[preferredColumn];
|
|
347
|
+
}
|
|
348
|
+
console.log(wrapColumns(currentDB.columns));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function backupsCommand(args) {
|
|
352
|
+
const [dirArg, subcommand, extra] = args;
|
|
353
|
+
if (!dirArg) {
|
|
354
|
+
console.log(`Usage: ${hl('backups <dir> [subcommand] [arg]')}`);
|
|
355
|
+
console.log(`Subcommands: ${BACKUP_SUBCOMMANDS.map((s) => hl(s)).join(', ')}\n`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const backupDir = resolve(dirArg);
|
|
360
|
+
|
|
361
|
+
if (subcommand && !BACKUP_SUBCOMMANDS.includes(subcommand)) {
|
|
362
|
+
console.log(`Unknown subcommand: ${hl(subcommand)}`);
|
|
363
|
+
console.log(`Valid subcommands: ${BACKUP_SUBCOMMANDS.map((s) => hl(s)).join(', ')}\n`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The backup engine creates the directory when creating a backup; everything
|
|
368
|
+
// else operates on an existing backup directory.
|
|
369
|
+
if (subcommand !== 'backup' && !existsSync(backupDir)) {
|
|
370
|
+
console.log(`Backup directory ${hl(backupDir)} does not exist`);
|
|
371
|
+
console.log(note(`Create a first backup with "backups ${dirArg} backup"\n`));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!subcommand || subcommand === 'ls' || subcommand === 'list') {
|
|
376
|
+
await listBackups(backupDir);
|
|
377
|
+
console.log();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (subcommand === 'backup') {
|
|
382
|
+
const { result: id, time } = await run(() => currentDB.backup(backupDir));
|
|
383
|
+
invalidateBackupListCache();
|
|
384
|
+
console.log(`Created backup ${hl(id)} ${note(`(${time}ms)`)}\n`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (extra === undefined) {
|
|
389
|
+
const list = await listBackups(backupDir);
|
|
390
|
+
if (list?.length) {
|
|
391
|
+
const argName = subcommand === 'purge' ? '<keep-count>' : '<backup-id>';
|
|
392
|
+
console.log(`\nUsage: ${hl(`backups ${dirArg} ${subcommand} ${argName}`)}`);
|
|
393
|
+
}
|
|
394
|
+
console.log();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
switch (subcommand) {
|
|
399
|
+
case 'restore': {
|
|
400
|
+
if (argv.readonly) {
|
|
401
|
+
console.log(
|
|
402
|
+
'Restore is not available in read-only mode. Restart without --readonly to restore.\n'
|
|
403
|
+
);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (backupDir === dbPath) {
|
|
407
|
+
console.log(bad('Backup directory and database directory must be different\n'));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const found = await requireBackupId(backupDir, extra);
|
|
411
|
+
if (!found) {
|
|
412
|
+
console.log();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const answer = await ask(`Are you sure you want to restore backup ${hl(found.id)}? (y/N) `);
|
|
416
|
+
if (answer !== 'y' && answer !== 'Y') {
|
|
417
|
+
console.log();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const preferredColumn = currentDB?.name;
|
|
421
|
+
closeAllDatabases();
|
|
422
|
+
let time;
|
|
423
|
+
try {
|
|
424
|
+
({ time } = await run(() => backups.restore(backupDir, dbPath, { backupId: found.id })));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
// Don't reopen: the destructive restore may have purged the database
|
|
427
|
+
// directory and a read-write open would create a new, empty database.
|
|
428
|
+
console.error(bad(`Restore failed, the database may be incomplete: ${error}`));
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
reopenDatabases(preferredColumn);
|
|
432
|
+
console.log(`Restored backup ${hl(found.id)} ${note(`(${time}ms)`)}\n`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
case 'verify': {
|
|
437
|
+
const found = await requireBackupId(backupDir, extra);
|
|
438
|
+
if (!found) {
|
|
439
|
+
console.log();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const { time } = await run(() =>
|
|
444
|
+
backups.verify(backupDir, found.id, { verifyWithChecksum: true })
|
|
445
|
+
);
|
|
446
|
+
console.log(`Backup ${hl(found.id)} verified OK ${note(`(${time}ms)`)}\n`);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error(bad(`Verify failed: ${error.toString()}`));
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
case 'delete': {
|
|
454
|
+
const found = await requireBackupId(backupDir, extra);
|
|
455
|
+
if (!found) {
|
|
456
|
+
console.log();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const sizeBefore = await dirSize(backupDir);
|
|
460
|
+
const { time } = await run(() => backups.delete(backupDir, found.id));
|
|
461
|
+
invalidateBackupListCache();
|
|
462
|
+
const sizeAfter = await dirSize(backupDir);
|
|
463
|
+
console.log(
|
|
464
|
+
`Deleted backup ${hl(found.id)}, recovered ${hl(formatBytes(Math.max(0, sizeBefore - sizeAfter)))} ${note(`(${time}ms)`)}\n`
|
|
465
|
+
);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
case 'purge': {
|
|
470
|
+
const keepCount = Number(extra);
|
|
471
|
+
// The native layer reads keepCount as a uint32; values above that wrap.
|
|
472
|
+
if (!Number.isInteger(keepCount) || keepCount < 0 || keepCount > 0xffffffff) {
|
|
473
|
+
console.log(`Invalid keep count: ${hl(extra)}`);
|
|
474
|
+
console.log(`Usage: ${hl(`backups ${dirArg} purge <keep-count>`)}\n`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const sizeBefore = await dirSize(backupDir);
|
|
478
|
+
const { time } = await run(() => backups.purge(backupDir, keepCount));
|
|
479
|
+
invalidateBackupListCache();
|
|
480
|
+
const sizeAfter = await dirSize(backupDir);
|
|
481
|
+
console.log(
|
|
482
|
+
`Purged backups, keeping ${hl(keepCount)}, recovered ${hl(formatBytes(Math.max(0, sizeBefore - sizeAfter)))} ${note(`(${time}ms)`)}\n`
|
|
483
|
+
);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
203
489
|
async function clearCommand() {
|
|
204
490
|
const answer = await ask(`Are you sure you want to clear all data? (y/N) `);
|
|
205
491
|
if (answer !== 'y' && answer !== 'Y') {
|
|
@@ -300,6 +586,9 @@ async function propCommand(args) {
|
|
|
300
586
|
}
|
|
301
587
|
|
|
302
588
|
function helpCommand() {
|
|
589
|
+
console.log(
|
|
590
|
+
`${hl('backups <dir> [subcommand]')} Manage backups (list, backup, restore, verify, delete, purge)`
|
|
591
|
+
);
|
|
303
592
|
console.log(`${hl('clear')} Clear all data in the current column family`);
|
|
304
593
|
console.log(`${hl('columns')} List column families`);
|
|
305
594
|
console.log(`${hl('compact')} Compact the current column family`);
|
|
@@ -454,7 +743,6 @@ async function putCommand(args) {
|
|
|
454
743
|
value = value.slice(1, -1);
|
|
455
744
|
}
|
|
456
745
|
await currentDB.put(key, value);
|
|
457
|
-
console.log();
|
|
458
746
|
}
|
|
459
747
|
|
|
460
748
|
async function queryCommand(args) {
|