pixl-server-storage 4.0.3 → 4.1.1
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 +81 -10
- package/engines/Couchbase.js +0 -1
- package/engines/Filesystem.js +59 -28
- package/engines/Postgres.js +515 -0
- package/engines/Redis.js +129 -29
- package/engines/RedisCluster.js +128 -29
- package/engines/S3.js +53 -25
- package/engines/SQLite.js +65 -22
- package/package.json +1 -1
- package/test/config.json +19 -0
- package/test/data/backups/backup-2026-04-05-20-23-56.db.gz +0 -0
- package/test/data/backups/backup-2026-06-08-20-33-01.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-10-21.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-10-33.db.gz +0 -0
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Here is the table of contents for this current document:
|
|
|
58
58
|
* [Redis](#redis)
|
|
59
59
|
+ [RedisCluster](#rediscluster)
|
|
60
60
|
* [SQLite](#sqlite)
|
|
61
|
+
* [Postgres](#postgres)
|
|
61
62
|
* [Hybrid](#hybrid)
|
|
62
63
|
- [Key Normalization](#key-normalization)
|
|
63
64
|
- [Basic Functions](#basic-functions)
|
|
@@ -396,8 +397,8 @@ To enable the filesystem cache, include a `cache` object in your `Filesystem` co
|
|
|
396
397
|
"base_dir": "/let/data/myserver",
|
|
397
398
|
"cache": {
|
|
398
399
|
"enabled": true,
|
|
399
|
-
"maxItems":
|
|
400
|
-
"maxBytes":
|
|
400
|
+
"maxItems": 100000,
|
|
401
|
+
"maxBytes": 104857600
|
|
401
402
|
}
|
|
402
403
|
}
|
|
403
404
|
}
|
|
@@ -440,8 +441,8 @@ If you want to use [Amazon S3](http://aws.amazon.com/s3/) as a backing store, co
|
|
|
440
441
|
},
|
|
441
442
|
"cache": {
|
|
442
443
|
"enabled": true,
|
|
443
|
-
"maxItems":
|
|
444
|
-
"maxBytes":
|
|
444
|
+
"maxItems": 100000,
|
|
445
|
+
"maxBytes": 104857600
|
|
445
446
|
}
|
|
446
447
|
}
|
|
447
448
|
}
|
|
@@ -498,8 +499,8 @@ To enable the S3 cache, include a `cache` object in your `S3` configuration with
|
|
|
498
499
|
{
|
|
499
500
|
"cache": {
|
|
500
501
|
"enabled": true,
|
|
501
|
-
"maxItems":
|
|
502
|
-
"maxBytes":
|
|
502
|
+
"maxItems": 100000,
|
|
503
|
+
"maxBytes": 104857600
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
```
|
|
@@ -564,8 +565,8 @@ Here is a complete example with the new parameters added:
|
|
|
564
565
|
},
|
|
565
566
|
"cache": {
|
|
566
567
|
"enabled": true,
|
|
567
|
-
"maxItems":
|
|
568
|
-
"maxBytes":
|
|
568
|
+
"maxItems": 100000,
|
|
569
|
+
"maxBytes": 104857600
|
|
569
570
|
}
|
|
570
571
|
}
|
|
571
572
|
}
|
|
@@ -589,7 +590,12 @@ Then configure your storage thusly:
|
|
|
589
590
|
"host": "127.0.0.1",
|
|
590
591
|
"port": 6379,
|
|
591
592
|
"keyPrefix": "",
|
|
592
|
-
"keyTemplate": ""
|
|
593
|
+
"keyTemplate": "",
|
|
594
|
+
"cache": {
|
|
595
|
+
"enabled": true,
|
|
596
|
+
"maxItems": 100000,
|
|
597
|
+
"maxBytes": 104857600
|
|
598
|
+
}
|
|
593
599
|
}
|
|
594
600
|
}
|
|
595
601
|
```
|
|
@@ -600,6 +606,8 @@ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key
|
|
|
600
606
|
|
|
601
607
|
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.
|
|
602
608
|
|
|
609
|
+
See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
|
|
610
|
+
|
|
603
611
|
### RedisCluster
|
|
604
612
|
|
|
605
613
|
If you want to use a Redis cluster (e.g. [AWS ElastiCache](https://aws.amazon.com/elasticache/)), then here is how to do that. First, you will need to manually install the following module into your app:
|
|
@@ -625,7 +633,12 @@ Then configure your storage thusly:
|
|
|
625
633
|
}
|
|
626
634
|
},
|
|
627
635
|
"keyPrefix": "",
|
|
628
|
-
"keyTemplate": ""
|
|
636
|
+
"keyTemplate": "",
|
|
637
|
+
"cache": {
|
|
638
|
+
"enabled": true,
|
|
639
|
+
"maxItems": 100000,
|
|
640
|
+
"maxBytes": 104857600
|
|
641
|
+
}
|
|
629
642
|
}
|
|
630
643
|
}
|
|
631
644
|
```
|
|
@@ -640,6 +653,8 @@ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key
|
|
|
640
653
|
|
|
641
654
|
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.
|
|
642
655
|
|
|
656
|
+
See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
|
|
657
|
+
|
|
643
658
|
## SQLite
|
|
644
659
|
|
|
645
660
|
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 [better-sqlite3](https://www.npmjs.com/package/better-sqlite3) module into your app:
|
|
@@ -668,6 +683,11 @@ Then configure your storage thusly:
|
|
|
668
683
|
"compress": true,
|
|
669
684
|
"keep": 7
|
|
670
685
|
},
|
|
686
|
+
"cache": {
|
|
687
|
+
"enabled": true,
|
|
688
|
+
"maxItems": 100000,
|
|
689
|
+
"maxBytes": 104857600
|
|
690
|
+
},
|
|
671
691
|
"keyPrefix": "",
|
|
672
692
|
"keyTemplate": ""
|
|
673
693
|
}
|
|
@@ -680,10 +700,61 @@ The optional `pragmas` object allows you set one or more [SQLite Pragmas](https:
|
|
|
680
700
|
|
|
681
701
|
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.
|
|
682
702
|
|
|
703
|
+
See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
|
|
704
|
+
|
|
683
705
|
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.
|
|
684
706
|
|
|
685
707
|
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.
|
|
686
708
|
|
|
709
|
+
## Postgres
|
|
710
|
+
|
|
711
|
+
If you want to use [Postgres](https://www.postgresql.org/) as a backing store, here is how to do so. First, you need to manually install the [pg](https://www.npmjs.com/package/pg) module into your app:
|
|
712
|
+
|
|
713
|
+
```sh
|
|
714
|
+
npm install --save pg
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
Then configure your storage thusly:
|
|
718
|
+
|
|
719
|
+
```json
|
|
720
|
+
{
|
|
721
|
+
"engine": "Postgres",
|
|
722
|
+
"Postgres": {
|
|
723
|
+
"min": 0,
|
|
724
|
+
"max": 32,
|
|
725
|
+
"host": "localhost",
|
|
726
|
+
"database": "postgres",
|
|
727
|
+
"user": "postgres",
|
|
728
|
+
"password": "postgres",
|
|
729
|
+
"port": 5432,
|
|
730
|
+
"statement_timeout": 5000,
|
|
731
|
+
"query_timeout": 6000,
|
|
732
|
+
"connectionTimeoutMillis": 30000,
|
|
733
|
+
"idleTimeoutMillis": 10000,
|
|
734
|
+
"table": "items",
|
|
735
|
+
"cache": {
|
|
736
|
+
"enabled": true,
|
|
737
|
+
"maxItems": 100000,
|
|
738
|
+
"maxBytes": 104857600
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
The Postgres engine uses a single table to act as the key/value store. The table is automatically created on startup if needed:
|
|
745
|
+
|
|
746
|
+
```sql
|
|
747
|
+
CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BYTEA NOT NULL, modified BIGINT NOT NULL )
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
You can customize the table name via the `table` configuration property. It defaults to simply `items`.
|
|
751
|
+
|
|
752
|
+
[Connection Pooling](https://node-postgres.com/features/pooling) is utilized to open many simultaneous connections to greatly increase throughput. Set the `max` property to the maximum number of concurrent connections your database can handle. Ideally this should be set to the global [concurrency](#concurrency) setting.
|
|
753
|
+
|
|
754
|
+
See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
|
|
755
|
+
|
|
756
|
+
Note that pooling and caching have little effect when the database is running locally (i.e. localhost). They offer much more benefit when the database is located remotely over some number of network hops.
|
|
757
|
+
|
|
687
758
|
## Hybrid
|
|
688
759
|
|
|
689
760
|
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:
|
package/engines/Couchbase.js
CHANGED
|
@@ -288,7 +288,6 @@ module.exports = Class.create({
|
|
|
288
288
|
end = buf.length ? buf.length - 1 : 0;
|
|
289
289
|
}
|
|
290
290
|
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) {
|
|
291
|
-
download.destroy();
|
|
292
291
|
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null );
|
|
293
292
|
return;
|
|
294
293
|
}
|
package/engines/Filesystem.js
CHANGED
|
@@ -215,7 +215,7 @@ module.exports = Class.create({
|
|
|
215
215
|
}
|
|
216
216
|
else {
|
|
217
217
|
this.logDebug(9, "Storing JSON Object: " + key, this.debugLevel(10) ? value : file);
|
|
218
|
-
value = this.pretty ? JSON.stringify( value, null, "\t" ) : JSON.stringify( value );
|
|
218
|
+
value = Buffer.from( this.pretty ? JSON.stringify( value, null, "\t" ) : JSON.stringify( value ) );
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
var dir = path.dirname( file );
|
|
@@ -378,13 +378,22 @@ module.exports = Class.create({
|
|
|
378
378
|
if (this.cache && this.cache.has(key)) {
|
|
379
379
|
var item = this.cache.getMeta(key);
|
|
380
380
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
381
|
+
if (item.value.length == 0) {
|
|
382
|
+
process.nextTick( function() {
|
|
383
|
+
var err = new Error("Failed to head key: " + key + ": File not found");
|
|
384
|
+
err.code = "NoSuchKey";
|
|
385
|
+
callback( err );
|
|
386
386
|
} );
|
|
387
|
-
}
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
process.nextTick( function() {
|
|
390
|
+
self.logDebug(9, "Cached head complete: " + key);
|
|
391
|
+
callback( null, {
|
|
392
|
+
mod: item.date,
|
|
393
|
+
len: item.value.length
|
|
394
|
+
} );
|
|
395
|
+
} );
|
|
396
|
+
}
|
|
388
397
|
return;
|
|
389
398
|
} // cache
|
|
390
399
|
|
|
@@ -393,6 +402,11 @@ module.exports = Class.create({
|
|
|
393
402
|
if (err.message.match(/ENOENT/)) {
|
|
394
403
|
err.message = "File not found";
|
|
395
404
|
err.code = "NoSuchKey";
|
|
405
|
+
|
|
406
|
+
if (self.cache && !self.storage.isBinaryKey(key)) {
|
|
407
|
+
// store 'empty' stub in cache
|
|
408
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
409
|
+
}
|
|
396
410
|
}
|
|
397
411
|
else {
|
|
398
412
|
// log fs errors that aren't simple missing files (i.e. I/O errors)
|
|
@@ -421,30 +435,42 @@ module.exports = Class.create({
|
|
|
421
435
|
|
|
422
436
|
// check cache first
|
|
423
437
|
if (this.cache && !is_binary && this.cache.has(key)) {
|
|
424
|
-
var
|
|
438
|
+
var item = this.cache.getMeta(key);
|
|
425
439
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
callback(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
440
|
+
if (item.value.length == 0) {
|
|
441
|
+
process.nextTick( function() {
|
|
442
|
+
var err = new Error("Failed to fetch key: " + key + ": File not found");
|
|
443
|
+
err.code = "NoSuchKey";
|
|
444
|
+
callback( err );
|
|
445
|
+
} );
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
process.nextTick( function() {
|
|
449
|
+
var data = null;
|
|
450
|
+
try { data = JSON.parse( item.value.toString() ); }
|
|
451
|
+
catch (e) {
|
|
452
|
+
self.logError('file', "Failed to parse JSON record: " + key + ": " + e);
|
|
453
|
+
callback( e, null );
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
|
|
457
|
+
|
|
458
|
+
callback( null, data );
|
|
459
|
+
} );
|
|
460
|
+
}
|
|
437
461
|
return;
|
|
438
462
|
} // cache
|
|
439
463
|
|
|
440
|
-
|
|
441
|
-
if (!this.storage.isBinaryKey(key)) opts = { encoding: 'utf8' };
|
|
442
|
-
|
|
443
|
-
fs.readFile(file, opts, function (err, data) {
|
|
464
|
+
fs.readFile(file, function (err, data) {
|
|
444
465
|
if (err) {
|
|
445
466
|
if (err.message.match(/ENOENT/)) {
|
|
446
467
|
err.message = "File not found";
|
|
447
468
|
err.code = "NoSuchKey";
|
|
469
|
+
|
|
470
|
+
if (self.cache && !is_binary) {
|
|
471
|
+
// store 'empty' stub in cache
|
|
472
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
473
|
+
}
|
|
448
474
|
}
|
|
449
475
|
else {
|
|
450
476
|
// log fs errors that aren't simple missing files (i.e. I/O errors)
|
|
@@ -461,7 +487,7 @@ module.exports = Class.create({
|
|
|
461
487
|
}
|
|
462
488
|
|
|
463
489
|
if (!is_binary) {
|
|
464
|
-
try { data = JSON.parse( data ); }
|
|
490
|
+
try { data = JSON.parse( data.toString() ); }
|
|
465
491
|
catch (e) {
|
|
466
492
|
self.logError('file', "Failed to parse JSON record: " + key + ": " + e);
|
|
467
493
|
callback( e, null );
|
|
@@ -569,7 +595,6 @@ module.exports = Class.create({
|
|
|
569
595
|
end = stats.size ? stats.size - 1 : 0;
|
|
570
596
|
}
|
|
571
597
|
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= stats.size) || (end < start) || (end >= stats.size)) {
|
|
572
|
-
download.destroy();
|
|
573
598
|
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + stats.size + ")"), null );
|
|
574
599
|
return;
|
|
575
600
|
}
|
|
@@ -596,6 +621,11 @@ module.exports = Class.create({
|
|
|
596
621
|
if (err.message.match(/ENOENT/)) {
|
|
597
622
|
err.message = "File not found";
|
|
598
623
|
err.code = "NoSuchKey";
|
|
624
|
+
|
|
625
|
+
if (self.cache && !self.storage.isBinaryKey(key)) {
|
|
626
|
+
// store 'empty' stub in cache
|
|
627
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
628
|
+
}
|
|
599
629
|
}
|
|
600
630
|
|
|
601
631
|
self.logError('file', "Failed to delete file: " + key + ": " + file + ": " + err.message);
|
|
@@ -606,9 +636,10 @@ module.exports = Class.create({
|
|
|
606
636
|
else {
|
|
607
637
|
self.logDebug(9, "Delete complete: " + key);
|
|
608
638
|
|
|
609
|
-
// possibly delete from LRU cache as well
|
|
610
|
-
if (self.cache && self.
|
|
611
|
-
|
|
639
|
+
// possibly "delete" from LRU cache as well
|
|
640
|
+
if (self.cache && !self.storage.isBinaryKey(key)) {
|
|
641
|
+
// store 'empty' stub in cache
|
|
642
|
+
self.cache.set( key, Buffer.alloc(0), { date: 0 } );
|
|
612
643
|
}
|
|
613
644
|
|
|
614
645
|
// cleanup parent dirs if empty
|