hfs 0.48.0-alpha2 → 0.48.0-alpha4

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.
@@ -1,4 +1,4 @@
1
- import{g as OF,c as UF}from"./index-12411ff6.js";function gF(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 dF={exports:{}};/*
1
+ import{g as OF,c as UF}from"./index-110410d0.js";function gF(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 dF={exports:{}};/*
2
2
  * [js-sha512]{@link https://github.com/emn178/js-sha512}
3
3
  *
4
4
  * @version 0.8.0
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" />
6
6
  <link href="/fontello.css" rel="stylesheet" />
7
7
 
8
- <script type="module" crossorigin src="/assets/index-12411ff6.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-110410d0.js"></script>
9
9
  <link rel="stylesheet" href="/assets/index-05dfd82f.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hfs",
3
- "version": "0.48.0-alpha2",
3
+ "version": "0.48.0-alpha4",
4
4
  "description": "HTTP File Server",
5
5
  "keywords": [
6
6
  "file server",
@@ -32,8 +32,8 @@
32
32
  "dist-bin": "npm run dist-modules && cd dist && pkg . --public -C gzip && mv -f hfs-win-x64.exe hfs.exe && zip hfs-windows.zip hfs.exe -r plugins && cp -f hfs-linux-x64 hfs && zip hfs-linux.zip hfs -r plugins && cp -f hfs-macos-x64 hfs && zip hfs-mac.zip hfs -r plugins && cp -f hfs-macos-arm64 hfs && zip hfs-mac-arm.zip hfs -r plugins && rm hfs",
33
33
  "dist-modules": "cp package*.json central.json dist && cd dist && npm ci --omit=dev && npm run dist-crclib && rm package-lock.json && cd .. && node prune_modules",
34
34
  "dist-crclib": "npm i -f --no-save --omit=dev @node-rs/crc32-win32-x64-msvc @node-rs/crc32-darwin-arm64 @node-rs/crc32-darwin-x64 @node-rs/crc32-linux-x64-gnu",
35
- "dist-win": "npm run dist-modules && cd dist && pkg . --public -C gzip -t node16-win-x64",
36
- "dist-mac": "npm run dist-modules && cd dist && pkg . --public -C gzip -t node16-macos-arm64",
35
+ "dist-win": "npm run dist-modules && cd dist && pkg . --public -C gzip -t node18-win-x64",
36
+ "dist-mac": "npm run dist-modules && cd dist && pkg . --public -C gzip -t node18-macos-arm64",
37
37
  "dist-node": "npm run dist-modules && cd dist && zip hfs-node.zip -r * -x *.zip *.exe hfs-* *.log logs"
38
38
  },
39
39
  "engines": {
@@ -68,6 +68,7 @@
68
68
  "dependencies": {
69
69
  "@koa/router": "^10.1.1",
70
70
  "@node-rs/crc32": "^1.6.0",
71
+ "acme-client": "^5.0.0",
71
72
  "basic-auth": "^2.0.1",
72
73
  "buffer-crc32": "^0.2.13",
73
74
  "fast-glob": "^3.2.7",
@@ -162,6 +162,7 @@ class QuickZipStream extends stream_1.Readable {
162
162
  data.on('error', (err) => console.error(err));
163
163
  data.on('end', () => {
164
164
  this.workingFile = undefined;
165
+ entry.crc = crc;
165
166
  if (sourcePath)
166
167
  crcCache[sourcePath] = { ts, crc };
167
168
  this.entries.push(entry);
@@ -34,7 +34,8 @@ const get_file_list = async ({ uri, offset, limit, search, c }, ctx) => {
34
34
  const walker = (0, vfs_1.walkNode)(node, ctx, search ? Infinity : 0);
35
35
  const onDirEntryHandlers = (0, plugins_1.mapPlugins)(plug => plug.onDirEntry);
36
36
  const can_upload = (0, vfs_1.hasPermission)(node, 'can_upload', ctx);
37
- const can_delete = (0, vfs_1.hasPermission)(node, 'can_delete', ctx);
37
+ const fakeChild = (0, vfs_1.applyParentToChild)({}, node); // we want to know if we want to delete children
38
+ const can_delete = (0, vfs_1.hasPermission)(fakeChild, 'can_delete', ctx);
38
39
  const props = { can_upload, can_delete, accept: node.accept };
39
40
  if (!list)
40
41
  return { ...props, list: await (0, misc_1.asyncGeneratorToArray)(produceEntries()) };
@@ -84,41 +85,41 @@ const get_file_list = async ({ uri, offset, limit, search, c }, ctx) => {
84
85
  break;
85
86
  }
86
87
  }
88
+ async function nodeToDirEntry(ctx, node) {
89
+ let { source, default: def } = node;
90
+ const name = (0, vfs_1.getNodeName)(node);
91
+ if (!source)
92
+ return name ? { n: name + '/' } : null;
93
+ if (def)
94
+ return { n: name };
95
+ try {
96
+ const st = await (0, promises_1.stat)(source);
97
+ const folder = st.isDirectory();
98
+ const { ctime, mtime } = st;
99
+ const pl = node.can_list === vfs_1.WHO_NO_ONE ? 'l'
100
+ : !(0, vfs_1.hasPermission)(node, 'can_list', ctx) ? 'L'
101
+ : '';
102
+ // no download here, but maybe inside?
103
+ const pr = node.can_read === vfs_1.WHO_NO_ONE && !(folder && filesInsideCould()) ? 'r'
104
+ : !(0, vfs_1.hasPermission)(node, 'can_read', ctx) ? 'R'
105
+ : '';
106
+ const pd = !can_delete && (0, vfs_1.hasPermission)(node, 'can_delete', ctx) ? 'd' : '';
107
+ return {
108
+ n: name + (folder ? '/' : ''),
109
+ c: ctime,
110
+ m: Math.abs(+mtime - +ctime) < 1000 ? undefined : mtime,
111
+ s: folder ? undefined : st.size,
112
+ p: (pr + pl + pd) || undefined
113
+ };
114
+ }
115
+ catch (_a) {
116
+ return null;
117
+ }
118
+ function filesInsideCould(n = node) {
119
+ var _a;
120
+ return (0, vfs_1.masksCouldGivePermission)(n.masks, 'can_read')
121
+ || ((_a = n.children) === null || _a === void 0 ? void 0 : _a.some(c => c.can_read || filesInsideCould(c))); // we count on the boolean-compliant nature of the permission type here
122
+ }
123
+ }
87
124
  };
88
125
  exports.get_file_list = get_file_list;
89
- async function nodeToDirEntry(ctx, node) {
90
- let { source, default: def } = node;
91
- const name = (0, vfs_1.getNodeName)(node);
92
- if (!source)
93
- return name ? { n: name + '/' } : null;
94
- if (def)
95
- return { n: name };
96
- try {
97
- const st = await (0, promises_1.stat)(source);
98
- const folder = st.isDirectory();
99
- const { ctime, mtime } = st;
100
- const pl = node.can_list === vfs_1.WHO_NO_ONE ? 'l'
101
- : !(0, vfs_1.hasPermission)(node, 'can_list', ctx) ? 'L'
102
- : '';
103
- // no download here, but maybe inside?
104
- const pr = node.can_read === vfs_1.WHO_NO_ONE && !(folder && filesInsideCould()) ? 'r'
105
- : !(0, vfs_1.hasPermission)(node, 'can_read', ctx) ? 'R'
106
- : '';
107
- const pd = (0, vfs_1.hasPermission)(node, 'can_delete', ctx) ? 'd' : '';
108
- return {
109
- n: name + (folder ? '/' : ''),
110
- c: ctime,
111
- m: Math.abs(+mtime - +ctime) < 1000 ? undefined : mtime,
112
- s: folder ? undefined : st.size,
113
- p: (pr + pl + pd) || undefined
114
- };
115
- }
116
- catch (_a) {
117
- return null;
118
- }
119
- function filesInsideCould(n = node) {
120
- var _a;
121
- return (0, vfs_1.masksCouldGivePermission)(n.masks, 'can_read')
122
- || ((_a = n.children) === null || _a === void 0 ? void 0 : _a.some(c => c.can_read || filesInsideCould(c))); // we count on the boolean-compliant nature of the permission type here
123
- }
124
- }
package/src/api.net.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.externalIp = void 0;
7
+ exports.acmeMiddleware = exports.externalIp = void 0;
8
8
  const apiMiddleware_1 = require("./apiMiddleware");
9
9
  const nat_upnp_1 = require("nat-upnp");
10
10
  const const_1 = require("./const");
@@ -16,17 +16,15 @@ const github_1 = require("./github");
16
16
  const util_http_1 = require("./util-http");
17
17
  const child_process_1 = require("child_process");
18
18
  const misc_1 = require("./misc");
19
- const client = new nat_upnp_1.Client({ timeout: 5000 });
20
- const original = client.getGateway;
19
+ const acme_client_1 = __importDefault(require("acme-client"));
20
+ const promises_1 = __importDefault(require("fs/promises"));
21
+ const http_1 = require("http");
22
+ const promises_2 = require("dns/promises");
23
+ const client = new nat_upnp_1.Client({ timeout: 4000 });
24
+ const originalMethod = client.getGateway;
21
25
  // other client methods call getGateway too, so this will ensure they reuse this same result
22
- client.getGateway = function getGatewayCaching() {
23
- const promise = original.apply(client);
24
- client.getGateway = () => promise; // multiple callings = same job
25
- promise.then(() => console.debug('caching gateway'), // store in cache only if successful.
26
- () => client.getGateway = getGatewayCaching); // failed, try again
27
- return promise;
28
- };
29
- client.getGateway();
26
+ client.getGateway = (0, misc_1.debounceAsync)(() => originalMethod.apply(client), 0, { retain: misc_1.HOUR, retainFailure: 30000 });
27
+ client.getGateway().catch(() => { });
30
28
  exports.externalIp = Promise.resolve(''); // poll external ip
31
29
  (0, misc_1.repeat)(10 * misc_1.MINUTE, () => {
32
30
  const was = exports.externalIp;
@@ -42,8 +40,7 @@ const getNatInfo = (0, misc_1.debounceAsync)(async () => {
42
40
  const gatewayIp = res ? new URL(res.gateway.description).hostname : await findGateway().catch(() => null);
43
41
  const localIp = (res === null || res === void 0 ? void 0 : res.address) || (await (0, listen_1.getIps)())[0];
44
42
  const internalPort = ((_a = status === null || status === void 0 ? void 0 : status.https) === null || _a === void 0 ? void 0 : _a.listening) && status.https.port || ((_b = status === null || status === void 0 ? void 0 : status.http) === null || _b === void 0 ? void 0 : _b.listening) && status.http.port;
45
- const mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort || x.description === 'hfs');
46
- console.debug('responding');
43
+ const mapped = lodash_1.default.find(mappings, x => x.private.host === localIp && x.private.port === internalPort);
47
44
  return {
48
45
  upnp: Boolean(res),
49
46
  localIp,
@@ -80,57 +77,155 @@ function findGateway() {
80
77
  resolve((_a = re.exec(out)) === null || _a === void 0 ? void 0 : _a[1]);
81
78
  }));
82
79
  }
80
+ let acmeMiddlewareEnabled = false;
81
+ const acmeTokens = {};
82
+ const acmeListener = (req, res) => {
83
+ var _a;
84
+ const BASE = '/.well-known/acme-challenge/';
85
+ if (!((_a = req.url) === null || _a === void 0 ? void 0 : _a.startsWith(BASE)))
86
+ return;
87
+ const token = req.url.slice(BASE.length);
88
+ console.debug("got http challenge", token);
89
+ res.statusCode = const_1.HTTP_OK;
90
+ res.end(acmeTokens[token]);
91
+ return true;
92
+ };
93
+ const acmeMiddleware = (ctx, next) => {
94
+ if (!acmeMiddlewareEnabled || !Boolean(acmeListener(ctx.req, ctx.res)))
95
+ return next();
96
+ };
97
+ exports.acmeMiddleware = acmeMiddleware;
98
+ async function checkDomain(domain) {
99
+ const { address: domainIp } = await (0, promises_2.lookup)(domain).catch(e => {
100
+ throw e.code !== 'ENOTFOUND' ? e : new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "this domain doesn't exist");
101
+ });
102
+ const { publicIp } = await getNatInfo(); // do this before stopping the server
103
+ if (publicIp !== domainIp)
104
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, `please configure your domain to point to ${publicIp} (currently on ${domainIp}) --- a change can take hours to be effective`);
105
+ }
106
+ async function generateSSLCert(domain, email) {
107
+ await checkDomain(domain);
108
+ // will answer challenge through our koa app (if on port 80) or must we spawn a dedicated server?
109
+ const { http } = await (0, listen_1.getServerStatus)();
110
+ const tempSrv = http.listening && http.port === 80 ? undefined : (0, http_1.createServer)(acmeListener);
111
+ if (tempSrv)
112
+ await new Promise((resolve, reject) => tempSrv.listen(80, resolve).on('error', (e) => reject(e.code || e)));
113
+ else
114
+ acmeMiddlewareEnabled = true;
115
+ console.debug('acme challenge server ready');
116
+ try {
117
+ const { upnp, externalPort } = await getNatInfo(); // do this before stopping the server
118
+ let check = await checkPort(domain, 80); // some check services may not consider the domain, but we already verified that
119
+ if (check && !check.success && upnp && externalPort !== 80) { // consider a short-lived mapping
120
+ // @ts-ignore
121
+ await client.createMapping({ private: 80, public: { host: '', port: 80 }, description: 'hfs challenge', ttl: 0 }).catch(() => { });
122
+ check = await checkPort(domain, 80); // repeat test
123
+ }
124
+ if (!check)
125
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "couldn't test port 80");
126
+ if (!check.success)
127
+ throw new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "port 80 is not working on the specified domain");
128
+ const client = new acme_client_1.default.Client({
129
+ accountKey: await acme_client_1.default.crypto.createPrivateKey(),
130
+ directoryUrl: acme_client_1.default.directory.letsencrypt.production
131
+ });
132
+ const [key, csr] = await acme_client_1.default.crypto.createCsr({ commonName: domain });
133
+ const cert = await client.auto({
134
+ csr,
135
+ email,
136
+ challengePriority: ['http-01'],
137
+ skipChallengeVerification: true,
138
+ termsOfServiceAgreed: true,
139
+ async challengeCreateFn(_, c, ka) {
140
+ console.debug("producing challenge");
141
+ acmeTokens[c.token] = ka;
142
+ },
143
+ async challengeRemoveFn(_, c) {
144
+ delete acmeTokens[c.token];
145
+ },
146
+ });
147
+ return { key, cert };
148
+ }
149
+ finally {
150
+ acmeMiddlewareEnabled = false;
151
+ if (tempSrv)
152
+ await new Promise(res => tempSrv.close(res));
153
+ console.debug('acme terminated');
154
+ }
155
+ }
156
+ async function checkPort(ip, port) {
157
+ const prjInfo = await (0, github_1.getProjectInfo)();
158
+ for (const services of lodash_1.default.chunk(lodash_1.default.shuffle(prjInfo.checkServerServices), 2)) {
159
+ try {
160
+ return Promise.any(services.map(async (svc) => {
161
+ var _a, _b;
162
+ const service = new URL(svc.url).hostname;
163
+ console.log('trying service', service);
164
+ const api = axios_1.default[svc.method];
165
+ const body = ((_a = svc.body) === null || _a === void 0 ? void 0 : _a.replace('$IP', ip).replace('$PORT', String(port))) || '';
166
+ const res = await api(svc.url, body, { headers: svc.headers });
167
+ const parsed = (_b = (0, node_html_parser_1.parse)(res.data).querySelector(svc.selector)) === null || _b === void 0 ? void 0 : _b.innerText;
168
+ if (!parsed)
169
+ throw console.debug('empty:' + service);
170
+ const success = new RegExp(svc.regexpSuccess).test(parsed);
171
+ const failure = new RegExp(svc.regexpFailure).test(parsed);
172
+ if (success === failure)
173
+ throw console.debug('inconsistent:' + service); // this result cannot be trusted
174
+ console.debug(service, 'responded', success);
175
+ return { success, service };
176
+ }));
177
+ }
178
+ catch (_a) { }
179
+ }
180
+ }
83
181
  const apis = {
84
182
  get_nat: getNatInfo,
85
- async map_port({ external }) {
86
- const { gatewayIp, mapped, internalPort } = await getNatInfo();
87
- if (!gatewayIp)
183
+ check_domain({ domain }) {
184
+ (0, misc_1.apiAssertTypes)({ string: domain });
185
+ return checkDomain(domain);
186
+ },
187
+ async map_port({ external, internal }) {
188
+ const { upnp, externalPort, internalPort } = await getNatInfo();
189
+ if (!upnp)
88
190
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'upnp failed');
89
191
  if (!internalPort)
90
192
  return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'no internal port');
91
- if (mapped)
193
+ if (externalPort)
92
194
  try {
93
- await client.removeMapping({ public: { host: '', port: mapped.public.port } });
195
+ await client.removeMapping({ public: { host: '', port: externalPort } });
94
196
  }
95
197
  catch (e) {
96
198
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR, 'removeMapping failed: ' + String(e));
97
199
  }
98
- if (external) // must use the object form of 'public' to workaround a bug of the library
99
- await client.createMapping({ private: internalPort, public: { host: '', port: external }, description: 'hfs', ttl: 0 });
200
+ if (external) // must use the object form of 'public' to work around a bug of the library
201
+ await client.createMapping({ private: internal || internalPort, public: { host: '', port: external }, description: 'hfs', ttl: 0 });
100
202
  return {};
101
203
  },
102
- async check_server() {
204
+ async check_server({ port }) {
103
205
  const { publicIp, internalPort, externalPort } = await getNatInfo();
104
206
  if (!publicIp)
105
207
  return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'cannot detect public ip');
106
208
  if (!internalPort)
107
209
  return new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, 'no internal port');
108
- const prjInfo = await (0, github_1.getProjectInfo)();
109
- const port = externalPort || internalPort;
210
+ port || (port = externalPort || internalPort);
110
211
  console.log(`checking server ${publicIp}:${port}`);
111
- for (const services of lodash_1.default.chunk(lodash_1.default.shuffle(prjInfo.checkServerServices), 2)) {
112
- try {
113
- return Promise.any(services.map(async (svc) => {
114
- var _a, _b;
115
- const service = new URL(svc.url).hostname;
116
- console.log('trying service', service);
117
- const api = axios_1.default[svc.method];
118
- const body = ((_a = svc.body) === null || _a === void 0 ? void 0 : _a.replace('$IP', publicIp).replace('$PORT', String(port))) || '';
119
- const res = await api(svc.url, body, { headers: svc.headers });
120
- const parsed = (_b = (0, node_html_parser_1.parse)(res.data).querySelector(svc.selector)) === null || _b === void 0 ? void 0 : _b.innerText;
121
- if (!parsed)
122
- throw console.debug('empty:' + service);
123
- const success = new RegExp(svc.regexpSuccess).test(parsed);
124
- const failure = new RegExp(svc.regexpFailure).test(parsed);
125
- if (success === failure)
126
- throw console.debug('inconsistent:' + service); // this result cannot be trusted
127
- console.debug(service, 'responded', success);
128
- return { success, service };
129
- }));
130
- }
131
- catch (_a) { }
132
- }
133
- return new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE, 'no service available to detect upnp mapping');
212
+ return await checkPort(publicIp, port)
213
+ || new apiMiddleware_1.ApiError(const_1.HTTP_SERVICE_UNAVAILABLE);
214
+ },
215
+ async make_cert({ domain, email }) {
216
+ if (!domain)
217
+ return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad params');
218
+ const res = await generateSSLCert(domain, email);
219
+ const CERT_FILE = 'acme.cert';
220
+ const KEY_FILE = 'acme.key';
221
+ await promises_1.default.writeFile(CERT_FILE, res.cert);
222
+ await promises_1.default.writeFile(KEY_FILE, res.key);
223
+ listen_1.cert.set(CERT_FILE); // update config
224
+ listen_1.privateKey.set(KEY_FILE);
225
+ return {};
134
226
  },
227
+ get_cert() {
228
+ return (0, misc_1.objSameKeys)(lodash_1.default.pick((0, listen_1.getCertObject)(), ['subject', 'issuer', 'validFrom', 'validTo']), v => v);
229
+ }
135
230
  };
136
231
  exports.default = apis;
@@ -24,13 +24,6 @@ const apis = {
24
24
  },
25
25
  pluginUninstalled: id => list.remove({ id }),
26
26
  });
27
- function serialize(p) {
28
- const o = 'getData' in p ? Object.assign(lodash_1.default.pick(p, ['id', 'started']), p.getData())
29
- : { ...p }; // _.defaults mutates object, and we don't want that
30
- if (typeof o.repo === 'object') // custom repo
31
- o.repo = o.repo.web;
32
- return lodash_1.default.defaults(o, { started: null, badApi: null }); // nulls should be used to be sure to overwrite previous values,
33
- }
34
27
  },
35
28
  async get_plugin_updates() {
36
29
  return new apiMiddleware_1.SendListReadable({
@@ -44,8 +37,13 @@ const apis = {
44
37
  if (!(online === null || online === void 0 ? void 0 : online.apiRequired) || online.badApi)
45
38
  return;
46
39
  const disk = (0, plugins_1.getPluginInfo)(folder);
47
- if (online.version > disk.version)
40
+ if (!disk)
41
+ return; // plugin removed in the meantime?
42
+ if (online.version > disk.version) { // it IS newer
43
+ online.id = disk.id; // id is installation-dependant, and online cannot know
44
+ online.repo = serialize(disk).repo; // show the user the current repo we are getting this update from, not a possibly-changed future one
48
45
  list.add(online);
46
+ }
49
47
  }
50
48
  catch (err) {
51
49
  if (err.message === '404') // the plugin is declaring a wrong repo
@@ -97,8 +95,8 @@ const apis = {
97
95
  const undo = [];
98
96
  ctx.req.once('close', () => undo.forEach(x => x()));
99
97
  const folder2repo = (0, github_1.getFolder2repo)();
100
- for await (const pl of (0, github_1.searchPlugins)(text)) {
101
- const repo = pl.id;
98
+ for await (const pl of await (0, github_1.searchPlugins)(text)) {
99
+ const repo = pl.repo || pl.id; // .repo property can be more trustworthy in case github user renamed and left the previous link in 'repo'
102
100
  if (lodash_1.default.includes(folder2repo, repo))
103
101
  continue; // don't include installed plugins
104
102
  list.add(pl);
@@ -125,24 +123,24 @@ const apis = {
125
123
  }
126
124
  });
127
125
  },
128
- async download_plugin(pl) {
129
- await checkDependencies(pl.id, pl.branch);
130
- const res = await (0, github_1.downloadPlugin)(pl.id, pl.branch);
126
+ async download_plugin({ id, branch }) {
127
+ await checkDependencies(id, branch);
128
+ const res = await (0, github_1.downloadPlugin)(id, { branch });
131
129
  if (typeof res !== 'string')
132
130
  return res;
133
131
  return (await (0, misc_1.waitFor)(() => (0, plugins_1.getPluginInfo)(res), { timeout: 5000 }))
134
132
  || new apiMiddleware_1.ApiError(const_1.HTTP_SERVER_ERROR);
135
133
  },
136
- async update_plugin(pl) {
137
- await checkDependencies(pl.id, pl.branch);
138
- const found = (0, plugins_1.findPluginByRepo)(pl.id); // github id !== local id
134
+ async update_plugin({ id }) {
135
+ const found = (0, plugins_1.getPluginInfo)(id);
139
136
  if (!found)
140
137
  return new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND);
141
- const enabled = (0, plugins_1.isPluginEnabled)(found.id);
142
- await (0, plugins_1.stopPlugin)(found.id);
143
- await (0, github_1.downloadPlugin)(pl.id, pl.branch, true);
138
+ await checkDependencies(found.repo);
139
+ const enabled = (0, plugins_1.isPluginEnabled)(id);
140
+ await (0, plugins_1.stopPlugin)(id);
141
+ await (0, github_1.downloadPlugin)(found.repo, { overwrite: true });
144
142
  if (enabled)
145
- (0, plugins_1.startPlugin)(found.id).then(); // don't wait, in case it fails to start
143
+ (0, plugins_1.startPlugin)(id).then(); // don't wait, in case it fails to start
146
144
  return {};
147
145
  },
148
146
  async uninstall_plugin({ id }) {
@@ -152,6 +150,13 @@ const apis = {
152
150
  }
153
151
  };
154
152
  exports.default = apis;
153
+ function serialize(p) {
154
+ const o = 'getData' in p ? Object.assign(lodash_1.default.pick(p, ['id', 'started']), p.getData())
155
+ : { ...p }; // _.defaults mutates object, and we don't want that
156
+ if (typeof o.repo === 'object') // custom repo
157
+ o.repo = o.repo.web;
158
+ return lodash_1.default.defaults(o, { started: null, badApi: null }); // nulls should be used to be sure to overwrite previous values,
159
+ }
155
160
  async function checkDependencies(repo, branch) {
156
161
  const rec = await (0, github_1.readOnlinePlugin)(repo, branch);
157
162
  if (!rec)
package/src/config.js CHANGED
@@ -178,11 +178,10 @@ const saveDebounced = (0, misc_1.debounceAsync)(async () => {
178
178
  await (0, misc_1.wait)(100);
179
179
  // keep backup
180
180
  const bak = filePath + '.bak';
181
- const aWeekAgo = Date.now() - const_1.DAY * 7;
181
+ const aWeekAgo = Date.now() - misc_1.DAY * 7;
182
182
  if (await (0, promises_1.stat)(bak).then(x => aWeekAgo > Number(x.mtime || x.ctime), () => true))
183
183
  await (0, promises_1.copyFile)(filePath, bak).catch(() => { }); // ignore errors
184
- const txt = yaml_1.default.stringify({ ...state, version: const_1.VERSION }, { lineWidth: 1000 });
185
- save(filePath, txt)
184
+ await save(yaml_1.default.stringify({ ...state, version: const_1.VERSION }, { lineWidth: 1000 }))
186
185
  .catch(err => console.error('Failed at saving config file, please ensure it is writable.', String(err)));
187
186
  });
188
187
  const saveConfigAsap = () => void (saveDebounced());
package/src/const.js CHANGED
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  return (mod && mod.__esModule) ? mod : { "default": mod };
28
28
  };
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.APP_PATH = exports.IS_BINARY = exports.IS_MAC = exports.IS_WINDOWS = exports.HTTP_SERVICE_UNAVAILABLE = exports.HTTP_SERVER_ERROR = exports.HTTP_FAILED_DEPENDENCY = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = exports.HTTP_CONFLICT = exports.HTTP_NOT_ACCEPTABLE = exports.HTTP_METHOD_NOT_ALLOWED = exports.HTTP_NOT_FOUND = exports.HTTP_FORBIDDEN = exports.HTTP_UNAUTHORIZED = exports.HTTP_BAD_REQUEST = exports.HTTP_NOT_MODIFIED = exports.HTTP_TEMPORARY_REDIRECT = exports.HTTP_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = exports.HFS_REPO = exports.COMPATIBLE_API_VERSION = exports.API_VERSION = exports.DAY = exports.RUNNING_BETA = exports.VERSION = exports.BUILD_TIMESTAMP = exports.HFS_STARTED = exports.ORIGINAL_CWD = exports.DEV = exports.argv = void 0;
30
+ exports.APP_PATH = exports.IS_BINARY = exports.IS_MAC = exports.IS_WINDOWS = exports.HTTP_SERVICE_UNAVAILABLE = exports.HTTP_SERVER_ERROR = exports.HTTP_FAILED_DEPENDENCY = exports.HTTP_FOOL = exports.HTTP_RANGE_NOT_SATISFIABLE = exports.HTTP_PAYLOAD_TOO_LARGE = exports.HTTP_CONFLICT = exports.HTTP_NOT_ACCEPTABLE = exports.HTTP_METHOD_NOT_ALLOWED = exports.HTTP_NOT_FOUND = exports.HTTP_FORBIDDEN = exports.HTTP_UNAUTHORIZED = exports.HTTP_BAD_REQUEST = exports.HTTP_NOT_MODIFIED = exports.HTTP_TEMPORARY_REDIRECT = exports.HTTP_PARTIAL_CONTENT = exports.HTTP_NO_CONTENT = exports.HTTP_OK = exports.PLUGINS_PUB_URI = exports.API_URI = exports.ADMIN_URI = exports.FRONTEND_URI = exports.SPECIAL_URI = exports.HFS_REPO = exports.COMPATIBLE_API_VERSION = exports.API_VERSION = exports.RUNNING_BETA = exports.VERSION = exports.BUILD_TIMESTAMP = exports.HFS_STARTED = exports.ORIGINAL_CWD = exports.DEV = exports.argv = void 0;
31
31
  const minimist_1 = __importDefault(require("minimist"));
32
32
  const fs = __importStar(require("fs"));
33
33
  const os_1 = require("os");
@@ -38,11 +38,10 @@ exports.DEV = process.env.DEV || exports.argv.dev ? 'DEV' : '';
38
38
  exports.ORIGINAL_CWD = process.cwd();
39
39
  exports.HFS_STARTED = new Date();
40
40
  const PKG_PATH = (0, path_1.join)(__dirname, '..', 'package.json');
41
- exports.BUILD_TIMESTAMP = "2023-09-07T17:51:10.938Z";
41
+ exports.BUILD_TIMESTAMP = "2023-09-16T14:11:25.343Z";
42
42
  const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
43
43
  exports.VERSION = pkg.version;
44
44
  exports.RUNNING_BETA = exports.VERSION.includes('-');
45
- exports.DAY = 86400000;
46
45
  exports.API_VERSION = 8.3;
47
46
  exports.COMPATIBLE_API_VERSION = 1; // while changes in the api are not breaking, this number stays the same, otherwise it is made equal to API_VERSION
48
47
  exports.HFS_REPO = 'rejetto/hfs';
@@ -99,3 +98,4 @@ else if (!winExe) { // still considering whether to use this behavior with Windo
99
98
  process.chdir(dir);
100
99
  }
101
100
  console.log('cwd', process.cwd());
101
+ console.log('node', process.version);
package/src/cross.js CHANGED
@@ -3,13 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.repeat = exports.asyncGeneratorToArray = exports.filterMapGenerator = exports.throw_ = exports.hasProp = exports.typedEntries = exports.typedKeys = exports.objRenameKey = exports.randomId = exports.getOrSet = exports.waitFor = exports.newObj = exports.removeStarting = exports.findDefined = exports.isOrderedEqual = exports.swap = exports.tryJson = exports.basename = exports.pendingPromise = exports._log = exports.wantArray = exports.formatPerc = exports.with_ = exports.try_ = exports.setHidden = exports.onlyTruthy = exports.truthy = exports.enforceFinal = exports.objSameKeys = exports.wait = exports.prefix = exports.formatBytes = exports.MINUTE = exports.WIKI_URL = exports.REPO_URL = void 0;
6
+ exports.xlate = exports.isEqualLax = exports.isWindowsDrive = exports.isIP = exports.isPrimitive = exports.formatTimestamp = exports.repeat = exports.asyncGeneratorToArray = exports.filterMapGenerator = exports.throw_ = exports.hasProp = exports.typedEntries = exports.typedKeys = exports.objRenameKey = exports.randomId = exports.getOrSet = exports.waitFor = exports.newObj = exports.removeStarting = exports.findDefined = exports.isOrderedEqual = exports.swap = exports.tryJson = exports.basename = exports.pendingPromise = exports._log = exports.wantArray = exports.formatPerc = exports.with_ = exports.try_ = exports.setHidden = exports.onlyTruthy = exports.truthy = exports.enforceFinal = exports.objSameKeys = exports.wait = exports.prefix = exports.formatBytes = exports.DAY = exports.HOUR = exports.MINUTE = exports.WIKI_URL = exports.REPO_URL = void 0;
7
7
  // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
8
8
  // all content here is shared between client and server
9
9
  const lodash_1 = __importDefault(require("lodash"));
10
10
  exports.REPO_URL = 'https://github.com/rejetto/hfs/';
11
11
  exports.WIKI_URL = exports.REPO_URL + 'wiki/';
12
12
  exports.MINUTE = 60000;
13
+ exports.HOUR = 60 * exports.MINUTE;
14
+ exports.DAY = 24 * exports.HOUR;
13
15
  const MULTIPLIERS = ['', 'K', 'M', 'G', 'T'];
14
16
  function formatBytes(n, { post = 'B', k = 1024, digits = NaN } = {}) {
15
17
  if (isNaN(Number(n)) || n < 0)
@@ -219,3 +221,30 @@ function repeat(every, cb) {
219
221
  Promise.allSettled([cb()]).then(() => setTimeout(() => repeat(every, cb), every));
220
222
  }
221
223
  exports.repeat = repeat;
224
+ function formatTimestamp(x) {
225
+ return x ? new Date(x).toLocaleString() : '-';
226
+ }
227
+ exports.formatTimestamp = formatTimestamp;
228
+ function isPrimitive(x) {
229
+ return !x || Object(x) !== x;
230
+ }
231
+ exports.isPrimitive = isPrimitive;
232
+ function isIP(address) {
233
+ return /^([.:\da-f]+)$/i.test(address);
234
+ }
235
+ exports.isIP = isIP;
236
+ function isWindowsDrive(s) {
237
+ return s && /^[a-zA-Z]:$/.test(s);
238
+ }
239
+ exports.isWindowsDrive = isWindowsDrive;
240
+ function isEqualLax(a, b) {
241
+ return a == b //eslint-disable-line
242
+ || (a && b && typeof a === 'object' && typeof b === 'object'
243
+ && Object.entries(a).every(([k, v]) => isEqualLax(v, b[k])));
244
+ }
245
+ exports.isEqualLax = isEqualLax;
246
+ function xlate(input, table) {
247
+ var _a;
248
+ return (_a = table[input]) !== null && _a !== void 0 ? _a : input;
249
+ }
250
+ exports.xlate = xlate;
@@ -3,12 +3,15 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  // like lodash.debounce, but also avoids async invocations to overlap
5
5
  function debounceAsync(callback, wait = 100, options = {}) {
6
- const { leading = false, maxWait = Infinity, cancelable = false } = options;
6
+ const { leading = false, maxWait = Infinity, cancelable = false, retain = 0, retainFailure } = options;
7
7
  let started = 0; // latest callback invocation
8
8
  let runningCallback; // latest callback invocation result
9
9
  let runningDebouncer; // latest wrapper invocation
10
10
  let waitingSince = 0; // we are delaying invocation since
11
11
  let whoIsWaiting; // args object identifies the pending instance, and incidentally stores args
12
+ let last;
13
+ let lastFailed = false;
14
+ let lastSince = 0;
12
15
  const interceptingWrapper = (...args) => runningDebouncer = debouncer(...args);
13
16
  return Object.assign(interceptingWrapper, {
14
17
  flush: () => runningCallback !== null && runningCallback !== void 0 ? runningCallback : exec(),
@@ -22,10 +25,13 @@ function debounceAsync(callback, wait = 100, options = {}) {
22
25
  async function debouncer(...args) {
23
26
  if (runningCallback)
24
27
  return runningCallback;
28
+ const now = Date.now();
29
+ if (last && now - lastSince < (lastFailed ? retainFailure !== null && retainFailure !== void 0 ? retainFailure : retain : retain))
30
+ return await last;
25
31
  whoIsWaiting = args;
26
- waitingSince || (waitingSince = Date.now());
27
- const waitingCap = maxWait - (Date.now() - (waitingSince || started));
28
- const waitFor = Math.min(waitingCap, leading ? wait - (Date.now() - started) : wait);
32
+ waitingSince || (waitingSince = now);
33
+ const waitingCap = maxWait - (now - (waitingSince || started));
34
+ const waitFor = Math.min(waitingCap, leading ? wait - (now - started) : wait);
29
35
  if (waitFor > 0)
30
36
  await new Promise(resolve => setTimeout(resolve, waitFor));
31
37
  if (!whoIsWaiting) // canceled
@@ -44,6 +50,9 @@ function debounceAsync(callback, wait = 100, options = {}) {
44
50
  return await runningCallback; // await necessary to go-finally at the right time and even on exceptions
45
51
  }
46
52
  finally {
53
+ last = runningCallback;
54
+ last.then(() => lastFailed = false, () => lastFailed = true);
55
+ lastSince = Date.now();
47
56
  whoIsWaiting = undefined;
48
57
  runningCallback = undefined;
49
58
  }