chore: convert to LF line ending

This commit is contained in:
Panagiotis Papadopoulos 2025-04-03 23:27:32 +02:00
parent d41e2443bc
commit a3b6d4d151
13 changed files with 326 additions and 326 deletions

298
Readme.md
View File

@ -1,149 +1,149 @@
![Travis CI Status](https://travis-ci.com/SukantGujar/express-partial-content.svg?branch=master)
# About
A HTTP 206 Partial Content handler to serve any readable stream partially in Express.
Based on this blog post: https://www.codeproject.com/Articles/813480/HTTP-Partial-Content-In-Node-js.
# Installation
`yarn add express-partial-content`
OR
`npm install express-partial-content`
> Note: `Express` package is a peer dependency for `express-partial-content` and must be present in dependencies of the host package.
# Usage
From the `express-file-server` example:
1. Implement a `ContentProvider` function which prepares and returns a `Content` object:
import { promisify } from "util";
import fs from "fs";
import { Range, ContentDoesNotExistError, ContentProvider } from "express-partial-content";
import {logger} from "./logger";
const statAsync = promisify(fs.stat);
const existsAsync = promisify(fs.exists);
export const fileContentProvider: ContentProvider = async (req: Request) => {
// Read file name from route params.
const fileName = req.params.name;
const file = `${__dirname}/files/${fileName}`;
if (!(await existsAsync(file))) {
throw new ContentDoesNotExistError(`File doesn't exist: ${file}`);
}
const stats = await statAsync(file);
const totalSize = stats.size;
const mimeType = "application/octet-stream";
const getStream = (range?: Range) => {
if (!range) {
// Request if for complete content.
return fs.createReadStream(file);
}
// Partial content request.
const { start, end } = range;
logger.debug(`start: ${start}, end: ${end}`);
return fs.createReadStream(file, { start, end });
};
return {
fileName,
totalSize,
mimeType,
getStream
};
};
2. In your express code, use `createPartialContentHandler` factory method to generate an express handler for serving partial content for the route of your choice:
import {createPartialContentHandler} from "express-partial-content";
import {logger} from "./logger";
const handler = createPartialContentHandler(fileContentProvider, logger);
const app = express();
const port = 8080;
// File name is a route param.
app.get("/files/:name", handler);
app.listen(port, () => {
logger.debug("Server started!");
});
3. Run your server and use a multi-part/multi-connection download utility like [aria2c](https://aria2.github.io/) to test it:
aria -x5 -k1M http://localhost:8080/files/readme.txt
# Examples
There one examples in the `src/examples` folder:
1. `express-file-server`: Implements a file based `ContentProvider`.
## Running the examples:
1. `express-file-server`: Run the following commands, the server will listen on http://localhost:8080/.
yarn build:dev
yarn copy-assets
yarn run:examples:file
## Connecting to the running server:
Browse to `https://localhost:8080/files/readme.txt`
# Reference
## createPartialContentHandler function:
This is a factory method which generates a partial content handler for express routes.
### Arguments:
- `contentProvider`: An `async` function which returns a Promise resolved to a `Content` object (see below).
- `logger`: Any logging implementation which has a `debug(message:string, extra: any)` method. Either `winston` or `bunyan` loggers should work.
### Returns:
- Express Route Handler: `createPartialContentHandler` returns an express handler which can be mapped to an Express route to serve partial content.
## ContentProvider function:
This function _needs to be implemented by you_. It's purpose is to fetch and return `Content` object containing necessary metadata and methods to stream the content partially. This method is invoked by the express handler (returned by `createPartialContentHandler`) on each request.
### Arguments:
- `Request`: It receives the `Request` object as it's only input. Use the information available in `Request` to find the requested content, e.g. through `Request.params` or query string, headers etc.
### Returns:
- `Promise<Content>`: See below.
### Throws:
- `ContentDoesNotExistError`: Throw this to indicate that the content doesn't exist. The generated express handler will return a 404 in this case.
> Note: Any message provided to the `ContentDoesNotExistError` object is returned to the client.
## Content object:
This object contains metadata and methods which describe the content. The `ContentProvider` method builds and returns it.
### Properties:
All the properties of this object are used to return content metadata to the client as various `Response` headers.
- `fileName`: Used as the `Content-Disposition` header's `filename` value.
- `mimeType`: Used as the `Content-Type` header value.
- `totalSize`: Used as the `Content-Length` header value.
### Methods:
- `getStream(range?: Range)`: This method should return a readable stream initialized to the provided `range` (optional). You need to handle two cases:
- range is `null`: When `range` is not-specified, the client is requesting the full content. In this case, return the stream as it is.
- range is `{start, end}`: When client requests partial content, the `start` and `end` values will point to the corresponding byte positions (0 based and inclusive) of the content. You need to return stream limited to these positions.
![Travis CI Status](https://travis-ci.com/SukantGujar/express-partial-content.svg?branch=master)
# About
A HTTP 206 Partial Content handler to serve any readable stream partially in Express.
Based on this blog post: https://www.codeproject.com/Articles/813480/HTTP-Partial-Content-In-Node-js.
# Installation
`yarn add express-partial-content`
OR
`npm install express-partial-content`
> Note: `Express` package is a peer dependency for `express-partial-content` and must be present in dependencies of the host package.
# Usage
From the `express-file-server` example:
1. Implement a `ContentProvider` function which prepares and returns a `Content` object:
import { promisify } from "util";
import fs from "fs";
import { Range, ContentDoesNotExistError, ContentProvider } from "express-partial-content";
import {logger} from "./logger";
const statAsync = promisify(fs.stat);
const existsAsync = promisify(fs.exists);
export const fileContentProvider: ContentProvider = async (req: Request) => {
// Read file name from route params.
const fileName = req.params.name;
const file = `${__dirname}/files/${fileName}`;
if (!(await existsAsync(file))) {
throw new ContentDoesNotExistError(`File doesn't exist: ${file}`);
}
const stats = await statAsync(file);
const totalSize = stats.size;
const mimeType = "application/octet-stream";
const getStream = (range?: Range) => {
if (!range) {
// Request if for complete content.
return fs.createReadStream(file);
}
// Partial content request.
const { start, end } = range;
logger.debug(`start: ${start}, end: ${end}`);
return fs.createReadStream(file, { start, end });
};
return {
fileName,
totalSize,
mimeType,
getStream
};
};
2. In your express code, use `createPartialContentHandler` factory method to generate an express handler for serving partial content for the route of your choice:
import {createPartialContentHandler} from "express-partial-content";
import {logger} from "./logger";
const handler = createPartialContentHandler(fileContentProvider, logger);
const app = express();
const port = 8080;
// File name is a route param.
app.get("/files/:name", handler);
app.listen(port, () => {
logger.debug("Server started!");
});
3. Run your server and use a multi-part/multi-connection download utility like [aria2c](https://aria2.github.io/) to test it:
aria -x5 -k1M http://localhost:8080/files/readme.txt
# Examples
There one examples in the `src/examples` folder:
1. `express-file-server`: Implements a file based `ContentProvider`.
## Running the examples:
1. `express-file-server`: Run the following commands, the server will listen on http://localhost:8080/.
yarn build:dev
yarn copy-assets
yarn run:examples:file
## Connecting to the running server:
Browse to `https://localhost:8080/files/readme.txt`
# Reference
## createPartialContentHandler function:
This is a factory method which generates a partial content handler for express routes.
### Arguments:
- `contentProvider`: An `async` function which returns a Promise resolved to a `Content` object (see below).
- `logger`: Any logging implementation which has a `debug(message:string, extra: any)` method. Either `winston` or `bunyan` loggers should work.
### Returns:
- Express Route Handler: `createPartialContentHandler` returns an express handler which can be mapped to an Express route to serve partial content.
## ContentProvider function:
This function _needs to be implemented by you_. It's purpose is to fetch and return `Content` object containing necessary metadata and methods to stream the content partially. This method is invoked by the express handler (returned by `createPartialContentHandler`) on each request.
### Arguments:
- `Request`: It receives the `Request` object as it's only input. Use the information available in `Request` to find the requested content, e.g. through `Request.params` or query string, headers etc.
### Returns:
- `Promise<Content>`: See below.
### Throws:
- `ContentDoesNotExistError`: Throw this to indicate that the content doesn't exist. The generated express handler will return a 404 in this case.
> Note: Any message provided to the `ContentDoesNotExistError` object is returned to the client.
## Content object:
This object contains metadata and methods which describe the content. The `ContentProvider` method builds and returns it.
### Properties:
All the properties of this object are used to return content metadata to the client as various `Response` headers.
- `fileName`: Used as the `Content-Disposition` header's `filename` value.
- `mimeType`: Used as the `Content-Type` header value.
- `totalSize`: Used as the `Content-Length` header value.
### Methods:
- `getStream(range?: Range)`: This method should return a readable stream initialized to the provided `range` (optional). You need to handle two cases:
- range is `null`: When `range` is not-specified, the client is requesting the full content. In this case, return the stream as it is.
- range is `{start, end}`: When client requests partial content, the `start` and `end` values will point to the corresponding byte positions (0 based and inclusive) of the content. You need to return stream limited to these positions.

View File

@ -1,22 +1,22 @@
import { Range } from "./Range";
import { Stream } from "stream";
export interface Content {
/**
* Returns a readable stream based on the provided range (optional).
* @param {Range} range The start-end range of stream data.
* @returns {Stream} A readable stream
*/
getStream(range?: Range): Stream;
/**
* Total size of the content
*/
readonly totalSize: number;
/**
* Mime type to be sent in Content-Type header
*/
readonly mimeType: string;
/**
* File name to be sent in Content-Disposition header
*/
readonly fileName: string;
};
import { Range } from "./Range";
import { Stream } from "stream";
export interface Content {
/**
* Returns a readable stream based on the provided range (optional).
* @param {Range} range The start-end range of stream data.
* @returns {Stream} A readable stream
*/
getStream(range?: Range): Stream;
/**
* Total size of the content
*/
readonly totalSize: number;
/**
* Mime type to be sent in Content-Type header
*/
readonly mimeType: string;
/**
* File name to be sent in Content-Disposition header
*/
readonly fileName: string;
};

View File

@ -1,2 +1,2 @@
export class ContentDoesNotExistError extends Error {
}
export class ContentDoesNotExistError extends Error {
}

View File

@ -1,6 +1,6 @@
import { Request } from "express";
import { Content } from "./Content";
/**
* @type {function (Request): Promise<Content>}
*/
export type ContentProvider = (req: Request) => Promise<Content>;
import { Request } from "express";
import { Content } from "./Content";
/**
* @type {function (Request): Promise<Content>}
*/
export type ContentProvider = (req: Request) => Promise<Content>;

View File

@ -1,3 +1,3 @@
export interface Logger {
debug(message: string, extra?: any): void;
}
export interface Logger {
debug(message: string, extra?: any): void;
}

View File

@ -1,5 +1,5 @@
export class RangeParserError extends Error {
constructor(start: any, end: any) {
super(`Invalid start and end values: ${start}-${end}.`);
}
}
export class RangeParserError extends Error {
constructor(start: any, end: any) {
super(`Invalid start and end values: ${start}-${end}.`);
}
}

View File

@ -1,60 +1,60 @@
import { Request, Response } from "express";
import { parseRangeHeader } from "./parseRangeHeader";
import { RangeParserError } from "./RangeParserError";
import { Logger } from "./Logger";
import { ContentProvider } from "./ContentProvider";
import { ContentDoesNotExistError } from "./ContentDoesNotExistError";
import {
getRangeHeader,
setContentRangeHeader,
setContentTypeHeader,
setContentDispositionHeader,
setAcceptRangesHeader,
setContentLengthHeader,
setCacheControlHeaderNoCache
} from "./utils";
export function createPartialContentHandler(contentProvider: ContentProvider, logger: Logger) {
return async function handler(req: Request, res: Response) {
let content;
try {
content = await contentProvider(req);
} catch (error) {
logger.debug("createPartialContentHandler: ContentProvider threw exception: ", error);
if (error instanceof ContentDoesNotExistError) {
return res.status(404).send(error.message);
}
return res.sendStatus(500);
}
let { getStream, mimeType, fileName, totalSize } = content;
const rangeHeader = getRangeHeader(req);
let range;
try {
range = parseRangeHeader(rangeHeader, totalSize, logger);
} catch (error) {
logger.debug(`createPartialContentHandler: parseRangeHeader error: `, error);
if (error instanceof RangeParserError) {
setContentRangeHeader(null, totalSize, res);
return res.status(416).send(`Invalid value for Range: ${rangeHeader}`);
}
return res.sendStatus(500);
}
setContentTypeHeader(mimeType, res);
setContentDispositionHeader(fileName, res);
setAcceptRangesHeader(res);
// If range is not specified, or the file is empty, return the full stream
if (range === null) {
logger.debug("createPartialContentHandler: No range found, returning full content.");
setContentLengthHeader(totalSize, res);
return getStream().pipe(res);
}
setContentRangeHeader(range, totalSize, res);
let { start, end } = range;
setContentLengthHeader(start === end ? 0 : end - start + 1, res);
setCacheControlHeaderNoCache(res);
// Return 206 Partial Content status
logger.debug("createPartialContentHandler: Returning partial content for range: ", JSON.stringify(range));
res.status(206);
return getStream(range).pipe(res);
};
}
import { Request, Response } from "express";
import { parseRangeHeader } from "./parseRangeHeader";
import { RangeParserError } from "./RangeParserError";
import { Logger } from "./Logger";
import { ContentProvider } from "./ContentProvider";
import { ContentDoesNotExistError } from "./ContentDoesNotExistError";
import {
getRangeHeader,
setContentRangeHeader,
setContentTypeHeader,
setContentDispositionHeader,
setAcceptRangesHeader,
setContentLengthHeader,
setCacheControlHeaderNoCache
} from "./utils";
export function createPartialContentHandler(contentProvider: ContentProvider, logger: Logger) {
return async function handler(req: Request, res: Response) {
let content;
try {
content = await contentProvider(req);
} catch (error) {
logger.debug("createPartialContentHandler: ContentProvider threw exception: ", error);
if (error instanceof ContentDoesNotExistError) {
return res.status(404).send(error.message);
}
return res.sendStatus(500);
}
let { getStream, mimeType, fileName, totalSize } = content;
const rangeHeader = getRangeHeader(req);
let range;
try {
range = parseRangeHeader(rangeHeader, totalSize, logger);
} catch (error) {
logger.debug(`createPartialContentHandler: parseRangeHeader error: `, error);
if (error instanceof RangeParserError) {
setContentRangeHeader(null, totalSize, res);
return res.status(416).send(`Invalid value for Range: ${rangeHeader}`);
}
return res.sendStatus(500);
}
setContentTypeHeader(mimeType, res);
setContentDispositionHeader(fileName, res);
setAcceptRangesHeader(res);
// If range is not specified, or the file is empty, return the full stream
if (range === null) {
logger.debug("createPartialContentHandler: No range found, returning full content.");
setContentLengthHeader(totalSize, res);
return getStream().pipe(res);
}
setContentRangeHeader(range, totalSize, res);
let { start, end } = range;
setContentLengthHeader(start === end ? 0 : end - start + 1, res);
setCacheControlHeaderNoCache(res);
// Return 206 Partial Content status
logger.debug("createPartialContentHandler: Returning partial content for range: ", JSON.stringify(range));
res.status(206);
return getStream(range).pipe(res);
};
}

View File

@ -1,31 +1,31 @@
import { Request } from "express";
import fs from "fs";
import { Range, ContentDoesNotExistError, ContentProvider } from "../../index";
import { existsAsync, statAsync } from "./utils";
import { logger } from "./logger";
export const fileContentProvider: ContentProvider = async (req: Request) => {
const fileName = req.params.name;
const file = `${__dirname}/../sample-files/${fileName}`;
if (!(await existsAsync(file))) {
throw new ContentDoesNotExistError(`File doesn't exist: ${file}`);
}
const stats = await statAsync(file);
const totalSize = stats.size;
const mimeType = "application/octet-stream";
const getStream = (range?: Range) => {
if (!range) {
return fs.createReadStream(file);
}
const { start, end } = range;
logger.debug(`start: ${start}, end: ${end}`);
return fs.createReadStream(file, { start, end });
};
return {
fileName,
totalSize,
mimeType,
getStream
};
};
import { Request } from "express";
import fs from "fs";
import { Range, ContentDoesNotExistError, ContentProvider } from "../../index";
import { existsAsync, statAsync } from "./utils";
import { logger } from "./logger";
export const fileContentProvider: ContentProvider = async (req: Request) => {
const fileName = req.params.name;
const file = `${__dirname}/../sample-files/${fileName}`;
if (!(await existsAsync(file))) {
throw new ContentDoesNotExistError(`File doesn't exist: ${file}`);
}
const stats = await statAsync(file);
const totalSize = stats.size;
const mimeType = "application/octet-stream";
const getStream = (range?: Range) => {
if (!range) {
return fs.createReadStream(file);
}
const { start, end } = range;
logger.debug(`start: ${start}, end: ${end}`);
return fs.createReadStream(file, { start, end });
};
return {
fileName,
totalSize,
mimeType,
getStream
};
};

View File

@ -1,15 +1,15 @@
import express from "express";
import { createPartialContentHandler } from "../../index";
import { fileContentProvider } from "./fileContentProvider";
import { logger } from "./logger";
const handler = createPartialContentHandler(fileContentProvider, logger);
const app = express();
const port = 8080;
app.get("/files/:name", handler);
app.listen(port, () => {
logger.debug("Server started!");
});
import express from "express";
import { createPartialContentHandler } from "../../index";
import { fileContentProvider } from "./fileContentProvider";
import { logger } from "./logger";
const handler = createPartialContentHandler(fileContentProvider, logger);
const app = express();
const port = 8080;
app.get("/files/:name", handler);
app.listen(port, () => {
logger.debug("Server started!");
});

View File

@ -1,9 +1,9 @@
export const logger = {
debug(message: string, extra?: any) {
if (extra) {
console.log(`[debug]: ${message}`, extra);
} else {
console.log(`[debug]: ${message}`);
}
}
};
export const logger = {
debug(message: string, extra?: any) {
if (extra) {
console.log(`[debug]: ${message}`, extra);
} else {
console.log(`[debug]: ${message}`);
}
}
};

View File

@ -1,5 +1,5 @@
import { promisify } from "util";
import fs from "fs";
export const statAsync = promisify(fs.stat);
export const existsAsync = promisify(fs.exists);
import { promisify } from "util";
import fs from "fs";
export const statAsync = promisify(fs.stat);
export const existsAsync = promisify(fs.exists);

View File

@ -1,7 +1,7 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Magna etiam tempor orci eu lobortis elementum nibh. In egestas erat imperdiet sed euismod. Amet consectetur adipiscing elit pellentesque habitant.
Vel quam elementum pulvinar etiam non quam lacus suspendisse.
Nibh sit amet commodo nulla facilisi. Vel risus commodo viverra maecenas accumsan lacus. Ornare arcu dui vivamus arcu felis bibendum ut tristique et.
Vitae semper quis lectus nulla at volutpat diam. Mauris vitae ultricies leo integer malesuada nunc. Donec massa sapien faucibus et. Senectus et netus et malesuada. Vitae tortor condimentum lacinia quis vel. Sagittis id consectetur purus ut faucibus pulvinar elementum. Nisi est sit amet facilisis magna etiam tempor orci eu.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Magna etiam tempor orci eu lobortis elementum nibh. In egestas erat imperdiet sed euismod. Amet consectetur adipiscing elit pellentesque habitant.
Vel quam elementum pulvinar etiam non quam lacus suspendisse.
Nibh sit amet commodo nulla facilisi. Vel risus commodo viverra maecenas accumsan lacus. Ornare arcu dui vivamus arcu felis bibendum ut tristique et.
Vitae semper quis lectus nulla at volutpat diam. Mauris vitae ultricies leo integer malesuada nunc. Donec massa sapien faucibus et. Senectus et netus et malesuada. Vitae tortor condimentum lacinia quis vel. Sagittis id consectetur purus ut faucibus pulvinar elementum. Nisi est sit amet facilisis magna etiam tempor orci eu.
Dictum varius duis at consectetur lorem donec massa sapien. Odio pellentesque diam volutpat commodo. Egestas dui id ornare arcu odio ut sem nulla. Consequat id porta nibh venenatis cras sed felis eget. Placerat in egestas erat imperdiet. Dui nunc mattis enim ut tellus elementum sagittis vitae. Aliquet bibendum enim facilisis gravida neque convallis a cras. Id semper risus in hendrerit gravida. Tempor orci eu lobortis elementum nibh tellus molestie. Semper auctor neque vitae tempus quam pellentesque. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque. Bibendum at varius vel pharetra vel turpis. Tellus integer feugiat scelerisque varius morbi enim nunc. Volutpat commodo sed egestas egestas fringilla. Congue eu consequat ac felis donec et odio. Venenatis cras sed felis eget velit aliquet. Urna neque viverra justo nec. Dictum non consectetur a erat nam. Lacinia quis vel eros donec.

View File

@ -1,13 +1,13 @@
import { Request, Response } from "express";
import { Range } from "./Range";
export const getHeader = (name: string, req: Request) => req.headers[name];
export const getRangeHeader = getHeader.bind(null, "range");
export const setHeader = (name: string, value: string, res: Response) => res.setHeader(name, value);
export const setContentTypeHeader = setHeader.bind(null, "Content-Type");
export const setContentLengthHeader = setHeader.bind(null, "Content-Length");
export const setAcceptRangesHeader = setHeader.bind(null, "Accept-Ranges", "bytes");
export const setContentRangeHeader = (range: Range | null, size: number, res: Response) =>
setHeader("Content-Range", `bytes ${range ? `${range.start}-${range.end}` : "*"}/${size}`, res);
export const setContentDispositionHeader = (fileName: string, res: Response) =>
setHeader("Content-Disposition", `attachment; filename*=utf-8''${encodeURIComponent(fileName)}`, res);
export const setCacheControlHeaderNoCache = setHeader.bind(null, "Cache-Control", "no-cache");
import { Request, Response } from "express";
import { Range } from "./Range";
export const getHeader = (name: string, req: Request) => req.headers[name];
export const getRangeHeader = getHeader.bind(null, "range");
export const setHeader = (name: string, value: string, res: Response) => res.setHeader(name, value);
export const setContentTypeHeader = setHeader.bind(null, "Content-Type");
export const setContentLengthHeader = setHeader.bind(null, "Content-Length");
export const setAcceptRangesHeader = setHeader.bind(null, "Accept-Ranges", "bytes");
export const setContentRangeHeader = (range: Range | null, size: number, res: Response) =>
setHeader("Content-Range", `bytes ${range ? `${range.start}-${range.end}` : "*"}/${size}`, res);
export const setContentDispositionHeader = (fileName: string, res: Response) =>
setHeader("Content-Disposition", `attachment; filename*=utf-8''${encodeURIComponent(fileName)}`, res);
export const setCacheControlHeaderNoCache = setHeader.bind(null, "Cache-Control", "no-cache");