hfs 3.3.0 → 3.3.2
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/README.md +4 -2
- package/central.json +3 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/api.auth.js +4 -4
- package/src/api.vfs.js +8 -4
- package/src/auth.js +2 -2
- package/src/config.js +1 -1
- package/src/const.js +1 -1
- package/src/middlewares.js +6 -2
- package/src/update.js +11 -2
- package/src/updateChangelog.js +31 -0
- package/src/upload.js +5 -2
- package/src/util-os.js +12 -4
package/README.md
CHANGED
|
@@ -85,8 +85,10 @@ Troubleshooting
|
|
|
85
85
|
you can [edit the config file to add your admin account](config.md#accounts)
|
|
86
86
|
- If you don't want to use an editor, you can create the file with this command:
|
|
87
87
|
`echo "create-admin: PASSWORD" > config.yaml`
|
|
88
|
-
- For a new Docker installation, set `
|
|
89
|
-
|
|
88
|
+
- For a new Docker installation, set `HFS_INITIAL_ADMIN_PASSWORD` to **only the password**, and log in as `admin`.
|
|
89
|
+
It is used only when creating `config.yaml`; afterwards, manage the password in HFS and keep the config volume persistent.
|
|
90
|
+
The older `HFS_CREATE_ADMIN` takes precedence when non-empty and, by default, reapplies its password at every container start.
|
|
91
|
+
See [Docker setup](https://github.com/rejetto/hfs/wiki/Docker).
|
|
90
92
|
|
|
91
93
|
By default, HFS does not require a login when you access the *Admin-panel* from localhost.
|
|
92
94
|
If you don't like this behavior, disable it in the Admin-panel or enter this console command `config localhost_admin false`.
|
package/central.json
CHANGED
|
@@ -52,6 +52,9 @@
|
|
|
52
52
|
"byVersion": {
|
|
53
53
|
"^(0\\..*|3\\.[01]\\..*|3\\.2\\.0.*)$": {
|
|
54
54
|
"alert": "This version is dangerous – please update"
|
|
55
|
+
},
|
|
56
|
+
"^3\\.2\\.[1-9]": {
|
|
57
|
+
"alert": "Please update. Your installation may be at risk if:\n - shared folders contain hidden subfolders: their contents can be downloaded if the folder name is known;\n - untrusted users can download folders as ZIP: they can make HFS unavailable;\n - you use WebDAV, LDAP, or `?login=` links: passwords can be exposed during login;\n - multiple users manage files in the same folders: one user can alter another's files"
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hfs",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "hfs",
|
|
9
|
-
"version": "3.3.
|
|
9
|
+
"version": "3.3.2",
|
|
10
10
|
"license": "GPL-3.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@gregoranders/csv": "^0.0.13",
|
package/package.json
CHANGED
package/src/api.auth.js
CHANGED
|
@@ -46,11 +46,11 @@ exports.authApis = {
|
|
|
46
46
|
const account = await (0, auth_1.clearTextLogin)(ctx, username, password, 'api');
|
|
47
47
|
if (!account)
|
|
48
48
|
return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
|
|
49
|
-
await (0, auth_1.setLoggedIn)(ctx, account.username);
|
|
49
|
+
await (0, auth_1.setLoggedIn)(ctx, account.username, ctx.state.params ? 'body' : 'url');
|
|
50
50
|
}
|
|
51
51
|
catch (e) {
|
|
52
52
|
events_1.default.emit('failedLogin', { ctx, username, via: 'api' });
|
|
53
|
-
return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED,
|
|
53
|
+
return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, e);
|
|
54
54
|
}
|
|
55
55
|
return {
|
|
56
56
|
redirect: ctx.state.account?.redirect,
|
|
@@ -117,7 +117,7 @@ exports.authApis = {
|
|
|
117
117
|
try {
|
|
118
118
|
const M2 = await step1.step2(BigInt(pubKey), BigInt(proof))
|
|
119
119
|
.catch(() => { throw ''; }); // falsy value for later
|
|
120
|
-
await (0, auth_1.setLoggedIn)(ctx, username);
|
|
120
|
+
await (0, auth_1.setLoggedIn)(ctx, username, 'srp');
|
|
121
121
|
return {
|
|
122
122
|
proof: String(M2),
|
|
123
123
|
redirect: ctx.state.account?.redirect,
|
|
@@ -128,7 +128,7 @@ exports.authApis = {
|
|
|
128
128
|
ctx.logExtra({ u: username });
|
|
129
129
|
ctx.state.dontLog = false; // log even if log_api is false
|
|
130
130
|
events_1.default.emit('failedLogin', { ctx, username });
|
|
131
|
-
return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, e
|
|
131
|
+
return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, e);
|
|
132
132
|
}
|
|
133
133
|
finally {
|
|
134
134
|
delete ongoingLogins[sid];
|
package/src/api.vfs.js
CHANGED
|
@@ -168,8 +168,12 @@ exports.default = {
|
|
|
168
168
|
async resolve_path({ path, closestFolder }) {
|
|
169
169
|
path = (0, path_1.resolve)(path);
|
|
170
170
|
if (closestFolder)
|
|
171
|
-
while (path && !await (0, misc_1.isDirectory)(path))
|
|
172
|
-
|
|
171
|
+
while (path && !await (0, misc_1.isDirectory)(path)) {
|
|
172
|
+
const parent = (0, path_1.dirname)(path);
|
|
173
|
+
if (parent === path)
|
|
174
|
+
break; // an unavailable root has no other parent to try
|
|
175
|
+
path = parent;
|
|
176
|
+
}
|
|
173
177
|
return { path, isFolder: await (0, misc_1.isDirectory)(path) };
|
|
174
178
|
},
|
|
175
179
|
async mkdir({ path }) {
|
|
@@ -191,7 +195,7 @@ exports.default = {
|
|
|
191
195
|
}
|
|
192
196
|
return list.close();
|
|
193
197
|
}
|
|
194
|
-
const sendPropsAsap = (0, util_os_1.getDiskSpace)(path).then(x => x && list.props(x));
|
|
198
|
+
const sendPropsAsap = (0, util_os_1.getDiskSpace)(path).then(x => x && list.props(x)).catch(() => { }); // listing can fail before we await the disk-space query
|
|
195
199
|
try {
|
|
196
200
|
const matching = (0, misc_1.makeMatcher)(fileMask);
|
|
197
201
|
path = (0, misc_1.isWindowsDrive)(path) ? path + '\\' : (0, path_1.resolve)(path || '/');
|
|
@@ -215,7 +219,7 @@ exports.default = {
|
|
|
215
219
|
}
|
|
216
220
|
catch { } // just ignore entries we can't stat
|
|
217
221
|
});
|
|
218
|
-
await sendPropsAsap
|
|
222
|
+
await sendPropsAsap;
|
|
219
223
|
list.close();
|
|
220
224
|
}
|
|
221
225
|
catch (e) {
|
package/src/auth.js
CHANGED
|
@@ -90,7 +90,7 @@ async function clearTextLogin(ctx, u, p, via) {
|
|
|
90
90
|
return a;
|
|
91
91
|
}
|
|
92
92
|
// centralized log-in state
|
|
93
|
-
async function setLoggedIn(ctx, username) {
|
|
93
|
+
async function setLoggedIn(ctx, username, via) {
|
|
94
94
|
const s = ctx.session;
|
|
95
95
|
if (!s)
|
|
96
96
|
return ctx.throw(cross_const_1.HTTP_SERVER_ERROR, 'session');
|
|
@@ -106,7 +106,7 @@ async function setLoggedIn(ctx, username) {
|
|
|
106
106
|
const a = ctx.state.account = (0, perm_1.getAccount)(username);
|
|
107
107
|
if (!a)
|
|
108
108
|
return;
|
|
109
|
-
const result = await events_1.default.emitAsync('finalizingLogin', { ctx, username: a.username, inputs: { ...ctx.state.params, ...ctx.query } });
|
|
109
|
+
const result = await events_1.default.emitAsync('finalizingLogin', { ctx, username: a.username, via, inputs: { ...ctx.state.params, ...ctx.query } });
|
|
110
110
|
const error = result?.find(x => x && lodash_1.default.isString(x)) || result?.isDefaultPrevented() && "Login denied";
|
|
111
111
|
if (error) {
|
|
112
112
|
// restore the session identity: the candidate account was exposed only for plugin checks
|
package/src/config.js
CHANGED
|
@@ -139,7 +139,7 @@ async function setConfig(newCfg, save) {
|
|
|
139
139
|
const version = lodash_1.default.isString(newCfg.version) ? new Version(newCfg.version) : undefined;
|
|
140
140
|
const considerEnvs = !process.env['HFS_ENV_BOOTSTRAP'] || !started && lodash_1.default.isEmpty(newCfg);
|
|
141
141
|
// first time we consider also CLI args
|
|
142
|
-
const argCfg = !started && lodash_1.default.pickBy((0, cross_1.newObj)(configProps, (_x, k) => (0, cross_1.tryJson)(k in argv_1.argv ? argv_1.argv[k] : considerEnvs ? process.env['HFS_' + k.toUpperCase().replaceAll('-', '_')] :
|
|
142
|
+
const argCfg = !started && lodash_1.default.pickBy((0, cross_1.newObj)(configProps, (_x, k) => (0, cross_1.tryJson)(k in argv_1.argv ? argv_1.argv[k] : considerEnvs ? process.env['HFS_' + k.toUpperCase().replaceAll('-', '_')] : undefined, lodash_1.default.identity)), x => x !== undefined);
|
|
143
143
|
if (!lodash_1.default.isEmpty(argCfg)) {
|
|
144
144
|
(0, exports.saveConfigAsap)(); // don't set `save` argument, as it would interfere below, at check `save===false`
|
|
145
145
|
Object.assign(newCfg, argCfg);
|
package/src/const.js
CHANGED
|
@@ -49,7 +49,7 @@ const path_1 = require("path");
|
|
|
49
49
|
const cross_1 = require("./cross");
|
|
50
50
|
const argv_1 = require("./argv");
|
|
51
51
|
__exportStar(require("./cross-const"), exports);
|
|
52
|
-
exports.API_VERSION = 13.
|
|
52
|
+
exports.API_VERSION = 13.5;
|
|
53
53
|
exports.COMPATIBLE_API_VERSION = 1; // the day we break with the past, we'll update this
|
|
54
54
|
// you can add arguments with this file, currently used for the update process on mac/linux.
|
|
55
55
|
// we are using homedir because it's the only stable path both the old and new process can agree on (open() doesn't preserve cwd)
|
package/src/middlewares.js
CHANGED
|
@@ -114,6 +114,7 @@ const prepareState = async (ctx, next) => {
|
|
|
114
114
|
// calculate these once and for all
|
|
115
115
|
ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
|
|
116
116
|
// explicit credentials and existing sessions must take precedence, so a matching IP cannot override a chosen account
|
|
117
|
+
let via;
|
|
117
118
|
let a = await urlLogin() || await getHttpAccount() || !s?.username && autoLogin();
|
|
118
119
|
const loggedInNotBySession = a;
|
|
119
120
|
ctx.state.account = a ||= (0, perm_1.getAccount)(s?.username, false); // with least precedence, we consider session
|
|
@@ -123,12 +124,12 @@ const prepareState = async (ctx, next) => {
|
|
|
123
124
|
else if (loggedInNotBySession) {
|
|
124
125
|
try {
|
|
125
126
|
if (a.username)
|
|
126
|
-
await (0, auth_1.setLoggedIn)(ctx, a.username);
|
|
127
|
+
await (0, auth_1.setLoggedIn)(ctx, a.username, via);
|
|
127
128
|
}
|
|
128
129
|
catch (e) {
|
|
129
130
|
events_1.default.emit('failedLogin', { ctx, username: a.username, via: ctx.query.login ? 'url' : ctx.get('authorization') ? 'header' : undefined });
|
|
130
131
|
ctx.status = const_1.HTTP_UNAUTHORIZED;
|
|
131
|
-
ctx.body = String(e);
|
|
132
|
+
ctx.body = e instanceof Error ? e.message : String(e);
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
134
135
|
ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
|
|
@@ -137,6 +138,7 @@ const prepareState = async (ctx, next) => {
|
|
|
137
138
|
(0, connections_1.updateConnectionForCtx)(ctx);
|
|
138
139
|
await next();
|
|
139
140
|
function urlLogin() {
|
|
141
|
+
via = 'url';
|
|
140
142
|
const { login } = ctx.query;
|
|
141
143
|
if (!login)
|
|
142
144
|
return;
|
|
@@ -145,6 +147,7 @@ const prepareState = async (ctx, next) => {
|
|
|
145
147
|
return u && (0, auth_1.clearTextLogin)(ctx, u, p, 'url');
|
|
146
148
|
}
|
|
147
149
|
function getHttpAccount() {
|
|
150
|
+
via = 'header';
|
|
148
151
|
const b64 = allowAuthorizationHeader.get() && ctx.get('authorization')?.split(' ')[1];
|
|
149
152
|
if (!b64)
|
|
150
153
|
return;
|
|
@@ -157,6 +160,7 @@ const prepareState = async (ctx, next) => {
|
|
|
157
160
|
catch { }
|
|
158
161
|
}
|
|
159
162
|
function autoLogin() {
|
|
163
|
+
via = 'net';
|
|
160
164
|
// keep the mask direct so group inheritance cannot make identity depend on account order
|
|
161
165
|
return Object.values(perm_1.accounts.get()).find(a => (0, perm_1.accountCanLogin)(a) && a.auto_login_net && (0, misc_1.netMatches)(ctx.ip, a.auto_login_net, true));
|
|
162
166
|
}
|
package/src/update.js
CHANGED
|
@@ -28,6 +28,7 @@ const persistence_1 = require("./persistence");
|
|
|
28
28
|
const lodash_1 = __importDefault(require("lodash"));
|
|
29
29
|
const argv_1 = require("./argv");
|
|
30
30
|
const promises_2 = require("stream/promises");
|
|
31
|
+
const updateChangelog_1 = require("./updateChangelog");
|
|
31
32
|
const updateToBeta = (0, config_1.defineConfig)(misc_1.CFG.update_to_beta, false);
|
|
32
33
|
const autoCheckUpdate = (0, config_1.defineConfig)(misc_1.CFG.auto_check_update, true);
|
|
33
34
|
const lastCheckUpdate = persistence_1.storedMap.singleSync('lastCheckUpdate', 0);
|
|
@@ -69,14 +70,17 @@ function prepareRelease(r) {
|
|
|
69
70
|
assets: r.assets.map((a) => lodash_1.default.pick(a, ReleaseAssetKeys))
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
|
-
async function getVersions(filter, max = 30) {
|
|
73
|
+
async function getVersions(filter, max = 30, stopAtCurrent = false) {
|
|
73
74
|
const ret = [];
|
|
74
75
|
for await (const x of (0, github_1.apiGithubPaginated)(`repos/${const_1.HFS_REPO}/releases`)) {
|
|
75
76
|
if (x.name.endsWith('-ignore'))
|
|
76
77
|
continue;
|
|
77
78
|
const rel = prepareRelease(x);
|
|
78
|
-
if (rel.versionScalar === curV)
|
|
79
|
+
if (rel.versionScalar === curV) {
|
|
80
|
+
if (stopAtCurrent)
|
|
81
|
+
break; // stop at the installed release, not at older maintenance releases published more recently
|
|
79
82
|
continue;
|
|
83
|
+
}
|
|
80
84
|
if (!filter || filter(rel))
|
|
81
85
|
ret.push(rel);
|
|
82
86
|
if (ret.length >= max)
|
|
@@ -93,6 +97,11 @@ async function getUpdates(strict = false) {
|
|
|
93
97
|
const betas = !includeBetas ? [] : await getVersions(x => x.prerelease && x.versionScalar > stable.versionScalar && (!strict || x.isNewer));
|
|
94
98
|
if (stable.isNewer || const_1.RUNNING_BETA && !strict)
|
|
95
99
|
betas.push(stable);
|
|
100
|
+
if (stable.isNewer && stable.body) {
|
|
101
|
+
const history = await getVersions(r => !r.prerelease && r.isNewer && r.versionScalar <= stable.versionScalar, Infinity, true)
|
|
102
|
+
.catch(() => []); // missing history must not prevent installing an available update
|
|
103
|
+
stable.body = (0, updateChangelog_1.updateChangelog)(stable, history, curV);
|
|
104
|
+
}
|
|
96
105
|
return betas;
|
|
97
106
|
}
|
|
98
107
|
const LOCAL_UPDATE = 'hfs-update.zip'; // update from file takes precedence over net
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.updateChangelog = updateChangelog;
|
|
7
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
8
|
+
function updateChangelog(destination, releases, currentVersion) {
|
|
9
|
+
const newer = lodash_1.default.sortBy(lodash_1.default.uniqBy([destination, ...releases], 'tag_name')
|
|
10
|
+
.filter(r => !r.prerelease && r.versionScalar > currentVersion && r.versionScalar <= destination.versionScalar), r => -r.versionScalar);
|
|
11
|
+
const level = Math.max(...newer.map(releaseLevel));
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const sections = newer.filter(r => releaseLevel(r) === level).map(r => {
|
|
14
|
+
let body = r.body.trim();
|
|
15
|
+
if (level === 0) {
|
|
16
|
+
// deduplicate complete entries, preserving continuation lines and nested lists
|
|
17
|
+
body = body.replace(/\r\n/g, '\n').split(/\n{2,}|\n(?=[*+-] )/).filter(entry => {
|
|
18
|
+
const key = entry.trim().replace(/^[*+-] /, '');
|
|
19
|
+
if (!key || seen.has(key))
|
|
20
|
+
return false;
|
|
21
|
+
seen.add(key);
|
|
22
|
+
return true;
|
|
23
|
+
}).join('\n\n');
|
|
24
|
+
}
|
|
25
|
+
return { tag: r.tag_name, body };
|
|
26
|
+
}).filter(r => r.body);
|
|
27
|
+
return sections.map(r => r.tag === destination.tag_name ? r.body : `**${r.tag}**\n\n${r.body}`).join('\n\n') || destination.body;
|
|
28
|
+
function releaseLevel(r) {
|
|
29
|
+
return r.versionScalar % 1E6 === 0 ? 2 : r.versionScalar % 1E3 === 0 ? 1 : 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/upload.js
CHANGED
|
@@ -125,11 +125,14 @@ function uploadWriter(base, baseUri, filename, ctx) {
|
|
|
125
125
|
if (firstCreated) {
|
|
126
126
|
setUploadMeta(dir, ctx);
|
|
127
127
|
// recursive mkdir returns the first created directory; existing ancestors must not receive ownership
|
|
128
|
-
const
|
|
128
|
+
const firstCreatedPath = (0, path_1.toNamespacedPath)((0, path_1.resolve)(firstCreated)); // Node 24 mkdir can return a namespace-prefixed Windows path
|
|
129
129
|
let source = (0, path_1.resolve)(dir);
|
|
130
130
|
let uri = path_1.posix.dirname(vfsUri);
|
|
131
|
-
while (
|
|
131
|
+
while (true) {
|
|
132
132
|
createdFolders.push({ uri, source });
|
|
133
|
+
// stop before the existing parent: namespaced drive and UNC roots can differ in their trailing separator
|
|
134
|
+
if ((0, path_1.toNamespacedPath)(source) === firstCreatedPath)
|
|
135
|
+
break;
|
|
133
136
|
source = (0, path_1.dirname)(source);
|
|
134
137
|
uri = path_1.posix.dirname(uri);
|
|
135
138
|
}
|
package/src/util-os.js
CHANGED
|
@@ -23,8 +23,12 @@ const const_1 = require("./const");
|
|
|
23
23
|
const promises_1 = require("node:fs/promises");
|
|
24
24
|
const DF_TIMEOUT = 2000;
|
|
25
25
|
function getDiskSpaceSync(path) {
|
|
26
|
-
while (path && !(0, misc_1.isWindowsDrive)(path) && !(0, fs_1.existsSync)(path))
|
|
27
|
-
|
|
26
|
+
while (path && !(0, misc_1.isWindowsDrive)(path) && !(0, fs_1.existsSync)(path)) {
|
|
27
|
+
const parent = (0, path_1.dirname)(path);
|
|
28
|
+
if (parent === path)
|
|
29
|
+
break; // an unavailable root has no other parent to try
|
|
30
|
+
path = parent;
|
|
31
|
+
}
|
|
28
32
|
const res = (0, fs_1.statfsSync)(path);
|
|
29
33
|
return { free: res.bavail * res.bsize, total: res.blocks * res.bsize, name: path };
|
|
30
34
|
}
|
|
@@ -35,8 +39,12 @@ function cmdEscape(par) {
|
|
|
35
39
|
return `"${par.replaceAll('"', '\\"')}"`;
|
|
36
40
|
}
|
|
37
41
|
async function getDiskSpace(path) {
|
|
38
|
-
while (path && !(0, misc_1.isWindowsDrive)(path) && !await (0, misc_1.exists)(path))
|
|
39
|
-
|
|
42
|
+
while (path && !(0, misc_1.isWindowsDrive)(path) && !await (0, misc_1.exists)(path)) {
|
|
43
|
+
const parent = (0, path_1.dirname)(path);
|
|
44
|
+
if (parent === path)
|
|
45
|
+
break; // an unavailable root has no other parent to try; this test covers UNC as well
|
|
46
|
+
path = parent;
|
|
47
|
+
}
|
|
40
48
|
const res = await (0, promises_1.statfs)(path);
|
|
41
49
|
return { free: res.bavail * res.bsize, total: res.blocks * res.bsize, name: path };
|
|
42
50
|
}
|