pixl-server-storage 4.0.2 → 4.1.0
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 +64 -8
- package/engines/Couchbase.js +0 -1
- package/engines/Filesystem.js +0 -1
- package/engines/Postgres.js +475 -0
- package/engines/Redis.js +0 -1
- package/engines/RedisCluster.js +0 -1
- package/engines/SQLite.js +5 -2
- package/package.json +2 -2
- 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-02-15-18-10-21.db.gz +0 -0
package/README.md
CHANGED
|
@@ -396,8 +396,8 @@ To enable the filesystem cache, include a `cache` object in your `Filesystem` co
|
|
|
396
396
|
"base_dir": "/let/data/myserver",
|
|
397
397
|
"cache": {
|
|
398
398
|
"enabled": true,
|
|
399
|
-
"maxItems":
|
|
400
|
-
"maxBytes":
|
|
399
|
+
"maxItems": 100000,
|
|
400
|
+
"maxBytes": 104857600
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
}
|
|
@@ -440,8 +440,8 @@ If you want to use [Amazon S3](http://aws.amazon.com/s3/) as a backing store, co
|
|
|
440
440
|
},
|
|
441
441
|
"cache": {
|
|
442
442
|
"enabled": true,
|
|
443
|
-
"maxItems":
|
|
444
|
-
"maxBytes":
|
|
443
|
+
"maxItems": 100000,
|
|
444
|
+
"maxBytes": 104857600
|
|
445
445
|
}
|
|
446
446
|
}
|
|
447
447
|
}
|
|
@@ -498,8 +498,8 @@ To enable the S3 cache, include a `cache` object in your `S3` configuration with
|
|
|
498
498
|
{
|
|
499
499
|
"cache": {
|
|
500
500
|
"enabled": true,
|
|
501
|
-
"maxItems":
|
|
502
|
-
"maxBytes":
|
|
501
|
+
"maxItems": 100000,
|
|
502
|
+
"maxBytes": 104857600
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
```
|
|
@@ -564,8 +564,8 @@ Here is a complete example with the new parameters added:
|
|
|
564
564
|
},
|
|
565
565
|
"cache": {
|
|
566
566
|
"enabled": true,
|
|
567
|
-
"maxItems":
|
|
568
|
-
"maxBytes":
|
|
567
|
+
"maxItems": 100000,
|
|
568
|
+
"maxBytes": 104857600
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
571
|
}
|
|
@@ -668,6 +668,11 @@ Then configure your storage thusly:
|
|
|
668
668
|
"compress": true,
|
|
669
669
|
"keep": 7
|
|
670
670
|
},
|
|
671
|
+
"cache": {
|
|
672
|
+
"enabled": true,
|
|
673
|
+
"maxItems": 100000,
|
|
674
|
+
"maxBytes": 104857600
|
|
675
|
+
},
|
|
671
676
|
"keyPrefix": "",
|
|
672
677
|
"keyTemplate": ""
|
|
673
678
|
}
|
|
@@ -680,10 +685,61 @@ The optional `pragmas` object allows you set one or more [SQLite Pragmas](https:
|
|
|
680
685
|
|
|
681
686
|
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
687
|
|
|
688
|
+
See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
|
|
689
|
+
|
|
683
690
|
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
691
|
|
|
685
692
|
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
693
|
|
|
694
|
+
## Postgres
|
|
695
|
+
|
|
696
|
+
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:
|
|
697
|
+
|
|
698
|
+
```sh
|
|
699
|
+
npm install --save pg
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Then configure your storage thusly:
|
|
703
|
+
|
|
704
|
+
```json
|
|
705
|
+
{
|
|
706
|
+
"engine": "Postgres",
|
|
707
|
+
"Postgres": {
|
|
708
|
+
"min": 0,
|
|
709
|
+
"max": 32,
|
|
710
|
+
"host": "localhost",
|
|
711
|
+
"database": "postgres",
|
|
712
|
+
"user": "postgres",
|
|
713
|
+
"password": "postgres",
|
|
714
|
+
"port": 5432,
|
|
715
|
+
"statement_timeout": 5000,
|
|
716
|
+
"query_timeout": 6000,
|
|
717
|
+
"connectionTimeoutMillis": 30000,
|
|
718
|
+
"idleTimeoutMillis": 10000,
|
|
719
|
+
"table": "items",
|
|
720
|
+
"cache": {
|
|
721
|
+
"enabled": true,
|
|
722
|
+
"maxItems": 100000,
|
|
723
|
+
"maxBytes": 104857600
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
The Postgres engine uses a single table to act as the key/value store. The table is automatically created on startup if needed:
|
|
730
|
+
|
|
731
|
+
```sql
|
|
732
|
+
CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BYTEA NOT NULL, modified BIGINT NOT NULL )
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
You can customize the table name via the `table` configuration property. It defaults to simply `items`.
|
|
736
|
+
|
|
737
|
+
[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.
|
|
738
|
+
|
|
739
|
+
See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
|
|
740
|
+
|
|
741
|
+
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.
|
|
742
|
+
|
|
687
743
|
## Hybrid
|
|
688
744
|
|
|
689
745
|
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
|
@@ -569,7 +569,6 @@ module.exports = Class.create({
|
|
|
569
569
|
end = stats.size ? stats.size - 1 : 0;
|
|
570
570
|
}
|
|
571
571
|
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= stats.size) || (end < start) || (end >= stats.size)) {
|
|
572
|
-
download.destroy();
|
|
573
572
|
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + stats.size + ")"), null );
|
|
574
573
|
return;
|
|
575
574
|
}
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// Postgres Storage Plugin
|
|
2
|
+
// Copyright (c) 2026 Joseph Huckaby
|
|
3
|
+
// Released under the MIT License
|
|
4
|
+
|
|
5
|
+
// Requires the 'pg' module
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const Path = require('path');
|
|
9
|
+
const zlib = require('zlib');
|
|
10
|
+
const Class = require("pixl-class");
|
|
11
|
+
const Component = require("pixl-server/component");
|
|
12
|
+
const pg = require('pg');
|
|
13
|
+
const Tools = require("pixl-tools");
|
|
14
|
+
const Perf = require("pixl-perf");
|
|
15
|
+
const Cache = require("pixl-cache");
|
|
16
|
+
const async = require('async');
|
|
17
|
+
const noop = function() {};
|
|
18
|
+
|
|
19
|
+
module.exports = Class.create({
|
|
20
|
+
|
|
21
|
+
__name: 'Postgres',
|
|
22
|
+
__parent: Component,
|
|
23
|
+
|
|
24
|
+
defaultConfig: {
|
|
25
|
+
min: 0,
|
|
26
|
+
max: 32,
|
|
27
|
+
host: "localhost",
|
|
28
|
+
database: "",
|
|
29
|
+
user: "",
|
|
30
|
+
password: "",
|
|
31
|
+
port: 5432,
|
|
32
|
+
statement_timeout: 5000,
|
|
33
|
+
query_timeout: 6000,
|
|
34
|
+
connectionTimeoutMillis: 30000,
|
|
35
|
+
idleTimeoutMillis: 10000,
|
|
36
|
+
table: "items"
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
startup: function(callback) {
|
|
40
|
+
// setup Postgres connection
|
|
41
|
+
var self = this;
|
|
42
|
+
this.logDebug(2, "Setting up Postgres", Tools.copyHashRemoveKeys( this.config.get(), { password:1 }) );
|
|
43
|
+
this.setup(callback);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
setup: function(callback) {
|
|
47
|
+
// setup Postgres connection
|
|
48
|
+
var self = this;
|
|
49
|
+
var pg_config = this.config.get();
|
|
50
|
+
|
|
51
|
+
this.commands = {
|
|
52
|
+
create: `CREATE TABLE IF NOT EXISTS ${pg_config.table}( key TEXT PRIMARY KEY, value BYTEA NOT NULL, modified BIGINT NOT NULL )`,
|
|
53
|
+
get: `SELECT modified, value FROM ${pg_config.table} WHERE key = $key LIMIT 1`,
|
|
54
|
+
head: `SELECT modified, octet_length(value) AS len FROM ${pg_config.table} WHERE key = $key LIMIT 1`,
|
|
55
|
+
put: `INSERT INTO ${pg_config.table} (key, value, modified) VALUES ($key, $value, $now) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, modified = EXCLUDED.modified`,
|
|
56
|
+
delete: `DELETE FROM ${pg_config.table} WHERE key = $key`
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// optional LRU cache
|
|
60
|
+
this.cache = null;
|
|
61
|
+
var cache_opts = pg_config.cache;
|
|
62
|
+
if (cache_opts && cache_opts.enabled) {
|
|
63
|
+
this.logDebug(3, "Setting up LRU cache", cache_opts);
|
|
64
|
+
this.cache = new Cache( Tools.copyHashRemoveKeys(cache_opts, { enabled: 1 }) );
|
|
65
|
+
this.cache.on('expire', function(item, reason) {
|
|
66
|
+
self.logDebug(9, "Expiring LRU cache object: " + item.key + " due to: " + reason, {
|
|
67
|
+
key: item.key,
|
|
68
|
+
reason: reason,
|
|
69
|
+
totalCount: self.cache.count,
|
|
70
|
+
totalBytes: self.cache.bytes
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// pass entire config to pg.Pool, sans our custom keys
|
|
76
|
+
var db = this.db = new pg.Pool(
|
|
77
|
+
Tools.copyHashRemoveKeys( pg_config, { cache: 1, table: 1 })
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
db.on('error', function(err) {
|
|
81
|
+
self.logError('db', "DB Error: " + err, err);
|
|
82
|
+
if (callback) { callback(err); callback = null; }
|
|
83
|
+
});
|
|
84
|
+
db.on('connect', function(client) {
|
|
85
|
+
self.logDebug(9, "Opened DB pool connection");
|
|
86
|
+
});
|
|
87
|
+
db.on('remove', function(client) {
|
|
88
|
+
self.logDebug(9, "Closed DB pool connection");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// create initial table
|
|
92
|
+
this.logDebug(3, "Creating initial table: " + pg_config.table);
|
|
93
|
+
this.query( this.commands.create, {}, function(err, res) {
|
|
94
|
+
if (err) self.logError('db', "Failed to create initial table: " + err, err);
|
|
95
|
+
else self.logDebug(3, "Setup complete");
|
|
96
|
+
if (callback) { callback(err); callback = null; }
|
|
97
|
+
} );
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
query(sql, args, callback) {
|
|
101
|
+
// run and log db query, and error if applicable
|
|
102
|
+
// allow for named variables in sql, expand to numbered $ variables for pg
|
|
103
|
+
var self = this;
|
|
104
|
+
|
|
105
|
+
// perform $ placeholder substitution to populate sargs array
|
|
106
|
+
var sargs = [];
|
|
107
|
+
sql = sql.replace(/\$(\w+)/g, function(m_all, name) {
|
|
108
|
+
var value = args[name];
|
|
109
|
+
sargs.push(value);
|
|
110
|
+
value = '$' + sargs.length;
|
|
111
|
+
return value;
|
|
112
|
+
} );
|
|
113
|
+
|
|
114
|
+
this.db.query( sql, sargs, function(err, res) {
|
|
115
|
+
if (err) {
|
|
116
|
+
self.logError('db', "Failed to execute SQL: " + err, { sql, err });
|
|
117
|
+
return callback(err);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
self.logDebug(9, "SQL query completed successfully", {
|
|
121
|
+
command: res.command || '',
|
|
122
|
+
rowCount: res.rowCount || 0
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
callback(null, res);
|
|
126
|
+
} ); // db.query
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
prepKey: function(key) {
|
|
130
|
+
// prepare key (no-op)
|
|
131
|
+
return key;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
put: function(key, value, callback) {
|
|
135
|
+
// store key+value in Postgres
|
|
136
|
+
var self = this;
|
|
137
|
+
var now = Tools.timeNow(true);
|
|
138
|
+
key = this.prepKey(key);
|
|
139
|
+
|
|
140
|
+
var is_binary = this.storage.isBinaryKey(key);
|
|
141
|
+
|
|
142
|
+
if (is_binary) {
|
|
143
|
+
this.logDebug(9, "Storing Postgres Binary Object: " + key, '' + value.length + ' bytes');
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.logDebug(9, "Storing Postgres JSON Object: " + key, this.debugLevel(10) ? value : null);
|
|
147
|
+
value = Buffer.from( JSON.stringify( value ) );
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.query( this.commands.put, { key: key, value: value, now: now }, function(err, res) {
|
|
151
|
+
if (err) {
|
|
152
|
+
err.message = "Failed to store object: " + key + ": " + err;
|
|
153
|
+
self.logError('pg', '' + err);
|
|
154
|
+
if (callback) callback(err);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
self.logDebug(9, "Store complete: " + key);
|
|
159
|
+
// possibly cache in LRU
|
|
160
|
+
if (self.cache && !is_binary) {
|
|
161
|
+
self.cache.set( key, value, { date: now } );
|
|
162
|
+
}
|
|
163
|
+
if (callback) callback(null);
|
|
164
|
+
} ); // query
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
putStream: function(key, inp, callback) {
|
|
168
|
+
// store key+value in Postgres using read stream
|
|
169
|
+
var self = this;
|
|
170
|
+
|
|
171
|
+
// The Postgres API has no stream support.
|
|
172
|
+
// So, we have to do this the RAM-hard way...
|
|
173
|
+
|
|
174
|
+
var chunks = [];
|
|
175
|
+
var done = false;
|
|
176
|
+
var finish = function(err) {
|
|
177
|
+
if (done) return;
|
|
178
|
+
done = true;
|
|
179
|
+
callback(err);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
inp.on('error', function(err) {
|
|
183
|
+
err.message = "Failed to store stream: " + key + ": " + err;
|
|
184
|
+
self.logError('pg', '' + err);
|
|
185
|
+
finish(err);
|
|
186
|
+
} );
|
|
187
|
+
inp.on('data', function(chunk) {
|
|
188
|
+
if (done) return;
|
|
189
|
+
chunks.push( chunk );
|
|
190
|
+
} );
|
|
191
|
+
inp.on('end', function() {
|
|
192
|
+
if (done) return;
|
|
193
|
+
var buf = Buffer.concat(chunks);
|
|
194
|
+
self.put( key, buf, finish );
|
|
195
|
+
} );
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
head: function(key, callback) {
|
|
199
|
+
// head pg item given key
|
|
200
|
+
var self = this;
|
|
201
|
+
key = this.prepKey(key);
|
|
202
|
+
|
|
203
|
+
// check cache first
|
|
204
|
+
if (this.cache && this.cache.has(key)) {
|
|
205
|
+
var item = this.cache.getMeta(key);
|
|
206
|
+
|
|
207
|
+
process.nextTick( function() {
|
|
208
|
+
self.logDebug(9, "Cached head complete: " + key);
|
|
209
|
+
callback( null, {
|
|
210
|
+
mod: item.date,
|
|
211
|
+
len: item.value.length
|
|
212
|
+
} );
|
|
213
|
+
} );
|
|
214
|
+
return;
|
|
215
|
+
} // cache
|
|
216
|
+
|
|
217
|
+
this.query( this.commands.head, { key: key }, function(err, res) {
|
|
218
|
+
if (err) {
|
|
219
|
+
err.message = "Failed to head key: " + key + ": " + err;
|
|
220
|
+
self.logError('pg', '' + err);
|
|
221
|
+
callback(err);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
var row = res.rows[0];
|
|
226
|
+
|
|
227
|
+
if (!row) {
|
|
228
|
+
var err = new Error("Failed to head key: " + key + ": Not found");
|
|
229
|
+
err.code = "NoSuchKey";
|
|
230
|
+
return callback(err, null);
|
|
231
|
+
}
|
|
232
|
+
callback(null, { mod: parseInt(row.modified, 10), len: row.len });
|
|
233
|
+
} ); // query
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
get: function(key, callback) {
|
|
237
|
+
// fetch Postgres value given key
|
|
238
|
+
var self = this;
|
|
239
|
+
key = this.prepKey(key);
|
|
240
|
+
|
|
241
|
+
var is_binary = this.storage.isBinaryKey(key);
|
|
242
|
+
|
|
243
|
+
// check cache first
|
|
244
|
+
if (this.cache && !is_binary && this.cache.has(key)) {
|
|
245
|
+
var data = this.cache.get(key);
|
|
246
|
+
|
|
247
|
+
process.nextTick( function() {
|
|
248
|
+
try { data = JSON.parse( data.toString() ); }
|
|
249
|
+
catch (e) {
|
|
250
|
+
self.logError('pg', "Failed to parse JSON record: " + key + ": " + e);
|
|
251
|
+
callback( e, null );
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
self.logDebug(9, "Cached JSON fetch complete: " + key, self.debugLevel(10) ? data : null);
|
|
255
|
+
|
|
256
|
+
callback( null, data );
|
|
257
|
+
} );
|
|
258
|
+
return;
|
|
259
|
+
} // cache
|
|
260
|
+
|
|
261
|
+
this.logDebug(9, "Fetching Postgres Object: " + key);
|
|
262
|
+
|
|
263
|
+
this.query( this.commands.get, { key: key }, function(err, res) {
|
|
264
|
+
if (err) {
|
|
265
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
266
|
+
self.logError('pg', '' + err);
|
|
267
|
+
callback(err);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var row = res.rows[0];
|
|
272
|
+
|
|
273
|
+
if (!row) {
|
|
274
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
275
|
+
err.code = "NoSuchKey";
|
|
276
|
+
return callback(err, null);
|
|
277
|
+
}
|
|
278
|
+
if (is_binary) {
|
|
279
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
|
|
280
|
+
return callback(null, row.value);
|
|
281
|
+
}
|
|
282
|
+
if (self.cache) {
|
|
283
|
+
self.cache.set( key, row.value, { date: parseInt(row.modified, 10) } );
|
|
284
|
+
}
|
|
285
|
+
var json = null;
|
|
286
|
+
try { json = JSON.parse( row.value.toString() ); }
|
|
287
|
+
catch (err) {
|
|
288
|
+
self.logError('pg', "Failed to parse JSON record: " + key + ": " + err);
|
|
289
|
+
return callback(err, null);
|
|
290
|
+
}
|
|
291
|
+
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? json : null);
|
|
292
|
+
callback(null, json);
|
|
293
|
+
} ); // query
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
getBuffer: function(key, callback) {
|
|
297
|
+
// fetch Postgres buffer given key
|
|
298
|
+
var self = this;
|
|
299
|
+
key = this.prepKey(key);
|
|
300
|
+
|
|
301
|
+
this.logDebug(9, "Fetching Postgres Object: " + key);
|
|
302
|
+
|
|
303
|
+
this.query( this.commands.get, { key: key }, function(err, res) {
|
|
304
|
+
if (err) {
|
|
305
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
306
|
+
self.logError('pg', '' + err);
|
|
307
|
+
callback(err);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
var row = res.rows[0];
|
|
312
|
+
|
|
313
|
+
if (!row) {
|
|
314
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
315
|
+
err.code = "NoSuchKey";
|
|
316
|
+
return callback(err, null);
|
|
317
|
+
}
|
|
318
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
|
|
319
|
+
callback(null, row.value);
|
|
320
|
+
} ); // query
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
getStream: function(key, callback) {
|
|
324
|
+
// get readable stream to record value given key
|
|
325
|
+
var self = this;
|
|
326
|
+
|
|
327
|
+
// The Postgres API has no stream support.
|
|
328
|
+
// So, we have to do this the RAM-hard way...
|
|
329
|
+
|
|
330
|
+
this.query( this.commands.get, { key: this.prepKey(key) }, function(err, res) {
|
|
331
|
+
if (err) {
|
|
332
|
+
// an actual error
|
|
333
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
334
|
+
self.logError('pg', '' + err);
|
|
335
|
+
return callback(err);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var row = res.rows[0];
|
|
339
|
+
|
|
340
|
+
if (!row) {
|
|
341
|
+
// record not found
|
|
342
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
343
|
+
err.code = "NoSuchKey";
|
|
344
|
+
return callback( err, null );
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
|
|
348
|
+
var stream = new BufferStream(row.value);
|
|
349
|
+
callback(null, stream, { mod: parseInt(row.modified, 10), len: row.value.length });
|
|
350
|
+
} ); // query
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
getStreamRange: function(key, start, end, callback) {
|
|
354
|
+
// get readable stream to record value given key and range
|
|
355
|
+
var self = this;
|
|
356
|
+
|
|
357
|
+
// The Postgres API has no stream support.
|
|
358
|
+
// So, we have to do this the RAM-hard way...
|
|
359
|
+
|
|
360
|
+
this.query( this.commands.get, { key: this.prepKey(key) }, function(err, res) {
|
|
361
|
+
if (err) {
|
|
362
|
+
// an actual error
|
|
363
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
364
|
+
self.logError('pg', '' + err);
|
|
365
|
+
return callback(err);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
var row = res.rows[0];
|
|
369
|
+
|
|
370
|
+
if (!row) {
|
|
371
|
+
// record not found
|
|
372
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
373
|
+
err.code = "NoSuchKey";
|
|
374
|
+
return callback( err, null );
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
var buf = row.value;
|
|
378
|
+
|
|
379
|
+
// validate byte range, now that we have the head info
|
|
380
|
+
if (isNaN(start) && !isNaN(end)) {
|
|
381
|
+
start = buf.length - end;
|
|
382
|
+
end = buf.length ? buf.length - 1 : 0;
|
|
383
|
+
}
|
|
384
|
+
else if (!isNaN(start) && isNaN(end)) {
|
|
385
|
+
end = buf.length ? buf.length - 1 : 0;
|
|
386
|
+
}
|
|
387
|
+
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) {
|
|
388
|
+
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null );
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
var range = buf.slice(start, end + 1);
|
|
393
|
+
var stream = new BufferStream(range);
|
|
394
|
+
callback(null, stream, { mod: parseInt(row.modified, 10), len: buf.length });
|
|
395
|
+
} ); // query
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
delete: function(key, callback) {
|
|
399
|
+
// delete Postgres key given key
|
|
400
|
+
var self = this;
|
|
401
|
+
key = this.prepKey(key);
|
|
402
|
+
|
|
403
|
+
this.logDebug(9, "Deleting Postgres Object: " + key);
|
|
404
|
+
|
|
405
|
+
this.query( this.commands.delete, { key: key }, function(err, res) {
|
|
406
|
+
if (err) {
|
|
407
|
+
self.logError('pg', "Failed to delete object: " + key + ": " + err);
|
|
408
|
+
callback(err);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!res.rowCount) {
|
|
413
|
+
var err = new Error("Failed to delete object: " + key + ": Not found");
|
|
414
|
+
err.code = "NoSuchKey";
|
|
415
|
+
return callback(err);
|
|
416
|
+
}
|
|
417
|
+
self.logDebug(9, "Delete complete: " + key);
|
|
418
|
+
if (self.cache && self.cache.has(key)) {
|
|
419
|
+
self.cache.delete(key);
|
|
420
|
+
}
|
|
421
|
+
callback();
|
|
422
|
+
} ); // query
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
runMaintenance: function(callback) {
|
|
426
|
+
// run daily maintenance
|
|
427
|
+
callback();
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
shutdown: function(callback) {
|
|
431
|
+
// shutdown storage
|
|
432
|
+
var self = this;
|
|
433
|
+
this.logDebug(2, "Shutting down Postgres");
|
|
434
|
+
|
|
435
|
+
if (this.db) {
|
|
436
|
+
this.db.end( function(err) {
|
|
437
|
+
if (err) self.logError('pg', "Failed to shutdown database cleanly: " + err);
|
|
438
|
+
else self.logDebug(3, "Shutdown complete");
|
|
439
|
+
delete self.db;
|
|
440
|
+
callback();
|
|
441
|
+
} );
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
this.logDebug(3, "Shutdown complete");
|
|
445
|
+
callback();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Modified the following snippet from node-streamifier:
|
|
452
|
+
// Copyright (c) 2014 Gabriel Llamas, MIT Licensed
|
|
453
|
+
|
|
454
|
+
var util = require('util');
|
|
455
|
+
var stream = require('stream');
|
|
456
|
+
|
|
457
|
+
var BufferStream = function (object, options) {
|
|
458
|
+
if (object instanceof Buffer || typeof object === 'string') {
|
|
459
|
+
options = options || {};
|
|
460
|
+
stream.Readable.call(this, {
|
|
461
|
+
highWaterMark: options.highWaterMark,
|
|
462
|
+
encoding: options.encoding
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
stream.Readable.call(this, { objectMode: true });
|
|
466
|
+
}
|
|
467
|
+
this._object = object;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
util.inherits(BufferStream, stream.Readable);
|
|
471
|
+
|
|
472
|
+
BufferStream.prototype._read = function () {
|
|
473
|
+
this.push(this._object);
|
|
474
|
+
this._object = null;
|
|
475
|
+
};
|
package/engines/Redis.js
CHANGED
|
@@ -289,7 +289,6 @@ module.exports = Class.create({
|
|
|
289
289
|
end = buf.length ? buf.length - 1 : 0;
|
|
290
290
|
}
|
|
291
291
|
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) {
|
|
292
|
-
download.destroy();
|
|
293
292
|
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null );
|
|
294
293
|
return;
|
|
295
294
|
}
|
package/engines/RedisCluster.js
CHANGED
|
@@ -303,7 +303,6 @@ module.exports = Class.create({
|
|
|
303
303
|
end = buf.length ? buf.length - 1 : 0;
|
|
304
304
|
}
|
|
305
305
|
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) {
|
|
306
|
-
download.destroy();
|
|
307
306
|
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null );
|
|
308
307
|
return;
|
|
309
308
|
}
|
package/engines/SQLite.js
CHANGED
|
@@ -153,6 +153,7 @@ module.exports = Class.create({
|
|
|
153
153
|
err.message = "Failed to store object: " + key + ": " + err;
|
|
154
154
|
self.logError('sqlite', '' + err);
|
|
155
155
|
if (callback) callback(err);
|
|
156
|
+
return;
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
self.logDebug(9, "Store complete: " + key);
|
|
@@ -208,7 +209,7 @@ module.exports = Class.create({
|
|
|
208
209
|
catch (err) {
|
|
209
210
|
err.message = "Failed to head key: " + key + ": " + err;
|
|
210
211
|
self.logError('sqlite', '' + err);
|
|
211
|
-
callback(err);
|
|
212
|
+
return callback(err);
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
if (!row) {
|
|
@@ -256,6 +257,7 @@ module.exports = Class.create({
|
|
|
256
257
|
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
257
258
|
self.logError('sqlite', '' + err);
|
|
258
259
|
callback(err);
|
|
260
|
+
return;
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
if (!row) {
|
|
@@ -297,6 +299,7 @@ module.exports = Class.create({
|
|
|
297
299
|
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
298
300
|
self.logError('sqlite', '' + err);
|
|
299
301
|
callback(err);
|
|
302
|
+
return;
|
|
300
303
|
}
|
|
301
304
|
|
|
302
305
|
if (!row) {
|
|
@@ -365,7 +368,6 @@ module.exports = Class.create({
|
|
|
365
368
|
end = buf.length ? buf.length - 1 : 0;
|
|
366
369
|
}
|
|
367
370
|
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= buf.length) || (end < start) || (end >= buf.length)) {
|
|
368
|
-
download.destroy();
|
|
369
371
|
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + buf.length + ")"), null );
|
|
370
372
|
return;
|
|
371
373
|
}
|
|
@@ -391,6 +393,7 @@ module.exports = Class.create({
|
|
|
391
393
|
catch (err) {
|
|
392
394
|
self.logError('sqlite', "Failed to delete object: " + key + ": " + err);
|
|
393
395
|
callback(err);
|
|
396
|
+
return;
|
|
394
397
|
}
|
|
395
398
|
|
|
396
399
|
if (!info.changes) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixl-server-storage",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"pixl-class": "^1.0.3",
|
|
35
35
|
"pixl-perf": "^1.0.9",
|
|
36
36
|
"pixl-server": "^1.0.39",
|
|
37
|
-
"pixl-tools": "^
|
|
37
|
+
"pixl-tools": "^2.0.2",
|
|
38
38
|
"porter-stemmer": "0.9.1",
|
|
39
39
|
"stream-meter": "1.0.4",
|
|
40
40
|
"unidecode": "0.1.8"
|
package/test/config.json
CHANGED
|
@@ -84,6 +84,25 @@
|
|
|
84
84
|
"compress": true,
|
|
85
85
|
"keep": 7
|
|
86
86
|
}
|
|
87
|
+
},
|
|
88
|
+
"Postgres": {
|
|
89
|
+
"min": 0,
|
|
90
|
+
"max": 32,
|
|
91
|
+
"host": "localhost",
|
|
92
|
+
"database": "postgres",
|
|
93
|
+
"user": "postgres",
|
|
94
|
+
"password": "postgres",
|
|
95
|
+
"port": 5432,
|
|
96
|
+
"statement_timeout": 5000,
|
|
97
|
+
"query_timeout": 6000,
|
|
98
|
+
"connectionTimeoutMillis": 30000,
|
|
99
|
+
"idleTimeoutMillis": 10000,
|
|
100
|
+
"table": "items",
|
|
101
|
+
"cache": {
|
|
102
|
+
"enabled": true,
|
|
103
|
+
"maxItems": 100000,
|
|
104
|
+
"maxBytes": 104857600
|
|
105
|
+
}
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
}
|
|
Binary file
|
|
Binary file
|