@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.
- package/es/cache.d.ts +18 -0
- package/es/cache.d.ts.map +1 -0
- package/es/cache.js +57 -0
- package/es/cache.js.map +1 -0
- package/es/datastore.d.ts +1 -0
- package/es/datastore.d.ts.map +1 -1
- package/es/datastore.js +8 -3
- package/es/datastore.js.map +1 -1
- package/es/driver.d.ts +16 -0
- package/es/driver.d.ts.map +1 -0
- package/es/driver.js +2 -0
- package/es/driver.js.map +1 -0
- package/es/drivers/leveldb.d.ts +21 -0
- package/es/drivers/leveldb.d.ts.map +1 -0
- package/es/drivers/leveldb.js +121 -0
- package/es/drivers/leveldb.js.map +1 -0
- package/es/drivers/rocksdb.d.ts +21 -0
- package/es/drivers/rocksdb.d.ts.map +1 -0
- package/es/drivers/rocksdb.js +98 -0
- package/es/drivers/rocksdb.js.map +1 -0
- package/es/heartbeat.d.ts +7 -0
- package/es/heartbeat.d.ts.map +1 -0
- package/es/heartbeat.js +15 -0
- package/es/heartbeat.js.map +1 -0
- package/es/levelme.d.ts +18 -18
- package/es/levelme.d.ts.map +1 -1
- package/es/levelme.js +290 -155
- package/es/levelme.js.map +1 -1
- package/es/newlinestream.d.ts +4 -0
- package/es/newlinestream.d.ts.map +1 -0
- package/es/newlinestream.js +28 -0
- package/es/newlinestream.js.map +1 -0
- package/es/selfhost.js +157 -58
- package/es/selfhost.js.map +1 -1
- package/es/sse.d.ts +10 -0
- package/es/sse.d.ts.map +1 -0
- package/es/sse.js +41 -0
- package/es/sse.js.map +1 -0
- package/lib/cache.d.ts +18 -0
- package/lib/cache.d.ts.map +1 -0
- package/lib/cache.js +62 -0
- package/lib/cache.js.map +1 -0
- package/lib/datastore.d.ts +1 -0
- package/lib/datastore.d.ts.map +1 -1
- package/lib/datastore.js +8 -3
- package/lib/datastore.js.map +1 -1
- package/lib/driver.d.ts +16 -0
- package/lib/driver.d.ts.map +1 -0
- package/lib/driver.js +3 -0
- package/lib/driver.js.map +1 -0
- package/lib/drivers/leveldb.d.ts +21 -0
- package/lib/drivers/leveldb.d.ts.map +1 -0
- package/lib/drivers/leveldb.js +125 -0
- package/lib/drivers/leveldb.js.map +1 -0
- package/lib/drivers/rocksdb.d.ts +21 -0
- package/lib/drivers/rocksdb.d.ts.map +1 -0
- package/lib/drivers/rocksdb.js +102 -0
- package/lib/drivers/rocksdb.js.map +1 -0
- package/lib/heartbeat.d.ts +7 -0
- package/lib/heartbeat.d.ts.map +1 -0
- package/lib/heartbeat.js +19 -0
- package/lib/heartbeat.js.map +1 -0
- package/lib/levelme.d.ts +18 -18
- package/lib/levelme.d.ts.map +1 -1
- package/lib/levelme.js +312 -157
- package/lib/levelme.js.map +1 -1
- package/lib/newlinestream.d.ts +4 -0
- package/lib/newlinestream.d.ts.map +1 -0
- package/lib/newlinestream.js +32 -0
- package/lib/newlinestream.js.map +1 -0
- package/lib/selfhost.js +157 -58
- package/lib/selfhost.js.map +1 -1
- package/lib/sse.d.ts +10 -0
- package/lib/sse.d.ts.map +1 -0
- package/lib/sse.js +45 -0
- package/lib/sse.js.map +1 -0
- package/package.json +8 -18
- package/src/cache.ts +72 -0
- package/src/datastore.ts +187 -0
- package/src/driver.ts +18 -0
- package/src/drivers/leveldb.ts +145 -0
- package/src/drivers/rocksdb.ts +118 -0
- package/src/fauxton/dashboard.assets/css/styles.d43d3249088a6c04b633.css +33 -0
- package/src/fauxton/dashboard.assets/fonts/fauxtonicon5.eot +0 -0
- package/src/fauxton/dashboard.assets/fonts/fauxtonicon5.ttf +0 -0
- package/src/fauxton/dashboard.assets/fonts/fauxtonicon5.woff +0 -0
- package/src/fauxton/dashboard.assets/fonts/fontawesome-webfont.eot +0 -0
- package/src/fauxton/dashboard.assets/fonts/fontawesome-webfont.ttf +0 -0
- package/src/fauxton/dashboard.assets/fonts/fontawesome-webfont.woff +0 -0
- package/src/fauxton/dashboard.assets/img/activeledger-logo.png +0 -0
- package/src/fauxton/dashboard.assets/img/activeledger.png +0 -0
- package/src/fauxton/dashboard.assets/img/asf-feather-logo.png +0 -0
- package/src/fauxton/dashboard.assets/img/couch-watermark.png +0 -0
- package/src/fauxton/dashboard.assets/img/couchdb-logo.png +0 -0
- package/src/fauxton/dashboard.assets/img/fauxtonicon5.svg +729 -0
- package/src/fauxton/dashboard.assets/img/fontawesome-webfont.svg +399 -0
- package/src/fauxton/dashboard.assets/img/github.png +0 -0
- package/src/fauxton/dashboard.assets/img/googleplus.png +0 -0
- package/src/fauxton/dashboard.assets/img/linkedin.png +0 -0
- package/src/fauxton/dashboard.assets/img/loader.gif +0 -0
- package/src/fauxton/dashboard.assets/img/twitter.png +0 -0
- package/src/fauxton/dashboard.assets/js/bundle.d43d3249088a6c04b633.js +1 -0
- package/src/fauxton/dashboard.assets/js/manifest.a154524a560e5f6f431b.js +1 -0
- package/src/fauxton/dashboard.assets/js/vendor.156793045cdeb7c63707.js +96 -0
- package/src/fauxton/index.html +52 -0
- package/src/heartbeat.ts +60 -0
- package/src/index.ts +25 -0
- package/src/levelme.ts +901 -0
- package/src/newlinestream.ts +32 -0
- package/src/selfhost.ts +931 -0
- package/src/sse.ts +136 -0
- package/src/types/rocksdb.d.ts +1 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/es/pouchdb.d.ts +0 -4
- package/es/pouchdb.d.ts.map +0 -1
- package/es/pouchdb.js +0 -16
- package/es/pouchdb.js.map +0 -1
- package/lib/pouchdb.d.ts +0 -4
- package/lib/pouchdb.d.ts.map +0 -1
- package/lib/pouchdb.js +0 -22
- package/lib/pouchdb.js.map +0 -1
package/src/selfhost.ts
ADDED
|
@@ -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
|
+
})();
|