pixl-server-storage 3.0.5 → 3.0.9
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 +1 -1
- package/docs/API.md +39 -3
- package/engines/Couchbase.js +27 -1
- package/engines/Filesystem.js +37 -1
- package/engines/Hybrid.js +6 -0
- package/engines/Redis.js +28 -1
- package/engines/RedisCluster.js +28 -1
- package/engines/S3.js +85 -12
- package/package.json +1 -1
- package/storage.js +15 -0
- package/test/test-main.js +51 -2
- package/transaction.js +9 -36
package/README.md
CHANGED
|
@@ -846,7 +846,7 @@ storage.getStream( 'test1.gif', function(err, readStream) {
|
|
|
846
846
|
} );
|
|
847
847
|
```
|
|
848
848
|
|
|
849
|
-
Please note that not all the storage engines support streams natively, so the content may actually be loaded into RAM in the background. Namely, as of this writing, the Couchbase and Redis APIs
|
|
849
|
+
Please note that not all the storage engines support streams natively, so the content may actually be loaded into RAM in the background. Namely, as of this writing, the Couchbase and Redis APIs do not support streams, so they are currently simulated in those engines. Streams *are* supported natively in both the Filesystem and Amazon S3 engines.
|
|
850
850
|
|
|
851
851
|
# Expiring Data
|
|
852
852
|
|
package/docs/API.md
CHANGED
|
@@ -18,6 +18,7 @@ var storage = server.Storage;
|
|
|
18
18
|
* [get](#get)
|
|
19
19
|
* [getMulti](#getmulti)
|
|
20
20
|
* [getStream](#getstream)
|
|
21
|
+
* [getStreamRange](#getstreamrange)
|
|
21
22
|
* [head](#head)
|
|
22
23
|
* [headMulti](#headmulti)
|
|
23
24
|
* [delete](#delete)
|
|
@@ -126,6 +127,8 @@ storage.putMulti( records, function(err) {
|
|
|
126
127
|
} );
|
|
127
128
|
```
|
|
128
129
|
|
|
130
|
+
Note that if any of the individual put operations fail, the entire `putMulti()` function is aborted, and the first error is passed to your callback. At this point the operation may have been partially successful, with some records written, and others not. Due to this uncertainty, you may want to use this method inside of a [transaction](./Transactions.md), which can be safely rolled back upon error.
|
|
131
|
+
|
|
129
132
|
## putStream
|
|
130
133
|
|
|
131
134
|
```javascript
|
|
@@ -142,6 +145,8 @@ storage.putStream( 'test1.gif', stream, function(err) {
|
|
|
142
145
|
} );
|
|
143
146
|
```
|
|
144
147
|
|
|
148
|
+
Please note that as of this writing, the `Couchbase`, `Redis` and `RedisCluster` engines have no native stream API, so the `putStream()` method has to load the entire record into memory.
|
|
149
|
+
|
|
145
150
|
## get
|
|
146
151
|
|
|
147
152
|
```javascript
|
|
@@ -173,19 +178,46 @@ storage.getMulti( ['test1', 'test2', 'test3'], function(err, values) {
|
|
|
173
178
|
} );
|
|
174
179
|
```
|
|
175
180
|
|
|
181
|
+
Note that if *any* of the records fail, the entire operation fails, and the first error is passed to your callback.
|
|
182
|
+
|
|
176
183
|
## getStream
|
|
177
184
|
|
|
178
185
|
```javascript
|
|
179
186
|
storage.getStream( KEY, CALLBACK );
|
|
180
187
|
```
|
|
181
188
|
|
|
182
|
-
The `getStream()` method retrieves a [readable stream](https://nodejs.org/api/stream.html#
|
|
189
|
+
The `getStream()` method retrieves a [readable stream](https://nodejs.org/api/stream.html#class-streamreadable) to a given record's data, so it can be read or piped to a writable stream. This is for very large records, so nothing is loaded into memory. Example of spooling to a local file:
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
var fs = require('fs');
|
|
193
|
+
var writeStream = fs.createWriteStream('/var/tmp/downloaded.gif');
|
|
194
|
+
|
|
195
|
+
storage.getStream( 'test1.gif', function(err, readStream, info) {
|
|
196
|
+
if (err) throw err;
|
|
197
|
+
writeStream.on('finish', function() {
|
|
198
|
+
// data is completely written
|
|
199
|
+
} );
|
|
200
|
+
readStream.pipe( writeStream );
|
|
201
|
+
} );
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
As you can see above, your callback is also passed a 3rd argument, which is an object containing the record's full byte length and modification date. These properties match those returned when you call [head()](#head) (i.e. `len` and `mod`).
|
|
205
|
+
|
|
206
|
+
Please note that as of this writing, the `Couchbase`, `Redis` and `RedisCluster` engines have no native stream API, so the `getStream()` method has to load the entire record into memory.
|
|
207
|
+
|
|
208
|
+
## getStreamRange
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
storage.getStreamRange( KEY, START, END, CALLBACK );
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The `getStreamRange()` method retrieves a [readable stream](https://nodejs.org/api/stream.html#class-streamreadable) to a specific slice of a record's data, so it can be read or piped to a writable stream. The `start` and `end` arguments should be set to the starting and ending byte offset of the slice you want. Both values are *inclusive*. Example of spooling bytes `0-99` of a record to a local file:
|
|
183
215
|
|
|
184
216
|
```javascript
|
|
185
217
|
var fs = require('fs');
|
|
186
218
|
var writeStream = fs.createWriteStream('/var/tmp/downloaded.gif');
|
|
187
219
|
|
|
188
|
-
storage.
|
|
220
|
+
storage.getStreamRange( 'test1.gif', 0, 99, function(err, readStream, info) {
|
|
189
221
|
if (err) throw err;
|
|
190
222
|
writeStream.on('finish', function() {
|
|
191
223
|
// data is completely written
|
|
@@ -194,6 +226,10 @@ storage.getStream( 'test1.gif', function(err, readStream) {
|
|
|
194
226
|
} );
|
|
195
227
|
```
|
|
196
228
|
|
|
229
|
+
As you can see above, your callback is also passed a 3rd argument, which is an object containing the record's full byte length and modification date. These properties match those returned when you call [head()](#head) (i.e. `len` and `mod`). The `len` is not affected by the `start` and `end` range -- it is always the full byte length of the record.
|
|
230
|
+
|
|
231
|
+
Please note that as of this writing, the `Couchbase`, `Redis` and `RedisCluster` engines have no native stream API, so the `getStreamRange()` method has to load the entire record into memory.
|
|
232
|
+
|
|
197
233
|
## head
|
|
198
234
|
|
|
199
235
|
```javascript
|
|
@@ -217,7 +253,7 @@ storage.head( 'test1', function(err, data) {
|
|
|
217
253
|
} );
|
|
218
254
|
```
|
|
219
255
|
|
|
220
|
-
Please note that as of this writing, the `Couchbase`
|
|
256
|
+
Please note that as of this writing, the `Couchbase`, `Redis` and `RedisCluster` engines have no native head API, so the `head()` method has to load the entire record. It does return the record size in to the `len` property, but there is no way to retrieve the last modified date.
|
|
221
257
|
|
|
222
258
|
## headMulti
|
|
223
259
|
|
package/engines/Couchbase.js
CHANGED
|
@@ -224,7 +224,33 @@ module.exports = Class.create({
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
var stream = new BufferStream(buf);
|
|
227
|
-
callback(null, stream);
|
|
227
|
+
callback(null, stream, { mod: 1, len: buf.length });
|
|
228
|
+
} );
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
getStreamRange: function(key, start, end, callback) {
|
|
232
|
+
// get readable stream to record value given key and range
|
|
233
|
+
var self = this;
|
|
234
|
+
|
|
235
|
+
// The Couchbase Node.JS 2.0 API has no stream support.
|
|
236
|
+
// So, we have to do this the RAM-hard way...
|
|
237
|
+
this.get( key, function(err, buf) {
|
|
238
|
+
if (err && (err.code != CouchbaseAPI.errors.keyNotFound)) {
|
|
239
|
+
// some other error
|
|
240
|
+
err.message = "Failed to fetch key: " + key + ": " + err.message;
|
|
241
|
+
self.logError('couchbase', err.message);
|
|
242
|
+
return callback(err);
|
|
243
|
+
}
|
|
244
|
+
else if (!buf) {
|
|
245
|
+
// record not found
|
|
246
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
247
|
+
err.code = "NoSuchKey";
|
|
248
|
+
return callback( err, null );
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
var range = buf.slice(start, end + 1);
|
|
252
|
+
var stream = new BufferStream(range);
|
|
253
|
+
callback(null, stream, { mod: 1, len: buf.length });
|
|
228
254
|
} );
|
|
229
255
|
},
|
|
230
256
|
|
package/engines/Filesystem.js
CHANGED
|
@@ -449,7 +449,43 @@ module.exports = Class.create({
|
|
|
449
449
|
// create read stream
|
|
450
450
|
var inp = fs.createReadStream( file );
|
|
451
451
|
|
|
452
|
-
callback( null, inp
|
|
452
|
+
callback( null, inp, {
|
|
453
|
+
mod: Math.floor(stats.mtime.getTime() / 1000),
|
|
454
|
+
len: stats.size
|
|
455
|
+
} );
|
|
456
|
+
} );
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
getStreamRange: function(key, start, end, callback) {
|
|
460
|
+
// get readable stream to record value given key and byte range
|
|
461
|
+
var self = this;
|
|
462
|
+
var file = this.getFilePath(key);
|
|
463
|
+
|
|
464
|
+
this.logDebug(9, "Fetching ranged binary stream: " + key, { file, start, end } );
|
|
465
|
+
|
|
466
|
+
// make sure record exists
|
|
467
|
+
fs.stat(file, function(err, stats) {
|
|
468
|
+
if (err) {
|
|
469
|
+
if (err.message.match(/ENOENT/)) {
|
|
470
|
+
err.message = "File not found";
|
|
471
|
+
err.code = "NoSuchKey";
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// log fs errors that aren't simple missing files (i.e. I/O errors)
|
|
475
|
+
self.logError('file', "Failed to stat file: " + key + ": " + file + ": " + err.message);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
err.message = "Failed to head key: " + key + ": " + err.message;
|
|
479
|
+
return callback( err, null );
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// create read stream
|
|
483
|
+
var inp = fs.createReadStream( file, { start, end } );
|
|
484
|
+
|
|
485
|
+
callback( null, inp, {
|
|
486
|
+
mod: Math.floor(stats.mtime.getTime() / 1000),
|
|
487
|
+
len: stats.size
|
|
488
|
+
} );
|
|
453
489
|
} );
|
|
454
490
|
},
|
|
455
491
|
|
package/engines/Hybrid.js
CHANGED
|
@@ -79,6 +79,12 @@ module.exports = Class.create({
|
|
|
79
79
|
this.binaryEngine.getStream( key, callback );
|
|
80
80
|
},
|
|
81
81
|
|
|
82
|
+
getStreamRange: function(key, start, end, callback) {
|
|
83
|
+
// get readable stream to record value given key and range
|
|
84
|
+
// streams are binary only!
|
|
85
|
+
this.binaryEngine.getStreamRange( key, start, end, callback );
|
|
86
|
+
},
|
|
87
|
+
|
|
82
88
|
delete: function(key, callback) {
|
|
83
89
|
// delete hybrid key given key
|
|
84
90
|
if (this.storage.isBinaryKey(key)) {
|
package/engines/Redis.js
CHANGED
|
@@ -227,7 +227,34 @@ module.exports = Class.create({
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
var stream = new BufferStream(buf);
|
|
230
|
-
callback(null, stream);
|
|
230
|
+
callback(null, stream, { mod: 1, len: buf.length });
|
|
231
|
+
} );
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
getStreamRange: function(key, start, end, callback) {
|
|
235
|
+
// get readable stream to record value given key and range
|
|
236
|
+
var self = this;
|
|
237
|
+
|
|
238
|
+
// The Redis API has no stream support.
|
|
239
|
+
// So, we have to do this the RAM-hard way...
|
|
240
|
+
|
|
241
|
+
this.get( key, function(err, buf) {
|
|
242
|
+
if (err) {
|
|
243
|
+
// an actual error
|
|
244
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
245
|
+
self.logError('redis', ''+err);
|
|
246
|
+
return callback(err);
|
|
247
|
+
}
|
|
248
|
+
else if (!buf) {
|
|
249
|
+
// record not found
|
|
250
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
251
|
+
err.code = "NoSuchKey";
|
|
252
|
+
return callback( err, null );
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
var range = buf.slice(start, end + 1);
|
|
256
|
+
var stream = new BufferStream(range);
|
|
257
|
+
callback(null, stream, { mod: 1, len: buf.length });
|
|
231
258
|
} );
|
|
232
259
|
},
|
|
233
260
|
|
package/engines/RedisCluster.js
CHANGED
|
@@ -244,7 +244,34 @@ module.exports = Class.create({
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
var stream = new BufferStream(buf);
|
|
247
|
-
callback(null, stream);
|
|
247
|
+
callback(null, stream, { mod: 1, len: buf.length });
|
|
248
|
+
} );
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
getStreamRange: function(key, start, end, callback) {
|
|
252
|
+
// get readable stream to record value given key and range
|
|
253
|
+
var self = this;
|
|
254
|
+
|
|
255
|
+
// The Redis API has no stream support.
|
|
256
|
+
// So, we have to do this the RAM-hard way...
|
|
257
|
+
|
|
258
|
+
this.get( key, function(err, buf) {
|
|
259
|
+
if (err) {
|
|
260
|
+
// an actual error
|
|
261
|
+
err.message = "Failed to fetch key: " + key + ": " + err;
|
|
262
|
+
self.logError('redis', ''+err);
|
|
263
|
+
return callback(err);
|
|
264
|
+
}
|
|
265
|
+
else if (!buf) {
|
|
266
|
+
// record not found
|
|
267
|
+
var err = new Error("Failed to fetch key: " + key + ": Not found");
|
|
268
|
+
err.code = "NoSuchKey";
|
|
269
|
+
return callback( err, null );
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
var range = buf.slice(start, end + 1);
|
|
273
|
+
var stream = new BufferStream(range);
|
|
274
|
+
callback(null, stream, { mod: 1, len: buf.length });
|
|
248
275
|
} );
|
|
249
276
|
},
|
|
250
277
|
|
package/engines/S3.js
CHANGED
|
@@ -125,7 +125,11 @@ module.exports = Class.create({
|
|
|
125
125
|
params.ContentType = 'application/json';
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// double-callback protection (bug in aws-sdk)
|
|
129
|
+
var done = false;
|
|
128
130
|
this.s3.putObject( params, function(err, data) {
|
|
131
|
+
if (done) return; else done = true;
|
|
132
|
+
|
|
129
133
|
if (err) {
|
|
130
134
|
self.logError('s3', "Failed to store object: " + key + ": " + err.message);
|
|
131
135
|
}
|
|
@@ -152,7 +156,11 @@ module.exports = Class.create({
|
|
|
152
156
|
|
|
153
157
|
this.logDebug(9, "Storing S3 Binary Stream: " + key);
|
|
154
158
|
|
|
159
|
+
// double-callback protection (bug in aws-sdk)
|
|
160
|
+
var done = false;
|
|
155
161
|
this.s3.upload(params, function(err, data) {
|
|
162
|
+
if (done) return; else done = true;
|
|
163
|
+
|
|
156
164
|
if (err) {
|
|
157
165
|
self.logError('s3', "Failed to store stream: " + key + ": " + err.message);
|
|
158
166
|
}
|
|
@@ -183,7 +191,11 @@ module.exports = Class.create({
|
|
|
183
191
|
return;
|
|
184
192
|
} // cache
|
|
185
193
|
|
|
194
|
+
// double-callback protection (bug in aws-sdk)
|
|
195
|
+
var done = false;
|
|
186
196
|
this.s3.headObject( { Key: this.extKey(key, orig_key) }, function(err, data) {
|
|
197
|
+
if (done) return; else done = true;
|
|
198
|
+
|
|
187
199
|
if (err) {
|
|
188
200
|
if (err.code != 'NoSuchKey') {
|
|
189
201
|
self.logError('s3', "Failed to head key: " + key + ": " + err.message);
|
|
@@ -227,7 +239,11 @@ module.exports = Class.create({
|
|
|
227
239
|
return;
|
|
228
240
|
} // cache
|
|
229
241
|
|
|
242
|
+
// double-callback protection (bug in aws-sdk)
|
|
243
|
+
var done = false;
|
|
230
244
|
this.s3.getObject( { Key: this.extKey(key, orig_key) }, function(err, data) {
|
|
245
|
+
if (done) return; else done = true;
|
|
246
|
+
|
|
231
247
|
if (err) {
|
|
232
248
|
if (err.code == 'NoSuchKey') {
|
|
233
249
|
// key not found, special case, don't log an error
|
|
@@ -278,29 +294,82 @@ module.exports = Class.create({
|
|
|
278
294
|
this.logDebug(9, "Fetching S3 Stream: " + key);
|
|
279
295
|
|
|
280
296
|
var params = { Key: this.extKey(key, orig_key) };
|
|
297
|
+
var download = self.s3.getObject(params).createReadStream();
|
|
298
|
+
var proceed = false;
|
|
299
|
+
|
|
300
|
+
download.on('error', function(err) {
|
|
301
|
+
if (proceed) self.logError('s3', "Failed to download key: " + key + ": " + err.message);
|
|
302
|
+
});
|
|
303
|
+
download.once('end', function() {
|
|
304
|
+
self.logDebug(9, "S3 stream download complete: " + key);
|
|
305
|
+
} );
|
|
306
|
+
download.once('close', function() {
|
|
307
|
+
self.logDebug(9, "S3 stream download closed: " + key);
|
|
308
|
+
} );
|
|
281
309
|
|
|
282
|
-
|
|
310
|
+
// double-callback protection (bug in aws-sdk)
|
|
311
|
+
var done = false;
|
|
312
|
+
this.s3.headObject( params, function(err, data) {
|
|
313
|
+
if (done) return; else done = true;
|
|
314
|
+
|
|
283
315
|
if (err) {
|
|
284
316
|
if (err.code != 'NoSuchKey') {
|
|
285
317
|
self.logError('s3', "Failed to head key: " + key + ": " + err.message);
|
|
286
318
|
}
|
|
319
|
+
download.destroy();
|
|
287
320
|
callback( err, null );
|
|
288
321
|
return;
|
|
289
322
|
}
|
|
290
323
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
});
|
|
296
|
-
download.on('end', function() {
|
|
297
|
-
self.logDebug(9, "S3 stream download complete: " + key);
|
|
298
|
-
} );
|
|
299
|
-
download.on('close', function() {
|
|
300
|
-
self.logDebug(9, "S3 stream download closed: " + key);
|
|
324
|
+
proceed = true;
|
|
325
|
+
callback( null, download, {
|
|
326
|
+
mod: Math.floor((new Date(data.LastModified)).getTime() / 1000),
|
|
327
|
+
len: data.ContentLength
|
|
301
328
|
} );
|
|
329
|
+
}); // headObject
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
getStreamRange: function(key, start, end, callback) {
|
|
333
|
+
// get readable stream to record value given key and range
|
|
334
|
+
var self = this;
|
|
335
|
+
var orig_key = key;
|
|
336
|
+
key = this.prepKey(key);
|
|
337
|
+
|
|
338
|
+
this.logDebug(9, "Fetching ranged S3 stream: " + key, { start, end });
|
|
339
|
+
|
|
340
|
+
var params = { Key: this.extKey(key, orig_key) };
|
|
341
|
+
var download = self.s3.getObject( Tools.mergeHashes(params, { Range: "bytes=" + start + "-" + end }) ).createReadStream();
|
|
342
|
+
var proceed = false;
|
|
343
|
+
|
|
344
|
+
download.on('error', function(err) {
|
|
345
|
+
if (proceed) self.logError('s3', "Failed to download key: " + key + ": " + err.message);
|
|
346
|
+
});
|
|
347
|
+
download.once('end', function() {
|
|
348
|
+
self.logDebug(9, "S3 stream download complete: " + key);
|
|
349
|
+
} );
|
|
350
|
+
download.once('close', function() {
|
|
351
|
+
self.logDebug(9, "S3 stream download closed: " + key);
|
|
352
|
+
} );
|
|
353
|
+
|
|
354
|
+
// double-callback protection (bug in aws-sdk)
|
|
355
|
+
var done = false;
|
|
356
|
+
this.s3.headObject( params, function(err, data) {
|
|
357
|
+
if (done) return; else done = true;
|
|
302
358
|
|
|
303
|
-
|
|
359
|
+
if (err) {
|
|
360
|
+
if (err.code != 'NoSuchKey') {
|
|
361
|
+
self.logError('s3', "Failed to head key: " + key + ": " + err.message);
|
|
362
|
+
}
|
|
363
|
+
download.destroy();
|
|
364
|
+
callback( err, null );
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
proceed = true;
|
|
369
|
+
callback( null, download, {
|
|
370
|
+
mod: Math.floor((new Date(data.LastModified)).getTime() / 1000),
|
|
371
|
+
len: data.ContentLength
|
|
372
|
+
} );
|
|
304
373
|
}); // headObject
|
|
305
374
|
},
|
|
306
375
|
|
|
@@ -312,7 +381,11 @@ module.exports = Class.create({
|
|
|
312
381
|
|
|
313
382
|
this.logDebug(9, "Deleting S3 Object: " + key);
|
|
314
383
|
|
|
384
|
+
// double-callback protection (bug in aws-sdk)
|
|
385
|
+
var done = false;
|
|
315
386
|
this.s3.deleteObject( { Key: this.extKey(key, orig_key) }, function(err, data) {
|
|
387
|
+
if (done) return; else done = true;
|
|
388
|
+
|
|
316
389
|
if (err) {
|
|
317
390
|
self.logError('s3', "Failed to delete object: " + key + ": " + err.message);
|
|
318
391
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixl-server-storage",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.9",
|
|
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",
|
package/storage.js
CHANGED
|
@@ -354,6 +354,21 @@ module.exports = Class.create({
|
|
|
354
354
|
this.engine.getStream( key, callback );
|
|
355
355
|
},
|
|
356
356
|
|
|
357
|
+
getStreamRange: function(key, start, end, callback) {
|
|
358
|
+
// fetch value via stream pipe and range
|
|
359
|
+
// start and end are both inclusive
|
|
360
|
+
var self = this;
|
|
361
|
+
if (!this.started) return callback( new Error("Storage has not completed startup.") );
|
|
362
|
+
|
|
363
|
+
key = this.normalizeKey( key );
|
|
364
|
+
|
|
365
|
+
if (!this.isBinaryKey(key)) {
|
|
366
|
+
return callback( new Error("Stream values are only allowed with keys containing file extensions, e.g. " + key + ".bin") );
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.engine.getStreamRange( key, start, end, callback );
|
|
370
|
+
},
|
|
371
|
+
|
|
357
372
|
getMulti: function(keys, callback) {
|
|
358
373
|
// fetch multiple records at once, given array of keys
|
|
359
374
|
// callback is provided an array of values in matching order to keys
|
package/test/test-main.js
CHANGED
|
@@ -375,7 +375,7 @@ module.exports = {
|
|
|
375
375
|
},
|
|
376
376
|
|
|
377
377
|
function testStream(test) {
|
|
378
|
-
test.expect(
|
|
378
|
+
test.expect(14);
|
|
379
379
|
var self = this;
|
|
380
380
|
|
|
381
381
|
var key = 'spacer-stream.gif';
|
|
@@ -395,10 +395,13 @@ module.exports = {
|
|
|
395
395
|
var tempFile = __dirname + '/' + filename + '.streamtemp';
|
|
396
396
|
var outStream = fs.createWriteStream( tempFile );
|
|
397
397
|
|
|
398
|
-
self.storage.getStream( key, function(err, storageStream) {
|
|
398
|
+
self.storage.getStream( key, function(err, storageStream, streamInfo) {
|
|
399
399
|
test.ok( !err, "No error fetching stream: " + err );
|
|
400
400
|
test.ok( !!storageStream, "Got storage stream as 2nd arg");
|
|
401
401
|
test.ok( !!storageStream.pipe, "Storage stream has a pipe");
|
|
402
|
+
test.ok( !!streamInfo, "Info was provided as the 3rd arg");
|
|
403
|
+
test.ok( streamInfo.len == 43, "Info has correct data length");
|
|
404
|
+
test.ok( streamInfo.mod > 0, "Info has a non-zero mod date");
|
|
402
405
|
|
|
403
406
|
outStream.on('finish', function() {
|
|
404
407
|
var newSpacerBuf = fs.readFileSync( tempFile );
|
|
@@ -420,6 +423,52 @@ module.exports = {
|
|
|
420
423
|
} ); // putStream
|
|
421
424
|
},
|
|
422
425
|
|
|
426
|
+
function testStreamRange(test) {
|
|
427
|
+
test.expect(14);
|
|
428
|
+
var self = this;
|
|
429
|
+
|
|
430
|
+
var key = 'spacer-stream.gif';
|
|
431
|
+
var filename = 'spacer.gif';
|
|
432
|
+
var spacerBuf = fs.readFileSync( __dirname + '/' + filename );
|
|
433
|
+
var spacerStream = fs.createReadStream( __dirname + '/' + filename );
|
|
434
|
+
|
|
435
|
+
test.ok( !!spacerBuf, "Got buffer from file" );
|
|
436
|
+
test.ok( typeof(spacerBuf) == 'object', "Buffer is an object" );
|
|
437
|
+
test.ok( spacerBuf.length > 0, "Buffer has size" );
|
|
438
|
+
test.ok( !!spacerStream, "Got read stream" );
|
|
439
|
+
|
|
440
|
+
this.storage.putStream( key, spacerStream, function(err) {
|
|
441
|
+
test.ok( !err, "No error creating stream: " + err );
|
|
442
|
+
|
|
443
|
+
var tempFile = __dirname + '/' + filename + '.streamtemp';
|
|
444
|
+
var outStream = fs.createWriteStream( tempFile );
|
|
445
|
+
|
|
446
|
+
self.storage.getStreamRange( key, 0, 5, function(err, storageStream, streamInfo) {
|
|
447
|
+
test.ok( !err, "No error fetching stream: " + err );
|
|
448
|
+
test.ok( !!storageStream, "Got storage stream as 2nd arg");
|
|
449
|
+
test.ok( !!storageStream.pipe, "Storage stream has a pipe");
|
|
450
|
+
test.ok( !!streamInfo, "Info was provided as the 3rd arg");
|
|
451
|
+
test.ok( streamInfo.len == 43, "Info has correct data length");
|
|
452
|
+
test.ok( streamInfo.mod > 0, "Info has a non-zero mod date");
|
|
453
|
+
|
|
454
|
+
outStream.on('finish', function() {
|
|
455
|
+
var newSpacerBuf = fs.readFileSync( tempFile );
|
|
456
|
+
test.ok( newSpacerBuf.length == 6, "Stream length is correct" );
|
|
457
|
+
test.ok( newSpacerBuf.toString() == "GIF89a", "Range buffer content is correct" );
|
|
458
|
+
|
|
459
|
+
self.storage.delete( key, function(err) {
|
|
460
|
+
test.ok( !err, "No error deleting stream key: " + err );
|
|
461
|
+
fs.unlinkSync( tempFile );
|
|
462
|
+
test.done();
|
|
463
|
+
} ); // delete
|
|
464
|
+
} ); // stream finish
|
|
465
|
+
|
|
466
|
+
storageStream.pipe( outStream );
|
|
467
|
+
|
|
468
|
+
} ); // getStream
|
|
469
|
+
} ); // putStream
|
|
470
|
+
},
|
|
471
|
+
|
|
423
472
|
function testPutMulti(test) {
|
|
424
473
|
// test storing multiple keys at once
|
|
425
474
|
test.expect(1);
|
package/transaction.js
CHANGED
|
@@ -27,14 +27,6 @@ var TransStorageFunctions = {
|
|
|
27
27
|
this.tempFileCounter = 1;
|
|
28
28
|
},
|
|
29
29
|
|
|
30
|
-
getFilePath: function(key) {
|
|
31
|
-
// get local path to file given storage key
|
|
32
|
-
var trans = this.transactions[ this.currentTransactionPath ];
|
|
33
|
-
var key_id = Tools.digestHex( key );
|
|
34
|
-
var file = Path.join( this.transDir, "data", trans.id + '-' + key_id + '.json' );
|
|
35
|
-
return file;
|
|
36
|
-
},
|
|
37
|
-
|
|
38
30
|
put: function(key, value, callback) {
|
|
39
31
|
// store key+value pair in transaction
|
|
40
32
|
var self = this;
|
|
@@ -645,35 +637,16 @@ module.exports = Class.create({
|
|
|
645
637
|
self.logError('rollback', "Unable to delete rollback log: " + trans.log + ": " + err);
|
|
646
638
|
}
|
|
647
639
|
|
|
648
|
-
//
|
|
649
|
-
|
|
640
|
+
// complete, unlock and remove transaction from memory
|
|
641
|
+
self.unlock( 'C|'+path );
|
|
642
|
+
self._transUnlock(path);
|
|
643
|
+
self.transactions[path].keys = {}; // release memory
|
|
644
|
+
self.transactions[path].values = {}; // release memory
|
|
645
|
+
self.transactions[path].queue = []; // release memory
|
|
646
|
+
delete self.transactions[path];
|
|
650
647
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
// delete temp key file
|
|
654
|
-
var key_id = Tools.digestHex( key );
|
|
655
|
-
var file = Path.join( self.transDir, "data", trans.id + '-' + key_id + '.json' );
|
|
656
|
-
|
|
657
|
-
fs.unlink( file, function(err) {
|
|
658
|
-
if (err && !err.message.match(/ENOENT/)) {
|
|
659
|
-
self.logError('rollback', "Unable to delete transaction file: " + file + ": " + err);
|
|
660
|
-
}
|
|
661
|
-
callback();
|
|
662
|
-
});
|
|
663
|
-
},
|
|
664
|
-
function(err) {
|
|
665
|
-
// complete, unlock and remove transaction from memory
|
|
666
|
-
self.unlock( 'C|'+path );
|
|
667
|
-
self._transUnlock(path);
|
|
668
|
-
self.transactions[path].keys = {}; // release memory
|
|
669
|
-
self.transactions[path].values = {}; // release memory
|
|
670
|
-
self.transactions[path].queue = []; // release memory
|
|
671
|
-
delete self.transactions[path];
|
|
672
|
-
|
|
673
|
-
self.logDebug(3, "Transaction rollback complete: " + trans.id, { path: path });
|
|
674
|
-
callback();
|
|
675
|
-
}
|
|
676
|
-
); // forEachOfLimit
|
|
648
|
+
self.logDebug(3, "Transaction rollback complete: " + trans.id, { path: path });
|
|
649
|
+
callback();
|
|
677
650
|
}); // fs.unlink
|
|
678
651
|
} // done with log
|
|
679
652
|
); // fileEachLine
|