diff --git a/lib/server/v2/commands/Get.js b/lib/server/v2/commands/Get.js index deea3ef5..7cff7bca 100644 --- a/lib/server/v2/commands/Get.js +++ b/lib/server/v2/commands/Get.js @@ -12,6 +12,33 @@ var __extends = (this && this.__extends) || (function () { Object.defineProperty(exports, "__esModule", { value: true }); var WebDAVRequest_1 = require("../WebDAVRequest"); var stream_1 = require("stream"); +var MultipleRangedStream = (function (_super) { + __extends(MultipleRangedStream, _super); + function MultipleRangedStream(ranges) { + var _this = _super.call(this) || this; + _this.ranges = ranges; + _this.streams = ranges.map(function (r) { + return { + stream: new RangedStream(r.min, r.max), + range: r + }; + }); + return _this; + } + MultipleRangedStream.prototype._transform = function (chunk, encoding, callback) { + this.streams.forEach(function (streamRange) { + streamRange.stream.write(chunk, encoding); + }); + callback(null, new Buffer(0)); + }; + MultipleRangedStream.prototype.end = function (chunk, encoding, cb) { + var _this = this; + if (this.onEnded) + process.nextTick(function () { return _this.onEnded(); }); + _super.prototype.end.call(this, chunk, encoding, cb); + }; + return MultipleRangedStream; +}(stream_1.Transform)); var RangedStream = (function (_super) { __extends(RangedStream, _super); function RangedStream(min, max) { @@ -46,6 +73,31 @@ var RangedStream = (function (_super) { }; return RangedStream; }(stream_1.Transform)); +function parseRangeBlock(size, block) { + size -= 1; + var rRange = /([0-9]+)-([0-9]+)/; + var match = rRange.exec(block); + if (match) + return { + min: Math.min(size, parseInt(match[1], 10)), + max: Math.min(size, parseInt(match[2], 10)) + }; + var rStart = /([0-9]+)-/; + match = rStart.exec(block); + if (match) + return { + min: Math.min(size + 1, parseInt(match[1], 10)), + max: size + }; + var rEnd = /-([0-9]+)/; + match = rEnd.exec(block); + if (match) + return { + min: Math.max(0, size - parseInt(match[1], 10) + 1), + max: size + }; + throw new Error('Cannot parse the range block'); +} var default_1 = (function () { function default_1() { } @@ -86,16 +138,65 @@ var default_1 = (function () { } //ctx.invokeEvent('read', r); if (range) { - var rex = /([0-9]+)/g; - var min = parseInt(rex.exec(range)[1], 10); - var max = parseInt(rex.exec(range)[1], 10); - ctx.setCode(WebDAVRequest_1.HTTPCodes.PartialContent); - ctx.response.setHeader('Accept-Ranges', 'bytes'); - ctx.response.setHeader('Content-Type', mimeType); - ctx.response.setHeader('Content-Length', Math.min(size, max - min + 1).toString()); - ctx.response.setHeader('Content-Range', 'bytes ' + min + '-' + max + '/*'); - rstream.on('end', callback); - rstream.pipe(new RangedStream(min, max)).pipe(ctx.response); + try { + var createMultipart_1 = function (range) { + return '--' + separator_1 + '\r\nContent-Type: ' + mimeType + '\r\nContent-Range: bytes ' + range.min + '-' + range.max + '/*\r\n\r\n'; + }; + var endMultipart_1 = function () { + return '\r\n--' + separator_1 + '--'; + }; + var ranges_1 = range.split(',').map(function (block) { return parseRangeBlock(size, block); }); + var separator_1 = Array.apply(null, { length: 20 }).map(function () { return String.fromCharCode('a'.charCodeAt(0) + Math.floor(Math.random() * 26)); }).join(''); + var len = ranges_1.reduce(function (previous, mm) { return mm.max - mm.min + 1 + previous; }, 0) + (ranges_1.length > 1 ? ranges_1.reduce(function (previous, mm) { return createMultipart_1(mm).length + previous; }, endMultipart_1().length + '\r\n'.length * (ranges_1.length - 1)) : 0); + ctx.setCode(WebDAVRequest_1.HTTPCodes.PartialContent); + ctx.response.setHeader('Accept-Ranges', 'bytes'); + ctx.response.setHeader('Content-Length', len.toString()); + if (ranges_1.length <= 1) { + ctx.response.setHeader('Content-Type', mimeType); + ctx.response.setHeader('Content-Range', 'bytes ' + ranges_1[0].min + '-' + ranges_1[0].max + '/*'); + rstream.on('end', callback); + return rstream.pipe(new RangedStream(ranges_1[0].min, ranges_1[0].max)).pipe(ctx.response); + } + ctx.response.setHeader('Content-Type', 'multipart/byteranges; boundary=' + separator_1); + var multi_1 = new MultipleRangedStream(ranges_1); + rstream.pipe(multi_1); + var current_1 = 0; + var dones_1 = {}; + var evalNext_1 = function () { + if (current_1 === ranges_1.length) { + return ctx.response.end(endMultipart_1(), function () { + callback(); + }); + } + var sr = dones_1[current_1]; + if (sr) { + if (current_1 > 0) + ctx.response.write('\r\n'); + ctx.response.write(createMultipart_1(sr.range)); + sr.stream.on('end', function () { + ++current_1; + evalNext_1(); + }); + sr.stream.on('data', function (chunk, encoding) { + ctx.response.write(chunk, encoding); + }); + //sr.stream.pipe(ctx.response); + } + }; + multi_1.streams.forEach(function (sr, index) { + dones_1[index] = sr; + }); + multi_1.onEnded = function () { + multi_1.streams.forEach(function (sr, index) { + sr.stream.end(); + }); + evalNext_1(); + }; + } + catch (ex) { + ctx.setCode(WebDAVRequest_1.HTTPCodes.BadRequest); + callback(); + } } else { ctx.setCode(WebDAVRequest_1.HTTPCodes.OK); diff --git a/src/server/v2/commands/Get.ts b/src/server/v2/commands/Get.ts index 31e817d1..3a711a5a 100644 --- a/src/server/v2/commands/Get.ts +++ b/src/server/v2/commands/Get.ts @@ -3,6 +3,39 @@ import { ResourceType } from '../../../manager/v2/fileSystem/CommonTypes' import { Errors } from '../../../Errors' import { Transform } from 'stream' +class MultipleRangedStream extends Transform +{ + streams : { stream : RangedStream, range : IRange }[] + onEnded : () => void + + constructor(public ranges : IRange[]) + { + super(); + + this.streams = ranges.map((r) => { + return { + stream: new RangedStream(r.min, r.max), + range: r + } + }); + } + + _transform(chunk : string | Buffer, encoding : string, callback : Function) + { + this.streams.forEach((streamRange) => { + streamRange.stream.write(chunk, encoding); + }); + + callback(null, new Buffer(0)); + } + + end(chunk ?: any, encoding?: any, cb?: Function): void + { + if(this.onEnded) + process.nextTick(() => this.onEnded()); + super.end(chunk, encoding, cb); + } +} class RangedStream extends Transform { nb : number; @@ -44,6 +77,43 @@ class RangedStream extends Transform } } +interface IRange +{ + min : number + max : number +} + +function parseRangeBlock(size : number, block : string) : IRange +{ + size -= 1; + + const rRange = /([0-9]+)-([0-9]+)/; + let match = rRange.exec(block); + if(match) + return { + min: Math.min(size, parseInt(match[1], 10)), + max: Math.min(size, parseInt(match[2], 10)) + }; + + const rStart = /([0-9]+)-/; + match = rStart.exec(block); + if(match) + return { + min: Math.min(size + 1, parseInt(match[1], 10)), + max: size + }; + + const rEnd = /-([0-9]+)/; + match = rEnd.exec(block); + if(match) + return { + min: Math.max(0, size - parseInt(match[1], 10) + 1), + max: size + }; + + throw new Error('Cannot parse the range block'); +} + export default class implements HTTPMethod { unchunked(ctx : HTTPRequestContext, data : Buffer, callback : () => void) : void @@ -95,18 +165,78 @@ export default class implements HTTPMethod if(range) { - const rex = /([0-9]+)/g; - const min = parseInt(rex.exec(range)[1], 10); - const max = parseInt(rex.exec(range)[1], 10); + try + { + const createMultipart = (range : IRange) => { + return '--' + separator + '\r\nContent-Type: ' + mimeType + '\r\nContent-Range: bytes ' + range.min + '-' + range.max + '/*\r\n\r\n'; + }; + const endMultipart = () => { + return '\r\n--' + separator + '--'; + }; - ctx.setCode(HTTPCodes.PartialContent); - ctx.response.setHeader('Accept-Ranges', 'bytes') - ctx.response.setHeader('Content-Type', mimeType) - ctx.response.setHeader('Content-Length', Math.min(size, max - min + 1).toString()) - ctx.response.setHeader('Content-Range', 'bytes ' + min + '-' + max + '/*') + const ranges = range.split(',').map((block) => parseRangeBlock(size, block)); + const separator = Array.apply(null, {length: 20}).map(() => String.fromCharCode('a'.charCodeAt(0) + Math.floor(Math.random() * 26))).join(''); + const len = ranges.reduce((previous, mm) => mm.max - mm.min + 1 + previous, 0) + (ranges.length > 1 ? ranges.reduce((previous, mm) => createMultipart(mm).length + previous, endMultipart().length + '\r\n'.length * (ranges.length - 1)) : 0); - rstream.on('end', callback); - rstream.pipe(new RangedStream(min, max)).pipe(ctx.response); + ctx.setCode(HTTPCodes.PartialContent); + ctx.response.setHeader('Accept-Ranges', 'bytes') + ctx.response.setHeader('Content-Length', len.toString()) + if(ranges.length <= 1) + { + ctx.response.setHeader('Content-Type', mimeType) + ctx.response.setHeader('Content-Range', 'bytes ' + ranges[0].min + '-' + ranges[0].max + '/*') + rstream.on('end', callback); + return rstream.pipe(new RangedStream(ranges[0].min, ranges[0].max)).pipe(ctx.response); + } + + ctx.response.setHeader('Content-Type', 'multipart/byteranges; boundary=' + separator) + + const multi = new MultipleRangedStream(ranges); + rstream.pipe(multi); + + let current = 0; + const dones = {}; + const evalNext = () => { + if(current === ranges.length) + { + return ctx.response.end(endMultipart(), () => { + callback(); + }); + } + + const sr = dones[current]; + if(sr) + { + if(current > 0) + ctx.response.write('\r\n'); + ctx.response.write(createMultipart(sr.range)); + + sr.stream.on('end', () => { + ++current; + evalNext(); + }); + sr.stream.on('data', (chunk, encoding) => { + ctx.response.write(chunk, encoding); + }) + //sr.stream.pipe(ctx.response); + } + } + multi.streams.forEach((sr, index) => { + dones[index] = sr; + }) + + multi.onEnded = () => { + multi.streams.forEach((sr, index) => { + sr.stream.end(); + }); + evalNext(); + } + } + catch(ex) + { + ctx.setCode(HTTPCodes.BadRequest); + callback(); + } } else {