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 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
- callback();
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) {
@@ -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.1",
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.414.0",
26
- "@aws-sdk/lib-storage": "3.414.0",
27
- "@smithy/node-http-handler": "2.1.4",
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