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 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": 1000,
400
- "maxBytes": 10485760
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": 1000,
444
- "maxBytes": 10485760
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": 1000,
502
- "maxBytes": 10485760
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": 1000,
568
- "maxBytes": 10485760
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:
@@ -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
  }
@@ -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
- process.nextTick( function() {
382
- self.logDebug(9, "Cached head complete: " + key);
383
- callback( null, {
384
- mod: item.date,
385
- len: item.value.length
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 data = this.cache.get(key);
438
+ var item = this.cache.getMeta(key);
425
439
 
426
- process.nextTick( function() {
427
- try { data = JSON.parse( data ); }
428
- catch (e) {
429
- self.logError('file', "Failed to parse JSON record: " + key + ": " + e);
430
- callback( e, null );
431
- return;
432
- }
433
- self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
434
-
435
- callback( null, data );
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
- var opts = {};
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.cache.has(key)) {
611
- self.cache.delete(key);
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