hfs 3.0.2 → 3.0.3

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 ADDED
@@ -0,0 +1,194 @@
1
+ # HFS: HTTP File Server
2
+
3
+ ![logo and motto](hfs-logo-color-motto.svg)
4
+
5
+ ## Index
6
+
7
+ - [Introduction](#introduction)
8
+ - [How does it work](#how-does-it-work)
9
+ - [Features](#features)
10
+ - [Installation](#installation)
11
+ - [Other systems](#other-systems)
12
+ - [Configuration](#configuration)
13
+ - [Where is it stored](#where-is-it-stored)
14
+ - [Internationalization](#internationalization)
15
+ - [Hidden features](#hidden-features)
16
+ - [Contribute](#contribute)
17
+ - [More](#more)
18
+
19
+ ## Introduction
20
+
21
+ Access your files via web, directly from your disk.
22
+
23
+ - Be your own server: share files **fresh from your disk** with **unlimited** space and bandwidth.
24
+ - **Fast:** try zipping 100GB – download starts immediately.
25
+ - **Smart:** HFS detects problems and suggests solutions.
26
+ - Present things the way you want: share **even a single file**, even with a different name, with our *virtual file system*.
27
+ - **Monitor** all activities in real-time.
28
+ - **Bandwidth throttling**: decide how much to give.
29
+ - **Direct transfers**: share large files with friends without having to upload them first.
30
+ - **Expandable**: install plugins for additional features.
31
+
32
+ Runs on: Windows, Linux, macOS, FreeBSD, Android
33
+
34
+ ## How does it work
35
+
36
+ - run HFS on your computer; an administration webpage automatically shows up
37
+ - select which files and folders you want to be accessible
38
+ - access those files from a phone or another computer just using a browser
39
+ - possibly create accounts and limit access to files
40
+
41
+ ## Features
42
+
43
+ - https
44
+ - easy certificate generation
45
+ - virtual file system
46
+ - mobile friendly
47
+ - search
48
+ - accounts
49
+ - resumable downloads & uploads
50
+ - download folders as zip archive
51
+ - delete, move and rename files
52
+ - plug-ins (anti-brute-force, thumbnails, ldap, themes, and more)
53
+ - simple website serving
54
+ - real-time monitoring of connections
55
+ - [show some files](https://github.com/rejetto/hfs/discussions/270)
56
+ - speed throttler
57
+ - geographic firewall
58
+ - admin web interface
59
+ - multi-language front-end
60
+ - virtual hosting
61
+ - [reverse-proxy support](https://github.com/rejetto/hfs/wiki/Reverse-proxy)
62
+ - comments in file descript.ion
63
+ - integrated media player
64
+ - [customizable with html, css, and javascript](https://github.com/rejetto/hfs/wiki/Customization)
65
+ - dynamic-dns updater
66
+
67
+ ## Installation
68
+
69
+ For Docker, see https://github.com/rejetto/hfs/wiki/Docker .
70
+
71
+ For service installation, see https://github.com/rejetto/hfs/wiki/Service-installation.
72
+
73
+ The minimum Windows version required is 10 or Server 2019.
74
+
75
+ 1. Download the zip file for your operating system from https://github.com/rejetto/hfs/releases
76
+ - ⚠️ Antivirus problems on Windows? [READ THIS](https://github.com/rejetto/hfs/wiki/Antivirus)
77
+ - ⚠️ If you have Linux ARM or other unlisted/unsupported platforms, please see the [Other systems](#other-systems) section.
78
+ 2. Unzip and launch the `hfs` file.
79
+ - ⚠️ Mac: if you get *"cannot be opened because it is from an unidentified developer"*,
80
+ you can hold `control` key while clicking, then click `open`.
81
+ 3. The browser should automatically open at `localhost`, so you can configure the rest in the Admin-panel.
82
+
83
+ Troubleshooting
84
+ - If a browser cannot be opened on the computer where you are installing HFS,
85
+ you should enter this command in the HFS console: `create-admin <PASSWORD>`
86
+ - If you cannot access the console (like when you are running as a service),
87
+ you can [edit the config file to add your admin account](config.md#accounts)
88
+ - If you don't want to use an editor, you can create the file with this command:
89
+
90
+ `echo "create-admin: PASSWORD" > config.yaml`
91
+
92
+ By default, HFS does not require a login when you access the *Admin-panel* from localhost.
93
+ If you don't like this behavior, disable it in the Admin-panel or enter this console command `config localhost_admin false`.
94
+
95
+ To uninstall, remove the files you unzipped and the configuration/data directory (see `config.md` for the location).
96
+
97
+ ### Other systems
98
+
99
+ If you can't or don't want to run our binary versions, you can try this:
100
+
101
+ 1. [install node.js](https://nodejs.org) version 20 (or greater, but then compatibility is not guaranteed)
102
+ 2. run at the command line `npx hfs@latest`
103
+
104
+ The `@latest` part is optional, and ensures that you are always up to date.
105
+
106
+ If this procedure fails, it may be that you are missing one of [these requirements](https://github.com/nodejs/node-gyp#installation).
107
+
108
+ Configuration and other files will be stored in `%HOME%/.vfs`
109
+
110
+ ## Configuration
111
+
112
+ For configuration please see [config.md](config.md); it explains also where all configurations are stored.
113
+
114
+ ## Internationalization
115
+
116
+ It is possible to show the Front-end in other languages.
117
+ Translation for some languages is already provided. If you find an error, consider reporting it
118
+ or [editing the source file](https://github.com/rejetto/hfs/tree/main/src/langs).
119
+
120
+ In the Languages section of the Admin-panel you can install additional language files.
121
+
122
+ If your language is missing, please consider [translating yourself](https://github.com/rejetto/hfs/wiki/Translation).
123
+
124
+ ## Hidden features
125
+
126
+ - Append `#LOGIN` to the URL to open the login dialog
127
+ - Append `?lang=CODE` to the URL to force a specific language
128
+ - `Right-click` on toggle-all checkbox to *invert* the state of all checkboxes
129
+ - Append `?login=USER:PASSWORD` to automatically log in to the browser
130
+ - Append `?overwrite` when uploading to override the `dont_overwrite_uploading` configuration, provided you also have *delete* permission
131
+ - Append `?search=PATTERN` to trigger a search on startup
132
+ - Append `?onlyFiles` or `?onlyFolders` to limit the type of results
133
+ - Append `?get=basic` to display a basic web interface, intended for older/simpler browsers
134
+ - This is automatic if a basic browser is detected.
135
+ - Append `?autoplay=shuffle` to trigger show & play; `?autoplay` will not shuffle, but also will not start until the list is complete
136
+ - `Right-click` on "check for updates" to enter a URL of a version to install
137
+ - `Shift+click` on a file to *show* and play
138
+ - `Ctrl+backspace` to navigate to the parent folder
139
+ - Start typing a filename to focus it in the list
140
+ - `--consoleFile PATH` to also output all stdout/stderr to a file
141
+ - Set env.var. `DISABLE_UPDATE=1` (for containers)
142
+ - Launch with `--debug` or env.var. `HFS_DEBUG=1` to generate additional console logs
143
+ - Launch with `--no-central` to skip fetching updated info from GitHub (uses built-in data only)
144
+
145
+ ## Contribute
146
+
147
+ There are several ways to contribute
148
+
149
+ - [Report bugs](https://github.com/rejetto/hfs/issues/new?labels=bug&template=bug_report.md)
150
+
151
+ It's very important to report bugs, and if you are not so sure about it, don't worry, we'll discuss it.
152
+ If you find important security problems, please [contact us privately](mailto:a@rejetto.com) so that we can publish a fix before
153
+ the problem is disclosed, for the safety of other users.
154
+
155
+ - Use beta versions, and give feedback.
156
+
157
+ While betas have more problems, you'll get more features and give a huge help to the project.
158
+
159
+ - [Translate to your language](https://github.com/rejetto/hfs/wiki/Translation).
160
+
161
+ - [Suggest ideas](https://github.com/rejetto/hfs/discussions)
162
+
163
+ While the project should not become too complex, yours may be an idea for a plugin.
164
+
165
+ - Write guides or make videos for other users. [We got a wiki](https://github.com/rejetto/hfs/wiki)!
166
+
167
+ - Submit your code
168
+
169
+ If you'd like to make a change yourself in the code, please first open an "issue" or "discussion" about it,
170
+ so we'll try to cooperate and understand what's the best path for it.
171
+
172
+ - [Make a plugin](dev-plugins.md)
173
+
174
+ A plugin can change the look (a theme), and/or introduce a new functionality.
175
+
176
+ ## Code Signing Policy
177
+
178
+ Free code signing 🙏 provided by SignPath.io, certificate by [SignPath Foundation](https://signpath.org).
179
+
180
+ Author/Reviewer/Approver: Massimo Melina.
181
+
182
+ Privacy: Update checks are opt-out; other outbound connections are user-triggered.
183
+
184
+ ## More
185
+
186
+ - [Additional information (Wiki)](https://github.com/rejetto/hfs/wiki)
187
+
188
+ - [APIs](https://github.com/rejetto/hfs/wiki/APIs)
189
+
190
+ - [Build yourself](dev.md)
191
+
192
+ - [License](LICENSE.txt)
193
+
194
+ - Flag images are public-domain, downloaded from https://flagpedia.net
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": ["file server", "http server"],
6
6
  "homepage": "https://rejetto.com/hfs",
@@ -33,7 +33,7 @@
33
33
  "dist-bin-mac-arm": "cd dist && pkg . --public -C gzip -t node20-macos-arm64 && zip hfs-mac-arm64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
34
34
  "dist-bin-mac": "cd dist && pkg . --public -C gzip -t node20-macos-x64 && zip hfs-mac-x64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
35
35
  "dist-bin-linux": "cd dist && pkg . --public -C gzip -t node20-linux-x64 && zip hfs-linux-x64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
36
- "dist-bin-linux-arm": "cd dist && pkg . --public -C gzip -t node20-linux-arm64 && zip hfs-linux-arm64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
36
+ "dist-bin-linux-arm": "cd dist && pkg . --public -C gzip -t node20-linux-arm64 ${GITHUB_ACTIONS:+--public-packages \"*\"} && zip hfs-linux-arm64-$(jq -r .version ../package.json).zip hfs -r plugins && cd ..",
37
37
  "dist-win": "npm run dist-modules && npm run dist-bin-win",
38
38
  "dist-mac-arm": "npm run dist-modules && npm run dist-bin-mac-arm",
39
39
  "dist-mac": "npm run dist-modules && npm run dist-bin-mac",
@@ -64,7 +64,10 @@
64
64
  "admin/**/*",
65
65
  "frontend/**/*",
66
66
  "**/node_modules/fswin/x64/*",
67
- "**/node_modules/axios/dist/node/*"
67
+ "**/node_modules/axios/dist/node/*",
68
+ "**/node_modules/buffers/**",
69
+ "**/node_modules/binary/**",
70
+ "**/node_modules/unzip-stream/**"
68
71
  ],
69
72
  "targets": [
70
73
  "node20-win-x64",
@@ -119,7 +122,7 @@
119
122
  "@types/node": "^20.17.30",
120
123
  "@types/node-forge": "^1.3.14",
121
124
  "@types/picomatch": "^4.0.2",
122
- "@yao-pkg/pkg": "^6.11.0",
125
+ "@yao-pkg/pkg": "6.13.1",
123
126
  "cross-env": "^10.0.0",
124
127
  "koa-better-http-proxy": "^0.2.10",
125
128
  "nm-prune": "^5.0.0",
package/src/api.auth.js CHANGED
@@ -24,6 +24,7 @@ const login = async ({ username, password }, ctx) => {
24
24
  const account = await (0, auth_1.clearTextLogin)(ctx, username, password, 'api');
25
25
  if (!account)
26
26
  return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED);
27
+ await (0, auth_1.setLoggedIn)(ctx, account.username);
27
28
  }
28
29
  catch (e) {
29
30
  return new apiMiddleware_1.ApiError(const_1.HTTP_UNAUTHORIZED, String(e));
package/src/auth.js CHANGED
@@ -51,11 +51,7 @@ async function clearTextLogin(ctx, u, p, via) {
51
51
  return;
52
52
  const plugins = await events_1.default.emitAsync('clearTextLogin', { ctx, username: u, password: p, via }); // provide clear password to plugins
53
53
  const a = plugins?.some(x => x === true) ? (0, perm_1.getAccount)(u) : await srpCheck(u, p);
54
- if (a) {
55
- await setLoggedIn(ctx, a.username);
56
- ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
57
- }
58
- else if (u)
54
+ if (!a && u)
59
55
  events_1.default.emit('failedLogin', { ctx, username: u, via });
60
56
  return a;
61
57
  }
@@ -64,8 +60,11 @@ async function setLoggedIn(ctx, username) {
64
60
  const s = ctx.session;
65
61
  if (!s)
66
62
  return ctx.throw(cross_const_1.HTTP_SERVER_ERROR, 'session');
63
+ delete ctx.state.usernames;
67
64
  if (username === false) {
68
- events_1.default.emit('logout', ctx);
65
+ if (s.username)
66
+ events_1.default.emit('logout', ctx);
67
+ delete ctx.state.account;
69
68
  delete s.username;
70
69
  delete s.allowNet;
71
70
  return;
package/src/config.js CHANGED
@@ -82,7 +82,7 @@ function defineConfig(k, defaultValue, compiler) {
82
82
  return; // avoid infinite loop in case a subscriber changes the value
83
83
  stack.push(cb);
84
84
  try {
85
- cb(v, { k, was, version, defaultValue, object, onlyCompileChanged });
85
+ return cb(v, { k, was, version, defaultValue, object, onlyCompileChanged });
86
86
  }
87
87
  finally {
88
88
  stack.pop();
@@ -106,7 +106,7 @@ function defineConfig(k, defaultValue, compiler) {
106
106
  if (compiler)
107
107
  object.sub((v, more) => {
108
108
  if (!more.onlyCompileChanged)
109
- compiled = compiler(v, more);
109
+ return compiled = compiler(v, more);
110
110
  });
111
111
  return object;
112
112
  }
package/src/consoleLog.js CHANGED
@@ -10,7 +10,7 @@ const cross_1 = require("./cross");
10
10
  const fs_1 = require("fs");
11
11
  const argv_1 = require("./argv");
12
12
  exports.consoleLog = [];
13
- const f = argv_1.argv.consoleFile ? (0, fs_1.createWriteStream)(argv_1.argv.consoleFile, 'utf-8') : null;
13
+ const f = argv_1.argv.consoleFile ? (0, fs_1.createWriteStream)(argv_1.argv.consoleFile, { flags: 'a', encoding: 'utf8' }) : null;
14
14
  for (const k of ['log', 'warn', 'error', 'debug']) {
15
15
  const original = console[k];
16
16
  console[k] = (...args) => {
package/src/log.js CHANGED
@@ -102,6 +102,7 @@ const logSpam = (0, config_1.defineConfig)(misc_1.CFG.log_spam, false);
102
102
  const debounce = lodash_1.default.debounce(cb => cb(), 1000); // with this technique, i'll be able to debounce some code respecting the references in its closure
103
103
  const logMw = async (ctx, next) => {
104
104
  const now = new Date(); // request start
105
+ const userAtStart = (0, auth_1.getCurrentUsername)(ctx);
105
106
  // do it now so it's available for returning plugins
106
107
  ctx.state.completed = Promise.race([(0, events_1.once)(ctx.res, 'finish'), (0, events_1.once)(ctx.res, 'close')]);
107
108
  await next();
@@ -151,7 +152,7 @@ const logMw = async (ctx, next) => {
151
152
  const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s %s\n'; // Apache's Common Log Format
152
153
  const a = now.toString().split(' '); // like nginx, our default log contains the time of log writing
153
154
  const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' + a[5]?.slice(3);
154
- const user = (0, auth_1.getCurrentUsername)(ctx);
155
+ const user = (0, auth_1.getCurrentUsername)(ctx) || userAtStart;
155
156
  const length = ctx.state.length ?? ctx.length;
156
157
  const uri = ctx.originalUrl;
157
158
  const duration = (Date.now() - Number(now)) / 1000;
@@ -82,21 +82,30 @@ const someSecurity = (ctx, next) => {
82
82
  exports.someSecurity = someSecurity;
83
83
  // limited to http proxies
84
84
  function getProxyDetected() {
85
- if (proxyDetected?.state.whenProxyDetected < Date.now() - misc_1.DAY) // detection is reset after a day
85
+ if (Number(proxyDetected?.state.whenProxyDetected) < Date.now() - misc_1.DAY) // detection is reset after a day
86
86
  proxyDetected = undefined;
87
87
  return proxyDetected && { from: proxyDetected.socket.remoteAddress, for: proxyDetected.get('X-Forwarded-For') };
88
88
  }
89
89
  const prepareState = async (ctx, next) => {
90
- if (ctx.session?.username) {
91
- if (ctx.session.ts < auth_1.invalidateSessionBefore.get(ctx.session.username))
92
- delete ctx.session.username;
93
- ctx.session.maxAge = exports.sessionDuration.compiled();
90
+ const s = ctx.session;
91
+ if (s?.username) {
92
+ if (s.ts < auth_1.invalidateSessionBefore.get(s?.username))
93
+ delete s.username;
94
+ s.maxAge = exports.sessionDuration.compiled();
94
95
  }
95
96
  // calculate these once and for all
96
97
  ctx.state.connection = (0, connections_1.socket2connection)(ctx.socket);
97
- const a = ctx.state.account = await urlLogin() || await getHttpAccount() || (0, perm_1.getAccount)(ctx.session?.username, false);
98
- if (a && (!(0, perm_1.accountCanLogin)(a) || failAllowNet(ctx, a))) // enforce allow_net also after login
99
- ctx.state.account = undefined;
98
+ let a = await urlLogin() || await getHttpAccount();
99
+ const loggedInNotBySession = a;
100
+ ctx.state.account = a ||= (0, perm_1.getAccount)(s?.username, false); // with least precedence, we consider session
101
+ if (a)
102
+ if (!(0, perm_1.accountCanLogin)(a) || failAllowNet(ctx, a)) // enforce allow_net also after login
103
+ await (0, auth_1.setLoggedIn)(ctx, false);
104
+ else if (loggedInNotBySession) {
105
+ if (a.username)
106
+ await (0, auth_1.setLoggedIn)(ctx, a.username);
107
+ ctx.headers['x-username'] = a.username; // give an easier way to determine if the login was successful
108
+ }
100
109
  ctx.state.revProxyPath = ctx.get('x-forwarded-prefix');
101
110
  (0, connections_1.updateConnectionForCtx)(ctx);
102
111
  await next();
@@ -114,7 +123,7 @@ const prepareState = async (ctx, next) => {
114
123
  return;
115
124
  try {
116
125
  const [u, p] = atob(b64).split(':');
117
- if (!u || u === ctx.session?.username)
126
+ if (!u || u === s?.username)
118
127
  return; // providing credentials, but not needed
119
128
  return (0, auth_1.clearTextLogin)(ctx, u, p || '', 'header');
120
129
  }
@@ -1,14 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const config_1 = require("./config");
4
- const node_url_1 = require("node:url");
5
4
  const util_http_1 = require("./util-http");
6
5
  const util_os_1 = require("./util-os");
7
6
  const const_1 = require("./const");
8
7
  const cross_1 = require("./cross");
9
8
  const outboundProxy = (0, config_1.defineConfig)(cross_1.CFG.outbound_proxy, '', v => {
9
+ if (!v)
10
+ return;
10
11
  try {
11
- (0, node_url_1.parse)(v); // just validate
12
+ (0, util_http_1.parseHttpUrl)(v); // just validate
12
13
  util_http_1.httpStream.defaultProxy = v;
13
14
  if (!v || process.env.HFS_SKIP_PROXY_TEST)
14
15
  return;
package/src/serveFile.js CHANGED
@@ -101,12 +101,22 @@ async function serveFile(ctx, source, mime, content) {
101
101
  ctx.set('Cache-Control', `max-age=${cc}`);
102
102
  const { size } = stats;
103
103
  const range = applyRange(ctx, size);
104
- ctx.body = (0, fs_1.createReadStream)(source, range);
104
+ if (ctx.status >= 400)
105
+ return; // applyRange may have set an error
106
+ ctx.body = (0, fs_1.createReadStream)(source, range || undefined);
105
107
  if (ctx.state.vfsNode)
106
108
  monitorAsDownload(ctx, size, range?.start);
107
109
  }
108
110
  catch (e) {
109
- return ctx.status = const_1.HTTP_NOT_FOUND;
111
+ const status = {
112
+ ENOENT: const_1.HTTP_NOT_FOUND,
113
+ ENOTDIR: const_1.HTTP_NOT_FOUND,
114
+ EACCES: const_1.HTTP_FORBIDDEN,
115
+ EPERM: const_1.HTTP_FORBIDDEN,
116
+ }[String(e?.code)];
117
+ if (!status)
118
+ throw e;
119
+ ctx.status = status;
110
120
  }
111
121
  }
112
122
  function monitorAsDownload(ctx, size, offset) {
@@ -131,27 +141,32 @@ function applyRange(ctx, totalSize = ctx.response.length) {
131
141
  }
132
142
  const [unit, ranges] = range.split('=');
133
143
  if (unit !== 'bytes')
134
- return ctx.throw(const_1.HTTP_BAD_REQUEST, 'bad range unit');
144
+ return badRequest('bad range unit');
135
145
  if (ranges?.includes(','))
136
- return ctx.throw(const_1.HTTP_BAD_REQUEST, 'multi-range not supported');
137
- let bytes = ranges?.split('-');
138
- if (!bytes?.length)
139
- return ctx.throw(const_1.HTTP_BAD_REQUEST, 'bad range');
146
+ return badRequest('multi-range not supported');
147
+ const bytes = ranges?.split('-');
148
+ if (bytes?.length !== 2)
149
+ return badRequest('bad range');
140
150
  const max = totalSize - 1;
141
- const start = bytes[0] ? Number(bytes[0]) : Math.max(0, totalSize - Number(bytes[1])); // a negative start is relative to the end
142
- const end = (bytes[0] && bytes[1]) ? Math.min(max, Number(bytes[1])) : max;
151
+ const [startTxt, endTxt] = bytes;
152
+ const start = startTxt ? Number(startTxt) : Math.max(0, totalSize - Number(endTxt)); // a negative start is relative to the end
153
+ const end = (startTxt && endTxt) ? Math.min(max, Number(endTxt)) : max;
154
+ if (isNaN(start) || startTxt && endTxt && isNaN(end))
155
+ return badRequest('bad range');
143
156
  // we don't support last-bytes without knowing max
144
- if (isNaN(end) && isNaN(max) || end > max || start > max) {
145
- ctx.status = const_1.HTTP_RANGE_NOT_SATISFIABLE;
157
+ if (isNaN(end) && isNaN(max) || end > max || start > max || start > end) {
146
158
  ctx.set('Content-Range', `bytes */${totalSize}`);
147
- ctx.body = 'Requested Range Not Satisfiable';
148
- return;
159
+ return badRequest('Requested Range Not Satisfiable', const_1.HTTP_RANGE_NOT_SATISFIABLE);
149
160
  }
150
161
  ctx.state.includesLastByte = end === max;
151
162
  ctx.status = const_1.HTTP_PARTIAL_CONTENT;
152
163
  ctx.set('Content-Range', `bytes ${start}-${isNaN(end) ? '' : end}/${isNaN(totalSize) ? '*' : totalSize}`);
153
164
  ctx.response.length = end - start + 1;
154
165
  return { start, end };
166
+ function badRequest(message, status = const_1.HTTP_BAD_REQUEST) {
167
+ ctx.status = status;
168
+ ctx.body = message;
169
+ }
155
170
  }
156
171
  function downloadLimiter(configMax, cbKey) {
157
172
  const map = new Map();
package/src/upload.js CHANGED
@@ -189,7 +189,10 @@ function uploadWriter(base, baseUri, filename, ctx) {
189
189
  while (fs_1.default.existsSync(dest));
190
190
  }
191
191
  try {
192
- await (0, promises_1.rename)(tempName, dest);
192
+ const done = await (0, misc_1.waitFor)(// an antivirus may lock the temp file to scan it
193
+ () => (0, promises_1.rename)(tempName, dest).then(() => true, e => e?.code !== 'EBUSY' && Promise.reject(e)), { timeout: 10_000 });
194
+ if (!done)
195
+ throw 'EBUSY';
193
196
  if (mtime) // so we use it to touch the file
194
197
  await (0, promises_1.utimes)(dest, Date.now() / 1000, mtime / 1000);
195
198
  cancelDeletion(tempName); // not necessary, as deletion's failure is silent, but still
package/src/util-http.js CHANGED
@@ -41,6 +41,7 @@ exports.stream2string = void 0;
41
41
  exports.httpString = httpString;
42
42
  exports.httpWithBody = httpWithBody;
43
43
  exports.httpStream = httpStream;
44
+ exports.parseHttpUrl = parseHttpUrl;
44
45
  const node_url_1 = require("node:url");
45
46
  const node_https_1 = __importDefault(require("node:https"));
46
47
  const node_http_1 = __importDefault(require("node:http"));
@@ -49,6 +50,7 @@ const lodash_1 = __importDefault(require("lodash"));
49
50
  const consumers_1 = require("node:stream/consumers");
50
51
  Object.defineProperty(exports, "stream2string", { enumerable: true, get: function () { return consumers_1.text; } });
51
52
  const tls = __importStar(require("node:tls"));
53
+ const cross_1 = require("./cross");
52
54
  async function httpString(url, options) {
53
55
  return await (0, consumers_1.text)(await httpStream(url, options));
54
56
  }
@@ -74,11 +76,13 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
74
76
  if (!(body instanceof node_stream_1.Readable))
75
77
  options.headers['content-length'] ??= Buffer.byteLength(body);
76
78
  }
77
- if (jar)
78
- options.headers.cookie = lodash_1.default.map(jar, (v, k) => `${k}=${v}; `).join('')
79
+ const { auth, ...parsed } = parseHttpUrl(url);
80
+ const hostJar = jar && (jar[parsed.hostname || ''] ||= {});
81
+ if (hostJar) {
82
+ options.headers.cookie = lodash_1.default.map(hostJar, (v, k) => `${k}=${v}; `).join('')
79
83
  + (options.headers.cookie || ''); // preserve parameter
80
- const { auth, ...parsed } = (0, node_url_1.parse)(url);
81
- const proxyParsed = proxy ? (0, node_url_1.parse)(proxy) : null;
84
+ }
85
+ const proxyParsed = proxy ? parseHttpUrl(proxy) : null;
82
86
  Object.assign(options, lodash_1.default.pick(proxyParsed || parsed, ['hostname', 'port', 'path', 'protocol']));
83
87
  if (auth) {
84
88
  options.auth = auth;
@@ -87,7 +91,7 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
87
91
  }
88
92
  if (proxy) {
89
93
  options.path = url; // full url as path
90
- options.headers.host ??= (0, node_url_1.parse)(url).host || undefined; // keep original host header
94
+ options.headers.host ??= parsed.host || undefined; // keep original host header
91
95
  }
92
96
  // this needs the prefix "proxy-"
93
97
  const proxyAuth = proxyParsed?.auth ? { 'proxy-authorization': `Basic ${Buffer.from(proxyParsed.auth, 'utf8').toString('base64')}` } : undefined;
@@ -97,15 +101,15 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
97
101
  const proto = options.protocol === 'https:' ? node_https_1.default : node_http_1.default;
98
102
  const req = proto.request(options, res => {
99
103
  console.debug("http responded", res.statusCode, "to", url);
100
- if (jar)
104
+ if (hostJar)
101
105
  for (const entry of res.headers['set-cookie'] || []) {
102
106
  const [, k, v] = /(.+?)=([^;]+)/.exec(entry) || [];
103
107
  if (!k)
104
108
  continue;
105
109
  if (v)
106
- jar[k] = v;
110
+ hostJar[k] = v;
107
111
  else
108
- delete jar[k];
112
+ delete hostJar[k];
109
113
  }
110
114
  if (!res.statusCode || httpThrow && res.statusCode >= 400)
111
115
  return reject(new Error(String(res.statusCode), { cause: res }));
@@ -161,3 +165,15 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
161
165
  abort() { controller.abort(); }
162
166
  });
163
167
  }
168
+ // works the same way as the now deprecated url.parse()
169
+ function parseHttpUrl(url) {
170
+ const parsed = new URL(url);
171
+ const options = (0, node_url_1.urlToHttpOptions)(parsed);
172
+ const withoutHash = url.split('#', 1)[0];
173
+ const authority = /^[a-z][a-z\d+.-]*:\/\/[^/?#]*/i.exec(withoutHash)?.[0];
174
+ return {
175
+ ...options,
176
+ host: parsed.host,
177
+ path: !authority ? '/' : (0, cross_1.enforceStarting)('/', withoutHash.slice(authority.length)), // unresolved paths are useful in our tests
178
+ };
179
+ }