pixl-server-storage 3.1.1 → 3.1.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/README.md +28 -0
- package/engines/SQLite.js +375 -0
- package/package.json +8 -7
- package/test/config.json +8 -0
package/README.md
CHANGED
|
@@ -611,6 +611,34 @@ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key
|
|
|
611
611
|
|
|
612
612
|
The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3-key-template) feature. It allows you to specify an exact layout of MD5 hash characters, which can be prefixed, mixed in with or postfixed after the key.
|
|
613
613
|
|
|
614
|
+
## SQLite
|
|
615
|
+
|
|
616
|
+
If you want to use [SQLite](https://sqlite.com/) as a backing store, here is how to do so. First, you need to manually install the [sqlite3](https://www.npmjs.com/package/sqlite3) module into your app:
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
npm install --save sqlite3
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Then configure your storage thusly:
|
|
623
|
+
|
|
624
|
+
```javascript
|
|
625
|
+
{
|
|
626
|
+
"engine": "SQLite",
|
|
627
|
+
"SQLite": {
|
|
628
|
+
"base_dir": "data",
|
|
629
|
+
"filename": "sqlite.db",
|
|
630
|
+
"pragmas": {
|
|
631
|
+
"auto_vacuum": 0,
|
|
632
|
+
"cache_size": -100000
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
The `base_dir` defaults to the current working directory, and will be created on startup if necessary. The `filename` is the name of the SQLite DB file on disk (also created if necessary).
|
|
639
|
+
|
|
640
|
+
The optional `pragmas` object allows you set one or more [SQLite Pragmas](https://www.sqlite.org/pragma.html#toc) (configuration settings) on the database at startup. Here you can specify things such as [auto_vacuum](https://www.sqlite.org/pragma.html#pragma_auto_vacuum) and [cache_size](https://www.sqlite.org/pragma.html#pragma_cache_size), among many others.
|
|
641
|
+
|
|
614
642
|
## Hybrid
|
|
615
643
|
|
|
616
644
|
Your application may need the features of multiple engines. Specifically, you may want JSON (document) records to use one engine, and binary records to use another. Binary records are specified with keys that end in a file extension, e.g. `.jpg`. To facilitate this, there is a `Hybrid` engine available, which can load multiple sub-engines, one for JSON keys and one for binary keys. Example use:
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// SQLite Storage Plugin
|
|
2
|
+
// Copyright (c) 2023 Joseph Huckaby
|
|
3
|
+
// Released under the MIT License
|
|
4
|
+
|
|
5
|
+
// Requires the 'sqlite3' module from npm
|
|
6
|
+
// npm install --save sqlite3
|
|
7
|
+
|
|
8
|
+
const Path = require('path');
|
|
9
|
+
const Class = require("pixl-class");
|
|
10
|
+
const Component = require("pixl-server/component");
|
|
11
|
+
const SQLite3 = require('sqlite3');
|
|
12
|
+
const Tools = require("pixl-tools");
|
|
13
|
+
const async = require('async');
|
|
14
|
+
|
|
15
|
+
module.exports = Class.create({
|
|
16
|
+
|
|
17
|
+
__name: 'SQLite',
|
|
18
|
+
__parent: Component,
|
|
19
|
+
|
|
20
|
+
defaultConfig: {
|
|
21
|
+
base_dir: '',
|
|
22
|
+
filename: 'sqlite.db',
|
|
23
|
+
keyPrefix: "",
|
|
24
|
+
keyTemplate: ""
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
startup: function(callback) {
|
|
28
|
+
// setup SQLite connection
|
|
29
|
+
var self = this;
|
|
30
|
+
this.logDebug(2, "Setting up SQLite", this.config.get() );
|
|
31
|
+
this.setup(callback);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
setup: function(callback) {
|
|
35
|
+
// setup SQLite connection
|
|
36
|
+
var self = this;
|
|
37
|
+
var sql_config = this.config.get();
|
|
38
|
+
|
|
39
|
+
this.keyPrefix = (sql_config.keyPrefix || '').replace(/^\//, '');
|
|
40
|
+
if (this.keyPrefix && !this.keyPrefix.match(/\/$/)) this.keyPrefix += '/';
|
|
41
|
+
|
|
42
|
+
this.keyTemplate = (sql_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
|
|
43
|
+
|
|
44
|
+
this.baseDir = sql_config.base_dir || process.cwd();
|
|
45
|
+
this.commands = {};
|
|
46
|
+
|
|
47
|
+
// create initial data dir if necessary
|
|
48
|
+
try {
|
|
49
|
+
Tools.mkdirp.sync( this.baseDir );
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
var msg = "FATAL ERROR: Base directory could not be created: " + this.baseDir + ": " + e;
|
|
53
|
+
this.logError('sqlite', msg);
|
|
54
|
+
throw new Error(msg);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
var db_file = Path.join( this.baseDir, sql_config.filename );
|
|
58
|
+
this.logDebug(3, "Opening database file: " + db_file);
|
|
59
|
+
|
|
60
|
+
async.series([
|
|
61
|
+
function(callback) {
|
|
62
|
+
self.db = new SQLite3.Database( db_file, callback );
|
|
63
|
+
},
|
|
64
|
+
function(callback) {
|
|
65
|
+
// optionally set pragmas on the db
|
|
66
|
+
if (!sql_config.pragmas) return process.nextTick(callback);
|
|
67
|
+
async.eachSeries( Object.keys(sql_config.pragmas),
|
|
68
|
+
function(key, callback) {
|
|
69
|
+
var value = sql_config.pragmas[key];
|
|
70
|
+
self.db.run(`PRAGMA ${key} = ${value};`, callback);
|
|
71
|
+
},
|
|
72
|
+
callback
|
|
73
|
+
); // eachSeries
|
|
74
|
+
},
|
|
75
|
+
function(callback) {
|
|
76
|
+
// create our table if necessary
|
|
77
|
+
self.db.run( 'CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BLOB, modified INTEGER )', callback );
|
|
78
|
+
},
|
|
79
|
+
function(callback) {
|
|
80
|
+
self.commands.get = self.db.prepare( 'SELECT value FROM items WHERE key = $key LIMIT 1', callback );
|
|
81
|
+
},
|
|
82
|
+
function(callback) {
|
|
83
|
+
self.commands.head = self.db.prepare( 'SELECT modified, length(value) FROM items WHERE key = $key LIMIT 1', callback );
|
|
84
|
+
},
|
|
85
|
+
function(callback) {
|
|
86
|
+
self.commands.put = self.db.prepare( 'INSERT INTO items VALUES($key, $value, $now) ON CONFLICT (key) DO UPDATE SET value = $value, modified = $now WHERE key = $key', callback );
|
|
87
|
+
},
|
|
88
|
+
function(callback) {
|
|
89
|
+
self.commands.delete = self.db.prepare( 'DELETE FROM items WHERE key = $key', callback );
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
function(err) {
|
|
93
|
+
if (err) {
|
|
94
|
+
self.logError('sqlite', "FATAL ERROR: Database setup failed: " + err);
|
|
95
|
+
return callback(err);
|
|
96
|
+
}
|
|
97
|
+
self.logDebug(3, "Setup complete");
|
|
98
|
+
callback();
|
|
99
|
+
}); // async.series
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
prepKey: function(key) {
|
|
103
|
+
// prepare key based on config
|
|
104
|
+
var md5 = Tools.digestHex(key, 'md5');
|
|
105
|
+
|
|
106
|
+
if (this.keyPrefix) {
|
|
107
|
+
key = this.keyPrefix + key;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.keyTemplate) {
|
|
111
|
+
var idx = 0;
|
|
112
|
+
var temp = this.keyTemplate.replace( /\#/g, function() {
|
|
113
|
+
return md5.substr(idx++, 1);
|
|
114
|
+
} );
|
|
115
|
+
key = Tools.substitute( temp, { key: key, md5: md5 } );
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return key;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
put: function(key, value, callback) {
|
|
122
|
+
// store key+value in SQLite
|
|
123
|
+
var self = this;
|
|
124
|
+
var now = Tools.timeNow(true);
|
|
125
|
+
key = this.prepKey(key);
|
|
126
|
+
|
|
127
|
+
if (this.storage.isBinaryKey(key)) {
|
|
128
|
+
this.logDebug(9, "Storing SQLite Binary Object: " + key, '' + value.length + ' bytes');
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.logDebug(9, "Storing SQLite JSON Object: " + key, this.debugLevel(10) ? value : null);
|
|
132
|
+
value = Buffer.from( JSON.stringify( value ) );
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.commands.put.run({ $key: key, $value: value, $now: now }, function(err) {
|
|
136
|
+
if (err) {
|
|
137
|
+
err.message = "Failed to store object: " + key + ": " + err;
|
|
138
|
+
self.logError('sqlite', '' + err);
|
|
139
|
+
}
|
|
140
|
+
else self.logDebug(9, "Store complete: " + key);
|
|
141
|
+
|
|
142
|
+
if (callback) callback(err);
|
|
143
|
+
} ); // put
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
putStream: function(key, inp, callback) {
|
|
147
|
+
// store key+value in SQLite using read stream
|
|
148
|
+
var self = this;
|
|
149
|
+
|
|
150
|
+
// The SQLite API has no stream support.
|
|
151
|
+
// So, we have to do this the RAM-hard way...
|
|
152
|
+
|
|
153
|
+
var chunks = [];
|
|
154
|
+
inp.on('data', function(chunk) {
|
|
155
|
+
chunks.push( chunk );
|
|
156
|
+
} );
|
|
157
|
+
inp.on('end', function() {
|
|
158
|
+
var buf = Buffer.concat(chunks);
|
|
159
|
+
self.put( key, buf, callback );
|
|
160
|
+
} );
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
head: function(key, callback) {
|
|
164
|
+
// head sqlite item given key
|
|
165
|
+
var self = this;
|
|
166
|
+
key = this.prepKey(key);
|
|
167
|
+
|
|
168
|
+
this.commands.head.get({ $key: key }, function(err, row) {
|
|
169
|
+
if (err) {
|
|
170
|
+
// an actual error
|
|
171
|
+
err.message = "Failed to head key: " + key + ": " + err;
|
|
172
|
+
self.logError('sqlite', '' + err);
|
|
173
|
+
callback(err);
|
|
174
|
+
}
|
|
175
|
+
else if (!row) {
|
|
176
|
+
// record not found
|
|
177
|
+
// always use "NoSuchKey" in error code
|
|
178
|
+
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
179
|
+
err.code = "NoSuchKey";
|
|
180
|
+
callback( err, null );
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
callback( null, { mod: row.modified, len: row['length(value)'] } );
|
|
184
|
+
}
|
|
185
|
+
}); // head
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
get: function(key, callback) {
|
|
189
|
+
// fetch SQLite value given key
|
|
190
|
+
var self = this;
|
|
191
|
+
key = this.prepKey(key);
|
|
192
|
+
|
|
193
|
+
this.logDebug(9, "Fetching SQLite Object: " + key);
|
|
194
|
+
|
|
195
|
+
this.commands.get.get({ $key: key }, function(err, row) {
|
|
196
|
+
if (err) {
|
|
197
|
+
// an actual error
|
|
198
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
199
|
+
self.logError('sqlite', '' + err);
|
|
200
|
+
callback(err);
|
|
201
|
+
}
|
|
202
|
+
else if (!row) {
|
|
203
|
+
// record not found
|
|
204
|
+
// always use "NoSuchKey" in error code
|
|
205
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
206
|
+
err.code = "NoSuchKey";
|
|
207
|
+
callback( err, null );
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// success
|
|
211
|
+
if (self.storage.isBinaryKey(key)) {
|
|
212
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
|
|
213
|
+
callback( null, row.value );
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
var json = null;
|
|
217
|
+
try { json = JSON.parse( row.value.toString() ); }
|
|
218
|
+
catch (err) {
|
|
219
|
+
self.logError('sqlite', "Failed to parse JSON record: " + key + ": " + err);
|
|
220
|
+
callback( err, null );
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? json : null);
|
|
224
|
+
callback( null, json );
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}); // get
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
getStream: function(key, callback) {
|
|
231
|
+
// get readable stream to record value given key
|
|
232
|
+
var self = this;
|
|
233
|
+
|
|
234
|
+
// The SQLite API has no stream support.
|
|
235
|
+
// So, we have to do this the RAM-hard way...
|
|
236
|
+
|
|
237
|
+
this.get( key, function(err, buf) {
|
|
238
|
+
if (err) {
|
|
239
|
+
// an actual error
|
|
240
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
241
|
+
self.logError('sqlite', '' + err);
|
|
242
|
+
return callback(err);
|
|
243
|
+
}
|
|
244
|
+
else if (!buf) {
|
|
245
|
+
// record not found
|
|
246
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
247
|
+
err.code = "NoSuchKey";
|
|
248
|
+
return callback( err, null );
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
var stream = new BufferStream(buf);
|
|
252
|
+
callback(null, stream, { mod: 1, len: buf.length });
|
|
253
|
+
} );
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
getStreamRange: function(key, start, end, callback) {
|
|
257
|
+
// get readable stream to record value given key and range
|
|
258
|
+
var self = this;
|
|
259
|
+
|
|
260
|
+
// The SQLite API has no stream support.
|
|
261
|
+
// So, we have to do this the RAM-hard way...
|
|
262
|
+
|
|
263
|
+
this.get( key, function(err, buf) {
|
|
264
|
+
if (err) {
|
|
265
|
+
// an actual error
|
|
266
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
267
|
+
self.logError('sqlite', '' + err);
|
|
268
|
+
return callback(err);
|
|
269
|
+
}
|
|
270
|
+
else if (!buf) {
|
|
271
|
+
// record not found
|
|
272
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
273
|
+
err.code = "NoSuchKey";
|
|
274
|
+
return callback( err, null );
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// validate byte range, now that we have the head info
|
|
278
|
+
if (isNaN(start) && !isNaN(end)) {
|
|
279
|
+
start = buf.length - end;
|
|
280
|
+
end = buf.length ? buf.length - 1 : 0;
|
|
281
|
+
}
|
|
282
|
+
else if (!isNaN(start) && isNaN(end)) {
|
|
283
|
+
end = buf.length ? buf.length - 1 : 0;
|
|
284
|
+
}
|
|
285
|
+
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) {
|
|
286
|
+
download.destroy();
|
|
287
|
+
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null );
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
var range = buf.slice(start, end + 1);
|
|
292
|
+
var stream = new BufferStream(range);
|
|
293
|
+
callback(null, stream, { mod: 1, len: buf.length });
|
|
294
|
+
} );
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
delete: function(key, callback) {
|
|
298
|
+
// delete SQLite key given key
|
|
299
|
+
var self = this;
|
|
300
|
+
key = this.prepKey(key);
|
|
301
|
+
|
|
302
|
+
this.logDebug(9, "Deleting SQLite Object: " + key);
|
|
303
|
+
|
|
304
|
+
this.commands.delete.run({ $key: key }, function(err) {
|
|
305
|
+
// In sqlite3 callbacks `this` is special, and contains `changes`
|
|
306
|
+
if (!err && !this.changes) {
|
|
307
|
+
err = new Error("Not found");
|
|
308
|
+
err.code = "NoSuchKey";
|
|
309
|
+
}
|
|
310
|
+
if (err) {
|
|
311
|
+
self.logError('sqlite', "Failed to delete object: " + key + ": " + err);
|
|
312
|
+
err = new Error("Failed to delete object: " + key + ": " + err);
|
|
313
|
+
return callback(err);
|
|
314
|
+
}
|
|
315
|
+
self.logDebug(9, "Delete complete: " + key);
|
|
316
|
+
callback();
|
|
317
|
+
}); // delete
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
runMaintenance: function(callback) {
|
|
321
|
+
// run daily maintenance
|
|
322
|
+
callback();
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
shutdown: function(callback) {
|
|
326
|
+
// shutdown storage
|
|
327
|
+
var self = this;
|
|
328
|
+
this.logDebug(2, "Shutting down SQLite");
|
|
329
|
+
|
|
330
|
+
if (this.db) {
|
|
331
|
+
// finalize all statements and close db
|
|
332
|
+
async.eachSeries( Object.keys(this.commands),
|
|
333
|
+
function(key, callback) {
|
|
334
|
+
self.commands[key].finalize(callback);
|
|
335
|
+
},
|
|
336
|
+
function() {
|
|
337
|
+
self.db.close( function(err) {
|
|
338
|
+
if (err) self.logError('sqlite', "Failed to shutdown database cleanly: " + err);
|
|
339
|
+
else self.logDebug(3, "Shutdown complete");
|
|
340
|
+
callback();
|
|
341
|
+
} );
|
|
342
|
+
self.db = null;
|
|
343
|
+
self.commands = null;
|
|
344
|
+
}
|
|
345
|
+
); // eachSeries
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Modified the following snippet from node-streamifier:
|
|
352
|
+
// Copyright (c) 2014 Gabriel Llamas, MIT Licensed
|
|
353
|
+
|
|
354
|
+
var util = require('util');
|
|
355
|
+
var stream = require('stream');
|
|
356
|
+
|
|
357
|
+
var BufferStream = function (object, options) {
|
|
358
|
+
if (object instanceof Buffer || typeof object === 'string') {
|
|
359
|
+
options = options || {};
|
|
360
|
+
stream.Readable.call(this, {
|
|
361
|
+
highWaterMark: options.highWaterMark,
|
|
362
|
+
encoding: options.encoding
|
|
363
|
+
});
|
|
364
|
+
} else {
|
|
365
|
+
stream.Readable.call(this, { objectMode: true });
|
|
366
|
+
}
|
|
367
|
+
this._object = object;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
util.inherits(BufferStream, stream.Readable);
|
|
371
|
+
|
|
372
|
+
BufferStream.prototype._read = function () {
|
|
373
|
+
this.push(this._object);
|
|
374
|
+
this._object = null;
|
|
375
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixl-server-storage",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "A key/value/list storage component for the pixl-server framework.",
|
|
5
5
|
"author": "Joseph Huckaby <jhuckaby@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/jhuckaby/pixl-server-storage",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"nosql",
|
|
19
19
|
"s3",
|
|
20
20
|
"couchbase",
|
|
21
|
-
"redis"
|
|
21
|
+
"redis",
|
|
22
|
+
"sqlite"
|
|
22
23
|
],
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@aws-sdk/client-s3": "3.186.0",
|
|
@@ -27,16 +28,16 @@
|
|
|
27
28
|
"he": "1.1.1",
|
|
28
29
|
"moo": "0.4.3",
|
|
29
30
|
"nearley": "2.20.1",
|
|
30
|
-
"pixl-cache": "^1.0.
|
|
31
|
+
"pixl-cache": "^1.0.6",
|
|
31
32
|
"pixl-class": "^1.0.3",
|
|
32
|
-
"pixl-perf": "^1.0.
|
|
33
|
-
"pixl-server": "^1.0.
|
|
34
|
-
"pixl-tools": "^1.0.
|
|
33
|
+
"pixl-perf": "^1.0.9",
|
|
34
|
+
"pixl-server": "^1.0.39",
|
|
35
|
+
"pixl-tools": "^1.0.33",
|
|
35
36
|
"porter-stemmer": "0.9.1",
|
|
36
37
|
"unidecode": "0.1.8"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"pixl-unit": "^1.0.
|
|
40
|
+
"pixl-unit": "^1.0.14"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"test": "pixl-unit test/test.js",
|