hfs 0.26.7 → 0.26.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/assets/{index.0f549e00.js → index.bb5198ec.js} +3 -3
- package/admin/assets/{sha512.ea1121b3.js → sha512.9dfe82e1.js} +1 -1
- package/admin/index.html +1 -1
- package/frontend/assets/{index.1151988f.js → index.27a78796.js} +16 -16
- package/frontend/assets/{sha512.bb881250.js → sha512.6af42937.js} +1 -1
- package/frontend/index.html +1 -1
- package/package.json +2 -1
- package/plugins/vhosting/plugin.js +1 -1
- package/src/api.accounts.js +4 -4
- package/src/api.auth.js +2 -4
- package/src/apiMiddleware.js +1 -17
- package/src/index.js +1 -0
- package/src/middlewares.js +26 -5
- package/src/perm.js +16 -11
- package/src/serveFile.js +4 -3
- package/src/vfs.js +6 -3
- package/src/zip.js +5 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as SF}from"./index.
|
|
1
|
+
import{c as SF}from"./index.27a78796.js";function OF(sF,hF){for(var eF=0;eF<hF.length;eF++){const tF=hF[eF];if(typeof tF!="string"&&!Array.isArray(tF)){for(const w in tF)if(w!=="default"&&!(w in sF)){const lF=Object.getOwnPropertyDescriptor(tF,w);lF&&Object.defineProperty(sF,w,lF.get?lF:{enumerable:!0,get:()=>tF[w]})}}}return Object.freeze(Object.defineProperty(sF,Symbol.toStringTag,{value:"Module"}))}var yF={exports:{}};/*
|
|
2
2
|
* [js-sha512]{@link https://github.com/emn178/js-sha512}
|
|
3
3
|
*
|
|
4
4
|
* @version 0.8.0
|
package/frontend/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link href="fontello.css" rel="stylesheet" />
|
|
7
7
|
<script>SESSION = _HFS_SESSION_</script>
|
|
8
8
|
<title>File Server</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index.
|
|
9
|
+
<script type="module" crossorigin src="/assets/index.27a78796.js"></script>
|
|
10
10
|
<link rel="stylesheet" href="/assets/index.93366732.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hfs",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.9",
|
|
4
4
|
"description": "HTTP File Server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"file server",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"build-shared": "npm run build --workspace=shared",
|
|
25
25
|
"build-form": "npm run build --workspace=mui-grid-form",
|
|
26
26
|
"server-for-test": "node dist/src --cwd . --config tests",
|
|
27
|
+
"server-for-test-dev": "cross-env DEV=1 nodemon --ignore tests/ --watch src -e ts,tsx --exec ts-node src -- --cwd . --config tests",
|
|
27
28
|
"test": "mocha -r ts-node/register 'tests/**/*.ts'",
|
|
28
29
|
"pub": "cd dist && npm publish",
|
|
29
30
|
"dist": "npm run build-all && npm run dist-bin",
|
|
@@ -21,7 +21,7 @@ exports.init = api => ({
|
|
|
21
21
|
middleware(ctx) {
|
|
22
22
|
let toModify = ctx
|
|
23
23
|
if (ctx.path.startsWith(api.const.SPECIAL_URI)) { // special uris should be excluded...
|
|
24
|
-
toModify = ctx.
|
|
24
|
+
toModify = ctx.params
|
|
25
25
|
if (toModify.path === undefined) // ...unless they carry a path in the query. In that case we'll work that.
|
|
26
26
|
return
|
|
27
27
|
}
|
package/src/api.accounts.js
CHANGED
|
@@ -37,14 +37,14 @@ const apis = {
|
|
|
37
37
|
changes.admin = undefined;
|
|
38
38
|
else if (admin !== undefined && typeof admin !== 'boolean')
|
|
39
39
|
return new apiMiddleware_1.ApiError(400, "invalid admin");
|
|
40
|
-
|
|
40
|
+
const acc = (0, perm_1.setAccount)(username, changes);
|
|
41
|
+
return acc ? lodash_1.default.pick(acc, 'username') : new apiMiddleware_1.ApiError(400);
|
|
41
42
|
},
|
|
42
43
|
add_account({ username, ...rest }) {
|
|
43
44
|
if ((0, perm_1.getAccount)(username))
|
|
44
45
|
return new apiMiddleware_1.ApiError(const_1.FORBIDDEN);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return {};
|
|
46
|
+
const acc = (0, perm_1.addAccount)(username, rest);
|
|
47
|
+
return acc ? lodash_1.default.pick(acc, 'username') : new apiMiddleware_1.ApiError(400);
|
|
48
48
|
},
|
|
49
49
|
del_account({ username }) {
|
|
50
50
|
return (0, perm_1.delAccount)(username) ? {} : new apiMiddleware_1.ApiError(400);
|
package/src/api.auth.js
CHANGED
|
@@ -23,7 +23,7 @@ async function loggedIn(ctx, username) {
|
|
|
23
23
|
ctx.cookies.set('csrf', '');
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
|
-
s.username = username;
|
|
26
|
+
s.username = (0, perm_1.normalizeUsername)(username);
|
|
27
27
|
await (0, middlewares_1.prepareState)(ctx, async () => { }); // updating the state is necessary to send complete session data so that frontend shows admin button
|
|
28
28
|
delete s.login;
|
|
29
29
|
ctx.cookies.set('csrf', (0, misc_1.randomId)(), { signed: false, httpOnly: false });
|
|
@@ -34,7 +34,6 @@ function makeExp() {
|
|
|
34
34
|
const login = async ({ username, password }, ctx) => {
|
|
35
35
|
if (!username || !password) // some validation
|
|
36
36
|
return new apiMiddleware_1.ApiError(400);
|
|
37
|
-
username = username.toLocaleLowerCase(); // normalize username, to be case-insensitive
|
|
38
37
|
const acc = (0, perm_1.getAccount)(username);
|
|
39
38
|
if (!acc)
|
|
40
39
|
return new apiMiddleware_1.ApiError(const_1.UNAUTHORIZED);
|
|
@@ -51,7 +50,6 @@ exports.login = login;
|
|
|
51
50
|
const loginSrp1 = async ({ username }, ctx) => {
|
|
52
51
|
if (!username)
|
|
53
52
|
return new apiMiddleware_1.ApiError(400);
|
|
54
|
-
username = username.toLocaleLowerCase();
|
|
55
53
|
const account = (0, perm_1.getAccount)(username);
|
|
56
54
|
if (!ctx.session)
|
|
57
55
|
return new apiMiddleware_1.ApiError(500);
|
|
@@ -107,7 +105,7 @@ exports.loginSrp2 = loginSrp2;
|
|
|
107
105
|
const logout = async ({}, ctx) => {
|
|
108
106
|
if (!ctx.session)
|
|
109
107
|
return new apiMiddleware_1.ApiError(500);
|
|
110
|
-
loggedIn(ctx, false);
|
|
108
|
+
await loggedIn(ctx, false);
|
|
111
109
|
// 401 is a convenient code for OK: the browser clears a possible http authentication (hopefully), and Admin automatically triggers login dialog
|
|
112
110
|
return new apiMiddleware_1.ApiError(401);
|
|
113
111
|
};
|
package/src/apiMiddleware.js
CHANGED
|
@@ -20,8 +20,7 @@ class ApiError extends Error {
|
|
|
20
20
|
exports.ApiError = ApiError;
|
|
21
21
|
function apiMiddleware(apis) {
|
|
22
22
|
return async (ctx) => {
|
|
23
|
-
const params = ctx
|
|
24
|
-
: (0, misc_1.objSameKeys)(ctx.request.query, x => Array.isArray(x) ? x : (0, misc_1.tryJson)(x));
|
|
23
|
+
const { params } = ctx;
|
|
25
24
|
console.debug('API', ctx.method, ctx.path, { ...params });
|
|
26
25
|
if (!apis.hasOwnProperty(ctx.path)) {
|
|
27
26
|
ctx.body = 'invalid api';
|
|
@@ -55,21 +54,6 @@ exports.apiMiddleware = apiMiddleware;
|
|
|
55
54
|
function isAsyncGenerator(x) {
|
|
56
55
|
return typeof (x === null || x === void 0 ? void 0 : x.next) === 'function';
|
|
57
56
|
}
|
|
58
|
-
async function getJsonFromReq(req) {
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
let data = '';
|
|
61
|
-
req.on('data', chunk => data += chunk);
|
|
62
|
-
req.on('error', reject);
|
|
63
|
-
req.on('end', () => {
|
|
64
|
-
try {
|
|
65
|
-
resolve(data && JSON.parse(data));
|
|
66
|
-
}
|
|
67
|
-
catch (e) {
|
|
68
|
-
reject(e);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
57
|
class SendListReadable extends stream_1.Readable {
|
|
74
58
|
constructor({ addAtStart, doAtStart, bufferTime } = {}) {
|
|
75
59
|
super({ objectMode: true, read() { } });
|
package/src/index.js
CHANGED
|
@@ -33,6 +33,7 @@ exports.app.use(middlewares_1.someSecurity)
|
|
|
33
33
|
.use((0, log_1.log)())
|
|
34
34
|
.use(throttler_1.throttler)
|
|
35
35
|
.use(middlewares_1.gzipper)
|
|
36
|
+
.use(middlewares_1.paramsDecoder)
|
|
36
37
|
.use((0, plugins_1.pluginsMiddleware)())
|
|
37
38
|
.use((0, koa_mount_1.default)(const_1.API_URI, (0, apiMiddleware_1.apiMiddleware)({ ...frontEndApis_1.frontEndApis, ...adminApis_1.adminApis })))
|
|
38
39
|
.use(middlewares_1.serveGuiAndSharedFiles)
|
package/src/middlewares.js
CHANGED
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.serveGuiAndSharedFiles = exports.sessions = exports.headRequests = exports.gzipper = void 0;
|
|
7
|
+
exports.paramsDecoder = exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.serveGuiAndSharedFiles = exports.sessions = exports.headRequests = exports.gzipper = void 0;
|
|
8
8
|
const koa_compress_1 = __importDefault(require("koa-compress"));
|
|
9
9
|
const koa_session_1 = __importDefault(require("koa-session"));
|
|
10
10
|
const const_1 = require("./const");
|
|
@@ -82,7 +82,7 @@ const serveGuiAndSharedFiles = async (ctx, next) => {
|
|
|
82
82
|
return;
|
|
83
83
|
const browserDetected = ctx.get('Upgrade-Insecure-Requests') || ctx.get('Sec-Fetch-Mode'); // ugh, heuristics
|
|
84
84
|
if (!browserDetected) // we don't want to trigger basic authentication on browsers, it's meant for download managers only
|
|
85
|
-
ctx.set('WWW-Authenticate', 'Basic'); // we support basic authentication
|
|
85
|
+
return ctx.set('WWW-Authenticate', 'Basic'); // we support basic authentication
|
|
86
86
|
ctx.state.serveApp = true;
|
|
87
87
|
return serveFrontendFiles(ctx, next);
|
|
88
88
|
}
|
|
@@ -126,9 +126,9 @@ function getProxyDetected() {
|
|
|
126
126
|
}
|
|
127
127
|
exports.getProxyDetected = getProxyDetected;
|
|
128
128
|
const prepareState = async (ctx, next) => {
|
|
129
|
-
var _a;
|
|
129
|
+
var _a, _b;
|
|
130
130
|
// calculate these once and for all
|
|
131
|
-
ctx.state.account = (_a = await getHttpAccount(ctx)) !== null && _a !== void 0 ? _a : (0, perm_1.getAccount)((0,
|
|
131
|
+
ctx.state.account = (_a = await getHttpAccount(ctx)) !== null && _a !== void 0 ? _a : (0, perm_1.getAccount)((_b = ctx.session) === null || _b === void 0 ? void 0 : _b.username, false);
|
|
132
132
|
const conn = ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
133
133
|
await next();
|
|
134
134
|
if (conn)
|
|
@@ -142,7 +142,6 @@ async function getHttpAccount(ctx) {
|
|
|
142
142
|
return account;
|
|
143
143
|
}
|
|
144
144
|
async function srpCheck(username, password) {
|
|
145
|
-
username = username.toLocaleLowerCase();
|
|
146
145
|
const account = (0, perm_1.getAccount)(username);
|
|
147
146
|
if (!(account === null || account === void 0 ? void 0 : account.srp) || !password)
|
|
148
147
|
return false;
|
|
@@ -152,3 +151,25 @@ async function srpCheck(username, password) {
|
|
|
152
151
|
const clientRes2 = await clientRes1.step2(BigInt(salt), BigInt(pubKey));
|
|
153
152
|
return await step1.step2(clientRes2.A, clientRes2.M1).then(() => true, () => false);
|
|
154
153
|
}
|
|
154
|
+
// unify get/post parameters, with JSON decoding to not be limited to strings
|
|
155
|
+
const paramsDecoder = async (ctx, next) => {
|
|
156
|
+
ctx.params = ctx.method === 'POST' ? (0, misc_1.tryJson)(await getReqData(ctx.req))
|
|
157
|
+
: (0, misc_1.objSameKeys)(ctx.query, x => Array.isArray(x) ? x : (0, misc_1.tryJson)(x));
|
|
158
|
+
await next();
|
|
159
|
+
};
|
|
160
|
+
exports.paramsDecoder = paramsDecoder;
|
|
161
|
+
async function getReqData(req) {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
let data = '';
|
|
164
|
+
req.on('data', chunk => data += chunk);
|
|
165
|
+
req.on('error', reject);
|
|
166
|
+
req.on('end', () => {
|
|
167
|
+
try {
|
|
168
|
+
resolve(data);
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
reject(e);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
package/src/perm.js
CHANGED
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.anyAccountCanLoginAdmin = exports.accountCanLoginAdmin = exports.accountCanLogin = exports.accountHasPassword = exports.getFromAccount = exports.delAccount = exports.setAccount = exports.addAccount = exports.renameAccount = exports.updateAccount = exports.allowClearTextLogin = exports.saveSrpInfo = exports.getAccount = exports.getCurrentUsernameExpanded = exports.getCurrentUsername = exports.getAccounts = void 0;
|
|
7
|
+
exports.anyAccountCanLoginAdmin = exports.accountCanLoginAdmin = exports.accountCanLogin = exports.accountHasPassword = exports.getFromAccount = exports.delAccount = exports.setAccount = exports.addAccount = exports.renameAccount = exports.normalizeUsername = exports.updateAccount = exports.allowClearTextLogin = exports.saveSrpInfo = exports.getAccount = exports.getCurrentUsernameExpanded = exports.getCurrentUsername = exports.getAccounts = void 0;
|
|
8
8
|
const lodash_1 = __importDefault(require("lodash"));
|
|
9
9
|
const crypt_1 = require("./crypt");
|
|
10
10
|
const misc_1 = require("./misc");
|
|
@@ -17,8 +17,8 @@ function getAccounts() {
|
|
|
17
17
|
}
|
|
18
18
|
exports.getAccounts = getAccounts;
|
|
19
19
|
function getCurrentUsername(ctx) {
|
|
20
|
-
var _a
|
|
21
|
-
return ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.username) ||
|
|
20
|
+
var _a;
|
|
21
|
+
return ((_a = ctx.state.account) === null || _a === void 0 ? void 0 : _a.username) || '';
|
|
22
22
|
}
|
|
23
23
|
exports.getCurrentUsername = getCurrentUsername;
|
|
24
24
|
// provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions
|
|
@@ -35,7 +35,9 @@ function getCurrentUsernameExpanded(ctx) {
|
|
|
35
35
|
return ret;
|
|
36
36
|
}
|
|
37
37
|
exports.getCurrentUsernameExpanded = getCurrentUsernameExpanded;
|
|
38
|
-
function getAccount(username) {
|
|
38
|
+
function getAccount(username, normalize = true) {
|
|
39
|
+
if (normalize)
|
|
40
|
+
username = normalizeUsername(username);
|
|
39
41
|
return username ? accounts[username] : undefined;
|
|
40
42
|
}
|
|
41
43
|
exports.getAccount = getAccount;
|
|
@@ -77,15 +79,16 @@ accountsConfig.sub(async (v) => {
|
|
|
77
79
|
const norm = normalizeUsername(k);
|
|
78
80
|
if (!rec) // an empty object in yaml is stored as null
|
|
79
81
|
rec = accounts[norm] = { username: norm };
|
|
80
|
-
else
|
|
81
|
-
(
|
|
82
|
+
else if ((0, misc_1.objRenameKey)(accounts, k, norm))
|
|
83
|
+
saveAccountsAsap();
|
|
82
84
|
(0, misc_1.setHidden)(rec, { username: norm });
|
|
83
|
-
await updateAccount(rec);
|
|
85
|
+
await updateAccount(rec); // work password fields
|
|
84
86
|
}));
|
|
85
87
|
});
|
|
86
88
|
function normalizeUsername(username) {
|
|
87
89
|
return username.toLocaleLowerCase();
|
|
88
90
|
}
|
|
91
|
+
exports.normalizeUsername = normalizeUsername;
|
|
89
92
|
function renameAccount(from, to) {
|
|
90
93
|
from = normalizeUsername(from);
|
|
91
94
|
to = normalizeUsername(to);
|
|
@@ -112,9 +115,11 @@ exports.renameAccount = renameAccount;
|
|
|
112
115
|
// we consider all the following fields, when falsy, as equivalent to be missing. If this changes in the future, please adjust addAccount and setAccount
|
|
113
116
|
const assignableProps = ['redirect', 'ignore_limits', 'belongs', 'admin'];
|
|
114
117
|
function addAccount(username, props) {
|
|
115
|
-
|
|
118
|
+
username = normalizeUsername(username);
|
|
119
|
+
if (!username || getAccount(username, false))
|
|
116
120
|
return;
|
|
117
|
-
const
|
|
121
|
+
const filteredProps = lodash_1.default.pickBy(lodash_1.default.pick(props, assignableProps), Boolean);
|
|
122
|
+
const copy = (0, misc_1.setHidden)(filteredProps, { username }); // have the field in the object but hidden so that stringification won't include it
|
|
118
123
|
accountsConfig.set(accounts => Object.assign(accounts, { [username]: copy }));
|
|
119
124
|
saveAccountsAsap().then();
|
|
120
125
|
return copy;
|
|
@@ -132,13 +137,13 @@ function setAccount(username, changes) {
|
|
|
132
137
|
if (changes.username)
|
|
133
138
|
renameAccount(username, changes.username);
|
|
134
139
|
saveAccountsAsap().then();
|
|
135
|
-
return
|
|
140
|
+
return acc;
|
|
136
141
|
}
|
|
137
142
|
exports.setAccount = setAccount;
|
|
138
143
|
function delAccount(username) {
|
|
139
144
|
if (!getAccount(username))
|
|
140
145
|
return false;
|
|
141
|
-
accountsConfig.set(accounts => Object.assign(accounts, { [username]: undefined }));
|
|
146
|
+
accountsConfig.set(accounts => Object.assign(accounts, { [normalizeUsername(username)]: undefined }));
|
|
142
147
|
saveAccountsAsap().then();
|
|
143
148
|
return true;
|
|
144
149
|
}
|
package/src/serveFile.js
CHANGED
|
@@ -89,15 +89,16 @@ function getRange(ctx, totalSize) {
|
|
|
89
89
|
return ctx.throw(400, 'bad range');
|
|
90
90
|
const max = totalSize - 1;
|
|
91
91
|
const start = bytes[0] ? Number(bytes[0]) : Math.max(0, totalSize - Number(bytes[1])); // a negative start is relative to the end
|
|
92
|
-
const end = bytes[0] ? Number(bytes[1] || max) : max;
|
|
93
|
-
|
|
92
|
+
const end = bytes[0] ? Number(bytes[1] || max) : max;
|
|
93
|
+
// we don't support last-bytes without knowing max
|
|
94
|
+
if (isNaN(end) && isNaN(max) || end > max || start > max) {
|
|
94
95
|
ctx.status = 416;
|
|
95
96
|
ctx.set('Content-Range', `bytes ${totalSize}`);
|
|
96
97
|
ctx.body = 'Requested Range Not Satisfiable';
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
100
|
ctx.status = 206;
|
|
100
|
-
ctx.set('Content-Range', `bytes ${start}-${end}/${isNaN(totalSize) ? '*' : totalSize}`);
|
|
101
|
+
ctx.set('Content-Range', `bytes ${start}-${isNaN(end) ? '' : end}/${isNaN(totalSize) ? '*' : totalSize}`);
|
|
101
102
|
ctx.response.length = end - start + 1;
|
|
102
103
|
return { start, end };
|
|
103
104
|
}
|
package/src/vfs.js
CHANGED
|
@@ -38,11 +38,14 @@ function inheritFromParent(parent, child) {
|
|
|
38
38
|
}
|
|
39
39
|
async function urlToNode(url, ctx, parent = exports.vfs) {
|
|
40
40
|
var _a;
|
|
41
|
-
let
|
|
42
|
-
|
|
41
|
+
let initialSlashes = 0;
|
|
42
|
+
while (url[initialSlashes] === '/')
|
|
43
|
+
initialSlashes++;
|
|
44
|
+
let nextSlash = url.indexOf('/', initialSlashes);
|
|
45
|
+
const name = decodeURIComponent(url.slice(initialSlashes, nextSlash < 0 ? undefined : nextSlash));
|
|
43
46
|
if (!name)
|
|
44
47
|
return parent;
|
|
45
|
-
const rest =
|
|
48
|
+
const rest = nextSlash < 0 ? '' : url.slice(nextSlash + 1, url.endsWith('/') ? -1 : undefined);
|
|
46
49
|
if ((0, misc_1.dirTraversal)(name) || /[\/]/.test(name)) {
|
|
47
50
|
if (ctx)
|
|
48
51
|
ctx.status = 418;
|
package/src/zip.js
CHANGED
|
@@ -57,8 +57,11 @@ async function zipStreamFromFolder(node, ctx) {
|
|
|
57
57
|
});
|
|
58
58
|
const zip = new QuickZipStream_1.QuickZipStream(mappedWalker);
|
|
59
59
|
const time = 1000 * zipSeconds.get();
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const size = await zip.calculateSize(time);
|
|
61
|
+
ctx.response.length = size;
|
|
62
|
+
const range = (0, serveFile_1.getRange)(ctx, size); // keep var size as ctx.response.length won't preserve a NaN
|
|
63
|
+
if (ctx.status >= 400)
|
|
64
|
+
return;
|
|
62
65
|
if (range)
|
|
63
66
|
zip.applyRange(range.start, range.end);
|
|
64
67
|
ctx.body = zip;
|