pixl-server-storage 3.2.1 → 3.2.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 +9 -0
- package/engines/SQLite.js +136 -1
- package/index_types/Number.js +1 -1
- package/package.json +4 -4
- package/storage.js +1 -1
- package/transaction.js +2 -1
package/README.md
CHANGED
|
@@ -645,6 +645,13 @@ Then configure your storage thusly:
|
|
|
645
645
|
"cache_size": -100000,
|
|
646
646
|
"journal_mode": "WAL"
|
|
647
647
|
},
|
|
648
|
+
"backups": {
|
|
649
|
+
"enabled": false,
|
|
650
|
+
"dir": "data/backups",
|
|
651
|
+
"filename": "backup-[yyyy]-[mm]-[dd].db",
|
|
652
|
+
"compress": true,
|
|
653
|
+
"keep": 7
|
|
654
|
+
},
|
|
648
655
|
"keyPrefix": "",
|
|
649
656
|
"keyTemplate": ""
|
|
650
657
|
}
|
|
@@ -655,6 +662,8 @@ The `base_dir` defaults to the current working directory, and will be created on
|
|
|
655
662
|
|
|
656
663
|
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), [cache_size](https://www.sqlite.org/pragma.html#pragma_cache_size) and [journal_mode](https://www.sqlite.org/pragma.html#pragma_journal_mode), among many others.
|
|
657
664
|
|
|
665
|
+
The optional `backups` object allows you to configure daily backups of the SQLite database file, which happens during the [daily maintenance](#daily-maintenance) event. Set `enabled` to `true` to enable backups, then set `dir` to the directory to hold the backups, `filename` to the destination filename (you can use date/time placeholders here), `compress` to compress the backups with gzip, and `keep` to keep the latest N backups in the dir (i.e. auto-delete the oldest ones). Please note that the backup operation locks the database, so it will cause other storage operations to hang while it is writing to the file.
|
|
666
|
+
|
|
658
667
|
The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key-prefix) feature. It allows you to prefix all the SQLite keys with a common string, to separate your application's data in a shared database situation.
|
|
659
668
|
|
|
660
669
|
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.
|
package/engines/SQLite.js
CHANGED
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
// Requires the 'sqlite3' module from npm
|
|
6
6
|
// npm install --save sqlite3
|
|
7
7
|
|
|
8
|
+
const fs = require('fs');
|
|
8
9
|
const Path = require('path');
|
|
10
|
+
const zlib = require('zlib');
|
|
9
11
|
const Class = require("pixl-class");
|
|
10
12
|
const Component = require("pixl-server/component");
|
|
11
13
|
const SQLite3 = require('sqlite3');
|
|
12
14
|
const Tools = require("pixl-tools");
|
|
15
|
+
const Perf = require("pixl-perf");
|
|
13
16
|
const async = require('async');
|
|
17
|
+
const noop = function() {};
|
|
14
18
|
|
|
15
19
|
module.exports = Class.create({
|
|
16
20
|
|
|
@@ -348,7 +352,138 @@ module.exports = Class.create({
|
|
|
348
352
|
|
|
349
353
|
runMaintenance: function(callback) {
|
|
350
354
|
// run daily maintenance
|
|
351
|
-
|
|
355
|
+
// config.backups: { enabled, dir, filename, compress?, keep? }
|
|
356
|
+
var self = this;
|
|
357
|
+
var backups = this.config.get('backups') || null;
|
|
358
|
+
if (!backups || !backups.enabled || !backups.dir || !backups.filename) return callback();
|
|
359
|
+
|
|
360
|
+
// here be dragons: try to wait for a moment BETWEEN transaction commits, to minimize risk of an "inconsistent" backup
|
|
361
|
+
if (this.storage.transactions && Tools.findObject(Object.values(this.storage.transactions), { committing: true })) {
|
|
362
|
+
// wait for next commitEnd event and try again
|
|
363
|
+
this.logDebug(9, "Maintenance: Transaction commit(s) are in progress, waiting until next commitEnd event...");
|
|
364
|
+
|
|
365
|
+
this.storage.once('commitEnd', function(trans) {
|
|
366
|
+
self.logDebug(9, "Maintenance: Transaction commit " + trans.id + " (" + trans.path + ") ended, retrying now...");
|
|
367
|
+
self.runMaintenance(callback);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// track perf for logging
|
|
374
|
+
var perf = new Perf();
|
|
375
|
+
perf.begin();
|
|
376
|
+
perf.begin('prep');
|
|
377
|
+
|
|
378
|
+
// allow dir & filename to have date/time placeholders, e.g. `backup-[yyyy]-[mm]-[dd].db`
|
|
379
|
+
var dargs = Tools.getDateArgs( Tools.timeNow() );
|
|
380
|
+
var file = Tools.sub( Path.join(backups.dir, backups.filename), dargs, true );
|
|
381
|
+
if (!file) {
|
|
382
|
+
this.logError('backup', "Failed to expand placeholders on backup file: " + backups.filename);
|
|
383
|
+
return callback();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// strip .gz prefix, just in case the user added it
|
|
387
|
+
file = file.replace(/\.gz$/i, '');
|
|
388
|
+
|
|
389
|
+
// auto-create parent dirs as needed
|
|
390
|
+
if (!fs.existsSync(backups.dir)) {
|
|
391
|
+
try { Tools.mkdirp.sync(backups.dir); }
|
|
392
|
+
catch (err) {
|
|
393
|
+
this.logError('backup', "Failed to create backup directory: " + backups.dir);
|
|
394
|
+
return callback();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// here we go
|
|
399
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
400
|
+
this.logDebug(6, "Performing database backup to: " + file);
|
|
401
|
+
perf.end('prep');
|
|
402
|
+
|
|
403
|
+
var finish = function() {
|
|
404
|
+
// all done
|
|
405
|
+
perf.end();
|
|
406
|
+
self.logDebug(6, "Maintenance complete", perf.metrics());
|
|
407
|
+
callback();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
var keep = function() {
|
|
411
|
+
// delete oldest backups if over N on disk
|
|
412
|
+
var files = Tools.findFilesSync( backups.dir, { stats: true } );
|
|
413
|
+
if (files.length <= backups.keep) return finish();
|
|
414
|
+
perf.begin('keep');
|
|
415
|
+
|
|
416
|
+
self.logDebug(6, "There are " + files.length + " backup files in " + backups.dir + ", keeping the latest " + backups.keep);
|
|
417
|
+
|
|
418
|
+
// sort mtime descending so the newest are at the start of the array, then splice those off, leaving only the oldest to delete
|
|
419
|
+
Tools.sortBy( files, 'mtime', { type: 'number', dir: -1 } );
|
|
420
|
+
files.splice( 0, backups.keep );
|
|
421
|
+
|
|
422
|
+
files.forEach( function(file) {
|
|
423
|
+
self.logDebug(7, "Deleting old backup file: " + file.path, file);
|
|
424
|
+
try { fs.unlinkSync(file.path); } catch (err) {
|
|
425
|
+
self.logError('backup', "Failed to delete backup file: " + file.path + ": " + err);
|
|
426
|
+
}
|
|
427
|
+
} );
|
|
428
|
+
|
|
429
|
+
perf.end('keep');
|
|
430
|
+
finish();
|
|
431
|
+
}; // keep
|
|
432
|
+
|
|
433
|
+
var compress = function() {
|
|
434
|
+
// compress backup and delete original
|
|
435
|
+
var gz_file = file + '.gz';
|
|
436
|
+
perf.begin('compress');
|
|
437
|
+
|
|
438
|
+
if (fs.existsSync(gz_file)) fs.unlinkSync(gz_file);
|
|
439
|
+
self.logDebug(6, "Compressing backup to: " + gz_file);
|
|
440
|
+
|
|
441
|
+
var gzip = zlib.createGzip();
|
|
442
|
+
var inp = fs.createReadStream( file );
|
|
443
|
+
var outp = fs.createWriteStream( gz_file );
|
|
444
|
+
|
|
445
|
+
var handleError = function(err) {
|
|
446
|
+
self.logError('backup', "Failed to compress backup: " + gz_file + ": " + err);
|
|
447
|
+
fs.unlink(gz_file, noop);
|
|
448
|
+
|
|
449
|
+
perf.end('compress');
|
|
450
|
+
if (backups.keep) keep();
|
|
451
|
+
else finish();
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
gzip.on('error', handleError);
|
|
455
|
+
inp.on('error', handleError);
|
|
456
|
+
outp.on('error', handleError);
|
|
457
|
+
|
|
458
|
+
inp.pipe(gzip).pipe(outp).on('finish', function() {
|
|
459
|
+
self.logDebug(6, "Backup compression complete: " + gz_file);
|
|
460
|
+
fs.unlink(file, noop);
|
|
461
|
+
|
|
462
|
+
perf.end('compress');
|
|
463
|
+
if (backups.keep) keep();
|
|
464
|
+
else finish();
|
|
465
|
+
} );
|
|
466
|
+
}; // compress
|
|
467
|
+
|
|
468
|
+
// Perform the backup -- unfortunately this locks the DB, and we can't use progressive mode
|
|
469
|
+
// because of how we handle transactions "outside" of SQLite's control.
|
|
470
|
+
perf.begin('backup');
|
|
471
|
+
|
|
472
|
+
this.db.run('VACUUM INTO $filename', { $filename: Path.resolve(file) }, function(err) {
|
|
473
|
+
perf.end('backup');
|
|
474
|
+
if (err) {
|
|
475
|
+
self.logError('backup', "SQLite backup operation failed: " + err);
|
|
476
|
+
fs.unlink(file, noop);
|
|
477
|
+
return finish();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
self.logDebug(6, "SQLite backup operation completed: " + file);
|
|
481
|
+
|
|
482
|
+
// now compress or keep or done
|
|
483
|
+
if (backups.compress) compress();
|
|
484
|
+
else if (backups.keep) keep();
|
|
485
|
+
else finish();
|
|
486
|
+
});
|
|
352
487
|
},
|
|
353
488
|
|
|
354
489
|
shutdown: function(callback) {
|
package/index_types/Number.js
CHANGED
|
@@ -83,7 +83,7 @@ module.exports = Class.create({
|
|
|
83
83
|
|
|
84
84
|
filterWords_number: function(value) {
|
|
85
85
|
// filter number queries
|
|
86
|
-
value = value.replace(/[^\d\-]+/g, '').replace(/\-/, 'N');
|
|
86
|
+
value = value.replace(/\.\d+$/, '').replace(/[^\d\-]+/g, '').replace(/\-/, 'N');
|
|
87
87
|
return value;
|
|
88
88
|
},
|
|
89
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixl-server-storage",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.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",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"sqlite"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@aws-sdk/client-s3": "3.
|
|
26
|
-
"@aws-sdk/lib-storage": "3.
|
|
27
|
-
"@smithy/node-http-handler": "
|
|
25
|
+
"@aws-sdk/client-s3": "3.621.0",
|
|
26
|
+
"@aws-sdk/lib-storage": "3.621.0",
|
|
27
|
+
"@smithy/node-http-handler": "3.1.4",
|
|
28
28
|
"async": "2.6.4",
|
|
29
29
|
"fast-stream-to-buffer": "1.0.0",
|
|
30
30
|
"he": "1.1.1",
|
package/storage.js
CHANGED
|
@@ -721,7 +721,7 @@ module.exports = Class.create({
|
|
|
721
721
|
var doEngineMaint = function() {
|
|
722
722
|
// allow engine to run maint as well
|
|
723
723
|
self.engine.runMaintenance( function() {
|
|
724
|
-
stats.elapsed_sec = Tools.timeNow() - stats.time_start;
|
|
724
|
+
stats.elapsed_sec = Tools.shortFloat( Tools.timeNow() - stats.time_start );
|
|
725
725
|
self.logDebug(3, "Daily maintenance complete");
|
|
726
726
|
self.logTransaction('maint', cleanup_list_path, stats);
|
|
727
727
|
if (callback) callback();
|
package/transaction.js
CHANGED
|
@@ -648,6 +648,7 @@ module.exports = Class.create({
|
|
|
648
648
|
self.logDebug(3, "Transaction rollback complete: " + trans.id, { path: path });
|
|
649
649
|
|
|
650
650
|
// unlock at the VERY end, as a new transaction may be waiting on the same path
|
|
651
|
+
self.emit('commitEnd', trans, "rollback");
|
|
651
652
|
self.unlock( 'C|'+path );
|
|
652
653
|
self._transUnlock(path);
|
|
653
654
|
|
|
@@ -787,9 +788,9 @@ module.exports = Class.create({
|
|
|
787
788
|
var elapsed = pf.end();
|
|
788
789
|
|
|
789
790
|
if (err) {
|
|
791
|
+
// Note: in this case unlocking happens in abortTransaction
|
|
790
792
|
var msg = "Failed to commit transaction: " + path + ": " + err.message;
|
|
791
793
|
self.logError('commit', msg, { id: trans.id });
|
|
792
|
-
self.emit('commitEnd', trans, err);
|
|
793
794
|
return callback( new Error(msg) );
|
|
794
795
|
}
|
|
795
796
|
|