forked from oakserver/oak
-
Notifications
You must be signed in to change notification settings - Fork 0
/
send.ts
173 lines (152 loc) · 4.59 KB
/
send.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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/*!
* Adapted from koa-send at https://github.com/koajs/send and which is licensed
* with the MIT license.
*/
import { Context } from "./context.ts";
import { createHttpError } from "./httpError.ts";
import { basename, extname, parse, sep } from "./deps.ts";
import { decodeComponent, resolvePath } from "./util.ts";
export interface SendOptions {
/** Try to serve the brotli version of a file automatically when brotli is
* supported by a client and if the requested file with `.br` extension
* exists. (defaults to `true`) */
brotli?: boolean;
/** Try to match extensions from passed array to search for file when no
* extension is sufficed in URL. First found is served. (defaults to
* `undefined`) */
extensions?: string[];
/** If `true`, format the path to serve static file servers and not require a
* trailing slash for directories, so that you can do both `/directory` and
* `/directory/`. (defaults to `true`) */
format?: boolean;
/** Try to serve the gzipped version of a file automatically when gzip is
* supported by a client and if the requested file with `.gz` extension
* exists. (defaults to `true`). */
gzip?: boolean;
/** Allow transfer of hidden files. (defaults to `false`) */
hidden?: boolean;
/** Tell the browser the resource is immutable and can be cached
* indefinitely. (defaults to `false`) */
immutable?: boolean;
/** Name of the index file to serve automatically when visiting the root
* location. (defaults to none) */
index?: string;
/** Browser cache max-age in milliseconds. (defaults to `0`) */
maxage?: number;
/** Root directory to restrict file access. */
root: string;
}
function isHidden(root: string, path: string) {
const pathArr = path.substr(root.length).split(sep);
for (const segment of pathArr) {
if (segment[0] === ".") {
return true;
}
return false;
}
}
async function exists(path: string): Promise<boolean> {
try {
return (await Deno.stat(path)).isFile;
} catch {
return false;
}
}
/** Asynchronously fulfill a response with a file from the local file
* system.
*
* Requires Deno read permission. */
export async function send(
{ request, response }: Context<any>,
path: string,
options: SendOptions = { root: "" },
): Promise<string | undefined> {
const {
brotli = true,
extensions,
format = true,
gzip = true,
hidden = false,
immutable = false,
index,
maxage = 0,
root,
} = options;
const trailingSlash = path[path.length - 1] === "/";
path = decodeComponent(path.substr(parse(path).root.length));
if (index && trailingSlash) {
path += index;
}
path = resolvePath(root, path);
if (!hidden && isHidden(root, path)) {
return;
}
let encodingExt = "";
if (
brotli &&
request.acceptsEncodings("br", "identity") === "br" &&
(await exists(`${path}.br`))
) {
path = `${path}.br`;
response.headers.set("Content-Encoding", "br");
response.headers.delete("Content-Length");
encodingExt = ".br";
} else if (
gzip &&
request.acceptsEncodings("gzip", "identity") === "gzip" &&
(await exists(`${path}.gz`))
) {
path = `${path}.gz`;
response.headers.set("Content-Encoding", "gzip");
response.headers.delete("Content-Length");
encodingExt = ".gz";
}
if (extensions && !/\.[^/]*$/.exec(path)) {
for (let ext of extensions) {
if (!/^\./.exec(ext)) {
ext = `.${ext}`;
}
if (await exists(`${path}${ext}`)) {
path += ext;
break;
}
}
}
let stats: Deno.FileInfo;
try {
stats = await Deno.stat(path);
if (stats.isDirectory) {
if (format && index) {
path += `/${index}`;
stats = await Deno.stat(path);
} else {
return;
}
}
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
throw createHttpError(404, err.message);
}
throw createHttpError(500, err.message);
}
response.headers.set("Content-Length", String(stats.size));
if (!response.headers.has("Last-Modified") && stats.mtime) {
response.headers.set("Last-Modified", stats.mtime.toUTCString());
}
if (!response.headers.has("Cache-Control")) {
const directives = [`max-age=${(maxage / 1000) | 0}`];
if (immutable) {
directives.push("immutable");
}
response.headers.set("Cache-Control", directives.join(","));
}
if (!response.type) {
response.type = encodingExt !== ""
? extname(basename(path, encodingExt))
: extname(path);
}
const file = await Deno.open(path, { read: true });
response.addResource(file.rid);
response.body = file;
return path;
}