streamer service: split to separate files

This commit is contained in:
Jurgis Sakalauskas
2022-12-15 11:00:15 +02:00
parent 72c5684da9
commit 2fb78b61c4
14 changed files with 204 additions and 128 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ export class AllFilesController {
@Param('fileId') fileId: string,
@Req() request: Request,
@Res() response: Response,
) {
this.allFilesService.play(fileId, request, response);
): Promise<void> {
return this.allFilesService.play(fileId, request, response);
}
@Get(':titleId/:seasonId?')
+6 -7
View File
@@ -135,17 +135,16 @@ export class AllFilesService {
response: Response,
): Promise<void> {
const id = parseInt(fileId);
if (isNaN(id)) {
throw new NotFoundException(`id #${fileId} not correct`);
}
const entity = await this.fileRepository.findOne({ where: { id: id } });
if (entity === null) {
response
.status(404)
.send('entity not found id: ' + fileId)
.end();
return;
throw new NotFoundException(`id #${fileId} not found`);
}
this.streamerFacade.streamFile(request, response, entity.path);
return this.streamerFacade.streamFile(request, response, entity.path);
}
}
@@ -0,0 +1,19 @@
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
import { createReadStream, ReadStream } from 'fs';
import { ReadStreamOptions } from '../Interface/read-stream-options.interface';
export class ReadStreamCreatableFile implements ReadStreamCreatable {
constructor(
private readonly path: string,
readonly length: number,
readonly mimeType: string,
) {
this.path = path;
this.length = length;
this.mimeType = mimeType;
}
createReadStream(options: ReadStreamOptions): ReadStream {
return createReadStream(this.path, options);
}
}
@@ -0,0 +1,8 @@
import { ReadStream } from 'fs';
import { ReadStreamOptions } from './read-stream-options.interface';
export interface ReadStreamCreatable {
readonly mimeType: string;
readonly length: number;
createReadStream(options: ReadStreamOptions): ReadStream;
}
@@ -0,0 +1,4 @@
export class ReadStreamOptions {
start?: number;
end?: number;
}
@@ -1,6 +1,8 @@
export default class RequestRangeBytes {
readonly end: number;
import { ReadStreamOptions } from '../Interface/read-stream-options.interface';
export default class RequestRangeBytes implements ReadStreamOptions {
readonly start: number;
readonly end: number;
constructor(start: number, end: number) {
this.start = start;
@@ -29,4 +29,12 @@ export class RequestRangeService {
return new RequestRangeBytes(start, end);
}
isRangeSatisfiable(requestRange: RequestRangeBytes, size: number): boolean {
return (
requestRange.start < size &&
requestRange.end < size &&
requestRange.end >= requestRange.start
);
}
}
@@ -0,0 +1,28 @@
import RequestRangeBytes from '../RequestRange/request-range-bytes.class';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ResponseHeadersService {
createResponseHeaders(
mimeType: string,
length: number,
requestRange: RequestRangeBytes | null,
): Record<string, string> {
const responseHeaders = {};
responseHeaders['Content-Type'] = mimeType;
responseHeaders['Accept-Ranges'] = 'bytes';
responseHeaders['Cache-Control'] = 'no-cache';
responseHeaders['Content-Length'] = length;
if (requestRange) {
responseHeaders[
'Content-Range'
] = `bytes ${requestRange.start}-${requestRange.end}/${length}`;
responseHeaders['Content-Length'] =
requestRange.end - requestRange.start + 1;
}
return responseHeaders;
}
}
@@ -0,0 +1,21 @@
import { Response } from 'express';
import { ReadStream } from 'fs';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ResponseSenderService {
send(
response: Response,
status: number,
headers: Record<string, string>,
readable: ReadStream | null = null,
): void {
response.writeHead(status, headers);
if (!readable) {
response.end();
return;
}
readable.pipe(response);
}
}
@@ -0,0 +1,34 @@
import { Request, Response } from 'express';
import { Stats } from 'fs';
import { stat } from 'fs/promises';
import { Injectable, NotFoundException } from '@nestjs/common';
import { ReadStreamCreatableFile } from '../Dto/read-stream-creatable-file.dto';
import { StreamerService } from './streamer.service';
import { MimeService } from '../Mime/mime.service';
@Injectable()
export class FileStreamerService {
constructor(
private readonly streamerService: StreamerService,
private readonly mimeService: MimeService,
) {}
async streamFile(
request: Request,
response: Response,
filePath: string,
): Promise<void> {
const stats: Stats | false = await stat(filePath).catch(() => false);
if (stats === false) {
//todo - set the file to deleted, since no longer available. Events?
throw new NotFoundException('file not found');
}
const mimeType = this.mimeService.getMime(filePath);
const streamCreatable = new ReadStreamCreatableFile(
filePath,
stats.size,
mimeType,
);
return this.streamerService.stream(request, response, streamCreatable);
}
}
@@ -0,0 +1,55 @@
import { ReadStreamCreatable } from '../Interface/read-stream-creatable.interface';
import { RequestRangeService } from '../RequestRange/request-range.service';
import { MimeService } from '../Mime/mime.service';
import { Injectable } from '@nestjs/common';
import { Request, Response } from 'express';
import { ResponseHeadersService } from '../ResponseHeadersService/response-headers.service';
import { ResponseSenderService } from '../ResponseSender/response-sender.service';
import RequestRangeBytes from '../RequestRange/request-range-bytes.class';
@Injectable()
export class StreamerService {
constructor(
private readonly mimeService: MimeService,
private readonly requestRangeService: RequestRangeService,
private readonly responseHeadersService: ResponseHeadersService,
private readonly responseSender: ResponseSenderService,
) {}
stream(
req: Request,
res: Response,
streamCreatable: ReadStreamCreatable,
): void {
const size = streamCreatable.length;
const requestRange = this.getRange(req, size);
if (!this.requestRangeService.isRangeSatisfiable(requestRange, size)) {
return this.handleUnsatisfiable(size, res);
}
const responseHeaders = this.responseHeadersService.createResponseHeaders(
streamCreatable.mimeType,
size,
requestRange,
);
const status = req.headers.range ? 206 : 200;
const readable =
req.method === 'HEAD'
? null
: streamCreatable.createReadStream(requestRange);
return this.responseSender.send(res, status, responseHeaders, readable);
}
private getRange(req: Request, size: number): RequestRangeBytes {
return this.requestRangeService.getRangeFromHeaders(req, size);
}
private handleUnsatisfiable(size: number, res: Response) {
const responseHeaders = { 'Content-Range': 'bytes */' + size };
this.responseSender.send(res, 416, responseHeaders, null);
return;
}
}
+8 -4
View File
@@ -1,12 +1,16 @@
import { Injectable } from '@nestjs/common';
import { Request, Response } from 'express';
import { StreamerService } from './streamer.service';
import { FileStreamerService } from './StreamerService/file-streamer.service';
@Injectable()
export class StreamerFacade {
constructor(private readonly streamerService: StreamerService) {}
constructor(private readonly fileStreamerService: FileStreamerService) {}
streamFile(request: Request, response: Response, filePath: string): void {
this.streamerService.streamFile(request, response, filePath);
async streamFile(
request: Request,
response: Response,
filePath: string,
): Promise<void> {
return this.fileStreamerService.streamFile(request, response, filePath);
}
}
+7 -1
View File
@@ -2,13 +2,19 @@ import { Module } from '@nestjs/common';
import { StreamerFacade } from './streamer.facade';
import { MimeService } from './Mime/mime.service';
import { RequestRangeService } from './RequestRange/request-range.service';
import { StreamerService } from './streamer.service';
import { StreamerService } from './StreamerService/streamer.service';
import { FileStreamerService } from './StreamerService/file-streamer.service';
import { ResponseHeadersService } from './ResponseHeadersService/response-headers.service';
import { ResponseSenderService } from './ResponseSender/response-sender.service';
@Module({
imports: [],
providers: [
MimeService,
RequestRangeService,
ResponseHeadersService,
ResponseSenderService,
FileStreamerService,
StreamerService,
StreamerFacade,
],
-112
View File
@@ -1,112 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Request, Response } from 'express';
import { MimeService } from './Mime/mime.service';
import { RequestRangeService } from './RequestRange/request-range.service';
import { stat } from 'fs/promises';
import { createReadStream, ReadStream, Stats } from 'fs';
import RequestRangeBytes from './RequestRange/request-range-bytes.class';
@Injectable()
export class StreamerService {
constructor(
readonly mimeService: MimeService,
readonly requestRangeService: RequestRangeService,
) {}
async streamFile(
request: Request,
response: Response,
filePath: string,
): Promise<void> {
const stats: Stats | false = await stat(filePath).catch(() => false);
if (stats === false) {
this.sendResponse(response, 404, {});
return;
}
const requestRange = this.requestRangeService.getRangeFromHeaders(
request,
stats.size,
);
console.log(requestRange, request.method);
if (!this.isRangeSatisfiable(requestRange, stats.size)) {
const responseHeaders = { 'Content-Range': 'bytes */' + stats.size };
this.sendResponse(response, 416, responseHeaders, null);
return;
}
const responseHeaders = this.createResponseHeaders(
filePath,
stats.size,
requestRange,
);
if (request.method == 'HEAD') {
this.sendResponse(
response,
request.headers.range ? 206 : 200,
responseHeaders,
null,
);
} else {
this.sendResponse(
response,
request.headers.range ? 206 : 200,
responseHeaders,
createReadStream(filePath, requestRange),
);
}
}
private createResponseHeaders(
filePath: string,
contentSize: number,
requestRange: RequestRangeBytes | null,
): Record<string, string> {
const responseHeaders = {};
responseHeaders['Content-Type'] = this.mimeService.getMime(filePath);
responseHeaders['Accept-Ranges'] = 'bytes';
responseHeaders['Cache-Control'] = 'no-cache';
responseHeaders['Content-Length'] = contentSize;
if (requestRange) {
responseHeaders[
'Content-Range'
] = `bytes ${requestRange.start}-${requestRange.end}/${contentSize}`;
responseHeaders['Content-Length'] =
requestRange.end - requestRange.start + 1;
}
return responseHeaders;
}
private sendResponse(
response: Response,
status: number,
headers: Record<string, string>,
readable: ReadStream | null = null,
): void {
response.writeHead(status, headers);
if (!readable) {
response.end();
return;
}
readable.pipe(response);
}
private isRangeSatisfiable(
requestRange: RequestRangeBytes,
fileSize: number,
): boolean {
return (
requestRange.start < fileSize &&
requestRange.end < fileSize &&
requestRange.end >= requestRange.start
);
}
}