pixl-server-storage 3.0.23 → 3.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 +12 -21
- package/engines/S3.js +191 -205
- package/index_types/Date.js +5 -0
- package/index_types/Number.js +8 -3
- package/indexer.js +61 -10
- package/package.json +11 -9
- package/test/test-main.js +4 -3
package/README.md
CHANGED
|
@@ -252,6 +252,12 @@ The `expiration_updates` property activates additional features in the [expirati
|
|
|
252
252
|
|
|
253
253
|
Note that this feature incurs additional overhead, because the expiration date of every record needs to be stored in a global [Hash](docs/Hashes.md). This slows down both the expiration set operation, and the nightly maintenance sweep to delete expired records. For this reason, the `expiration_dates` property defaults to `false` (disabled).
|
|
254
254
|
|
|
255
|
+
## lower_case_keys
|
|
256
|
+
|
|
257
|
+
The `lower_case_keys` property causes all storage keys to be internally lower-cased, effectively making all storage paths case-insensitive. This is the default (`true`). If you set this property to `false`, then all storage keys retain their natural casing, effectively making them case-sensitive.
|
|
258
|
+
|
|
259
|
+
Please note that if you use the [Local Filesystem](#local-filesystem) engine, then the filesystem itself may be case-insensitive (e.g. legacy macOS HFS).
|
|
260
|
+
|
|
255
261
|
## debug (standalone)
|
|
256
262
|
|
|
257
263
|
The `debug` property is only used when using [Standalone Mode](#standalone-mode). Setting this to `true` will cause the engine to emit debugging messages to the console.
|
|
@@ -409,27 +415,18 @@ Note that binary records are **not** cached. This system is for JSON records on
|
|
|
409
415
|
|
|
410
416
|
## Amazon S3
|
|
411
417
|
|
|
412
|
-
If you want to use [Amazon S3](http://aws.amazon.com/s3/) as a backing store,
|
|
413
|
-
|
|
414
|
-
```
|
|
415
|
-
npm install --save aws-sdk
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
Then configure your storage thusly:
|
|
418
|
+
If you want to use [Amazon S3](http://aws.amazon.com/s3/) as a backing store, configure your storage thusly:
|
|
419
419
|
|
|
420
420
|
```javascript
|
|
421
421
|
{
|
|
422
422
|
"engine": "S3",
|
|
423
423
|
"AWS": {
|
|
424
|
-
"accessKeyId": "YOUR_AMAZON_ACCESS_KEY",
|
|
425
|
-
"secretAccessKey": "YOUR_AMAZON_SECRET_KEY",
|
|
426
424
|
"region": "us-west-1",
|
|
427
|
-
"
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
425
|
+
"credentials": {
|
|
426
|
+
"accessKeyId": "YOUR_AMAZON_ACCESS_KEY",
|
|
427
|
+
"secretAccessKey": "YOUR_AMAZON_SECRET_KEY",
|
|
428
|
+
},
|
|
429
|
+
"maxAttempts": 5
|
|
433
430
|
},
|
|
434
431
|
"S3": {
|
|
435
432
|
"keyPrefix": "",
|
|
@@ -448,10 +445,6 @@ Then configure your storage thusly:
|
|
|
448
445
|
|
|
449
446
|
Replace `YOUR_AMAZON_ACCESS_KEY` and `YOUR_AMAZON_SECRET_KEY` with your Amazon Access Key and Secret Key, respectively. These can be generated on the Security Credentials page. Replace `MY_S3_BUCKET_ID` with the ID if your own S3 bucket. Make sure you match up the region too.
|
|
450
447
|
|
|
451
|
-
The `AWS` object is passed directly to the `config.update()` function from the [aws-sdk](https://www.npmjs.com/package/aws-sdk) module, so you can also include any properties supported there. See the [docs](http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html) for more details.
|
|
452
|
-
|
|
453
|
-
The `S3` object is passed directly to the S3 class constructor, so check the [docs](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property) to see what other properties are supported. There are a few special `S3` properties used by pixl-server-storage, which are described below.
|
|
454
|
-
|
|
455
448
|
If you plan on using Amazon AWS in other parts of your application, you can actually move the `AWS` config object into your outer server configuration. The storage module will look for it there.
|
|
456
449
|
|
|
457
450
|
### S3 File Extensions
|
|
@@ -495,8 +488,6 @@ Besides hash marks, the special macro `[key]` will be substituted with the full
|
|
|
495
488
|
|
|
496
489
|
It is *highly* recommended that you enable caching for S3, which keeps a copy of the most recently used JSON records in RAM. Not only will this increase overall performance, but it is especially important if you use any of the advanced storage features like [Lists](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/Lists.md), [Hashes](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/Hashes.md), [Transactions](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/Transactions.md) or the [Indexer](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/Indexer.md).
|
|
497
490
|
|
|
498
|
-
The reason why caching is so important is because Amazon S3 is only [eventually consistent](https://docs.aws.amazon.com/AmazonS3/latest/dev/Introduction.html#ConsistencyModel) when replacing existing records, so corruption can occur when writing compound objects (i.e. a list or a hash) and then quickly reading them back in. To protect against this, all writes can be cached in RAM, so subsequent reads can often bypass S3 and just read from the RAM cache instead, ensuring that the correct (latest) data is read.
|
|
499
|
-
|
|
500
491
|
To enable the S3 cache, include a `cache` object in your `S3` configuration with the following properties:
|
|
501
492
|
|
|
502
493
|
```js
|
package/engines/S3.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
// Amazon AWS S3 Storage Plugin
|
|
2
|
-
// Copyright (c) 2015 -
|
|
2
|
+
// Copyright (c) 2015 - 2022 Joseph Huckaby
|
|
3
3
|
// Released under the MIT License
|
|
4
4
|
|
|
5
|
-
// Requires the 'aws-sdk' module from npm
|
|
6
|
-
// npm install aws-sdk
|
|
7
|
-
|
|
8
5
|
var Class = require("pixl-class");
|
|
9
6
|
var Component = require("pixl-server/component");
|
|
10
7
|
var Tools = require("pixl-tools");
|
|
11
|
-
var AWS = require('aws-sdk');
|
|
12
8
|
var Cache = require("pixl-cache");
|
|
9
|
+
var S3 = require("@aws-sdk/client-s3");
|
|
10
|
+
var streamToBuffer = require("fast-stream-to-buffer");
|
|
13
11
|
|
|
14
12
|
module.exports = Class.create({
|
|
15
13
|
|
|
@@ -41,13 +39,6 @@ module.exports = Class.create({
|
|
|
41
39
|
this.keyTemplate = (s3_config.keyTemplate || '').replace(/^\//, '').replace(/\/$/, '');
|
|
42
40
|
this.fileExtensions = !!s3_config.fileExtensions;
|
|
43
41
|
|
|
44
|
-
if (this.debugLevel(10)) {
|
|
45
|
-
// S3 has a logger API but it's extremely verbose -- restrict to level 10 only
|
|
46
|
-
s3_config.logger = {
|
|
47
|
-
log: function(msg) { self.logDebug(10, "S3 Debug: " + msg); }
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
42
|
// optional LRU cache
|
|
52
43
|
this.cache = null;
|
|
53
44
|
var cache_opts = s3_config.cache;
|
|
@@ -63,10 +54,36 @@ module.exports = Class.create({
|
|
|
63
54
|
});
|
|
64
55
|
});
|
|
65
56
|
}
|
|
66
|
-
delete s3_config.cache;
|
|
67
57
|
|
|
68
|
-
AWS
|
|
69
|
-
|
|
58
|
+
// merge AWS and S3 configs
|
|
59
|
+
var combo_config = Tools.mergeHashes( aws_config, s3_config );
|
|
60
|
+
|
|
61
|
+
// convert v2 config to v3
|
|
62
|
+
if (!combo_config.maxAttempts && combo_config.maxRetries) {
|
|
63
|
+
combo_config.maxAttempts = combo_config.maxRetries;
|
|
64
|
+
delete combo_config.maxRetries;
|
|
65
|
+
}
|
|
66
|
+
if (combo_config.accessKeyId) {
|
|
67
|
+
if (!combo_config.credentials) combo_config.credentials = {};
|
|
68
|
+
combo_config.credentials.accessKeyId = combo_config.accessKeyId;
|
|
69
|
+
delete combo_config.accessKeyId;
|
|
70
|
+
}
|
|
71
|
+
if (combo_config.secretAccessKey) {
|
|
72
|
+
if (!combo_config.credentials) combo_config.credentials = {};
|
|
73
|
+
combo_config.credentials.secretAccessKey = combo_config.secretAccessKey;
|
|
74
|
+
delete combo_config.secretAccessKey;
|
|
75
|
+
}
|
|
76
|
+
delete combo_config.correctClockSkew;
|
|
77
|
+
delete combo_config.httpOptions;
|
|
78
|
+
delete combo_config.keyPrefix;
|
|
79
|
+
delete combo_config.keyTemplate;
|
|
80
|
+
delete combo_config.fileExtensions;
|
|
81
|
+
delete combo_config.cache;
|
|
82
|
+
|
|
83
|
+
this.s3Params = combo_config.params || {};
|
|
84
|
+
delete combo_config.params;
|
|
85
|
+
|
|
86
|
+
this.s3 = new S3.S3Client(combo_config);
|
|
70
87
|
},
|
|
71
88
|
|
|
72
89
|
prepKey: function(key) {
|
|
@@ -107,7 +124,7 @@ module.exports = Class.create({
|
|
|
107
124
|
var is_binary = this.storage.isBinaryKey(key);
|
|
108
125
|
key = this.prepKey(key);
|
|
109
126
|
|
|
110
|
-
var params =
|
|
127
|
+
var params = Tools.copyHash( this.s3Params );
|
|
111
128
|
params.Key = this.extKey(key, orig_key);
|
|
112
129
|
params.Body = value;
|
|
113
130
|
|
|
@@ -121,23 +138,21 @@ module.exports = Class.create({
|
|
|
121
138
|
params.ContentType = 'application/json';
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
this.s3.send( new S3.PutObjectCommand(params) )
|
|
142
|
+
.then( function(data) {
|
|
143
|
+
self.logDebug(9, "Store complete: " + key);
|
|
144
|
+
|
|
145
|
+
// possibly cache in LRU
|
|
146
|
+
if (self.cache && !is_binary) {
|
|
147
|
+
self.cache.set( orig_key, params.Body, { date: Tools.timeNow(true) } );
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (callback) callback(null, data);
|
|
151
|
+
} )
|
|
152
|
+
.catch( function(err) {
|
|
130
153
|
self.logError('s3', "Failed to store object: " + key + ": " + (err.message || err), err);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// possibly cache in LRU
|
|
135
|
-
if (self.cache && !is_binary) {
|
|
136
|
-
self.cache.set( orig_key, params.Body, { date: Tools.timeNow(true) } );
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (callback) callback(err, data);
|
|
140
|
-
} );
|
|
154
|
+
if (callback) callback(err);
|
|
155
|
+
} );
|
|
141
156
|
},
|
|
142
157
|
|
|
143
158
|
putStream: function(key, inp, callback) {
|
|
@@ -146,24 +161,21 @@ module.exports = Class.create({
|
|
|
146
161
|
var orig_key = key;
|
|
147
162
|
key = this.prepKey(key);
|
|
148
163
|
|
|
149
|
-
var params =
|
|
164
|
+
var params = Tools.copyHash( this.s3Params );
|
|
150
165
|
params.Key = this.extKey(key, orig_key);
|
|
151
166
|
params.Body = inp;
|
|
152
167
|
|
|
153
168
|
this.logDebug(9, "Storing S3 Binary Stream: " + key);
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
this.s3.send( new S3.PutObjectCommand(params) )
|
|
171
|
+
.then( function(data) {
|
|
172
|
+
self.logDebug(9, "Stream store complete: " + key);
|
|
173
|
+
if (callback) callback(null, data);
|
|
174
|
+
} )
|
|
175
|
+
.catch( function(err) {
|
|
161
176
|
self.logError('s3', "Failed to store stream: " + key + ": " + (err.message || err), err);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (callback) callback(err, data);
|
|
166
|
-
} );
|
|
177
|
+
if (callback) callback(err);
|
|
178
|
+
} );
|
|
167
179
|
},
|
|
168
180
|
|
|
169
181
|
putStreamCustom: function(key, inp, opts, callback) {
|
|
@@ -172,25 +184,22 @@ module.exports = Class.create({
|
|
|
172
184
|
var orig_key = key;
|
|
173
185
|
key = this.prepKey(key);
|
|
174
186
|
|
|
175
|
-
var params =
|
|
187
|
+
var params = Tools.copyHash( this.s3Params );
|
|
176
188
|
params.Key = this.extKey(key, orig_key);
|
|
177
189
|
params.Body = inp;
|
|
178
190
|
if (opts) Tools.mergeHashInto(params, opts);
|
|
179
191
|
|
|
180
192
|
this.logDebug(9, "Storing S3 Binary Stream: " + key);
|
|
181
193
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
this.s3.send( new S3.PutObjectCommand(params) )
|
|
195
|
+
.then( function(data) {
|
|
196
|
+
self.logDebug(9, "Stream store complete: " + key);
|
|
197
|
+
if (callback) callback(null, data);
|
|
198
|
+
} )
|
|
199
|
+
.catch( function(err) {
|
|
188
200
|
self.logError('s3', "Failed to store stream: " + key + ": " + (err.message || err), err);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (callback) callback(err, data);
|
|
193
|
-
} );
|
|
201
|
+
if (callback) callback(err);
|
|
202
|
+
} );
|
|
194
203
|
},
|
|
195
204
|
|
|
196
205
|
head: function(key, callback) {
|
|
@@ -214,13 +223,20 @@ module.exports = Class.create({
|
|
|
214
223
|
return;
|
|
215
224
|
} // cache
|
|
216
225
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
var params = Tools.copyHash( this.s3Params );
|
|
227
|
+
params.Key = this.extKey(key, orig_key);
|
|
228
|
+
|
|
229
|
+
this.s3.send( new S3.HeadObjectCommand(params) )
|
|
230
|
+
.then( function(data) {
|
|
231
|
+
self.logDebug(9, "Head complete: " + key);
|
|
232
|
+
|
|
233
|
+
callback( null, {
|
|
234
|
+
mod: Math.floor( (new Date(data.LastModified)).getTime() / 1000 ),
|
|
235
|
+
len: data.ContentLength
|
|
236
|
+
} );
|
|
237
|
+
} )
|
|
238
|
+
.catch( function(err) {
|
|
239
|
+
if ((err.name == 'NoSuchKey') || (err.name == 'NotFound') || (err.code == 'NoSuchKey') || (err.code == 'NotFound')) {
|
|
224
240
|
// key not found, special case, don't log an error
|
|
225
241
|
// always include "Not found" in error message
|
|
226
242
|
err = new Error("Failed to head key: " + key + ": Not found");
|
|
@@ -230,16 +246,8 @@ module.exports = Class.create({
|
|
|
230
246
|
// some other error
|
|
231
247
|
self.logError('s3', "Failed to head key: " + key + ": " + (err.message || err), err);
|
|
232
248
|
}
|
|
233
|
-
callback( err
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
self.logDebug(9, "Head complete: " + key);
|
|
238
|
-
callback( null, {
|
|
239
|
-
mod: Math.floor((new Date(data.LastModified)).getTime() / 1000),
|
|
240
|
-
len: data.ContentLength
|
|
249
|
+
callback( err );
|
|
241
250
|
} );
|
|
242
|
-
} );
|
|
243
251
|
},
|
|
244
252
|
|
|
245
253
|
get: function(key, callback) {
|
|
@@ -269,13 +277,46 @@ module.exports = Class.create({
|
|
|
269
277
|
return;
|
|
270
278
|
} // cache
|
|
271
279
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
var params = Tools.copyHash( this.s3Params );
|
|
281
|
+
params.Key = this.extKey(key, orig_key);
|
|
282
|
+
|
|
283
|
+
this.s3.send( new S3.GetObjectCommand(params) )
|
|
284
|
+
.then( function(data) {
|
|
285
|
+
// stream to buffer
|
|
286
|
+
streamToBuffer( data.Body, function (err, body) {
|
|
287
|
+
if (err) {
|
|
288
|
+
self.logError('s3', "Failed to fetch key: " + key + ": " + (err.message || err), err);
|
|
289
|
+
return callback(err);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (is_binary) {
|
|
293
|
+
self.logDebug(9, "Binary fetch complete: " + key, '' + body.length + ' bytes');
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
body = body.toString();
|
|
297
|
+
|
|
298
|
+
// possibly cache in LRU
|
|
299
|
+
if (self.cache) {
|
|
300
|
+
self.cache.set( orig_key, body, { date: Tools.timeNow(true) } );
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try { body = JSON.parse( body ); }
|
|
304
|
+
catch (e) {
|
|
305
|
+
self.logError('s3', "Failed to parse JSON record: " + key + ": " + e);
|
|
306
|
+
callback( e, null );
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? body : null);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
callback( null, body, {
|
|
313
|
+
mod: Math.floor((new Date(data.LastModified)).getTime() / 1000),
|
|
314
|
+
len: data.ContentLength
|
|
315
|
+
} );
|
|
316
|
+
} ); // streamToBuffer
|
|
317
|
+
} )
|
|
318
|
+
.catch( function(err) {
|
|
319
|
+
if ((err.name == 'NoSuchKey') || (err.name == 'NotFound') || (err.code == 'NoSuchKey') || (err.code == 'NotFound')) {
|
|
279
320
|
// key not found, special case, don't log an error
|
|
280
321
|
// always include "Not found" in error message
|
|
281
322
|
err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
@@ -285,37 +326,8 @@ module.exports = Class.create({
|
|
|
285
326
|
// some other error
|
|
286
327
|
self.logError('s3', "Failed to fetch key: " + key + ": " + (err.message || err), err);
|
|
287
328
|
}
|
|
288
|
-
callback( err
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
var body = null;
|
|
293
|
-
if (is_binary) {
|
|
294
|
-
body = data.Body;
|
|
295
|
-
self.logDebug(9, "Binary fetch complete: " + key, '' + body.length + ' bytes');
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
body = data.Body.toString();
|
|
299
|
-
|
|
300
|
-
// possibly cache in LRU
|
|
301
|
-
if (self.cache) {
|
|
302
|
-
self.cache.set( orig_key, body, { date: Tools.timeNow(true) } );
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
try { body = JSON.parse( body ); }
|
|
306
|
-
catch (e) {
|
|
307
|
-
self.logError('s3', "Failed to parse JSON record: " + key + ": " + e);
|
|
308
|
-
callback( e, null );
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? body : null);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
callback( null, body, {
|
|
315
|
-
mod: Math.floor((new Date(data.LastModified)).getTime() / 1000),
|
|
316
|
-
len: data.ContentLength
|
|
329
|
+
callback( err );
|
|
317
330
|
} );
|
|
318
|
-
} );
|
|
319
331
|
},
|
|
320
332
|
|
|
321
333
|
getStream: function(key, callback) {
|
|
@@ -326,27 +338,30 @@ module.exports = Class.create({
|
|
|
326
338
|
|
|
327
339
|
this.logDebug(9, "Fetching S3 Stream: " + key);
|
|
328
340
|
|
|
329
|
-
var params =
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
341
|
+
var params = Tools.copyHash( this.s3Params );
|
|
342
|
+
params.Key = this.extKey(key, orig_key);
|
|
343
|
+
|
|
344
|
+
this.s3.send( new S3.GetObjectCommand(params) )
|
|
345
|
+
.then( function(data) {
|
|
346
|
+
var download = data.Body;
|
|
347
|
+
|
|
348
|
+
download.on('error', function(err) {
|
|
349
|
+
self.logError('s3', "Failed to download key: " + key + ": " + (err.message || err), err);
|
|
350
|
+
});
|
|
351
|
+
download.once('end', function() {
|
|
352
|
+
self.logDebug(9, "S3 stream download complete: " + key);
|
|
353
|
+
} );
|
|
354
|
+
download.once('close', function() {
|
|
355
|
+
self.logDebug(9, "S3 stream download closed: " + key);
|
|
356
|
+
} );
|
|
357
|
+
|
|
358
|
+
callback( null, download, {
|
|
359
|
+
mod: Math.floor( (new Date(data.LastModified)).getTime() / 1000 ),
|
|
360
|
+
len: data.ContentLength
|
|
361
|
+
} );
|
|
362
|
+
})
|
|
363
|
+
.catch( function(err) {
|
|
364
|
+
if ((err.name == 'NoSuchKey') || (err.name == 'NotFound') || (err.code == 'NoSuchKey') || (err.code == 'NotFound')) {
|
|
350
365
|
// key not found, special case, don't log an error
|
|
351
366
|
// always include "Not found" in error message
|
|
352
367
|
err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
@@ -356,18 +371,8 @@ module.exports = Class.create({
|
|
|
356
371
|
// some other error
|
|
357
372
|
self.logError('s3', "Failed to fetch key: " + key + ": " + (err.message || err), err);
|
|
358
373
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
callback( err, null );
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
proceed = true;
|
|
366
|
-
callback( null, download, {
|
|
367
|
-
mod: Math.floor((new Date(data.LastModified)).getTime() / 1000),
|
|
368
|
-
len: data.ContentLength
|
|
369
|
-
} );
|
|
370
|
-
}); // headObject
|
|
374
|
+
callback( err );
|
|
375
|
+
});
|
|
371
376
|
},
|
|
372
377
|
|
|
373
378
|
getStreamRange: function(key, start, end, callback) {
|
|
@@ -378,7 +383,8 @@ module.exports = Class.create({
|
|
|
378
383
|
|
|
379
384
|
this.logDebug(9, "Fetching ranged S3 stream: " + key, { start, end });
|
|
380
385
|
|
|
381
|
-
var params =
|
|
386
|
+
var params = Tools.copyHash( this.s3Params );
|
|
387
|
+
params.Key = this.extKey(key, orig_key);
|
|
382
388
|
|
|
383
389
|
// convert start/end to HTTP range header string
|
|
384
390
|
var range = "bytes=";
|
|
@@ -386,26 +392,29 @@ module.exports = Class.create({
|
|
|
386
392
|
range += '-';
|
|
387
393
|
if (!isNaN(end)) range += end;
|
|
388
394
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
395
|
+
params.Range = range;
|
|
396
|
+
|
|
397
|
+
this.s3.send( new S3.GetObjectCommand(params) )
|
|
398
|
+
.then( function(data) {
|
|
399
|
+
var download = data.Body;
|
|
400
|
+
|
|
401
|
+
download.on('error', function(err) {
|
|
402
|
+
self.logError('s3', "Failed to download key: " + key + ": " + (err.message || err), err);
|
|
403
|
+
});
|
|
404
|
+
download.once('end', function() {
|
|
405
|
+
self.logDebug(9, "S3 stream download complete: " + key);
|
|
406
|
+
} );
|
|
407
|
+
download.once('close', function() {
|
|
408
|
+
self.logDebug(9, "S3 stream download closed: " + key);
|
|
409
|
+
} );
|
|
410
|
+
|
|
411
|
+
callback( null, download, {
|
|
412
|
+
mod: Math.floor( (new Date(data.LastModified)).getTime() / 1000 ),
|
|
413
|
+
len: data.ContentLength
|
|
414
|
+
} );
|
|
415
|
+
})
|
|
416
|
+
.catch( function(err) {
|
|
417
|
+
if ((err.name == 'NoSuchKey') || (err.name == 'NotFound') || (err.code == 'NoSuchKey') || (err.code == 'NotFound')) {
|
|
409
418
|
// key not found, special case, don't log an error
|
|
410
419
|
// always include "Not found" in error message
|
|
411
420
|
err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
@@ -415,32 +424,8 @@ module.exports = Class.create({
|
|
|
415
424
|
// some other error
|
|
416
425
|
self.logError('s3', "Failed to fetch key: " + key + ": " + (err.message || err), err);
|
|
417
426
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
callback( err, null );
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// validate byte range, now that we have the head info
|
|
425
|
-
if (isNaN(start) && !isNaN(end)) {
|
|
426
|
-
start = data.ContentLength - end;
|
|
427
|
-
end = data.ContentLength ? data.ContentLength - 1 : 0;
|
|
428
|
-
}
|
|
429
|
-
else if (!isNaN(start) && isNaN(end)) {
|
|
430
|
-
end = data.ContentLength ? data.ContentLength - 1 : 0;
|
|
431
|
-
}
|
|
432
|
-
if (isNaN(start) || isNaN(end) || (start < 0) || (start >= data.ContentLength) || (end < start) || (end >= data.ContentLength)) {
|
|
433
|
-
download.destroy();
|
|
434
|
-
callback( new Error("Invalid byte range (" + start + '-' + end + ") for key: " + key + " (len: " + data.ContentLength + ")"), null );
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
proceed = true;
|
|
439
|
-
callback( null, download, {
|
|
440
|
-
mod: Math.floor((new Date(data.LastModified)).getTime() / 1000),
|
|
441
|
-
len: data.ContentLength
|
|
442
|
-
} );
|
|
443
|
-
}); // headObject
|
|
427
|
+
callback( err );
|
|
428
|
+
});
|
|
444
429
|
},
|
|
445
430
|
|
|
446
431
|
delete: function(key, callback) {
|
|
@@ -451,23 +436,24 @@ module.exports = Class.create({
|
|
|
451
436
|
|
|
452
437
|
this.logDebug(9, "Deleting S3 Object: " + key);
|
|
453
438
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
439
|
+
var params = Tools.copyHash( this.s3Params );
|
|
440
|
+
params.Key = this.extKey(key, orig_key);
|
|
441
|
+
|
|
442
|
+
this.s3.send( new S3.DeleteObjectCommand(params) )
|
|
443
|
+
.then( function(data) {
|
|
444
|
+
self.logDebug(9, "Delete complete: " + key);
|
|
445
|
+
|
|
446
|
+
// possibly delete from LRU cache as well
|
|
447
|
+
if (self.cache && self.cache.has(orig_key)) {
|
|
448
|
+
self.cache.delete(orig_key);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (callback) callback(null, data);
|
|
452
|
+
} )
|
|
453
|
+
.catch( function(err) {
|
|
460
454
|
self.logError('s3', "Failed to delete object: " + key + ": " + (err.message || err), err);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// possibly delete from LRU cache as well
|
|
465
|
-
if (self.cache && self.cache.has(orig_key)) {
|
|
466
|
-
self.cache.delete(orig_key);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (callback) callback(err, data);
|
|
470
|
-
} );
|
|
455
|
+
if (callback) callback(err);
|
|
456
|
+
} );
|
|
471
457
|
},
|
|
472
458
|
|
|
473
459
|
runMaintenance: function(callback) {
|
package/index_types/Date.js
CHANGED
|
@@ -159,7 +159,9 @@ module.exports = Class.create({
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// load index summary for list of all populated dates
|
|
162
|
+
var dspf = state.perf.begin('date_summary');
|
|
162
163
|
this.get( sum_path, function(err, summary) {
|
|
164
|
+
dspf.end();
|
|
163
165
|
if (err || !summary) {
|
|
164
166
|
summary = { id: query.def.id, values: {} };
|
|
165
167
|
}
|
|
@@ -190,6 +192,7 @@ module.exports = Class.create({
|
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
// now perform OR search for all applicable words
|
|
195
|
+
var drpf = state.perf.begin('date_range');
|
|
193
196
|
async.eachLimit( words, self.concurrency,
|
|
194
197
|
function(word, callback) {
|
|
195
198
|
// for each word, iterate over record ids
|
|
@@ -203,6 +206,8 @@ module.exports = Class.create({
|
|
|
203
206
|
},
|
|
204
207
|
function(err) {
|
|
205
208
|
// all done, perform final merge
|
|
209
|
+
drpf.end();
|
|
210
|
+
state.perf.count('date_buckets', words.length);
|
|
206
211
|
if (err) return callback(err);
|
|
207
212
|
self.mergeIndex( record_ids, temp_results, state.first ? 'or' : state.mode );
|
|
208
213
|
state.first = false;
|
package/index_types/Number.js
CHANGED
|
@@ -59,7 +59,7 @@ module.exports = Class.create({
|
|
|
59
59
|
|
|
60
60
|
if (value.match(/^(N?)(\d+)$/i)) {
|
|
61
61
|
var neg = RegExp.$1.toUpperCase();
|
|
62
|
-
var value = parseInt( RegExp.$2 );
|
|
62
|
+
var value = Math.floor( parseInt( RegExp.$2 ) * (def.multiply || 1) / (def.divide || 1) );
|
|
63
63
|
value = Math.min( NUMBER_INDEX_MAX, value );
|
|
64
64
|
|
|
65
65
|
var tkey = 'T' + neg + Math.floor( Math.floor(value / 1000) * 1000 );
|
|
@@ -103,7 +103,7 @@ module.exports = Class.create({
|
|
|
103
103
|
|
|
104
104
|
// clean number up
|
|
105
105
|
word = word.replace(/^N/i, '-').replace(/[^\d\-]+/g, '');
|
|
106
|
-
word = '' + Math.min( NUMBER_INDEX_MAX, Math.max( NUMBER_INDEX_MIN, parseInt(word) ) );
|
|
106
|
+
word = '' + Math.min( NUMBER_INDEX_MAX, Math.max( NUMBER_INDEX_MIN, Math.floor( parseInt(word) * (query.def.multiply || 1) / (query.def.divide || 1) ) ) );
|
|
107
107
|
word = word.replace(/\-/, 'N');
|
|
108
108
|
query.word = word;
|
|
109
109
|
|
|
@@ -119,7 +119,9 @@ module.exports = Class.create({
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// load index summary for list of all populated numbers
|
|
122
|
+
var nspf = state.perf.begin('number_summary');
|
|
122
123
|
this.get( sum_path, function(err, summary) {
|
|
124
|
+
nspf.end();
|
|
123
125
|
if (err || !summary) {
|
|
124
126
|
summary = { id: query.def.id, values: {} };
|
|
125
127
|
}
|
|
@@ -150,6 +152,7 @@ module.exports = Class.create({
|
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
// now perform OR search for all applicable words
|
|
155
|
+
var nrpf = state.perf.begin('number_range');
|
|
153
156
|
async.eachLimit( words, self.concurrency,
|
|
154
157
|
function(word, callback) {
|
|
155
158
|
// for each word, iterate over record ids
|
|
@@ -163,6 +166,8 @@ module.exports = Class.create({
|
|
|
163
166
|
},
|
|
164
167
|
function(err) {
|
|
165
168
|
// all done, perform final merge
|
|
169
|
+
nrpf.end();
|
|
170
|
+
state.perf.count('number_buckets', words.length);
|
|
166
171
|
if (err) return callback(err);
|
|
167
172
|
self.mergeIndex( record_ids, temp_results, state.first ? 'or' : state.mode );
|
|
168
173
|
state.first = false;
|
|
@@ -185,7 +190,7 @@ module.exports = Class.create({
|
|
|
185
190
|
|
|
186
191
|
// clean number up
|
|
187
192
|
word = word.replace(/^N/i, '-').replace(/[^\d\-]+/g, '');
|
|
188
|
-
word = '' + Math.min( NUMBER_INDEX_MAX, Math.max( NUMBER_INDEX_MIN, parseInt(word) ) );
|
|
193
|
+
word = '' + Math.min( NUMBER_INDEX_MAX, Math.max( NUMBER_INDEX_MIN, Math.floor( parseInt(word) * (query.def.multiply || 1) / (query.def.divide || 1) ) ) );
|
|
189
194
|
word = word.replace(/\-/, 'N');
|
|
190
195
|
query.word = word;
|
|
191
196
|
|
package/indexer.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
var async = require('async');
|
|
6
6
|
var Class = require("pixl-class");
|
|
7
7
|
var Tools = require("pixl-tools");
|
|
8
|
+
var Perf = require("pixl-perf");
|
|
8
9
|
var nearley = require("nearley");
|
|
9
10
|
var pxql_grammar = require("./pxql.js");
|
|
10
11
|
var stemmer = require('porter-stemmer').stemmer;
|
|
@@ -1220,15 +1221,16 @@ module.exports = Class.create({
|
|
|
1220
1221
|
var path = config.base_path;
|
|
1221
1222
|
var pf = this.perf.begin('search');
|
|
1222
1223
|
|
|
1224
|
+
var orig_query = query;
|
|
1225
|
+
if (typeof(query) == 'object') query = Tools.copyHash(query, true);
|
|
1226
|
+
|
|
1223
1227
|
this.shareLock( 'C|'+path, true, function(err, lock) {
|
|
1224
1228
|
// got shared lock
|
|
1225
1229
|
self._searchRecords( query, config, function(err, results, state) {
|
|
1226
1230
|
// search complete
|
|
1227
|
-
var elapsed = pf.end();
|
|
1228
|
-
|
|
1229
1231
|
if (!err) self.logTransaction('search', path, {
|
|
1230
|
-
query:
|
|
1231
|
-
|
|
1232
|
+
query: orig_query,
|
|
1233
|
+
perf: state.perf ? state.perf.metrics() : {},
|
|
1232
1234
|
results: (self.logEventTypes.search || self.logEventTypes.all) ? Tools.numKeys(results) : 0
|
|
1233
1235
|
});
|
|
1234
1236
|
|
|
@@ -1251,9 +1253,15 @@ module.exports = Class.create({
|
|
|
1251
1253
|
if (query == '*') {
|
|
1252
1254
|
// fetch all records
|
|
1253
1255
|
this.logDebug(8, "Fetching all records: " + config.base_path);
|
|
1256
|
+
var apf = new Perf();
|
|
1257
|
+
apf.begin();
|
|
1258
|
+
apf.begin('all');
|
|
1259
|
+
|
|
1254
1260
|
return this.hashGetAll( config.base_path + '/_id', function(err, results) {
|
|
1255
1261
|
// ignore error, just return empty hash
|
|
1256
|
-
|
|
1262
|
+
apf.end('all');
|
|
1263
|
+
apf.end();
|
|
1264
|
+
callback( null, results || {}, { perf: apf } );
|
|
1257
1265
|
} );
|
|
1258
1266
|
}
|
|
1259
1267
|
else if (query.match(/^\([\s\S]+\)$/)) {
|
|
@@ -1282,6 +1290,12 @@ module.exports = Class.create({
|
|
|
1282
1290
|
state.record_ids = Object.create(null);
|
|
1283
1291
|
state.first = true;
|
|
1284
1292
|
|
|
1293
|
+
// track detailed perf of search operations
|
|
1294
|
+
if (!state.perf) {
|
|
1295
|
+
state.perf = new Perf();
|
|
1296
|
+
state.perf.begin();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1285
1299
|
// first, split criteria into subs (sub-queries),
|
|
1286
1300
|
// stds (standard queries) and negs (negative queries)
|
|
1287
1301
|
var subs = [], stds = [], negs = [];
|
|
@@ -1302,11 +1316,14 @@ module.exports = Class.create({
|
|
|
1302
1316
|
}
|
|
1303
1317
|
|
|
1304
1318
|
// stds need to be weighed and sorted by weight ascending
|
|
1319
|
+
var wpf = state.perf.begin('weigh');
|
|
1305
1320
|
async.eachLimit( (query.mode == 'and') ? stds : [], this.concurrency,
|
|
1306
1321
|
function(crit, callback) {
|
|
1307
1322
|
self.weighCriterion(crit, config, callback);
|
|
1308
1323
|
},
|
|
1309
1324
|
function(err) {
|
|
1325
|
+
wpf.end();
|
|
1326
|
+
|
|
1310
1327
|
// sort stds by weight ascending (only needed in AND mode)
|
|
1311
1328
|
if (query.mode == 'and') {
|
|
1312
1329
|
stds = stds.sort( function(a, b) { return a.weight - b.weight; } );
|
|
@@ -1317,9 +1334,12 @@ module.exports = Class.create({
|
|
|
1317
1334
|
var tasks = [].concat( subs, stds, negs );
|
|
1318
1335
|
async.eachSeries( tasks,
|
|
1319
1336
|
function(task, callback) {
|
|
1337
|
+
task.perf = state.perf;
|
|
1338
|
+
|
|
1320
1339
|
if (task.criteria) {
|
|
1321
|
-
// sub-query
|
|
1340
|
+
// sub-query
|
|
1322
1341
|
self._searchRecords( task, config, function(err, records) {
|
|
1342
|
+
state.perf.count('subs', 1);
|
|
1323
1343
|
self.mergeIndex( state.record_ids, records, state.first ? 'or' : state.mode );
|
|
1324
1344
|
state.first = false;
|
|
1325
1345
|
callback();
|
|
@@ -1333,13 +1353,31 @@ module.exports = Class.create({
|
|
|
1333
1353
|
// custom index type, e.g. date, time, number
|
|
1334
1354
|
var func = 'searchIndex_' + task.def.type;
|
|
1335
1355
|
if (!self[func]) return callback( new Error("Unknown index type: " + task.def.type) );
|
|
1336
|
-
|
|
1356
|
+
|
|
1357
|
+
var cpf = state.perf.begin('search_' + task.def.id + '_' + task.def.type);
|
|
1358
|
+
self[func]( task, state, function(err) {
|
|
1359
|
+
cpf.end();
|
|
1360
|
+
state.perf.count(task.def.type + 's', 1);
|
|
1361
|
+
callback(err);
|
|
1362
|
+
} );
|
|
1337
1363
|
}
|
|
1338
1364
|
else if (task.literal) {
|
|
1339
|
-
|
|
1365
|
+
// literal multi-word phrase
|
|
1366
|
+
var spf = state.perf.begin('search_' + task.def.id + '_literal');
|
|
1367
|
+
self.searchWordIndexLiteral(task, state, function(err) {
|
|
1368
|
+
spf.end();
|
|
1369
|
+
state.perf.count('literals', 1);
|
|
1370
|
+
callback(err);
|
|
1371
|
+
});
|
|
1340
1372
|
}
|
|
1341
1373
|
else {
|
|
1342
|
-
|
|
1374
|
+
// single word search
|
|
1375
|
+
var spf = state.perf.begin('search_' + task.def.id + '_word');
|
|
1376
|
+
self.searchWordIndex(task, state, function(err) {
|
|
1377
|
+
spf.end();
|
|
1378
|
+
state.perf.count('words', 1);
|
|
1379
|
+
callback(err);
|
|
1380
|
+
});
|
|
1343
1381
|
}
|
|
1344
1382
|
},
|
|
1345
1383
|
function(err) {
|
|
@@ -1347,6 +1385,7 @@ module.exports = Class.create({
|
|
|
1347
1385
|
if (err) {
|
|
1348
1386
|
self.logError('index', "Index search failed: " + err);
|
|
1349
1387
|
state.record_ids = {};
|
|
1388
|
+
state.err = err;
|
|
1350
1389
|
}
|
|
1351
1390
|
self.logDebug(10, "Search complete", state.record_ids);
|
|
1352
1391
|
callback(null, state.record_ids, Tools.copyHashRemoveKeys(state, { config:1, record_ids:1, first:1 }));
|
|
@@ -1357,7 +1396,7 @@ module.exports = Class.create({
|
|
|
1357
1396
|
},
|
|
1358
1397
|
|
|
1359
1398
|
searchWordIndex: function(query, state, callback) {
|
|
1360
|
-
// run one
|
|
1399
|
+
// run one word query (single word against one index)
|
|
1361
1400
|
var self = this;
|
|
1362
1401
|
var config = state.config;
|
|
1363
1402
|
var def = query.def;
|
|
@@ -1385,6 +1424,7 @@ module.exports = Class.create({
|
|
|
1385
1424
|
// then it would probably be faster to apply the logic using _data getMulti (a.k.a row scan).
|
|
1386
1425
|
// Otherwise, perform a normal hash merge (which has to read every hash page).
|
|
1387
1426
|
var hash_page_size = config.hash_page_size || 1000;
|
|
1427
|
+
var wpf = state.perf.begin('word_' + query.word);
|
|
1388
1428
|
|
|
1389
1429
|
if ((mode == 'and') && query.weight && (query.weight / hash_page_size > num_cur_items)) {
|
|
1390
1430
|
this.logDebug(10, "Performing row scan on " + num_cur_items + " items", query);
|
|
@@ -1394,7 +1434,9 @@ module.exports = Class.create({
|
|
|
1394
1434
|
return config.base_path + '/_data/' + record_id;
|
|
1395
1435
|
} );
|
|
1396
1436
|
|
|
1437
|
+
var rspf = state.perf.begin('row_scan');
|
|
1397
1438
|
this.getMulti( data_paths, function(err, datas) {
|
|
1439
|
+
rspf.end();
|
|
1398
1440
|
if (err) return callback(err);
|
|
1399
1441
|
|
|
1400
1442
|
datas.forEach( function(data, idx) {
|
|
@@ -1404,11 +1446,15 @@ module.exports = Class.create({
|
|
|
1404
1446
|
}
|
|
1405
1447
|
} );
|
|
1406
1448
|
|
|
1449
|
+
state.perf.count('rows_scanned', datas.length);
|
|
1450
|
+
wpf.end();
|
|
1407
1451
|
callback();
|
|
1408
1452
|
} ); // getMulti
|
|
1409
1453
|
} // row scan
|
|
1410
1454
|
else {
|
|
1411
1455
|
this.logDebug(10, "Performing '" + mode + "' hash merge on " + num_cur_items + " items", query);
|
|
1456
|
+
|
|
1457
|
+
var hmpf = state.perf.begin('hash_merge');
|
|
1412
1458
|
this.hashEachPage( path,
|
|
1413
1459
|
function(items, callback) {
|
|
1414
1460
|
switch (mode) {
|
|
@@ -1430,9 +1476,12 @@ module.exports = Class.create({
|
|
|
1430
1476
|
}
|
|
1431
1477
|
break;
|
|
1432
1478
|
}
|
|
1479
|
+
state.perf.count('hash_pages', 1);
|
|
1433
1480
|
callback();
|
|
1434
1481
|
},
|
|
1435
1482
|
function(err) {
|
|
1483
|
+
hmpf.end();
|
|
1484
|
+
wpf.end();
|
|
1436
1485
|
if (mode == 'and') state.record_ids = new_items;
|
|
1437
1486
|
callback(err);
|
|
1438
1487
|
}
|
|
@@ -1460,6 +1509,7 @@ module.exports = Class.create({
|
|
|
1460
1509
|
function(word, callback) {
|
|
1461
1510
|
// for each word, iterate over record ids
|
|
1462
1511
|
var keepers = Object.create(null);
|
|
1512
|
+
var wpf = state.perf.begin('literal_' + word);
|
|
1463
1513
|
|
|
1464
1514
|
self.hashEachSync( path_prefix + word,
|
|
1465
1515
|
function(record_id, raw_value) {
|
|
@@ -1495,6 +1545,7 @@ module.exports = Class.create({
|
|
|
1495
1545
|
else keepers[record_id] = 1;
|
|
1496
1546
|
},
|
|
1497
1547
|
function(err) {
|
|
1548
|
+
wpf.end();
|
|
1498
1549
|
// If in a subsequent word pass, make sure all temp_results
|
|
1499
1550
|
// ids are still matched in the latest word
|
|
1500
1551
|
if (temp_idx > 0) self.mergeIndex( temp_results, keepers, 'and' );
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixl-server-storage",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.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",
|
|
@@ -21,17 +21,19 @@
|
|
|
21
21
|
"redis"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
"pixl-class": "^1.0.3",
|
|
26
|
-
"pixl-tools": "^1.0.24",
|
|
27
|
-
"pixl-perf": "^1.0.0",
|
|
28
|
-
"pixl-cache": "^1.0.1",
|
|
24
|
+
"@aws-sdk/client-s3": "3.186.0",
|
|
29
25
|
"async": "2.6.4",
|
|
30
|
-
"
|
|
26
|
+
"fast-stream-to-buffer": "1.0.0",
|
|
27
|
+
"he": "1.1.1",
|
|
31
28
|
"moo": "0.4.3",
|
|
29
|
+
"nearley": "2.20.1",
|
|
30
|
+
"pixl-cache": "^1.0.1",
|
|
31
|
+
"pixl-class": "^1.0.3",
|
|
32
|
+
"pixl-perf": "^1.0.0",
|
|
33
|
+
"pixl-server": "^1.0.0",
|
|
34
|
+
"pixl-tools": "^1.0.24",
|
|
32
35
|
"porter-stemmer": "0.9.1",
|
|
33
|
-
"unidecode": "0.1.8"
|
|
34
|
-
"he": "1.1.1"
|
|
36
|
+
"unidecode": "0.1.8"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"pixl-unit": "^1.0.0"
|
package/test/test-main.js
CHANGED
|
@@ -446,10 +446,11 @@ module.exports = {
|
|
|
446
446
|
|
|
447
447
|
self.storage.getStreamRange( key, 0, 5, function(err, storageStream, streamInfo) {
|
|
448
448
|
test.ok( !err, "No error fetching stream: " + err );
|
|
449
|
+
test.debug( "streamInfo: ", streamInfo );
|
|
449
450
|
test.ok( !!storageStream, "Got storage stream as 2nd arg");
|
|
450
451
|
test.ok( !!storageStream.pipe, "Storage stream has a pipe");
|
|
451
452
|
test.ok( !!streamInfo, "Info was provided as the 3rd arg");
|
|
452
|
-
test.ok( streamInfo.len ==
|
|
453
|
+
test.ok( streamInfo.len == 6, "Info has correct data length");
|
|
453
454
|
test.ok( streamInfo.mod > 0, "Info has a non-zero mod date");
|
|
454
455
|
|
|
455
456
|
outStream.on('finish', function() {
|
|
@@ -496,7 +497,7 @@ module.exports = {
|
|
|
496
497
|
test.ok( !!storageStream, "Got storage stream as 2nd arg");
|
|
497
498
|
test.ok( !!storageStream.pipe, "Storage stream has a pipe");
|
|
498
499
|
test.ok( !!streamInfo, "Info was provided as the 3rd arg");
|
|
499
|
-
test.ok( streamInfo.len ==
|
|
500
|
+
test.ok( streamInfo.len == 23, "Info has correct data length");
|
|
500
501
|
test.ok( streamInfo.mod > 0, "Info has a non-zero mod date");
|
|
501
502
|
|
|
502
503
|
outStream.on('finish', function() {
|
|
@@ -543,7 +544,7 @@ module.exports = {
|
|
|
543
544
|
test.ok( !!storageStream, "Got storage stream as 2nd arg");
|
|
544
545
|
test.ok( !!storageStream.pipe, "Storage stream has a pipe");
|
|
545
546
|
test.ok( !!streamInfo, "Info was provided as the 3rd arg");
|
|
546
|
-
test.ok( streamInfo.len ==
|
|
547
|
+
test.ok( streamInfo.len == 10, "Info has correct data length");
|
|
547
548
|
test.ok( streamInfo.mod > 0, "Info has a non-zero mod date");
|
|
548
549
|
|
|
549
550
|
outStream.on('finish', function() {
|