Skip to content

Commit 8486136

Browse files
authored
Merge pull request #1829 from TriliumNext/monorepo/express-partial-content
Integrate express-partial-content into monorepo
2 parents c23bad3 + f8ccbb3 commit 8486136

31 files changed

+921
-13
lines changed

apps/edit-docs/tsconfig.app.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
"eslint.config.mjs"
1919
],
2020
"references": [
21+
{
22+
"path": "../server/tsconfig.app.json"
23+
},
2124
{
2225
"path": "../desktop/tsconfig.app.json"
2326
},

apps/edit-docs/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"files": [],
44
"include": [],
55
"references": [
6+
{
7+
"path": "../server"
8+
},
69
{
710
"path": "../desktop"
811
},

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"@anthropic-ai/sdk": "0.40.1",
5454
"@braintree/sanitize-url": "7.1.1",
5555
"@triliumnext/commons": "workspace:*",
56-
"@triliumnext/express-partial-content": "1.0.1",
56+
"@triliumnext/express-partial-content": "workspace:*",
5757
"@triliumnext/turndown-plugin-gfm": "workspace:*",
5858
"archiver": "7.0.1",
5959
"async-mutex": "0.5.0",

apps/server/src/services/import/mime.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ describe("#getMime", () => {
4545
["test.zip"], "application/zip"
4646
],
4747

48+
[
49+
"MP4 videos are supported",
50+
["video.mp4"], "video/mp4"
51+
],
52+
4853
[
4954
"unknown MIME type not recognized by mimeTypes.lookup",
5055
["test.fake"], false

apps/server/src/services/import/mime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ const EXTENSION_TO_MIME = new Map<string, string>([
7171
[".ts", "text/x-typescript"],
7272
[".excalidraw", "application/json"],
7373
[".mermaid", "text/vnd.mermaid"],
74-
[".mmd", "text/vnd.mermaid"]
74+
[".mmd", "text/vnd.mermaid"],
75+
[".mp4", "video/mp4"] // https://github.com/jshttp/mime-types/issues/138
7576
]);
7677

7778
/** @returns false if MIME is not detected */

apps/server/tsconfig.app.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
{
3737
"path": "../../packages/turndown-plugin-gfm/tsconfig.lib.json"
3838
},
39+
{
40+
"path": "../../packages/express-partial-content/tsconfig.lib.json"
41+
},
3942
{
4043
"path": "../../packages/commons/tsconfig.lib.json"
4144
}

apps/server/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
{
77
"path": "../../packages/turndown-plugin-gfm"
88
},
9+
{
10+
"path": "../../packages/express-partial-content"
11+
},
912
{
1013
"path": "../../packages/commons"
1114
},
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"jsc": {
3+
"target": "es2017",
4+
"parser": {
5+
"syntax": "typescript",
6+
"decorators": true,
7+
"dynamicImport": true
8+
},
9+
"transform": {
10+
"decoratorMetadata": true,
11+
"legacyDecorator": true
12+
},
13+
"keepClassNames": true,
14+
"externalHelpers": true,
15+
"loose": true
16+
},
17+
"module": {
18+
"type": "commonjs"
19+
},
20+
"sourceMaps": true,
21+
"exclude": ["jest.config.ts",".*\\.spec.tsx?$",".*\\.test.tsx?$","./src/jest-setup.ts$","./**/jest-setup.ts$",".*.js$"]
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Sukant Gujar
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
![Travis CI Status](https://travis-ci.com/SukantGujar/express-partial-content.svg?branch=master)
2+
3+
# About
4+
5+
A HTTP 206 Partial Content handler to serve any readable stream partially in Express.
6+
7+
Based on this blog post: https://www.codeproject.com/Articles/813480/HTTP-Partial-Content-In-Node-js.
8+
9+
# Installation
10+
11+
`yarn add express-partial-content`
12+
13+
OR
14+
15+
`npm install express-partial-content`
16+
17+
> Note: `Express` package is a peer dependency for `express-partial-content` and must be present in dependencies of the host package.
18+
19+
# Usage
20+
21+
From the `express-file-server` example:
22+
23+
1. Implement a `ContentProvider` function which prepares and returns a `Content` object:
24+
25+
import { promisify } from "util";
26+
import fs from "fs";
27+
import { Range, ContentDoesNotExistError, ContentProvider } from "express-partial-content";
28+
import {logger} from "./logger";
29+
30+
const statAsync = promisify(fs.stat);
31+
const existsAsync = promisify(fs.exists);
32+
33+
export const fileContentProvider: ContentProvider = async (req: Request) => {
34+
// Read file name from route params.
35+
const fileName = req.params.name;
36+
const file = `${__dirname}/files/${fileName}`;
37+
if (!(await existsAsync(file))) {
38+
throw new ContentDoesNotExistError(`File doesn't exist: ${file}`);
39+
}
40+
const stats = await statAsync(file);
41+
const totalSize = stats.size;
42+
const mimeType = "application/octet-stream";
43+
const getStream = (range?: Range) => {
44+
if (!range) {
45+
// Request if for complete content.
46+
return fs.createReadStream(file);
47+
}
48+
// Partial content request.
49+
const { start, end } = range;
50+
logger.debug(`start: ${start}, end: ${end}`);
51+
return fs.createReadStream(file, { start, end });
52+
};
53+
return {
54+
fileName,
55+
totalSize,
56+
mimeType,
57+
getStream
58+
};
59+
};
60+
61+
2. In your express code, use `createPartialContentHandler` factory method to generate an express handler for serving partial content for the route of your choice:
62+
63+
import {createPartialContentHandler} from "express-partial-content";
64+
import {logger} from "./logger";
65+
66+
const handler = createPartialContentHandler(fileContentProvider, logger);
67+
68+
const app = express();
69+
const port = 8080;
70+
71+
// File name is a route param.
72+
app.get("/files/:name", handler);
73+
74+
app.listen(port, () => {
75+
logger.debug("Server started!");
76+
});
77+
78+
3. Run your server and use a multi-part/multi-connection download utility like [aria2c](https://aria2.github.io/) to test it:
79+
80+
aria -x5 -k1M http://localhost:8080/files/readme.txt
81+
82+
# Examples
83+
84+
There one examples in the `src/examples` folder:
85+
86+
1. `express-file-server`: Implements a file based `ContentProvider`.
87+
88+
## Running the examples:
89+
90+
1. `express-file-server`: Run the following commands, the server will listen on http://localhost:8080/.
91+
92+
yarn build:dev
93+
yarn copy-assets
94+
yarn run:examples:file
95+
96+
## Connecting to the running server:
97+
98+
Browse to `https://localhost:8080/files/readme.txt`
99+
100+
# Reference
101+
102+
## createPartialContentHandler function:
103+
104+
This is a factory method which generates a partial content handler for express routes.
105+
106+
### Arguments:
107+
108+
- `contentProvider`: An `async` function which returns a Promise resolved to a `Content` object (see below).
109+
- `logger`: Any logging implementation which has a `debug(message:string, extra: any)` method. Either `winston` or `bunyan` loggers should work.
110+
111+
### Returns:
112+
113+
- Express Route Handler: `createPartialContentHandler` returns an express handler which can be mapped to an Express route to serve partial content.
114+
115+
## ContentProvider function:
116+
117+
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.
118+
119+
### Arguments:
120+
121+
- `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.
122+
123+
### Returns:
124+
125+
- `Promise<Content>`: See below.
126+
127+
### Throws:
128+
129+
- `ContentDoesNotExistError`: Throw this to indicate that the content doesn't exist. The generated express handler will return a 404 in this case.
130+
> Note: Any message provided to the `ContentDoesNotExistError` object is returned to the client.
131+
132+
## Content object:
133+
134+
This object contains metadata and methods which describe the content. The `ContentProvider` method builds and returns it.
135+
136+
### Properties:
137+
138+
All the properties of this object are used to return content metadata to the client as various `Response` headers.
139+
140+
- `fileName`: Used as the `Content-Disposition` header's `filename` value.
141+
- `mimeType`: Used as the `Content-Type` header value.
142+
- `totalSize`: Used as the `Content-Length` header value.
143+
144+
### Methods:
145+
146+
- `getStream(range?: Range)`: This method should return a readable stream initialized to the provided `range` (optional). You need to handle two cases:
147+
148+
- range is `null`: When `range` is not-specified, the client is requesting the full content. In this case, return the stream as it is.
149+
- 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.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import baseConfig from "../../eslint.config.mjs";
2+
3+
export default [
4+
...baseConfig,
5+
{
6+
"files": [
7+
"**/*.json"
8+
],
9+
"rules": {
10+
"@nx/dependency-checks": [
11+
"error",
12+
{
13+
"ignoredFiles": [
14+
"{projectRoot}/eslint.config.{js,cjs,mjs}"
15+
]
16+
}
17+
]
18+
},
19+
"languageOptions": {
20+
"parser": (await import('jsonc-eslint-parser'))
21+
}
22+
}
23+
];
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "@triliumnext/express-partial-content",
3+
"description": "A partial content handler implementation for any readable stream with Express. Based on this blog post: https://www.codeproject.com/Articles/813480/HTTP-Partial-Content-In-Node-js.",
4+
"license": "MIT",
5+
"version": "1.1.0",
6+
"type": "module",
7+
"private": true,
8+
"main": "./dist/index.js",
9+
"module": "./dist/index.js",
10+
"types": "./dist/index.d.ts",
11+
"exports": {
12+
"./package.json": "./package.json",
13+
".": {
14+
"development": "./src/index.ts",
15+
"types": "./dist/index.d.ts",
16+
"import": "./dist/index.js",
17+
"default": "./dist/index.js"
18+
}
19+
},
20+
"keywords": [
21+
"partial-content",
22+
"206",
23+
"stream",
24+
"typescript"
25+
],
26+
"nx": {
27+
"name": "express-partial-content",
28+
"targets": {
29+
"build": {
30+
"executor": "@nx/js:swc",
31+
"outputs": [
32+
"{options.outputPath}"
33+
],
34+
"options": {
35+
"outputPath": "packages/express-partial-content/dist",
36+
"tsConfig": "packages/express-partial-content/tsconfig.lib.json",
37+
"packageJson": "packages/express-partial-content/package.json",
38+
"main": "packages/express-partial-content/src/index.ts",
39+
"stripLeadingPaths": true
40+
}
41+
}
42+
}
43+
},
44+
"dependencies": {
45+
"tslib": "^2.3.0"
46+
}
47+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Range } from "./Range.js";
2+
import { Stream } from "stream";
3+
export interface Content {
4+
/**
5+
* Returns a readable stream based on the provided range (optional).
6+
* @param {Range} range The start-end range of stream data.
7+
* @returns {Stream} A readable stream
8+
*/
9+
getStream(range?: Range): Stream;
10+
/**
11+
* Total size of the content
12+
*/
13+
readonly totalSize: number;
14+
/**
15+
* Mime type to be sent in Content-Type header
16+
*/
17+
readonly mimeType: string;
18+
/**
19+
* File name to be sent in Content-Disposition header
20+
*/
21+
readonly fileName: string;
22+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export class ContentDoesNotExistError extends Error {
2+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Request } from "express";
2+
import type { Content } from "./Content.js";
3+
/**
4+
* @type {function (Request): Promise<Content>}
5+
*/
6+
export type ContentProvider = (req: Request) => Promise<Content>;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface Logger {
2+
debug(message: string, extra?: any): void;
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type Range = {
2+
start: number;
3+
end: number;
4+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class RangeParserError extends Error {
2+
constructor(start: any, end: any) {
3+
super(`Invalid start and end values: ${start}-${end}.`);
4+
}
5+
}

0 commit comments

Comments
 (0)