src/server/v2/commands/Get.ts
import { HTTPCodes, HTTPRequestContext, HTTPMethod } from '../WebDAVRequest'
import { ResourceType } from '../../../manager/v2/fileSystem/CommonTypes'
import { Transform } from 'stream'
class RangedStream extends Transform
{
nb : number;
constructor(public min : number, public max : number)
{
super();
this.nb = 0;
}
_transform(chunk : string | Buffer, encoding : string, callback : Function)
{
if(this.nb < this.min)
{
const lastNb = this.nb;
this.nb += chunk.length;
if(this.nb > this.min)
{
const start = this.min - lastNb;
chunk = chunk.slice(start, this.nb > this.max ? this.max - this.min + 1 + start : undefined);
callback(null, chunk);
}
else
callback(null, Buffer.alloc(0));
}
else if(this.nb > this.max)
{
this.nb += chunk.length;
callback(null, Buffer.alloc(0));
}
else
{
this.nb += chunk.length;
if(this.nb > this.max)
chunk = chunk.slice(0, this.max - (this.nb - chunk.length) + 1);
callback(null, chunk);
}
}
}
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, Buffer.alloc(0));
}
end(chunk ?: any, encoding?: any, cb?: Function): void
{
if(this.onEnded)
process.nextTick(() => this.onEnded());
super.end(chunk, encoding, cb);
}
}
export interface IRange
{
min : number
max : number
}
export function parseRangeHeader(mimeType : string, size : number, range : string)
{
const separator = Array.apply(null, { length: 20 })
.map(() => String.fromCharCode('a'.charCodeAt(0) + Math.floor(Math.random() * 26)))
.join('');
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}--`;
};
const ranges = range
.split(',')
.map((block) => parseRangeBlock(size, block));
const len = ranges.reduce((previous, mm) => mm.max - mm.min + 1 + previous, 0)
+ (ranges.length <= 1 ?
0 : ranges.reduce(
(previous, mm) => createMultipart(mm).length + previous,
endMultipart().length + '\r\n'.length * (ranges.length - 1)
)
);
return {
ranges,
separator,
len,
createMultipart,
endMultipart
}
}
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
{
ctx.noBodyExpected(() => {
ctx.getResource((e, r) => {
ctx.checkIfHeader(r, () => {
const targetSource = ctx.headers.isSource;
//ctx.requirePrivilegeEx(targetSource ? [ 'canRead', 'canSource', 'canGetMimeType' ] : [ 'canRead', 'canGetMimeType' ], () => {
r.type((e, type) => {
if(e)
{
if(!ctx.setCodeFromError(e))
ctx.setCode(HTTPCodes.InternalServerError)
return callback();
}
if(!type.isFile)
{
ctx.setCode(HTTPCodes.MethodNotAllowed)
return callback();
}
const range = ctx.headers.find('Range');
r.size(targetSource, (e, size) => process.nextTick(() => {
if(e && !range)
{
if(!ctx.setCodeFromError(e))
ctx.setCode(HTTPCodes.InternalServerError)
return callback();
}
r.mimeType(targetSource, (e, mimeType) => process.nextTick(() => {
if(e)
{
if(!ctx.setCodeFromError(e))
ctx.setCode(HTTPCodes.InternalServerError)
return callback();
}
r.openReadStream(targetSource, (e, rstream) => {
if(e)
{
if(!ctx.setCodeFromError(e))
ctx.setCode(HTTPCodes.MethodNotAllowed)
return callback();
}
//ctx.invokeEvent('read', r);
rstream.on('error', (e) => {
if(!ctx.setCodeFromError(e))
ctx.setCode(HTTPCodes.InternalServerError);
return callback();
})
if(range)
{
try
{
const { ranges, separator, len, createMultipart, endMultipart } = parseRangeHeader(mimeType, size, range);
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
{
ctx.setCode(HTTPCodes.OK);
ctx.response.setHeader('Accept-Ranges', 'bytes')
ctx.response.setHeader('Content-Type', mimeType);
if(size !== null && size !== undefined && size > -1)
ctx.response.setHeader('Content-Length', size.toString());
rstream.on('end', callback);
rstream.pipe(ctx.response);
}
})
}))
}))
})
//})
})
})
})
}
isValidFor(ctx : HTTPRequestContext, type : ResourceType)
{
return type && type.isFile;
}
}