Skip to content

Commit cae3cb6

Browse files
storage: createReadStream: accept start/end offsets
1 parent b7ac20e commit cae3cb6

File tree

3 files changed

+129
-14
lines changed

3 files changed

+129
-14
lines changed

lib/storage/file.js

+40-2
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ File.prototype.copy = function(destination, callback) {
223223
* hash wasn't returned from the API. CRC32c will provide better performance
224224
* with less reliability. You may also choose to skip validation completely,
225225
* however this is **not recommended**.
226+
* @param {number} options.start - A byte offset to begin the file's download
227+
* from. NOTE: Byte ranges are inclusive; that is, `options.start = 0` and
228+
* `options.end = 999` represent the first 1000 bytes in a file or object.
229+
* NOTE: when specifying a byte range, data integrity is not available.
230+
* @param {number} options.end - A byte offset to stop reading the file at.
231+
* NOTE: Byte ranges are inclusive; that is, `options.start = 0` and
232+
* `options.end = 999` represent the first 1000 bytes in a file or object.
233+
* NOTE: when specifying a byte range, data integrity is not available.
226234
*
227235
* @example
228236
* //-
@@ -238,12 +246,25 @@ File.prototype.copy = function(destination, callback) {
238246
* image.createReadStream()
239247
* .pipe(fs.createWriteStream('/Users/stephen/Photos/image.png'))
240248
* .on('error', function(err) {});
249+
*
250+
* //-
251+
* // To limit the downloaded data to only a byte range, pass an options object.
252+
* //-
253+
* var logFile = myBucket.file('access_log');
254+
* logFile.createReadStream({
255+
* start: 10000,
256+
* end: 20000
257+
* })
258+
* .pipe(fs.createWriteStream('/Users/stephen/logfile.txt'))
259+
* .on('error', function(err) {});
241260
*/
242261
File.prototype.createReadStream = function(options) {
243262
options = options || {};
244263

245264
var that = this;
246-
var throughStream = through();
265+
var rangeRequest =
266+
util.is(options.start, 'number') || util.is(options.end, 'number');
267+
var throughStream = streamEvents(through());
247268

248269
var validations = ['crc32c', 'md5'];
249270
var validation;
@@ -262,6 +283,10 @@ File.prototype.createReadStream = function(options) {
262283
validation = 'all';
263284
}
264285

286+
if (rangeRequest) {
287+
validation = false;
288+
}
289+
265290
var crc32c = validation === 'crc32c' || validation === 'all';
266291
var md5 = validation === 'md5' || validation === 'all';
267292

@@ -288,6 +313,12 @@ File.prototype.createReadStream = function(options) {
288313
uri: uri
289314
};
290315

316+
if (rangeRequest) {
317+
reqOpts.headers = {
318+
Range: 'bytes=' + [options.start || '', options.end || ''].join('-')
319+
};
320+
}
321+
291322
that.bucket.storage.makeAuthorizedRequest_(reqOpts, {
292323
onAuthorized: function(err, authorizedReqOpts) {
293324
if (err) {
@@ -318,6 +349,13 @@ File.prototype.createReadStream = function(options) {
318349
})
319350

320351
.on('complete', function(res) {
352+
if (rangeRequest) {
353+
// Range requests can't receive data integrity checks.
354+
throughStream.emit('complete', res);
355+
throughStream.end();
356+
return;
357+
}
358+
321359
var failed = false;
322360
var crcFail = true;
323361
var md5Fail = true;
@@ -356,7 +394,7 @@ File.prototype.createReadStream = function(options) {
356394

357395
throughStream.emit('error', error);
358396
} else {
359-
throughStream.emit('complete');
397+
throughStream.emit('complete', res);
360398
}
361399

362400
throughStream.end();

regression/storage.js

+24
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,30 @@ describe('storage', function() {
336336
});
337337
});
338338

339+
it('should read a byte range from a file', function(done) {
340+
bucket.upload(files.big.path, function(err, file) {
341+
assert.ifError(err);
342+
343+
var fileSize = file.metadata.size;
344+
var byteRange = {
345+
start: Math.floor(fileSize * 1/3),
346+
end: Math.floor(fileSize * 2/3)
347+
};
348+
var expectedContentSize = byteRange.start + 1;
349+
350+
var sizeStreamed = 0;
351+
file.createReadStream(byteRange)
352+
.on('data', function (chunk) {
353+
sizeStreamed += chunk.length;
354+
})
355+
.on('error', done)
356+
.on('complete', function() {
357+
assert.equal(sizeStreamed, expectedContentSize);
358+
file.delete(done);
359+
});
360+
});
361+
});
362+
339363
describe('stream write', function() {
340364
it('should stream write, then remove file (3mb)', function(done) {
341365
var file = bucket.file('LargeFile');

test/storage/file.js

+65-12
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ nodeutil.inherits(FakeDuplexify, stream.Duplex);
5353
var makeWritableStream_Override;
5454
var fakeUtil = extend({}, util, {
5555
makeWritableStream: function() {
56-
var args = [].slice.call(arguments);
56+
var args = util.toArray(arguments);
5757
(makeWritableStream_Override || util.makeWritableStream).apply(null, args);
5858
}
5959
});
@@ -62,7 +62,7 @@ var request_Cached = request;
6262
var request_Override;
6363

6464
function fakeRequest() {
65-
var args = [].slice.apply(arguments);
65+
var args = util.toArray(arguments);
6666
var results = (request_Override || request_Cached).apply(null, args);
6767
return results;
6868
}
@@ -85,14 +85,9 @@ function FakeConfigStore() {
8585
describe('File', function() {
8686
var File;
8787
var FILE_NAME = 'file-name.png';
88-
var options = {
89-
makeAuthorizedRequest_: function(req, callback) {
90-
(callback.onAuthorized || callback)(null, req);
91-
}
92-
};
93-
var bucket = new Bucket(options, 'bucket-name');
9488
var file;
9589
var directoryFile;
90+
var bucket;
9691

9792
before(function() {
9893
mockery.registerMock('configstore', FakeConfigStore);
@@ -112,14 +107,21 @@ describe('File', function() {
112107
});
113108

114109
beforeEach(function() {
115-
makeWritableStream_Override = null;
116-
request_Override = null;
110+
var options = {
111+
makeAuthorizedRequest_: function(req, callback) {
112+
(callback.onAuthorized || callback)(null, req);
113+
}
114+
};
115+
bucket = new Bucket(options, 'bucket-name');
117116

118117
file = new File(bucket, FILE_NAME);
119118
file.makeReq_ = util.noop;
120119

121120
directoryFile = new File(bucket, 'directory/file.jpg');
122121
directoryFile.makeReq_ = util.noop;
122+
123+
makeWritableStream_Override = null;
124+
request_Override = null;
123125
});
124126

125127
describe('initialization', function() {
@@ -387,7 +389,9 @@ describe('File', function() {
387389

388390
file.createReadStream({ validation: 'crc32c' })
389391
.on('error', done)
390-
.on('complete', done);
392+
.on('complete', function () {
393+
done();
394+
});
391395
});
392396

393397
it('should emit an error if crc32c validation fails', function(done) {
@@ -405,7 +409,9 @@ describe('File', function() {
405409

406410
file.createReadStream({ validation: 'md5' })
407411
.on('error', done)
408-
.on('complete', done);
412+
.on('complete', function () {
413+
done();
414+
});
409415
});
410416

411417
it('should emit an error if md5 validation fails', function(done) {
@@ -430,6 +436,53 @@ describe('File', function() {
430436
});
431437
});
432438
});
439+
440+
it('should accept a start range', function(done) {
441+
var startOffset = 100;
442+
443+
request_Override = function(opts) {
444+
setImmediate(function () {
445+
assert.equal(opts.headers.Range, 'bytes=' + startOffset + '-');
446+
done();
447+
});
448+
return duplexify();
449+
};
450+
451+
file.metadata = metadata;
452+
file.createReadStream({ start: startOffset });
453+
});
454+
455+
it('should accept an end range', function(done) {
456+
var endOffset = 100;
457+
458+
request_Override = function(opts) {
459+
setImmediate(function () {
460+
assert.equal(opts.headers.Range, 'bytes=-' + endOffset);
461+
done();
462+
});
463+
return duplexify();
464+
};
465+
466+
file.metadata = metadata;
467+
file.createReadStream({ end: endOffset });
468+
});
469+
470+
it('should accept both a start and end range', function(done) {
471+
var startOffset = 100;
472+
var endOffset = 101;
473+
474+
request_Override = function(opts) {
475+
setImmediate(function () {
476+
var expectedRange = 'bytes=' + startOffset + '-' + endOffset;
477+
assert.equal(opts.headers.Range, expectedRange);
478+
done();
479+
});
480+
return duplexify();
481+
};
482+
483+
file.metadata = metadata;
484+
file.createReadStream({ start: startOffset, end: endOffset });
485+
});
433486
});
434487

435488
describe('createWriteStream', function() {

0 commit comments

Comments
 (0)