-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split the code, add file server example.
- Loading branch information
1 parent
45439ef
commit 2d2d20f
Showing
16 changed files
with
560 additions
and
135 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Range } from "./parseRangeHeader"; | ||
import { Stream } from "stream"; | ||
export type 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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export class ContentDoesNotExistError extends Error { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { Request } from "express"; | ||
import { Content } from "./Content"; | ||
/** | ||
* @type {function (Request): Promise<Content>} | ||
*/ | ||
export type ContentProvider = (req: Request) => Promise<Content>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface Logger { | ||
debug(message: string, extra?: any): void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class RangeParserError extends Error { | ||
constructor(start: any, end: any) { | ||
super(`Invalid start and end values: ${start}-${end}.`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
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 createPartialStreamHandler(contentProvider: ContentProvider, logger: Logger) { | ||
return async function handler(req: Request, res: Response) { | ||
let content; | ||
try { | ||
content = await contentProvider(req); | ||
} catch (error) { | ||
logger.debug("ContentProvider threw exception: ", error); | ||
if (error instanceof ContentDoesNotExistError) { | ||
return res.status(400).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(`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("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("Returning partial content for range: ", JSON.stringify(range)); | ||
res.status(206); | ||
getStream(range).pipe(res); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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. | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import express, { Request } from "express"; | ||
import { promisify } from "util"; | ||
import fs from "fs"; | ||
import { Range, createPartialStreamHandler, ContentDoesNotExistError, ContentProvider } from "../../index"; | ||
|
||
const statAsync = promisify(fs.stat); | ||
const existsAsync = promisify(fs.exists); | ||
|
||
const fileContentProvider: ContentProvider = async (req: Request) => { | ||
const fileName = req.params.name; | ||
const file = `${__dirname}/files/${fileName}`; | ||
if (!(await existsAsync(file))) { | ||
throw new ContentDoesNotExistError(`File doesn't exists: ${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 | ||
}; | ||
}; | ||
|
||
const logger = { | ||
debug(message: string, extra?: any) { | ||
if (extra) { | ||
console.log(`[debug]: ${message}`, extra); | ||
} else { | ||
console.log(`[debug]: ${message}`); | ||
} | ||
} | ||
}; | ||
|
||
const handler = createPartialStreamHandler(fileContentProvider, logger); | ||
|
||
const app = express(); | ||
const port = 8080; | ||
|
||
app.get("/files/:name", handler); | ||
|
||
app.listen(port, () => { | ||
logger.debug("Server started!"); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,103 +1,5 @@ | ||
import { Request, Response } from "express"; | ||
import { parseRangeHeader, RangeParserError, Range } from "./parseRangeHeader"; | ||
import { Stream } from "stream"; | ||
|
||
/** | ||
* @type {function (Request): Promise<Content>} | ||
*/ | ||
export type ContentProvider = (req: Request) => Promise<Content>; | ||
|
||
export class ContentDoesNotExistError extends Error {} | ||
|
||
export interface Logger { | ||
debug(message: string, extra?: any): void; | ||
} | ||
|
||
export type 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; | ||
}; | ||
|
||
const getHeader = (name: string, req: Request) => req.headers[name]; | ||
const getRangeHeader = getHeader.bind(null, "range"); | ||
const setHeader = (name: string, value: string, res: Response) => res.setHeader(name, value); | ||
const setContentTypeHeader = setHeader.bind(null, "Content-Type"); | ||
const setContentLengthHeader = setHeader.bind(null, "Content-Length"); | ||
const setAcceptRangesHeader = setHeader.bind(null, "Accept-Ranges", "bytes"); | ||
const setContentRangeHeader = (range: Range | null, size: number, res: Response) => | ||
setHeader("Content-Range", `bytes ${range ? `${range.start}-${range.end}` : "*"}/${size}`, res); | ||
const setContentDispositionHeader = (fileName: string, res: Response) => | ||
setHeader("Content-Disposition", `attachment; filename="${fileName}"`, res); | ||
const setCacheControlHeaderNoCache = setHeader.bind(null, "Cache-Control", "no-cache"); | ||
|
||
export function create(contentProvider: ContentProvider, logger: Logger) { | ||
return async function handler(req: Request, res: Response) { | ||
let content; | ||
try { | ||
content = await contentProvider(req); | ||
} catch (error) { | ||
logger.debug("ContentProvider threw exception: ", error); | ||
if (error instanceof ContentDoesNotExistError) { | ||
return res.status(400).send(error.message); | ||
} | ||
|
||
return res.sendStatus(500); | ||
} | ||
|
||
let { getStream, mimeType, fileName, totalSize } = content; | ||
|
||
const rangeHeader = getRangeHeader(req); | ||
let range; | ||
try { | ||
range = parseRangeHeader(rangeHeader, totalSize); | ||
} catch (error) { | ||
logger.debug(`parseRangeHeader error: `, error); | ||
if (error instanceof RangeParserError) { | ||
setContentRangeHeader(null, totalSize, res); | ||
|
||
return res | ||
.send(error.message) | ||
.status(416) | ||
.end(); | ||
} | ||
|
||
return res.sendStatus(500); | ||
} | ||
|
||
let { start, end } = range; | ||
|
||
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) { | ||
setContentLengthHeader(totalSize, res); | ||
return getStream().pipe(res); | ||
} | ||
|
||
setContentRangeHeader(range, totalSize, res); | ||
setContentLengthHeader(start === end ? 0 : end - start + 1); | ||
setCacheControlHeaderNoCache(res); | ||
|
||
// Return 206 Partial Content status | ||
res.status(206); | ||
getStream(range).pipe(res); | ||
}; | ||
} | ||
export * from "./Content"; | ||
export * from "./ContentDoesNotExistError"; | ||
export * from "./ContentProvider"; | ||
export * from "./createPartialStreamHandler"; | ||
export * from "./Logger"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Request, Response } from "express"; | ||
import { Range } from "./parseRangeHeader"; | ||
const getHeader = (name: string, req: Request) => req.headers[name]; | ||
export const getRangeHeader = getHeader.bind(null, "range"); | ||
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="${fileName}"`, res); | ||
export const setCacheControlHeaderNoCache = setHeader.bind(null, "Cache-Control", "no-cache"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"compilerOptions": { | ||
"module": "commonjs", | ||
"esModuleInterop": true, | ||
"target": "es6", | ||
"noImplicitAny": true, | ||
"moduleResolution": "node", | ||
"sourceMap": true, | ||
"outDir": "dist", | ||
"declaration": true, | ||
"baseUrl": "src", | ||
"paths": { | ||
"*": ["../node_modules/*", "./types/*"] | ||
} | ||
} | ||
} |
Oops, something went wrong.