@liorandb/core 1.0.15 → 1.0.16

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/dist/index.d.ts CHANGED
@@ -102,13 +102,7 @@ declare class LioranDB {
102
102
  applyTransaction(ops: TXOp[]): Promise<void>;
103
103
  collection<T = any>(name: string, schema?: ZodSchema<T>): Collection<T>;
104
104
  createIndex(collection: string, field: string, options?: IndexOptions): Promise<void>;
105
- /**
106
- * Compact single collection safely (WAL + TX safe)
107
- */
108
105
  compactCollection(name: string): Promise<void>;
109
- /**
110
- * Compact entire database safely
111
- */
112
106
  compactAll(): Promise<void>;
113
107
  transaction<T>(fn: (tx: DBTransactionContext) => Promise<T>): Promise<T>;
114
108
  close(): Promise<void>;
@@ -130,6 +124,14 @@ declare class LioranManager {
130
124
  private tryAcquireLock;
131
125
  db(name: string): Promise<LioranDB>;
132
126
  openDatabase(name: string): Promise<LioranDB>;
127
+ /**
128
+ * Create TAR snapshot of full DB directory
129
+ */
130
+ snapshot(snapshotPath: string): Promise<unknown>;
131
+ /**
132
+ * Restore TAR snapshot safely
133
+ */
134
+ restore(snapshotPath: string): Promise<unknown>;
133
135
  closeAll(): Promise<void>;
134
136
  close(): Promise<void>;
135
137
  private _registerShutdownHooks;
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import process2 from "process";
6
6
  // src/core/database.ts
7
7
  import path3 from "path";
8
8
  import fs3 from "fs";
9
+ import { execFile } from "child_process";
10
+ import { promisify } from "util";
9
11
 
10
12
  // src/core/collection.ts
11
13
  import { ClassicLevel as ClassicLevel3 } from "classic-level";
@@ -669,6 +671,7 @@ var Collection = class {
669
671
  };
670
672
 
671
673
  // src/core/database.ts
674
+ var exec = promisify(execFile);
672
675
  var META_FILE = "__db_meta.json";
673
676
  var META_VERSION = 1;
674
677
  var DBTransactionContext = class {
@@ -726,11 +729,7 @@ var LioranDB = class _LioranDB {
726
729
  this.saveMeta();
727
730
  return;
728
731
  }
729
- try {
730
- this.meta = JSON.parse(fs3.readFileSync(this.metaPath, "utf8"));
731
- } catch {
732
- throw new Error("Database metadata corrupted");
733
- }
732
+ this.meta = JSON.parse(fs3.readFileSync(this.metaPath, "utf8"));
734
733
  }
735
734
  saveMeta() {
736
735
  fs3.writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
@@ -758,16 +757,12 @@ var LioranDB = class _LioranDB {
758
757
  const ops = /* @__PURE__ */ new Map();
759
758
  for (const line of raw.split("\n")) {
760
759
  if (!line.trim()) continue;
761
- try {
762
- const entry = JSON.parse(line);
763
- if ("commit" in entry) committed.add(entry.tx);
764
- else if ("applied" in entry) applied.add(entry.tx);
765
- else {
766
- if (!ops.has(entry.tx)) ops.set(entry.tx, []);
767
- ops.get(entry.tx).push(entry);
768
- }
769
- } catch {
770
- break;
760
+ const entry = JSON.parse(line);
761
+ if ("commit" in entry) committed.add(entry.tx);
762
+ else if ("applied" in entry) applied.add(entry.tx);
763
+ else {
764
+ if (!ops.has(entry.tx)) ops.set(entry.tx, []);
765
+ ops.get(entry.tx).push(entry);
771
766
  }
772
767
  }
773
768
  for (const tx of committed) {
@@ -817,18 +812,47 @@ var LioranDB = class _LioranDB {
817
812
  this.meta.indexes[collection].push({ field, options });
818
813
  this.saveMeta();
819
814
  }
820
- /* ---------------------- COMPACTION ORCHESTRATOR ---------------------- */
821
- /**
822
- * Compact single collection safely (WAL + TX safe)
823
- */
815
+ /* ------------------------- SNAPSHOT ENGINE ------------------------- */
816
+ // async snapshot(snapshotPath: string) {
817
+ // await this.clearWAL();
818
+ // for (const col of this.collections.values()) {
819
+ // await col.close();
820
+ // }
821
+ // fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
822
+ // await exec("tar", [
823
+ // "-czf",
824
+ // snapshotPath,
825
+ // "-C",
826
+ // this.basePath,
827
+ // "."
828
+ // ]);
829
+ // }
830
+ // async restore(snapshotPath: string) {
831
+ // if (!fs.existsSync(snapshotPath)) {
832
+ // throw new Error("Snapshot file does not exist");
833
+ // }
834
+ // await this.close();
835
+ // const tmp = this.basePath + ".restore";
836
+ // await fs.promises.rm(tmp, { recursive: true, force: true });
837
+ // await fs.promises.mkdir(tmp, { recursive: true });
838
+ // await exec("tar", [
839
+ // "-xzf",
840
+ // snapshotPath,
841
+ // "-C",
842
+ // tmp
843
+ // ]);
844
+ // await fs.promises.rm(this.basePath, { recursive: true, force: true });
845
+ // await fs.promises.rename(tmp, this.basePath);
846
+ // this.collections.clear();
847
+ // this.loadMeta();
848
+ // await this.recoverFromWAL();
849
+ // }
850
+ /* ------------------------- COMPACTION ------------------------- */
824
851
  async compactCollection(name) {
825
- const col = this.collection(name);
826
852
  await this.clearWAL();
853
+ const col = this.collection(name);
827
854
  await col.compact();
828
855
  }
829
- /**
830
- * Compact entire database safely
831
- */
832
856
  async compactAll() {
833
857
  await this.clearWAL();
834
858
  for (const name of this.collections.keys()) {
@@ -979,6 +1003,13 @@ var DBQueue = class {
979
1003
  compactAll() {
980
1004
  return this.exec("compact:all", {});
981
1005
  }
1006
+ /* ----------------------------- SNAPSHOT API ----------------------------- */
1007
+ snapshot(path7) {
1008
+ return this.exec("snapshot", { path: path7 });
1009
+ }
1010
+ restore(path7) {
1011
+ return this.exec("restore", { path: path7 });
1012
+ }
982
1013
  /* ------------------------------ SHUTDOWN ------------------------------ */
983
1014
  async shutdown() {
984
1015
  try {
@@ -1016,7 +1047,7 @@ var IPCServer = class {
1016
1047
  try {
1017
1048
  const msg = JSON.parse(raw);
1018
1049
  await this.handleMessage(socket, msg);
1019
- } catch (err) {
1050
+ } catch {
1020
1051
  socket.write(JSON.stringify({
1021
1052
  id: null,
1022
1053
  ok: false,
@@ -1059,12 +1090,27 @@ var IPCServer = class {
1059
1090
  case "compact:db": {
1060
1091
  const { db } = args;
1061
1092
  const database = await this.manager.db(db);
1062
- await database.compact();
1093
+ await database.compactAll();
1063
1094
  result = true;
1064
1095
  break;
1065
1096
  }
1066
1097
  case "compact:all": {
1067
- await this.manager.compactAll();
1098
+ for (const db of this.manager.openDBs.values()) {
1099
+ await db.compactAll();
1100
+ }
1101
+ result = true;
1102
+ break;
1103
+ }
1104
+ /* ---------------- SNAPSHOT ---------------- */
1105
+ case "snapshot": {
1106
+ const { path: snapshotPath } = args;
1107
+ await this.manager.snapshot(snapshotPath);
1108
+ result = true;
1109
+ break;
1110
+ }
1111
+ case "restore": {
1112
+ const { path: snapshotPath } = args;
1113
+ await this.manager.restore(snapshotPath);
1068
1114
  result = true;
1069
1115
  break;
1070
1116
  }
@@ -1122,6 +1168,7 @@ var LioranManager = class {
1122
1168
  this._registerShutdownHooks();
1123
1169
  }
1124
1170
  }
1171
+ /* ---------------- LOCK MANAGEMENT ---------------- */
1125
1172
  isProcessAlive(pid) {
1126
1173
  try {
1127
1174
  process2.kill(pid, 0);
@@ -1150,6 +1197,7 @@ var LioranManager = class {
1150
1197
  return false;
1151
1198
  }
1152
1199
  }
1200
+ /* ---------------- DB OPEN ---------------- */
1153
1201
  async db(name) {
1154
1202
  if (this.mode === "client" /* CLIENT */) {
1155
1203
  await dbQueue.exec("db", { db: name });
@@ -1168,6 +1216,51 @@ var LioranManager = class {
1168
1216
  this.openDBs.set(name, db);
1169
1217
  return db;
1170
1218
  }
1219
+ /* ---------------- SNAPSHOT ORCHESTRATION ---------------- */
1220
+ /**
1221
+ * Create TAR snapshot of full DB directory
1222
+ */
1223
+ async snapshot(snapshotPath) {
1224
+ if (this.mode === "client" /* CLIENT */) {
1225
+ return dbQueue.exec("snapshot", { path: snapshotPath });
1226
+ }
1227
+ for (const db of this.openDBs.values()) {
1228
+ for (const col of db.collections.values()) {
1229
+ try {
1230
+ await col.db.close();
1231
+ } catch {
1232
+ }
1233
+ }
1234
+ }
1235
+ fs6.mkdirSync(path6.dirname(snapshotPath), { recursive: true });
1236
+ const tar = await import("tar");
1237
+ await tar.c({
1238
+ gzip: true,
1239
+ file: snapshotPath,
1240
+ cwd: this.rootPath,
1241
+ portable: true
1242
+ }, ["./"]);
1243
+ return true;
1244
+ }
1245
+ /**
1246
+ * Restore TAR snapshot safely
1247
+ */
1248
+ async restore(snapshotPath) {
1249
+ if (this.mode === "client" /* CLIENT */) {
1250
+ return dbQueue.exec("restore", { path: snapshotPath });
1251
+ }
1252
+ await this.closeAll();
1253
+ fs6.rmSync(this.rootPath, { recursive: true, force: true });
1254
+ fs6.mkdirSync(this.rootPath, { recursive: true });
1255
+ const tar = await import("tar");
1256
+ await tar.x({
1257
+ file: snapshotPath,
1258
+ cwd: this.rootPath
1259
+ });
1260
+ console.log("Restore completed. Restart required.");
1261
+ process2.exit(0);
1262
+ }
1263
+ /* ---------------- SHUTDOWN ---------------- */
1171
1264
  async closeAll() {
1172
1265
  if (this.closed) return;
1173
1266
  this.closed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liorandb/core",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "LioranDB Core Module – Lightweight, local-first, peer-to-peer database management for Node.js.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,7 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/LioranGroupOfficial/Liorandb#readme",
30
30
  "dependencies": {
31
+ "@types/tar-stream": "^3.1.4",
31
32
  "classic-level": "^3.0.0",
33
+ "tar": "^7.5.9",
34
+ "tar-stream": "^3.1.7",
32
35
  "uuid": "^13.0.0",
33
36
  "zod": "^4.3.6"
34
37
  },
@@ -51,6 +51,8 @@ export class LioranManager {
51
51
  }
52
52
  }
53
53
 
54
+ /* ---------------- LOCK MANAGEMENT ---------------- */
55
+
54
56
  private isProcessAlive(pid: number): boolean {
55
57
  try {
56
58
  process.kill(pid, 0);
@@ -68,7 +70,6 @@ export class LioranManager {
68
70
  fs.writeSync(this.lockFd, String(process.pid));
69
71
  return true;
70
72
  } catch {
71
- // Possible stale lock → validate PID
72
73
  try {
73
74
  const pid = Number(fs.readFileSync(lockPath, "utf8"));
74
75
  if (!this.isProcessAlive(pid)) {
@@ -77,18 +78,18 @@ export class LioranManager {
77
78
  fs.writeSync(this.lockFd, String(process.pid));
78
79
  return true;
79
80
  }
80
- } catch {}
81
-
81
+ } catch { }
82
82
  return false;
83
83
  }
84
84
  }
85
85
 
86
+ /* ---------------- DB OPEN ---------------- */
87
+
86
88
  async db(name: string): Promise<LioranDB> {
87
89
  if (this.mode === ProcessMode.CLIENT) {
88
90
  await dbQueue.exec("db", { db: name });
89
91
  return new IPCDatabase(name) as any;
90
92
  }
91
-
92
93
  return this.openDatabase(name);
93
94
  }
94
95
 
@@ -107,6 +108,64 @@ export class LioranManager {
107
108
  return db;
108
109
  }
109
110
 
111
+ /* ---------------- SNAPSHOT ORCHESTRATION ---------------- */
112
+
113
+ /**
114
+ * Create TAR snapshot of full DB directory
115
+ */
116
+ async snapshot(snapshotPath: string) {
117
+ if (this.mode === ProcessMode.CLIENT) {
118
+ return dbQueue.exec("snapshot", { path: snapshotPath });
119
+ }
120
+
121
+ // Flush all DBs safely
122
+ for (const db of this.openDBs.values()) {
123
+ for (const col of db.collections.values()) {
124
+ try { await col.db.close(); } catch { }
125
+ }
126
+ }
127
+
128
+ // Ensure backup directory exists
129
+ fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
130
+
131
+ const tar = await import("tar");
132
+
133
+ await tar.c({
134
+ gzip: true,
135
+ file: snapshotPath,
136
+ cwd: this.rootPath,
137
+ portable: true
138
+ }, ["./"]);
139
+
140
+ return true;
141
+ }
142
+
143
+ /**
144
+ * Restore TAR snapshot safely
145
+ */
146
+ async restore(snapshotPath: string) {
147
+ if (this.mode === ProcessMode.CLIENT) {
148
+ return dbQueue.exec("restore", { path: snapshotPath });
149
+ }
150
+
151
+ await this.closeAll();
152
+
153
+ fs.rmSync(this.rootPath, { recursive: true, force: true });
154
+ fs.mkdirSync(this.rootPath, { recursive: true });
155
+
156
+ const tar = await import("tar");
157
+
158
+ await tar.x({
159
+ file: snapshotPath,
160
+ cwd: this.rootPath
161
+ });
162
+
163
+ console.log("Restore completed. Restart required.");
164
+ process.exit(0);
165
+ }
166
+
167
+ /* ---------------- SHUTDOWN ---------------- */
168
+
110
169
  async closeAll(): Promise<void> {
111
170
  if (this.closed) return;
112
171
  this.closed = true;
@@ -117,7 +176,7 @@ export class LioranManager {
117
176
  }
118
177
 
119
178
  for (const db of this.openDBs.values()) {
120
- try { await db.close(); } catch {}
179
+ try { await db.close(); } catch { }
121
180
  }
122
181
 
123
182
  this.openDBs.clear();
@@ -125,7 +184,7 @@ export class LioranManager {
125
184
  try {
126
185
  if (this.lockFd) fs.closeSync(this.lockFd);
127
186
  fs.unlinkSync(path.join(this.rootPath, ".lioran.lock"));
128
- } catch {}
187
+ } catch { }
129
188
 
130
189
  await this.ipcServer?.close();
131
190
  }
@@ -154,7 +213,7 @@ export class LioranManager {
154
213
  /* ---------------- IPC PROXY DB ---------------- */
155
214
 
156
215
  class IPCDatabase {
157
- constructor(private name: string) {}
216
+ constructor(private name: string) { }
158
217
 
159
218
  collection(name: string) {
160
219
  return new IPCCollection(this.name, name);
@@ -165,7 +224,7 @@ class IPCCollection {
165
224
  constructor(
166
225
  private db: string,
167
226
  private col: string
168
- ) {}
227
+ ) { }
169
228
 
170
229
  private call(method: string, params: any[]) {
171
230
  return dbQueue.exec("op", {
@@ -1,10 +1,14 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
+ import { execFile } from "child_process";
4
+ import { promisify } from "util";
3
5
  import { Collection } from "./collection.js";
4
6
  import { Index, IndexOptions } from "./index.js";
5
7
  import type { LioranManager } from "../LioranManager.js";
6
8
  import type { ZodSchema } from "zod";
7
9
 
10
+ const exec = promisify(execFile);
11
+
8
12
  /* ----------------------------- TYPES ----------------------------- */
9
13
 
10
14
  type TXOp = { tx: number; col: string; op: string; args: any[] };
@@ -95,11 +99,7 @@ export class LioranDB {
95
99
  return;
96
100
  }
97
101
 
98
- try {
99
- this.meta = JSON.parse(fs.readFileSync(this.metaPath, "utf8"));
100
- } catch {
101
- throw new Error("Database metadata corrupted");
102
- }
102
+ this.meta = JSON.parse(fs.readFileSync(this.metaPath, "utf8"));
103
103
  }
104
104
 
105
105
  private saveMeta() {
@@ -133,17 +133,13 @@ export class LioranDB {
133
133
  for (const line of raw.split("\n")) {
134
134
  if (!line.trim()) continue;
135
135
 
136
- try {
137
- const entry: WALEntry = JSON.parse(line);
138
-
139
- if ("commit" in entry) committed.add(entry.tx);
140
- else if ("applied" in entry) applied.add(entry.tx);
141
- else {
142
- if (!ops.has(entry.tx)) ops.set(entry.tx, []);
143
- ops.get(entry.tx)!.push(entry);
144
- }
145
- } catch {
146
- break;
136
+ const entry: WALEntry = JSON.parse(line);
137
+
138
+ if ("commit" in entry) committed.add(entry.tx);
139
+ else if ("applied" in entry) applied.add(entry.tx);
140
+ else {
141
+ if (!ops.has(entry.tx)) ops.set(entry.tx, []);
142
+ ops.get(entry.tx)!.push(entry);
147
143
  }
148
144
  }
149
145
 
@@ -177,7 +173,6 @@ export class LioranDB {
177
173
 
178
174
  const col = new Collection<T>(colPath, schema);
179
175
 
180
- // Auto-load indexes
181
176
  const metas = this.meta.indexes[name] ?? [];
182
177
  for (const m of metas) {
183
178
  col.registerIndex(new Index(colPath, m.field, m.options));
@@ -216,27 +211,63 @@ export class LioranDB {
216
211
  this.saveMeta();
217
212
  }
218
213
 
219
- /* ---------------------- COMPACTION ORCHESTRATOR ---------------------- */
214
+ /* ------------------------- SNAPSHOT ENGINE ------------------------- */
220
215
 
221
- /**
222
- * Compact single collection safely (WAL + TX safe)
223
- */
224
- async compactCollection(name: string) {
225
- const col = this.collection(name);
216
+ // async snapshot(snapshotPath: string) {
217
+ // await this.clearWAL();
226
218
 
227
- // Ensure WAL is fully flushed
228
- await this.clearWAL();
219
+ // for (const col of this.collections.values()) {
220
+ // await col.close();
221
+ // }
222
+
223
+ // fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
224
+
225
+ // await exec("tar", [
226
+ // "-czf",
227
+ // snapshotPath,
228
+ // "-C",
229
+ // this.basePath,
230
+ // "."
231
+ // ]);
232
+ // }
233
+
234
+ // async restore(snapshotPath: string) {
235
+ // if (!fs.existsSync(snapshotPath)) {
236
+ // throw new Error("Snapshot file does not exist");
237
+ // }
238
+
239
+ // await this.close();
240
+
241
+ // const tmp = this.basePath + ".restore";
242
+
243
+ // await fs.promises.rm(tmp, { recursive: true, force: true });
244
+ // await fs.promises.mkdir(tmp, { recursive: true });
229
245
 
230
- // Perform compaction inside write barrier
246
+ // await exec("tar", [
247
+ // "-xzf",
248
+ // snapshotPath,
249
+ // "-C",
250
+ // tmp
251
+ // ]);
252
+
253
+ // await fs.promises.rm(this.basePath, { recursive: true, force: true });
254
+ // await fs.promises.rename(tmp, this.basePath);
255
+
256
+ // this.collections.clear();
257
+ // this.loadMeta();
258
+ // await this.recoverFromWAL();
259
+ // }
260
+
261
+ /* ------------------------- COMPACTION ------------------------- */
262
+
263
+ async compactCollection(name: string) {
264
+ await this.clearWAL();
265
+ const col = this.collection(name);
231
266
  await col.compact();
232
267
  }
233
268
 
234
- /**
235
- * Compact entire database safely
236
- */
237
269
  async compactAll() {
238
270
  await this.clearWAL();
239
-
240
271
  for (const name of this.collections.keys()) {
241
272
  await this.compactCollection(name);
242
273
  }
@@ -247,10 +278,8 @@ export class LioranDB {
247
278
  async transaction<T>(fn: (tx: DBTransactionContext) => Promise<T>): Promise<T> {
248
279
  const txId = ++LioranDB.TX_SEQ;
249
280
  const tx = new DBTransactionContext(this, txId);
250
-
251
281
  const result = await fn(tx);
252
282
  await tx.commit();
253
-
254
283
  return result;
255
284
  }
256
285
 
@@ -262,4 +291,4 @@ export class LioranDB {
262
291
  }
263
292
  this.collections.clear();
264
293
  }
265
- }
294
+ }
package/src/ipc/index.ts CHANGED
@@ -72,6 +72,14 @@ class DBProxy {
72
72
  compact() {
73
73
  return dbQueue.exec("compact:db", { db: this.dbName });
74
74
  }
75
+
76
+ snapshot(path: string) {
77
+ return dbQueue.exec("snapshot", { path });
78
+ }
79
+
80
+ restore(path: string) {
81
+ return dbQueue.exec("restore", { path });
82
+ }
75
83
  }
76
84
 
77
85
  /* -------------------------------- MANAGER PROXY -------------------------------- */
@@ -85,6 +93,18 @@ class LioranManagerIPC {
85
93
  compactAll() {
86
94
  return dbQueue.exec("compact:all", {});
87
95
  }
96
+
97
+ snapshot(path: string) {
98
+ return dbQueue.exec("snapshot", { path });
99
+ }
100
+
101
+ restore(path: string) {
102
+ return dbQueue.exec("restore", { path });
103
+ }
104
+
105
+ shutdown() {
106
+ return dbQueue.shutdown();
107
+ }
88
108
  }
89
109
 
90
110
  export const manager = new LioranManagerIPC();
package/src/ipc/queue.ts CHANGED
@@ -10,7 +10,13 @@ export type IPCAction =
10
10
  | "compact:collection"
11
11
  | "compact:db"
12
12
  | "compact:all"
13
- | "shutdown";
13
+ | "shutdown"
14
+ | "open"
15
+ | "close"
16
+ | "minimize"
17
+ | "maximize"
18
+ | "restore"
19
+ | "snapshot";
14
20
 
15
21
  /* -------------------------------- DB QUEUE -------------------------------- */
16
22
 
@@ -39,6 +45,16 @@ export class DBQueue {
39
45
  return this.exec("compact:all", {});
40
46
  }
41
47
 
48
+ /* ----------------------------- SNAPSHOT API ----------------------------- */
49
+
50
+ snapshot(path: string) {
51
+ return this.exec("snapshot", { path });
52
+ }
53
+
54
+ restore(path: string) {
55
+ return this.exec("restore", { path });
56
+ }
57
+
42
58
  /* ------------------------------ SHUTDOWN ------------------------------ */
43
59
 
44
60
  async shutdown() {
package/src/ipc/server.ts CHANGED
@@ -32,7 +32,7 @@ export class IPCServer {
32
32
  try {
33
33
  const msg = JSON.parse(raw);
34
34
  await this.handleMessage(socket, msg);
35
- } catch (err) {
35
+ } catch {
36
36
  socket.write(JSON.stringify({
37
37
  id: null,
38
38
  ok: false,
@@ -85,13 +85,31 @@ export class IPCServer {
85
85
  case "compact:db": {
86
86
  const { db } = args;
87
87
  const database = await this.manager.db(db);
88
- await (database as any).compact();
88
+ await (database as any).compactAll();
89
89
  result = true;
90
90
  break;
91
91
  }
92
92
 
93
93
  case "compact:all": {
94
- await (this.manager as any).compactAll();
94
+ for (const db of this.manager.openDBs.values()) {
95
+ await db.compactAll();
96
+ }
97
+ result = true;
98
+ break;
99
+ }
100
+
101
+ /* ---------------- SNAPSHOT ---------------- */
102
+
103
+ case "snapshot": {
104
+ const { path: snapshotPath } = args;
105
+ await this.manager.snapshot(snapshotPath);
106
+ result = true;
107
+ break;
108
+ }
109
+
110
+ case "restore": {
111
+ const { path: snapshotPath } = args;
112
+ await this.manager.restore(snapshotPath);
95
113
  result = true;
96
114
  break;
97
115
  }
@@ -0,0 +1,89 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { pipeline } from "stream/promises";
4
+ import zlib from "zlib";
5
+ import tar from "tar-stream";
6
+
7
+ /**
8
+ * Create a TAR.GZ archive from a directory
9
+ */
10
+ export async function createTarGz(
11
+ sourceDir: string,
12
+ outFile: string
13
+ ): Promise<void> {
14
+ await fs.promises.mkdir(path.dirname(outFile), { recursive: true });
15
+
16
+ const pack = tar.pack();
17
+ const gzip = zlib.createGzip();
18
+ const out = fs.createWriteStream(outFile + ".tmp");
19
+
20
+ const writeStream = pipeline(pack, gzip, out);
21
+
22
+ async function walk(dir: string) {
23
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
24
+
25
+ for (const entry of entries) {
26
+ const fullPath = path.join(dir, entry.name);
27
+ const relPath = path.relative(sourceDir, fullPath);
28
+
29
+ if (entry.isDirectory()) {
30
+ await walk(fullPath);
31
+ } else if (entry.isFile()) {
32
+ const stat = await fs.promises.stat(fullPath);
33
+ const stream = fs.createReadStream(fullPath);
34
+
35
+ await new Promise<void>((resolve, reject) => {
36
+ const header = {
37
+ name: relPath,
38
+ size: stat.size,
39
+ mode: stat.mode,
40
+ mtime: stat.mtime
41
+ };
42
+
43
+ const entryStream = pack.entry(header, err => {
44
+ if (err) reject(err);
45
+ else resolve();
46
+ });
47
+
48
+ stream.pipe(entryStream);
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ await walk(sourceDir);
55
+
56
+ pack.finalize();
57
+ await writeStream;
58
+
59
+ await fs.promises.rename(outFile + ".tmp", outFile);
60
+ }
61
+
62
+ /**
63
+ * Extract TAR.GZ archive into directory
64
+ */
65
+ export async function extractTarGz(
66
+ archiveFile: string,
67
+ targetDir: string
68
+ ): Promise<void> {
69
+ await fs.promises.mkdir(targetDir, { recursive: true });
70
+
71
+ const extract = tar.extract();
72
+ const gunzip = zlib.createGunzip();
73
+ const input = fs.createReadStream(archiveFile);
74
+
75
+ extract.on("entry", async (header, stream, next) => {
76
+ const outPath = path.join(targetDir, header.name);
77
+
78
+ await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
79
+
80
+ const out = fs.createWriteStream(outPath, {
81
+ mode: header.mode
82
+ });
83
+
84
+ await pipeline(stream, out);
85
+ next();
86
+ });
87
+
88
+ await pipeline(input, gunzip, extract);
89
+ }