@activeledger/activestorage 2.15.7 → 4.3.3

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.
Files changed (122) hide show
  1. package/es/cache.d.ts +18 -0
  2. package/es/cache.d.ts.map +1 -0
  3. package/es/cache.js +57 -0
  4. package/es/cache.js.map +1 -0
  5. package/es/datastore.d.ts +1 -0
  6. package/es/datastore.d.ts.map +1 -1
  7. package/es/datastore.js +8 -3
  8. package/es/datastore.js.map +1 -1
  9. package/es/driver.d.ts +16 -0
  10. package/es/driver.d.ts.map +1 -0
  11. package/es/driver.js +2 -0
  12. package/es/driver.js.map +1 -0
  13. package/es/drivers/leveldb.d.ts +21 -0
  14. package/es/drivers/leveldb.d.ts.map +1 -0
  15. package/es/drivers/leveldb.js +121 -0
  16. package/es/drivers/leveldb.js.map +1 -0
  17. package/es/drivers/rocksdb.d.ts +21 -0
  18. package/es/drivers/rocksdb.d.ts.map +1 -0
  19. package/es/drivers/rocksdb.js +98 -0
  20. package/es/drivers/rocksdb.js.map +1 -0
  21. package/es/heartbeat.d.ts +7 -0
  22. package/es/heartbeat.d.ts.map +1 -0
  23. package/es/heartbeat.js +15 -0
  24. package/es/heartbeat.js.map +1 -0
  25. package/es/levelme.d.ts +18 -18
  26. package/es/levelme.d.ts.map +1 -1
  27. package/es/levelme.js +290 -155
  28. package/es/levelme.js.map +1 -1
  29. package/es/newlinestream.d.ts +4 -0
  30. package/es/newlinestream.d.ts.map +1 -0
  31. package/es/newlinestream.js +28 -0
  32. package/es/newlinestream.js.map +1 -0
  33. package/es/selfhost.js +157 -58
  34. package/es/selfhost.js.map +1 -1
  35. package/es/sse.d.ts +10 -0
  36. package/es/sse.d.ts.map +1 -0
  37. package/es/sse.js +41 -0
  38. package/es/sse.js.map +1 -0
  39. package/lib/cache.d.ts +18 -0
  40. package/lib/cache.d.ts.map +1 -0
  41. package/lib/cache.js +62 -0
  42. package/lib/cache.js.map +1 -0
  43. package/lib/datastore.d.ts +1 -0
  44. package/lib/datastore.d.ts.map +1 -1
  45. package/lib/datastore.js +8 -3
  46. package/lib/datastore.js.map +1 -1
  47. package/lib/driver.d.ts +16 -0
  48. package/lib/driver.d.ts.map +1 -0
  49. package/lib/driver.js +3 -0
  50. package/lib/driver.js.map +1 -0
  51. package/lib/drivers/leveldb.d.ts +21 -0
  52. package/lib/drivers/leveldb.d.ts.map +1 -0
  53. package/lib/drivers/leveldb.js +125 -0
  54. package/lib/drivers/leveldb.js.map +1 -0
  55. package/lib/drivers/rocksdb.d.ts +21 -0
  56. package/lib/drivers/rocksdb.d.ts.map +1 -0
  57. package/lib/drivers/rocksdb.js +102 -0
  58. package/lib/drivers/rocksdb.js.map +1 -0
  59. package/lib/heartbeat.d.ts +7 -0
  60. package/lib/heartbeat.d.ts.map +1 -0
  61. package/lib/heartbeat.js +19 -0
  62. package/lib/heartbeat.js.map +1 -0
  63. package/lib/levelme.d.ts +18 -18
  64. package/lib/levelme.d.ts.map +1 -1
  65. package/lib/levelme.js +312 -157
  66. package/lib/levelme.js.map +1 -1
  67. package/lib/newlinestream.d.ts +4 -0
  68. package/lib/newlinestream.d.ts.map +1 -0
  69. package/lib/newlinestream.js +32 -0
  70. package/lib/newlinestream.js.map +1 -0
  71. package/lib/selfhost.js +157 -58
  72. package/lib/selfhost.js.map +1 -1
  73. package/lib/sse.d.ts +10 -0
  74. package/lib/sse.d.ts.map +1 -0
  75. package/lib/sse.js +45 -0
  76. package/lib/sse.js.map +1 -0
  77. package/package.json +8 -18
  78. package/src/cache.ts +72 -0
  79. package/src/datastore.ts +187 -0
  80. package/src/driver.ts +18 -0
  81. package/src/drivers/leveldb.ts +145 -0
  82. package/src/drivers/rocksdb.ts +118 -0
  83. package/src/fauxton/dashboard.assets/css/styles.d43d3249088a6c04b633.css +33 -0
  84. package/src/fauxton/dashboard.assets/fonts/fauxtonicon5.eot +0 -0
  85. package/src/fauxton/dashboard.assets/fonts/fauxtonicon5.ttf +0 -0
  86. package/src/fauxton/dashboard.assets/fonts/fauxtonicon5.woff +0 -0
  87. package/src/fauxton/dashboard.assets/fonts/fontawesome-webfont.eot +0 -0
  88. package/src/fauxton/dashboard.assets/fonts/fontawesome-webfont.ttf +0 -0
  89. package/src/fauxton/dashboard.assets/fonts/fontawesome-webfont.woff +0 -0
  90. package/src/fauxton/dashboard.assets/img/activeledger-logo.png +0 -0
  91. package/src/fauxton/dashboard.assets/img/activeledger.png +0 -0
  92. package/src/fauxton/dashboard.assets/img/asf-feather-logo.png +0 -0
  93. package/src/fauxton/dashboard.assets/img/couch-watermark.png +0 -0
  94. package/src/fauxton/dashboard.assets/img/couchdb-logo.png +0 -0
  95. package/src/fauxton/dashboard.assets/img/fauxtonicon5.svg +729 -0
  96. package/src/fauxton/dashboard.assets/img/fontawesome-webfont.svg +399 -0
  97. package/src/fauxton/dashboard.assets/img/github.png +0 -0
  98. package/src/fauxton/dashboard.assets/img/googleplus.png +0 -0
  99. package/src/fauxton/dashboard.assets/img/linkedin.png +0 -0
  100. package/src/fauxton/dashboard.assets/img/loader.gif +0 -0
  101. package/src/fauxton/dashboard.assets/img/twitter.png +0 -0
  102. package/src/fauxton/dashboard.assets/js/bundle.d43d3249088a6c04b633.js +1 -0
  103. package/src/fauxton/dashboard.assets/js/manifest.a154524a560e5f6f431b.js +1 -0
  104. package/src/fauxton/dashboard.assets/js/vendor.156793045cdeb7c63707.js +96 -0
  105. package/src/fauxton/index.html +52 -0
  106. package/src/heartbeat.ts +60 -0
  107. package/src/index.ts +25 -0
  108. package/src/levelme.ts +901 -0
  109. package/src/newlinestream.ts +32 -0
  110. package/src/selfhost.ts +931 -0
  111. package/src/sse.ts +136 -0
  112. package/src/types/rocksdb.d.ts +1 -0
  113. package/tsconfig.json +8 -0
  114. package/tsconfig.tsbuildinfo +1 -0
  115. package/es/pouchdb.d.ts +0 -4
  116. package/es/pouchdb.d.ts.map +0 -1
  117. package/es/pouchdb.js +0 -16
  118. package/es/pouchdb.js.map +0 -1
  119. package/lib/pouchdb.d.ts +0 -4
  120. package/lib/pouchdb.d.ts.map +0 -1
  121. package/lib/pouchdb.js +0 -22
  122. package/lib/pouchdb.js.map +0 -1
@@ -0,0 +1,931 @@
1
+ /*
2
+ * MIT License (MIT)
3
+ * Copyright (c) 2018 Activeledger
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ import * as http from "http";
24
+ import * as path from "path";
25
+ import { promises as fsPromises } from "fs";
26
+ import { randomUUID } from "crypto";
27
+ import { ActiveHttpd, IActiveHttpIncoming } from "@activeledger/httpd";
28
+ import { LevelMe } from "./levelme";
29
+ //import { Socket } from "net";
30
+ import { SSE } from "./sse";
31
+ import { ActiveLogger } from "@activeledger/activelogger";
32
+ import { IActiveHttpResponse } from "@activeledger/httpd/lib/httpd";
33
+
34
+ (function () {
35
+ const fauxtonCache: { [key: string]: { mime: string; data: Buffer } } = {};
36
+ // Fauxton Path
37
+ const FAUXTON_PATH = __dirname + "/fauxton/";
38
+
39
+ // Directory Prefix
40
+ const DIR_PREFIX = "./" + process.argv[2] + "/";
41
+
42
+ // Listening Port
43
+ const PORT = process.argv[3];
44
+
45
+ // Data Storage Engine Provider, level provides backwards compatiblility
46
+ const DS_PROVIDER = process.argv[4] || "level";
47
+
48
+ // Database Connection Cache
49
+ let dbCache: { [index: string]: LevelMe } = {};
50
+
51
+ // Track if we are shutting down
52
+ let isShuttingDown = false;
53
+
54
+ /**
55
+ * Graceful Shutdown
56
+ *
57
+ * @param {ActiveHttpd} httpd host
58
+ */
59
+ const terminate = async (host: ActiveHttpd) => {
60
+ if (!isShuttingDown) {
61
+ isShuttingDown = true;
62
+ host.shutdown();
63
+
64
+ // Close all databases
65
+ const dbNames = Object.keys(dbCache);
66
+ for (const name of dbNames) {
67
+ try {
68
+ await dbCache[name].close();
69
+ ActiveLogger.info(`Closed database: ${name}`);
70
+ } catch (err) {
71
+ ActiveLogger.error(`Error closing database ${name}:`, err);
72
+ }
73
+ }
74
+ process.exit(0);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Manages Pouch Connections
80
+ *
81
+ * @param {string} name
82
+ * @returns
83
+ */
84
+ const getDB = (name: string): LevelMe => {
85
+ // Must start with activeledger (will cause problem with config need to resolve)
86
+ if (!name.startsWith("activeledger")) {
87
+ throw new Error(`invalid database - ${name}`);
88
+ }
89
+ if (!dbCache[name]) {
90
+ try {
91
+ dbCache[name] = new LevelMe(DIR_PREFIX, name, DS_PROVIDER);
92
+ } catch (err) {
93
+ throw err;
94
+ }
95
+ } else {
96
+ }
97
+ return dbCache[name];
98
+ };
99
+
100
+ /**
101
+ * Fast uuidV4 Generator
102
+ * Credit : https://gist.github.com/jed/982883
103
+ *
104
+ * @param {number} [a]
105
+ * @returns {number}
106
+ */
107
+ const uuidGenV4 = (): string => randomUUID();
108
+
109
+ /**
110
+ * Checks for the existant of _id if not ads it and returns
111
+ *
112
+ * @param {*} doc
113
+ * @returns {*}
114
+ */
115
+ const checkDoc = (doc: any): any => {
116
+ if (!doc._id) doc._id = uuidGenV4();
117
+ return doc;
118
+ };
119
+
120
+ // Create Light Server
121
+ let http = new ActiveHttpd();
122
+
123
+ // Attempt better shutdown
124
+ process.on("SIGINT", () => {
125
+ terminate(http);
126
+ });
127
+ process.on("SIGTERM", () => {
128
+ terminate(http);
129
+ });
130
+ process.on("SIGQUIT", () => {
131
+ terminate(http);
132
+ });
133
+
134
+
135
+ // A hand-rolled PID-file "already running" check used to live here:
136
+ // record our PID on start, and on the next start, if a previous PID file
137
+ // exists and `process.kill(pid, 0)` finds *any* live process with that
138
+ // PID, refuse to start. That's unreliable the moment PIDs get reused,
139
+ // which happens on every container restart (each gets a fresh, small PID
140
+ // namespace) and eventually on bare metal too - a stale recorded PID
141
+ // just needs to coincide with some unrelated process for this to produce
142
+ // a false "Instance already running" that no amount of restarting fixes,
143
+ // since the file was also never cleaned up except on a graceful
144
+ // SIGINT/SIGTERM/SIGQUIT (see terminate() above) - a SIGKILL or crash
145
+ // left it behind for good. Reproduced live: two nodes' storage refused
146
+ // to restart after being recreated mid-session, in full isolation, with
147
+ // no other process touching the same data - deleting the stale PID file
148
+ // was the only fix.
149
+ //
150
+ // Removed rather than patched to clean up more reliably: LevelDB (via
151
+ // LevelMe below) already takes a real OS-level advisory lock on its own
152
+ // LOCK file per database when opened, which the kernel releases
153
+ // automatically no matter how the holding process dies - no cleanup
154
+ // logic needed, and it actually enforces single-writer correctness
155
+ // instead of approximating it by PID. This custom check was redundant
156
+ // with that at best, actively wrong at worst.
157
+
158
+ http.use("/", "GET", () => {
159
+ return {
160
+ activeledger: "Welcome to Activeledger data!",
161
+ adapters: ["levelme"],
162
+ engine: DS_PROVIDER,
163
+ };
164
+ });
165
+
166
+ // Standard Session
167
+ http.use("_session", "GET", () => {
168
+ return { ok: true, userCtx: { name: null, roles: ["_admin"] } };
169
+ });
170
+
171
+ // List all databases
172
+ http.use("_all_dbs", "GET", async () => {
173
+ // Check to see if it is a directory and not a special directory
174
+ const files = await fsPromises.readdir(DIR_PREFIX);
175
+ // Same pattern deleteDb() below already uses - stat every entry
176
+ // concurrently instead of one at a time.
177
+ const stats = await Promise.all(
178
+ files.map((file) => fsPromises.lstat(DIR_PREFIX + file))
179
+ );
180
+ const dbs = files.filter(
181
+ (file, i) =>
182
+ stats[i].isDirectory() &&
183
+ file !== process.argv[2] &&
184
+ file !== "pouch__all_dbs__" &&
185
+ file !== "_replicator" &&
186
+ !file.includes("-mrview-")
187
+ );
188
+
189
+ // Return directories
190
+ return dbs;
191
+ });
192
+
193
+ // Get Database Info
194
+ http.use("*", "GET", async (incoming: IActiveHttpIncoming) => {
195
+ // Get Database
196
+ let db = getDB(incoming.url[0]);
197
+ let info = await db.info();
198
+
199
+ // Now add data_size
200
+ const files = await fsPromises.readdir(DIR_PREFIX + incoming.url[0]);
201
+ const stats = await Promise.all(
202
+ files.map((source) =>
203
+ fsPromises.stat(DIR_PREFIX + incoming.url[0] + "/" + source)
204
+ )
205
+ );
206
+ info.data_size = stats.reduce((total, stat) => total + stat.size, 0);
207
+ return info;
208
+ });
209
+
210
+ // Create Database
211
+ http.use("*", "PUT", async (incoming: IActiveHttpIncoming) => {
212
+ // Get Database
213
+ let db = getDB(incoming.body.name);
214
+
215
+ // Create Database
216
+ await db.info();
217
+ return { ok: true };
218
+ });
219
+
220
+ // Delete Database Local Var
221
+ const deleteDb = async (dir: string) => {
222
+ // Read all files and delete
223
+ const files = await fsPromises.readdir(dir);
224
+ await Promise.all(files.map(source => fsPromises.unlink(dir + "/" + source)));
225
+
226
+ // Delete the folder
227
+ await fsPromises.rmdir(dir);
228
+ };
229
+
230
+ // Delete Database
231
+ http.use("*", "DELETE", async (incoming: IActiveHttpIncoming) => {
232
+ // Can just delete the folder, It shouldn't have nested so can delete all the files first
233
+ let dir = DIR_PREFIX + incoming.url[0];
234
+
235
+ // Check Folder
236
+ try {
237
+ await fsPromises.access(dir);
238
+ } catch {
239
+ // Directory doesn't exist, so we're good.
240
+ return { ok: true };
241
+ }
242
+ // If we have this db open we need to close it
243
+ if (dbCache[incoming.url[0]]) {
244
+ await dbCache[incoming.url[0]].close();
245
+ delete dbCache[incoming.url[0]];
246
+ }
247
+ // Delete Database
248
+ await deleteDb(dir);
249
+ return { ok: true };
250
+ });
251
+
252
+ // Get UUID
253
+ http.use("_uuids", "GET", async (incoming: IActiveHttpIncoming) => {
254
+ // uuid holder
255
+ let uuids = [];
256
+
257
+ // Loop for how many requested
258
+ for (let i = 0; i < incoming.query.count; i++) {
259
+ uuids.push(uuidGenV4());
260
+ }
261
+ return { uuids };
262
+ });
263
+
264
+ // Get all docs from a database
265
+ http.use("*/_all_docs", "GET", async (incoming: IActiveHttpIncoming) => {
266
+ // Get Database
267
+ let db = getDB(incoming.url[0]);
268
+ return await db.allDocs(prepareAllDocs(incoming.query));
269
+ });
270
+
271
+ // Get all docs filtered from a database
272
+ http.use("*/_all_docs", "POST", async (incoming: IActiveHttpIncoming) => {
273
+ // Get Database
274
+ let db = getDB(incoming.url[0]);
275
+ return await db.allDocs(
276
+ prepareAllDocs(Object.assign(incoming.query, incoming.body))
277
+ );
278
+ });
279
+
280
+ // Bit of a proxy call to focus on umids
281
+ http.use("*/umids", "GET", async (incoming: IActiveHttpIncoming) => {
282
+ return getTransactionUmids(incoming);
283
+ });
284
+
285
+ http.use("*/transactions", "GET", async (incoming: IActiveHttpIncoming) => {
286
+ return getTransactionUmids(incoming);
287
+ });
288
+
289
+ // Filters the call to focus on umid transaction (assuming right database!)
290
+ const getTransactionUmids = async (incoming: IActiveHttpIncoming) => {
291
+ // Get Database
292
+ let db = getDB(incoming.url[0]);
293
+ // Lets make sure it starts with umid:
294
+ if (incoming.query["from"].startsWith("umid:")) {
295
+ incoming.query["startkey"] = incoming.query["from"];
296
+ } else {
297
+ incoming.query["startkey"] = `umid:${incoming.query["from"]}`;
298
+ }
299
+ const txs = (await db.allDocs(prepareAllDocs(incoming.query))) as any;
300
+ for (let i = txs.rows.length; i--;) {
301
+ const [timestamp, umid] = txs.rows[i]._id.split(",");
302
+ txs.rows[i] = {
303
+ umid,
304
+ timestamp: timestamp.replace("umid:", ""),
305
+ };
306
+ }
307
+ return txs;
308
+ };
309
+
310
+ /**
311
+ * C/Pouch conversion utility tool for _all_docs
312
+ *
313
+ * @param {*} options
314
+ * @returns
315
+ */
316
+ const prepareAllDocs = (options: any) => {
317
+ // Convert Limit
318
+ if (options.limit) {
319
+ options.limit = parseInt(options.limit) || null;
320
+ }
321
+
322
+ // Convert startkey to remove "
323
+ if (options.startkey) {
324
+ options.startkey = options.startkey.replace(/"/g, "");
325
+ }
326
+
327
+ // Convert endkey to remove " and keep unicode search
328
+ if (options.endkey) {
329
+ options.endkey = options.endkey.replace(/"/g, "");
330
+ options.endkey = options.endkey.slice(0, -1) + "\u9999";
331
+ }
332
+
333
+ // Filter prefixes
334
+ // Leaving this in even though filtering in permissionsChecker as well
335
+ if (options.keys) {
336
+ options.keys = options.keys.map((id: string) => filterPrefix(id));
337
+ }
338
+ return options;
339
+ };
340
+
341
+ /**
342
+ * Reusable Get for mutliple paths
343
+ *
344
+ * @param {string} dbLoc
345
+ * @param {string} path
346
+ * @returns {Promise<any>}
347
+ */
348
+ const genericGet = (dbLoc: string, path: string): Promise<any> => {
349
+ // Get Database
350
+ return new Promise(async (resolve, reject) => {
351
+ // Get Database
352
+ const db = getDB(dbLoc);
353
+ // Filter
354
+ db.get(filterPrefix(decodeURIComponent(path), dbLoc))
355
+ .then((doc: unknown) => resolve(doc))
356
+ .catch((e: unknown) => {
357
+ reject(e);
358
+ });
359
+ });
360
+ };
361
+
362
+ /**
363
+ * Reusable delete (purge like) for mutliple paths
364
+ * TODO: Allow recusive delete docName []
365
+ *
366
+ * @param {string} dbLoc
367
+ * @param {string} path
368
+ * @returns {Promise<any>}
369
+ */
370
+ const genericDelete = (dbLoc: string, docName: string): Promise<any> => {
371
+ return new Promise(async (resolve, reject) => {
372
+ // Get Database
373
+ const db = getDB(dbLoc);
374
+ try {
375
+ await db.del(filterPrefix(docName, dbLoc));
376
+ return resolve({ success: "ok" });
377
+ } catch (e) {
378
+ return reject(e);
379
+ }
380
+ });
381
+ };
382
+
383
+ // TODO : Verify request source
384
+ http.use("*/*", "DELETE", async (incoming: IActiveHttpIncoming) => {
385
+ return await genericDelete(
386
+ incoming.url[0],
387
+ decodeURIComponent(incoming.url[1])
388
+ );
389
+ });
390
+
391
+ // Start compacting process
392
+ http.use("*/_compact", "GET", async (incoming: IActiveHttpIncoming) => {
393
+ const db = getDB(incoming.url[0]);
394
+ return await db.compact();
395
+ });
396
+
397
+ // Direct sequence get
398
+ http.use("*/_seq/*", "GET", async (incoming: IActiveHttpIncoming) => {
399
+ const db = getDB(incoming.url[0]);
400
+ return await db.getSeq(incoming.url[2]);
401
+ });
402
+
403
+ // Direct sequence delete
404
+ http.use("*/_seq/*", "DELETE", async (incoming: IActiveHttpIncoming) => {
405
+ const db = getDB(incoming.url[0]);
406
+ try {
407
+ await db.delSeq([incoming.url[2]]);
408
+ return { success: "ok" };
409
+ } catch {
410
+ throw new Error("Seq Delete Failed");
411
+ }
412
+ });
413
+
414
+ // filename comes straight from the request body here - without this
415
+ // check it's an arbitrary file write/read primitive (LevelMe.backup()/
416
+ // restore() pass it directly to createWriteStream()/createReadStream()
417
+ // with no validation of their own, since they're also called from the
418
+ // CLI's --backup/--restore flags where an operator-chosen path outside
419
+ // the current directory is legitimate - the restriction belongs here,
420
+ // at the boundary where the value is actually untrusted remote input,
421
+ // not inside LevelMe itself). Restricting to a bare filename (no path
422
+ // separators, no ".." components) blocks traversal and absolute paths
423
+ // while still allowing any normal filename.
424
+ const isSafeFilename = (filename: unknown): filename is string =>
425
+ typeof filename === "string" &&
426
+ filename.length > 0 &&
427
+ path.basename(filename) === filename;
428
+
429
+ http.use("*/_backup", "POST", async (incoming: IActiveHttpIncoming) => {
430
+ const filename = incoming.body?.filename;
431
+ if (filename !== undefined && !isSafeFilename(filename)) {
432
+ throw new Error("Invalid backup filename");
433
+ }
434
+ getDB(incoming.url[0]).backup(filename);
435
+ return { status: "started" };
436
+ });
437
+
438
+ http.use("*/_restore", "POST", async (incoming: IActiveHttpIncoming) => {
439
+ const filename = incoming.body?.filename;
440
+ if (!isSafeFilename(filename)) {
441
+ throw new Error("Invalid restore filename");
442
+ }
443
+ getDB(incoming.url[0]).restore(filename);
444
+ return { status: "started" };
445
+ });
446
+
447
+ // Get specific docs from a database
448
+ http.use("*/*", "GET", async (incoming: IActiveHttpIncoming) => {
449
+ return await genericGet(incoming.url[0], incoming.url[1]);
450
+ });
451
+
452
+ // Gets raw unparsed document straight from leveldb
453
+ // Document Store http://localhost:5259/activeledger/_raw/[document._id]
454
+ // Sequence (data) http://localhost:5259/activeledger/_raw/[document._id]@[revision] (revision Example = 10-f0b21ef22ec86f7c6d71c250b4563bba)
455
+ http.use("*/_raw/*", "GET", async (incoming: IActiveHttpIncoming) => {
456
+ // Getting revision?
457
+ const [root, revision] = incoming.url[2].split("@");
458
+
459
+ // Get Database
460
+ const db = getDB(incoming.url[0]);
461
+
462
+ // Get Main Doc
463
+ const rootDoc = await db.get(decodeURIComponent(root), true);
464
+
465
+ // If getting revision we need sequence
466
+ if (revision) {
467
+ try {
468
+ return await db.getSeq(rootDoc.rev_map[revision]);
469
+ } catch (e) {
470
+ throw new Error("Revision Fetch Failed");
471
+ }
472
+ }
473
+ return rootDoc;
474
+ });
475
+
476
+ // Add new / updated document to the database
477
+ http.use("*/**", "PUT", async (incoming: IActiveHttpIncoming) => {
478
+ // Archive Document?
479
+ //await markAsArchived(incoming);
480
+ return genericPut(incoming.url[0], incoming.body);
481
+ });
482
+
483
+ /**
484
+ * Detects if root pouch document needs to be archived and archives
485
+ *
486
+ * @param {IActiveHttpIncoming} incoming
487
+ * @returns {Promise<boolean>}
488
+ */
489
+ // const markAsArchived = async (
490
+ // incoming: IActiveHttpIncoming
491
+ // ): Promise<boolean> => {
492
+ // // No more btree archives, return false for now
493
+ // return false;
494
+
495
+ // const position = isArchivable(incoming.body._rev);
496
+ // if (position) {
497
+ // try {
498
+ // // Get Database
499
+ // const db = getDB(incoming.url[0]);
500
+
501
+ // // Raw meta doc
502
+ // const metaDoc = await db.get(decodeURIComponent(incoming.url[1]), true);
503
+
504
+ // // Make sure archive database exists
505
+ // const archDb = await getCreateArchiveDb(incoming.url[0]);
506
+
507
+ // // Time to modify!
508
+ // const newMetaDoc = prepareArchiveDoc(metaDoc, position);
509
+
510
+ // // Prevent clashes
511
+ // metaDoc._id += ":" + position;
512
+
513
+ // // We could add to existing but then we would archive the archive
514
+ // // Write document to archive
515
+ // await archDb.post(checkDoc(metaDoc));
516
+
517
+ // // Rewrite meta root document
518
+ // await db.writeRaw(newMetaDoc._id, JSON.stringify(newMetaDoc));
519
+ // return true;
520
+ // } catch (e) {
521
+ // // Any errors continue
522
+ // return false;
523
+ // }
524
+ // }
525
+ // return false;
526
+ // };
527
+
528
+ /**
529
+ * Pouchdb Metadoc rewrite (trimming)
530
+ *
531
+ * @param {*} metaDoc
532
+ * @param {string} position
533
+ * @returns
534
+ */
535
+ // const prepareArchiveDoc = (metaDoc: any, position: number) => {
536
+ // // Time to modify!
537
+ // const newMetaDoc = {
538
+ // _id: metaDoc._id,
539
+ // rev_tree: [
540
+ // {
541
+ // // pos: metaDoc.rev_tree[0].pos,
542
+ // pos: position - 1, // Get parent starting pos
543
+ // ids: getLastInTree(metaDoc.rev_tree[0].ids),
544
+ // },
545
+ // ],
546
+ // rev_map: {
547
+ // [metaDoc.winningRev]: metaDoc.rev_map[metaDoc.winningRev],
548
+ // },
549
+ // winningRev: metaDoc.winningRev,
550
+ // deleted: metaDoc.deleted,
551
+ // seq: metaDoc.seq,
552
+ // };
553
+ // return newMetaDoc;
554
+ // };
555
+
556
+ /**
557
+ * Create Archive Database
558
+ *
559
+ * @param {string} name
560
+ * @returns
561
+ */
562
+ // const getCreateArchiveDb = async (name: string): Promise<LevelMe> => {
563
+ // const archDb = getDB(name + "_archive");
564
+ // await archDb.info();
565
+ // return archDb;
566
+ // };
567
+
568
+ /**
569
+ * Gets the last 2 leafs in the tree (parent, current)
570
+ *
571
+ * @param {any[]} tree
572
+ * @param {any[]} [prevTree]
573
+ * @returns {*}
574
+ */
575
+ // const getLastInTree = (tree: any[], prevTree?: any[]): any => {
576
+ // if (tree[2].length) {
577
+ // return getLastInTree(tree[2][0], tree);
578
+ // }
579
+ // return prevTree ? prevTree : tree;
580
+ // };
581
+
582
+ /**
583
+ * Should we Archive the document
584
+ *
585
+ * @param {string} revString
586
+ * @returns {number}
587
+ */
588
+ // const isArchivable = (revString: string): number => {
589
+ // if (revString) {
590
+ // const position = parseInt(revString.split("-")[0]);
591
+
592
+ // // Set at 300 to have 3 attempts before default 1000 id failure error triggered above
593
+ // //return position % 300 === 0 ? position : 0;
594
+ // return position % 15 === 0 ? position : 0;
595
+ // }
596
+ // return 0;
597
+ // };
598
+
599
+ /**
600
+ * Reusable Get for mutliple paths
601
+ *
602
+ * @param {string} dbLoc
603
+ * @param {string} path
604
+ * @returns {Promise<any>}
605
+ */
606
+ const genericPut = async (dbLoc: string, body: string): Promise<any> => {
607
+ // Get Database
608
+ let db = getDB(dbLoc);
609
+ try {
610
+ return await db.put(body);
611
+ } catch (e) {
612
+ e.reason = e.message;
613
+ throw e;
614
+ }
615
+ };
616
+
617
+ // Filters any prefix (so they're virtual) (: reserved character) to real stream id
618
+ const filterPrefix = (stream: string, db?: string): string => {
619
+
620
+ // Only need to filter activeledger, making optional just in case not passed
621
+ if (db && db !== 'activeledger') {
622
+ return stream;
623
+ } else {
624
+ // Remove any suffix like :volatile :stream :umid
625
+ let [streamId, suffix] = stream.split(":");
626
+
627
+ // If id length more than 64 trim the start
628
+ if (streamId.length > 64 && !streamId.startsWith("0x")) {
629
+ streamId = streamId.slice(-64);
630
+ }
631
+
632
+ // If suffix add it back to return
633
+ if (suffix) {
634
+ return streamId + ":" + suffix;
635
+ }
636
+
637
+ // Return just the id
638
+ return streamId;
639
+ }
640
+ };
641
+
642
+ // Add new / updated document to the database with auto id
643
+ http.use("*", "POST", async (incoming: IActiveHttpIncoming) => {
644
+ // Get Database
645
+ let db = getDB(incoming.url[0]);
646
+
647
+ // Archive Document?
648
+ //await markAsArchived(incoming);
649
+
650
+ try {
651
+ return await db.post(checkDoc(incoming.body));
652
+ } catch (e) {
653
+ e.reason = e.message;
654
+ throw e;
655
+ }
656
+ });
657
+
658
+ http.use(
659
+ "*/events",
660
+ "GET",
661
+ async (
662
+ incoming: IActiveHttpIncoming,
663
+ req: http.IncomingMessage,
664
+ res: IActiveHttpResponse
665
+ ) => {
666
+ // Get Database
667
+ let db = getDB(incoming.url[0]);
668
+
669
+ const sse = new SSE(res);
670
+ const lastEventId = req.headers["Last-Event-ID"];
671
+
672
+ // Fetch anything newer since then and push
673
+ if (lastEventId) {
674
+ // Merge with getTransactionUmids as there is little difference
675
+ const events = (await db.allDocs(
676
+ prepareAllDocs({
677
+ startkey: `event:${lastEventId}`,
678
+ endkey: `event:`,
679
+ include_docs: true,
680
+ })
681
+ )) as any;
682
+ // Start at 1 to skip the event that was already sent
683
+ for (let i = 1; i < events.rows.length; i++) {
684
+ const id = events.rows[i]._id.replace("event:", "");
685
+ delete events.rows[i]._id;
686
+ delete events.rows[i]._rev;
687
+ sse.write(id, events.rows[i]);
688
+ }
689
+ }
690
+
691
+ // Listener Process event (to turn off)
692
+ const listener = (change: any) => {
693
+ if (change.id.startsWith("event:")) {
694
+ delete change.doc._id;
695
+ delete change.doc._rev;
696
+ if (!sse.write(change.id.replace("event:", ""), change.doc)) {
697
+ cancelChanges();
698
+ }
699
+ }
700
+ };
701
+
702
+ // Stop listening for changes
703
+ const cancelChanges = () => {
704
+ changes.off("change", listener);
705
+ };
706
+
707
+ // Run on disconnect immediately, rather than only being discovered
708
+ // reactively the next time a change event fires and listener() sees
709
+ // sse.write() return false - on a quiet database that could be a long
710
+ // time (or never) after the client actually disconnects.
711
+ sse.onDisconnect(cancelChanges);
712
+
713
+ // Listening for changes
714
+ let changes = db.changes().on("change", listener);
715
+
716
+ return "handled";
717
+ }
718
+ );
719
+
720
+ // Listen for changes on the database
721
+ http.use(
722
+ "*/_changes",
723
+ "GET",
724
+ async (
725
+ incoming: IActiveHttpIncoming,
726
+ req: http.IncomingMessage,
727
+ res: http.ServerResponse
728
+ ) => {
729
+ // Get Database
730
+ let db = getDB(incoming.url[0]);
731
+
732
+ // Limit check
733
+ if (incoming.query.limit < 1) {
734
+ incoming.query.limit = 1;
735
+ }
736
+
737
+ // Convert since into number if not now
738
+ if (incoming.query.since !== "now") {
739
+ incoming.query.since = parseInt(incoming.query.since);
740
+ }
741
+
742
+ if (
743
+ incoming.query.feed === "continuous" ||
744
+ incoming.query.feed === "longpoll"
745
+ ) {
746
+ if (incoming.query.feed === "continuous") {
747
+ // Currently we do not do continuous
748
+ } else {
749
+ //Long polling heartbeat
750
+ let hBInterval = setInterval(() => {
751
+ //res.write("\n");
752
+ }, incoming.query.heartbeat || 60000);
753
+
754
+ // Clean up
755
+ let cleanUp = () => {
756
+ if (hBInterval) {
757
+ clearInterval(hBInterval);
758
+ }
759
+ };
760
+
761
+ // Set Header
762
+ //res.setHeader("Content-type", ActiveHttpd.mimeType[".json"]);
763
+
764
+ res.write(`HTTP/1.1 200 OK\r\n\r\n`);
765
+ res.write(`Connection: close\r\n`);
766
+
767
+ // Read Type
768
+ incoming.query.live = incoming.query.continuous = false;
769
+
770
+ db.changesFromSeq(incoming.query).then((complete: any) => {
771
+ if (complete.results.length) {
772
+ res.write(JSON.stringify(complete));
773
+ res.end();
774
+ cleanUp();
775
+ } else {
776
+ // do the longpolling
777
+ // mimicking CouchDB, start sending the JSON immediately
778
+ res.write('{"results":[\n');
779
+ incoming.query.live = incoming.query.continuous = true;
780
+
781
+ // Listener Process event (to turn off)
782
+ const listener = (change: any) => {
783
+ if (!req.connection.destroyed) {
784
+ res.write(JSON.stringify(change));
785
+ res.write('],\n"last_seq":' + change.seq + "}\n");
786
+ }
787
+ //res.end();
788
+ //cleanUp();
789
+ };
790
+
791
+ // Stop listening for changes
792
+ const cancelChanges = () => {
793
+ changes.off("change", listener);
794
+ //req.connection.off("close", cancelChanges);
795
+ };
796
+
797
+ // Run on close connection
798
+ //req.connection.on("close", cancelChanges);
799
+
800
+ // Listening for changes
801
+ let changes = db.changes().on("change", listener);
802
+ }
803
+ });
804
+ }
805
+ return "handled";
806
+ } else {
807
+ // Just get the latest
808
+ return await db.changesFromSeq(incoming.query);
809
+ }
810
+ }
811
+ );
812
+
813
+ // Add new / updated BULK document to the database
814
+ http.use("*/_bulk_docs", "POST", async (incoming: IActiveHttpIncoming) => {
815
+ // Can no longer be an array
816
+ if (!Array.isArray(incoming.body)) {
817
+ // Must have docs
818
+ if (incoming.body.docs) {
819
+ // Get Database
820
+ let db = getDB(incoming.url[0]);
821
+
822
+ // Prepare Archive database
823
+ // Make sure archive database exists
824
+ //const archDb = await getCreateArchiveDb(incoming.url[0]);
825
+
826
+ // We need to see if the docs need archiving
827
+ // for (let i = incoming.body.docs.length; i--; ) {
828
+ // const doc = incoming.body.docs[i];
829
+ // const position = isArchivable(doc._rev);
830
+ // if (position) {
831
+ // try {
832
+ // // Raw meta doc
833
+ // const metaDoc = await db.get(doc._id, true);
834
+
835
+ // // Time to modify!
836
+ // const newMetaDoc = prepareArchiveDoc(metaDoc, position);
837
+
838
+ // // Prevent clashes
839
+ // metaDoc._id += ":" + position;
840
+
841
+ // // We could add to existing but then we would archive the archive
842
+ // // Write document to archive
843
+ // await archDb.post(checkDoc(metaDoc));
844
+
845
+ // // Rewrite meta root document
846
+ // await db.writeRaw(newMetaDoc._id, JSON.stringify(newMetaDoc));
847
+ // } catch (e) {
848
+ // // Ignore errors and continue
849
+ // }
850
+ // }
851
+ // }
852
+
853
+ // Bulk Insert
854
+ if (await db.bulkDocs(incoming.body.docs, incoming.body.options)) {
855
+ return {
856
+ ok: true,
857
+ };
858
+ } else {
859
+ return {
860
+ ok: false,
861
+ };
862
+ }
863
+ }
864
+ }
865
+ });
866
+
867
+ // Find API
868
+ http.use("*/_find", "POST", async (incoming: IActiveHttpIncoming) => {
869
+ return { error: "find is deprecated" };
870
+ });
871
+
872
+ // Explain
873
+ http.use("*/_explain", "POST", async (incoming: IActiveHttpIncoming) => {
874
+ return { error: "explain is deprecated" };
875
+ });
876
+
877
+ // Fauxton
878
+ const fauxton = async (
879
+ incoming: IActiveHttpIncoming,
880
+ req: {
881
+ headers: {
882
+ [index: string]: string;
883
+ };
884
+ method: string;
885
+ url: string;
886
+ connection: {
887
+ remoteAddress: string;
888
+ };
889
+ },
890
+ res: IActiveHttpResponse
891
+ ) => {
892
+ // We want to force /_utils to /_utils/ as this is the CouchDB behavior
893
+ if (req.url === "/_utils") {
894
+ // res.writeHead(301, {
895
+ // Location: "/_utils/",
896
+ // });
897
+ // return;
898
+ req.url = "/_utils/";
899
+ }
900
+
901
+ // File to send
902
+ let file = FAUXTON_PATH + "/index.html";
903
+
904
+ // If path is not default overwrite
905
+ if (req.url !== "/_utils/") {
906
+ file = FAUXTON_PATH + (req.url as string).replace("/_utils", "");
907
+ }
908
+
909
+ if (fauxtonCache[file]) {
910
+ return fauxtonCache[file];
911
+ }
912
+
913
+ try {
914
+ await fsPromises.access(file);
915
+ const data = await fsPromises.readFile(file);
916
+ const response = {
917
+ mime: `${ActiveHttpd.mimeType[path.parse(file).ext] || "text/plain"}`,
918
+ data: data as Buffer,
919
+ }
920
+ fauxtonCache[file] = response;
921
+ return response;
922
+ } catch {
923
+ // Not found, will result in a 404 from httpd
924
+ }
925
+ };
926
+ http.use("_utils", "GET", fauxton);
927
+ http.use("_utils/**", "GET", fauxton);
928
+
929
+ // Start Server
930
+ http.listen(parseInt(PORT));
931
+ })();