-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathfile-server.ts
119 lines (104 loc) · 3.46 KB
/
file-server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { Exception } from '../interfaces';
import { Handler } from '../handler';
import { Message } from '../interfaces';
import { fromReadableStream } from '../utils';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { Inject } from 'injection-js';
import { Injectable } from 'injection-js';
import { InjectionToken } from 'injection-js';
import { Observable } from 'rxjs';
import { Optional } from 'injection-js';
import { catchError } from 'rxjs/operators';
import { from } from 'rxjs';
import { mapTo } from 'rxjs/operators';
import { mergeMap } from 'rxjs/operators';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { throwError } from 'rxjs';
import md5File from 'md5-file';
/**
* File server options
*/
export interface FileServerOpts {
maxAge?: number;
root?: string;
}
export const FILE_SERVER_OPTS = new InjectionToken<FileServerOpts>(
'FILE_SERVER_OPTS'
);
export const FILE_SERVER_DEFAULT_OPTS: FileServerOpts = {
maxAge: 600,
root: os.homedir()
};
/**
* File server
*/
@Injectable()
export class FileServer extends Handler {
private opts: FileServerOpts;
constructor(@Optional() @Inject(FILE_SERVER_OPTS) opts: FileServerOpts) {
super();
this.opts = opts
? { ...FILE_SERVER_DEFAULT_OPTS, ...opts }
: FILE_SERVER_DEFAULT_OPTS;
}
handle(message$: Observable<Message>): Observable<Message> {
return message$.pipe(
mergeMap((message: Message): Observable<Message> => {
const { request, response } = message;
const fpath = this.makeFPath(message);
// Etag is the fie hash
const etag = request.headers['If-None-Match'];
return of(message).pipe(
// NOTE: exception thrown if not found
mergeMap(
(_message: Message): Observable<string> => from(md5File(fpath))
),
// set the response headers
tap((hash: string) => {
response.headers['Cache-Control'] = `max-age=${this.opts.maxAge}`;
response.headers['Etag'] = hash;
}),
// flip to cached/not cached pipes
mergeMap((hash: string): Observable<Message> => {
const cached = etag === hash;
// cached pipe
const cached$ = of(hash).pipe(
tap(() => (response.statusCode = 304)),
mapTo(message)
);
// not cached pipe
const notCached$ = of(hash).pipe(
mergeMap(
(): Observable<Buffer> =>
fromReadableStream(fs.createReadStream(fpath))
),
tap((buffer: Buffer) => {
response.body = buffer;
response.statusCode = 200;
}),
mapTo(message)
);
return cached ? cached$ : notCached$;
}),
catchError(() => throwError(new Exception({ statusCode: 404 })))
);
})
);
}
// private methods
private makeFPath(message: Message): string {
const { context, request, response } = message;
const router = context.router;
// NOTE: we never allow dot files and router.validate takes care of that
let tail = router.tailOf(router.validate(request.path), request.route);
// TODO: hack if this is a client-side route and not a path, deploy default
if (!tail.includes('.')) {
tail = 'index.html';
response.headers['Content-Type'] = 'text/html';
}
return path.join(this.opts.root, tail);
}
}