hfs 0.45.0 → 0.46.0

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.
Files changed (49) hide show
  1. package/README.md +7 -16
  2. package/admin/assets/index-963c15a5.js +517 -0
  3. package/{frontend/assets/sha512-b32d8af7.js → admin/assets/sha512-471fbd54.js} +2 -2
  4. package/admin/index.html +1 -1
  5. package/frontend/assets/index-c0f02a72.js +94 -0
  6. package/frontend/assets/index-f2f8fd6b.css +1 -0
  7. package/{admin/assets/sha512-c0389c0d.js → frontend/assets/sha512-7dcc825b.js} +2 -2
  8. package/frontend/fontello.css +1 -1
  9. package/frontend/index.html +2 -3
  10. package/package.json +4 -3
  11. package/plugins/download-counter/plugin.js +3 -1
  12. package/plugins/download-counter/public/main.js +3 -2
  13. package/plugins/list-uploader/plugin.js +21 -0
  14. package/plugins/list-uploader/public/main.js +27 -0
  15. package/plugins/vhosting/plugin.js +6 -3
  16. package/src/adminApis.js +9 -3
  17. package/src/api.auth.js +4 -4
  18. package/src/api.file_list.js +4 -5
  19. package/src/api.monitor.js +7 -5
  20. package/src/api.plugins.js +57 -13
  21. package/src/api.vfs.js +2 -1
  22. package/src/apiMiddleware.js +4 -0
  23. package/src/commands.js +11 -0
  24. package/src/config.js +9 -5
  25. package/src/connections.js +1 -1
  26. package/src/const.js +4 -3
  27. package/src/frontEndApis.js +33 -1
  28. package/src/github.js +65 -33
  29. package/src/index.js +4 -4
  30. package/src/lang.js +3 -2
  31. package/src/langs/embedded.js +2 -1
  32. package/src/langs/hfs-lang-fr.json +16 -4
  33. package/src/langs/hfs-lang-it.json +11 -4
  34. package/src/langs/hfs-lang-ru.json +16 -4
  35. package/src/langs/hfs-lang-vi.json +116 -0
  36. package/src/log.js +50 -47
  37. package/src/middlewares.js +10 -3
  38. package/src/plugins.js +181 -98
  39. package/src/serveFile.js +13 -5
  40. package/src/serveGuiFiles.js +8 -10
  41. package/src/throttler.js +11 -6
  42. package/src/update.js +1 -4
  43. package/src/upload.js +26 -9
  44. package/src/util-files.js +25 -6
  45. package/src/vfs.js +6 -12
  46. package/src/zip.js +2 -2
  47. package/admin/assets/index-101f19c9.js +0 -510
  48. package/frontend/assets/index-59572489.css +0 -1
  49. package/frontend/assets/index-5aeff3af.js +0 -94
package/src/log.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.log = exports.loggers = void 0;
30
+ exports.logMw = exports.loggers = void 0;
31
31
  const stream_1 = require("stream");
32
32
  const config_1 = require("./config");
33
33
  const fs_1 = require("fs");
@@ -38,6 +38,7 @@ const lodash_1 = __importDefault(require("lodash"));
38
38
  const util_files_1 = require("./util-files");
39
39
  const perm_1 = require("./perm");
40
40
  const misc_1 = require("./misc");
41
+ const events_1 = __importDefault(require("./events"));
41
42
  class Logger {
42
43
  constructor(name) {
43
44
  this.name = name;
@@ -80,54 +81,56 @@ errorLogFile.sub(path => {
80
81
  });
81
82
  const logRotation = (0, config_1.defineConfig)('log_rotation', 'weekly');
82
83
  const dontLogNet = (0, config_1.defineConfig)('dont_log_net', '127.0.0.1|::1', v => (0, misc_1.makeNetMatcher)(v));
83
- function log() {
84
- const debounce = lodash_1.default.debounce(cb => cb(), 1000);
85
- return async (ctx, next) => {
86
- const now = new Date();
87
- await next();
88
- console.debug(ctx.status, ctx.method, ctx.path);
89
- Promise.race([(0, stream_1.once)(ctx.res, 'finish'), (0, stream_1.once)(ctx.res, 'close')]).then(() => {
90
- var _a, _b, _c, _d;
91
- if (dontLogNet.compiled()(ctx.ip))
92
- return;
93
- const isError = ctx.status >= 400;
94
- const logger = isError && accessErrorLog || accessLogger;
95
- const rotate = (_a = logRotation.get()) === null || _a === void 0 ? void 0 : _a[0];
96
- let { stream, last, path } = logger;
97
- if (!stream)
98
- return;
99
- logger.last = now;
100
- if (rotate && last) { // rotation enabled and a file exists?
101
- const passed = Number(now) - Number(last)
102
- - 3600000; // be pessimistic and count a possible DST change
103
- if (rotate === 'm' && (passed >= 31 * const_1.DAY || now.getMonth() !== last.getMonth())
104
- || rotate === 'd' && (passed >= const_1.DAY || now.getDate() !== last.getDate()) // checking passed will solve the case when the day of the month is the same but a month has passed
105
- || rotate === 'w' && (passed >= 7 * const_1.DAY || now.getDay() < last.getDay())) {
106
- stream.end();
107
- const postfix = last.getFullYear() + '-' + doubleDigit(last.getMonth() + 1) + '-' + doubleDigit(last.getDate());
108
- try { // other logging requests shouldn't happen while we are renaming. Since this is very infrequent we can tolerate solving this by making it sync.
109
- (0, fs_1.renameSync)(path, path + '-' + postfix);
110
- }
111
- catch (e) { // ok, rename failed, but this doesn't mean we ain't gonna log
112
- console.error(e);
113
- }
114
- stream = logger.reopen(); // keep variable updated
115
- if (!stream)
116
- return;
84
+ const debounce = lodash_1.default.debounce(cb => cb(), 1000);
85
+ const logMw = async (ctx, next) => {
86
+ const now = new Date();
87
+ await next();
88
+ console.debug(ctx.status, ctx.method, ctx.originalUrl);
89
+ Promise.race([(0, stream_1.once)(ctx.res, 'finish'), (0, stream_1.once)(ctx.res, 'close')]).then(() => {
90
+ var _a, _b, _c, _d, _e;
91
+ if (ctx.state.dont_log)
92
+ return;
93
+ if (dontLogNet.compiled()(ctx.ip))
94
+ return;
95
+ const isError = ctx.status >= 400;
96
+ const logger = isError && accessErrorLog || accessLogger;
97
+ const rotate = (_a = logRotation.get()) === null || _a === void 0 ? void 0 : _a[0];
98
+ let { stream, last, path } = logger;
99
+ if (!stream)
100
+ return;
101
+ logger.last = now;
102
+ if (rotate && last) { // rotation enabled and a file exists?
103
+ const passed = Number(now) - Number(last)
104
+ - 3600000; // be pessimistic and count a possible DST change
105
+ if (rotate === 'm' && (passed >= 31 * const_1.DAY || now.getMonth() !== last.getMonth())
106
+ || rotate === 'd' && (passed >= const_1.DAY || now.getDate() !== last.getDate()) // checking passed will solve the case when the day of the month is the same but a month has passed
107
+ || rotate === 'w' && (passed >= 7 * const_1.DAY || now.getDay() < last.getDay())) {
108
+ stream.end();
109
+ const postfix = last.getFullYear() + '-' + doubleDigit(last.getMonth() + 1) + '-' + doubleDigit(last.getDate());
110
+ try { // other logging requests shouldn't happen while we are renaming. Since this is very infrequent we can tolerate solving this by making it sync.
111
+ (0, fs_1.renameSync)(path, path + '-' + postfix);
117
112
  }
113
+ catch (e) { // ok, rename failed, but this doesn't mean we ain't gonna log
114
+ console.error(e);
115
+ }
116
+ stream = logger.reopen(); // keep variable updated
117
+ if (!stream)
118
+ return;
118
119
  }
119
- const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s\n'; // Apache's Common Log Format
120
- const a = now.toString().split(' ');
121
- const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' + ((_b = a[5]) === null || _b === void 0 ? void 0 : _b.slice(3));
122
- const user = (0, perm_1.getCurrentUsername)(ctx);
123
- const length = (_c = ctx.state.length) !== null && _c !== void 0 ? _c : ctx.length;
124
- debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
125
- (0, promises_1.stat)(logger.path).catch(() => logger.reopen())); // async = smoother but we may lose some entries
126
- stream.write(util.format(format, ctx.ip, user || '-', date, ctx.method, ctx.path, ctx.req.httpVersion, ctx.status, (_d = length === null || length === void 0 ? void 0 : length.toString()) !== null && _d !== void 0 ? _d : '-'));
127
- });
128
- };
129
- }
130
- exports.log = log;
120
+ }
121
+ const format = '%s - %s [%s] "%s %s HTTP/%s" %d %s\n'; // Apache's Common Log Format
122
+ const a = now.toString().split(' ');
123
+ const date = a[2] + '/' + a[1] + '/' + a[3] + ':' + a[4] + ' ' + ((_b = a[5]) === null || _b === void 0 ? void 0 : _b.slice(3));
124
+ const user = (0, perm_1.getCurrentUsername)(ctx);
125
+ const length = (_c = ctx.state.length) !== null && _c !== void 0 ? _c : ctx.length;
126
+ const uri = (_d = ctx.originalUrl) === null || _d === void 0 ? void 0 : _d.replace(/&?csrf=[^&]+/, ''); // temporary workaround. To avoid csrf in url we need to stop using EventSource.
127
+ events_1.default.emit(logger.name, Object.assign(lodash_1.default.pick(ctx, ['ip', 'method', 'status']), { length, user, ts: now, uri }));
128
+ debounce(() => // once in a while we check if the file is still good (not deleted, etc), or we'll reopen it
129
+ (0, promises_1.stat)(logger.path).catch(() => logger.reopen())); // async = smoother but we may lose some entries
130
+ stream.write(util.format(format, ctx.ip, user || '-', date, ctx.method, uri, ctx.req.httpVersion, ctx.status, (_e = length === null || length === void 0 ? void 0 : length.toString()) !== null && _e !== void 0 ? _e : '-'));
131
+ });
132
+ };
133
+ exports.logMw = logMw;
131
134
  function doubleDigit(n) {
132
135
  return n > 9 ? n : '0' + n;
133
136
  }
@@ -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.paramsDecoder = exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.sendErrorPage = exports.serveGuiAndSharedFiles = exports.headRequests = exports.gzipper = exports.sessionDuration = void 0;
7
+ exports.paramsDecoder = exports.prepareState = exports.getProxyDetected = exports.someSecurity = exports.baseUrl = exports.sendErrorPage = exports.serveGuiAndSharedFiles = exports.headRequests = exports.gzipper = exports.sessionDuration = void 0;
8
8
  const koa_compress_1 = __importDefault(require("koa-compress"));
9
9
  const const_1 = require("./const");
10
10
  const const_2 = require("./const");
@@ -159,13 +159,13 @@ const errorMessages = {
159
159
  [const_1.HTTP_UNAUTHORIZED]: "Unauthorized",
160
160
  [const_1.HTTP_FORBIDDEN]: "Forbidden",
161
161
  };
162
- const baseUrl = (0, config_1.defineConfig)('base_url', '');
162
+ exports.baseUrl = (0, config_1.defineConfig)('base_url', '');
163
163
  async function sendFolderList(node, ctx) {
164
164
  let { depth = 0, folders, prepend } = ctx.query;
165
165
  ctx.type = 'text';
166
166
  if (prepend === undefined || prepend === '*') { // * = force auto-detection even if we have baseUrl set
167
167
  const { URL } = ctx;
168
- const base = prepend === undefined && baseUrl.get()
168
+ const base = prepend === undefined && exports.baseUrl.get()
169
169
  || URL.protocol + '//' + URL.host + ctx.state.revProxyPath;
170
170
  prepend = base + ctx.originalUrl.split('?')[0];
171
171
  }
@@ -179,6 +179,13 @@ async function sendFolderList(node, ctx) {
179
179
  let proxyDetected;
180
180
  const someSecurity = async (ctx, next) => {
181
181
  ctx.request.ip = (0, connections_1.normalizeIp)(ctx.ip);
182
+ // don't allow sessions to change ip
183
+ const ss = ctx.session;
184
+ if (ss === null || ss === void 0 ? void 0 : ss.username)
185
+ if (!ss.ip)
186
+ ss.ip = ctx.ip;
187
+ else if (ss.ip !== ctx.ip)
188
+ delete ss.username;
182
189
  try {
183
190
  if ((0, misc_1.dirTraversal)(decodeURI(ctx.path)))
184
191
  return ctx.status = const_1.HTTP_FOOL;
package/src/plugins.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.parsePluginSource = exports.rescan = exports.pluginsConfig = exports.enablePlugins = exports.pluginsWatcher = exports.getAvailablePlugins = exports.Plugin = exports.pluginsMiddleware = exports.getPluginConfigFields = exports.mapPlugins = exports.getPluginInfo = exports.setPluginConfig = exports.enablePlugin = exports.isPluginRunning = exports.DISABLING_POSTFIX = exports.PATH = void 0;
30
+ exports.parsePluginSource = exports.rescan = exports.pluginsConfig = exports.enablePlugins = exports.pluginsWatcher = exports.getAvailablePlugins = exports.Plugin = exports.pluginsMiddleware = exports.getPluginConfigFields = exports.findPluginByRepo = exports.mapPlugins = exports.getPluginInfo = exports.setPluginConfig = exports.startPlugin = exports.stopPlugin = exports.enablePlugin = exports.isPluginEnabled = exports.isPluginRunning = exports.STORAGE_FOLDER = exports.DISABLING_POSTFIX = exports.PATH = void 0;
31
31
  const fast_glob_1 = __importDefault(require("fast-glob"));
32
32
  const watchLoad_1 = require("./watchLoad");
33
33
  const lodash_1 = __importDefault(require("lodash"));
@@ -44,18 +44,48 @@ const path_1 = require("path");
44
44
  const customHtml_1 = require("./customHtml");
45
45
  exports.PATH = 'plugins';
46
46
  exports.DISABLING_POSTFIX = '-disabled';
47
+ exports.STORAGE_FOLDER = 'storage';
47
48
  const plugins = {};
48
49
  function isPluginRunning(id) {
49
50
  var _a;
50
- return (_a = plugins[id]) === null || _a === void 0 ? void 0 : _a.started;
51
+ return Boolean((_a = plugins[id]) === null || _a === void 0 ? void 0 : _a.started);
51
52
  }
52
53
  exports.isPluginRunning = isPluginRunning;
54
+ function isPluginEnabled(id) {
55
+ return exports.enablePlugins.get().includes(id);
56
+ }
57
+ exports.isPluginEnabled = isPluginEnabled;
53
58
  function enablePlugin(id, state = true) {
54
- exports.enablePlugins.set(arr => arr.includes(id) === state ? arr
55
- : state ? [...arr, id]
56
- : arr.filter((x) => x !== id));
59
+ if (state && !getPluginInfo(id))
60
+ throw Error('miss');
61
+ exports.enablePlugins.set(arr => {
62
+ if (arr.includes(id) === state)
63
+ return arr;
64
+ console.log("switching plugin", id, state ? "on" : "off");
65
+ return arr.includes(id) === state ? arr
66
+ : state ? [...arr, id]
67
+ : arr.filter((x) => x !== id);
68
+ });
57
69
  }
58
70
  exports.enablePlugin = enablePlugin;
71
+ async function stopPlugin(id) {
72
+ enablePlugin(id, false);
73
+ await waitRunning(id, false);
74
+ }
75
+ exports.stopPlugin = stopPlugin;
76
+ async function startPlugin(id) {
77
+ enablePlugin(id);
78
+ await waitRunning(id);
79
+ }
80
+ exports.startPlugin = startPlugin;
81
+ async function waitRunning(id, state = true) {
82
+ while (isPluginRunning(id) !== state) {
83
+ await (0, misc_1.wait)(500);
84
+ const error = getError(id);
85
+ if (error)
86
+ throw Error(error);
87
+ }
88
+ }
59
89
  // nullish values are equivalent to defaultValues
60
90
  function setPluginConfig(id, changes) {
61
91
  exports.pluginsConfig.set(allConfigs => {
@@ -83,53 +113,56 @@ function mapPlugins(cb) {
83
113
  }).filter(x => x !== undefined);
84
114
  }
85
115
  exports.mapPlugins = mapPlugins;
116
+ function findPluginByRepo(repo) {
117
+ return lodash_1.default.find(plugins, pl => { var _a; return ((_a = pl.getData()) === null || _a === void 0 ? void 0 : _a.repo) === repo; })
118
+ || lodash_1.default.find(availablePlugins, { repo });
119
+ }
120
+ exports.findPluginByRepo = findPluginByRepo;
86
121
  function getPluginConfigFields(id) {
87
122
  var _a;
88
123
  return (_a = plugins[id]) === null || _a === void 0 ? void 0 : _a.getData().config;
89
124
  }
90
125
  exports.getPluginConfigFields = getPluginConfigFields;
91
- function pluginsMiddleware() {
92
- return async (ctx, next) => {
93
- var _a;
94
- const after = {};
95
- // run middleware plugins
96
- for (const [id, pl] of Object.entries(plugins))
97
- try {
98
- const res = await ((_a = pl.middleware) === null || _a === void 0 ? void 0 : _a.call(pl, ctx));
99
- if (res === true)
100
- ctx.pluginStopped = true;
101
- if (typeof res === 'function')
102
- after[id] = res;
103
- }
104
- catch (e) {
105
- printError(id, e);
106
- }
107
- // expose public plugins' files
108
- const { path } = ctx;
109
- if (!ctx.pluginStopped) {
110
- if (path.startsWith(const_1.PLUGINS_PUB_URI)) {
111
- const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
112
- const name = a.shift();
113
- if (plugins.hasOwnProperty(name)) // do it only if the plugin is loaded
114
- await (0, serveFile_1.serveFile)(ctx, plugins[name].folder + '/public/' + a.join('/'), 'auto');
115
- return;
116
- }
117
- await next();
126
+ const pluginsMiddleware = async (ctx, next) => {
127
+ var _a;
128
+ const after = {};
129
+ // run middleware plugins
130
+ for (const [id, pl] of Object.entries(plugins))
131
+ try {
132
+ const res = await ((_a = pl.middleware) === null || _a === void 0 ? void 0 : _a.call(pl, ctx));
133
+ if (res === true)
134
+ ctx.pluginStopped = true;
135
+ if (typeof res === 'function')
136
+ after[id] = res;
118
137
  }
119
- for (const [id, f] of Object.entries(after))
120
- try {
121
- await f();
122
- }
123
- catch (e) {
124
- printError(id, e);
125
- }
126
- };
127
- function printError(id, e) {
128
- console.log('error middleware plugin', id, String(e));
129
- console.debug(e);
138
+ catch (e) {
139
+ printError(id, e);
140
+ }
141
+ // expose public plugins' files
142
+ const { path } = ctx;
143
+ if (!ctx.pluginStopped) {
144
+ if (path.startsWith(const_1.PLUGINS_PUB_URI)) {
145
+ const a = path.substring(const_1.PLUGINS_PUB_URI.length).split('/');
146
+ const name = a.shift();
147
+ if (plugins.hasOwnProperty(name)) // do it only if the plugin is loaded
148
+ await (0, serveFile_1.serveFile)(ctx, plugins[name].folder + '/public/' + a.join('/'), 'auto');
149
+ return;
150
+ }
151
+ await next();
130
152
  }
131
- }
153
+ for (const [id, f] of Object.entries(after))
154
+ try {
155
+ await f();
156
+ }
157
+ catch (e) {
158
+ printError(id, e);
159
+ }
160
+ };
132
161
  exports.pluginsMiddleware = pluginsMiddleware;
162
+ function printError(id, e) {
163
+ console.log(`error middleware plugin ${id}: ${(e === null || e === void 0 ? void 0 : e.message) || e}`);
164
+ console.debug(e);
165
+ }
133
166
  class Plugin {
134
167
  constructor(id, folder, data, unwatch) {
135
168
  this.id = id;
@@ -139,9 +172,6 @@ class Plugin {
139
172
  this.started = new Date();
140
173
  if (!data)
141
174
  throw 'invalid data';
142
- if (plugins[id])
143
- throw "unload first: " + id;
144
- plugins[id] = this;
145
175
  this.data = data = { ...data }; // clone to make object modifiable. Objects coming from import are not.
146
176
  // some validation
147
177
  for (const k of ['frontend_css', 'frontend_js']) {
@@ -154,6 +184,10 @@ class Plugin {
154
184
  }
155
185
  }
156
186
  }
187
+ get version() {
188
+ var _a;
189
+ return (_a = this.data) === null || _a === void 0 ? void 0 : _a.version;
190
+ }
157
191
  get middleware() {
158
192
  var _a;
159
193
  return (_a = this.data) === null || _a === void 0 ? void 0 : _a.middleware;
@@ -175,6 +209,9 @@ class Plugin {
175
209
  }
176
210
  async unload(reloading = false) {
177
211
  var _a, _b;
212
+ if (!this.started)
213
+ return;
214
+ this.started = null;
178
215
  const { id } = this;
179
216
  try {
180
217
  await ((_b = (_a = this.data) === null || _a === void 0 ? void 0 : _a.unload) === null || _b === void 0 ? void 0 : _b.call(_a));
@@ -184,14 +221,9 @@ class Plugin {
184
221
  catch (e) {
185
222
  console.log('error unloading plugin', id, String(e));
186
223
  }
187
- delete plugins[id];
188
- if (reloading)
189
- return;
224
+ if (this.data)
225
+ this.data.unload = undefined;
190
226
  this.unwatch();
191
- if (availablePlugins[id])
192
- events_1.default.emit('pluginStopped', availablePlugins[id]);
193
- else
194
- events_1.default.emit('pluginUninstalled', id);
195
227
  }
196
228
  }
197
229
  exports.Plugin = Plugin;
@@ -210,57 +242,86 @@ exports.pluginsWatcher = (0, misc_1.watchDir)(exports.PATH, rescanAsap);
210
242
  exports.enablePlugins = (0, config_1.defineConfig)('enable_plugins', ['antibrute']);
211
243
  exports.enablePlugins.sub(rescanAsap);
212
244
  exports.pluginsConfig = (0, config_1.defineConfig)('plugins_config', {});
245
+ const pluginWatchers = new Map();
213
246
  async function rescan() {
214
247
  console.debug('scanning plugins');
215
- const found = [];
216
- const foundDisabled = {};
217
- const MASK = exports.PATH + '/*/plugin.js'; // be sure to not use path.join as fast-glob doesn't work with \
218
- const pluginSources = [MASK];
248
+ const patterns = [exports.PATH + '/*'];
219
249
  if (const_1.APP_PATH !== process.cwd())
220
- pluginSources.push((0, misc_1.adjustStaticPathForGlob)(const_1.APP_PATH) + '/' + MASK);
221
- for (const f of await (0, fast_glob_1.default)(pluginSources)) {
222
- const id = f.split('/').slice(-2)[0];
223
- if (id.endsWith(exports.DISABLING_POSTFIX))
224
- continue;
225
- if (!exports.enablePlugins.get().includes(id)) {
226
- try {
227
- const source = await (0, promises_1.readFile)(f, 'utf8');
228
- foundDisabled[id] = parsePluginSource(id, source);
229
- }
230
- catch (_a) { }
250
+ patterns.push((0, misc_1.adjustStaticPathForGlob)(const_1.APP_PATH) + '/' + patterns[0]);
251
+ const met = [];
252
+ for (const { path, dirent } of await (0, fast_glob_1.default)(patterns, { onlyFiles: false, suppressErrors: true, objectMode: true })) {
253
+ if (!dirent.isDirectory() || path.endsWith(exports.DISABLING_POSTFIX))
231
254
  continue;
232
- }
233
- if (found.includes(id)) // not twice
234
- continue;
235
- found.push(id);
236
- if (!plugins[id]) // already loaded
237
- loadPlugin(id, f);
238
- }
239
- for (const [id, p] of Object.entries(foundDisabled)) {
240
- const a = availablePlugins[id];
241
- if ((0, misc_1.same)(a, p))
255
+ const id = path.split('/').slice(-1)[0];
256
+ met.push(id);
257
+ const w = pluginWatchers.get(id);
258
+ if (w)
242
259
  continue;
243
- availablePlugins[id] = p;
244
- if (a)
245
- events_1.default.emit('pluginUpdated', p);
246
- else if (!plugins[id])
247
- events_1.default.emit('pluginInstalled', p);
260
+ console.debug('plugin watch', id);
261
+ pluginWatchers.set(id, watchPlugin(id, (0, path_1.join)(path, 'plugin.js')));
248
262
  }
249
- for (const id in availablePlugins)
250
- if (!foundDisabled[id] && !found.includes(id) && !plugins[id]) {
251
- delete availablePlugins[id];
252
- events_1.default.emit('pluginUninstalled', id);
263
+ for (const [id, cancelWatcher] of pluginWatchers.entries())
264
+ if (!met.includes(id)) {
265
+ enablePlugin(id, false);
266
+ console.debug('plugin unwatch', id);
267
+ cancelWatcher();
268
+ pluginWatchers.delete(id);
253
269
  }
254
- for (const [id, p] of Object.entries(plugins))
255
- if (!found.includes(id))
256
- await p.unload();
257
270
  }
258
271
  exports.rescan = rescan;
259
- function loadPlugin(id, path) {
272
+ function watchPlugin(id, path) {
260
273
  const module = (0, path_1.resolve)(path);
261
- const { unwatch } = (0, watchLoad_1.watchLoad)(path, async () => {
274
+ let starting;
275
+ exports.enablePlugins.sub(() => {
276
+ if (!getPluginInfo(id))
277
+ return; // not loaded yet
278
+ const enabled = isPluginEnabled(id);
279
+ if (enabled !== isPluginRunning(id))
280
+ return enabled ? start() : stop();
281
+ });
282
+ const { unwatch } = (0, watchLoad_1.watchLoad)(module, async (source) => {
283
+ const notRunning = availablePlugins[id];
284
+ if (!source)
285
+ return onUninstalled();
286
+ if (isPluginEnabled(id))
287
+ return start();
288
+ const p = parsePluginSource(id, source);
289
+ if ((0, misc_1.same)(notRunning, p))
290
+ return;
291
+ availablePlugins[id] = p;
292
+ events_1.default.emit(notRunning ? 'pluginUpdated' : 'pluginInstalled', p);
293
+ });
294
+ return () => {
295
+ unwatch();
296
+ return onUninstalled();
297
+ };
298
+ async function onUninstalled() {
299
+ await stop();
300
+ delete availablePlugins[id];
301
+ events_1.default.emit('pluginUninstalled', id);
302
+ }
303
+ async function markItAvailable() {
304
+ delete plugins[id];
305
+ const source = await (0, promises_1.readFile)(module, 'utf8');
306
+ availablePlugins[id] = parsePluginSource(id, source);
307
+ }
308
+ async function stop() {
309
+ await starting;
310
+ const p = plugins[id];
311
+ if (!p)
312
+ return;
313
+ await p.unload();
314
+ await markItAvailable();
315
+ events_1.default.emit('pluginStopped', p);
316
+ }
317
+ async function start() {
262
318
  var _a;
319
+ if (starting)
320
+ return;
263
321
  try {
322
+ starting = (0, misc_1.pendingPromise)();
323
+ if (getPluginInfo(id))
324
+ setError(id, '');
264
325
  const alreadyRunning = plugins[id];
265
326
  console.log(alreadyRunning ? "reloading plugin" : "loading plugin", id);
266
327
  const { init, ...data } = await (_a = module, Promise.resolve().then(() => __importStar(require(_a))));
@@ -271,14 +332,17 @@ function loadPlugin(id, path) {
271
332
  console.log("plugin", id, data.badApi);
272
333
  await (alreadyRunning === null || alreadyRunning === void 0 ? void 0 : alreadyRunning.unload(true));
273
334
  console.debug("starting plugin", id);
335
+ const storageDir = (0, path_1.resolve)(module, '..', exports.STORAGE_FOLDER) + (const_1.IS_WINDOWS ? '\\' : '/');
336
+ await (0, promises_1.mkdir)(storageDir, { recursive: true });
274
337
  const res = await (init === null || init === void 0 ? void 0 : init.call(null, {
275
338
  srcDir: __dirname,
339
+ storageDir,
276
340
  const: Const,
277
341
  require,
278
342
  getConnections: connections_1.getConnections,
279
343
  events: events_1.default,
280
344
  log(...args) {
281
- console.log('plugin', id, ':', ...args);
345
+ console.log('plugin', id + ':', ...args);
282
346
  },
283
347
  getConfig: (cfgKey) => { var _a, _b, _c, _d, _e; return (_c = (_b = (_a = exports.pluginsConfig.get()) === null || _a === void 0 ? void 0 : _a[id]) === null || _b === void 0 ? void 0 : _b[cfgKey]) !== null && _c !== void 0 ? _c : (_e = (_d = data.config) === null || _d === void 0 ? void 0 : _d[cfgKey]) === null || _e === void 0 ? void 0 : _e.defaultValue; },
284
348
  setConfig: (cfgKey, value) => setPluginConfig(id, { [cfgKey]: value }),
@@ -298,13 +362,16 @@ function loadPlugin(id, path) {
298
362
  });
299
363
  },
300
364
  getHfsConfig: config_1.getConfig,
365
+ customApiCall(method, params) {
366
+ return mapPlugins(pl => { var _a, _b; return (_b = (_a = pl.getData().customApi) === null || _a === void 0 ? void 0 : _a[method]) === null || _b === void 0 ? void 0 : _b.call(_a, params); });
367
+ }
301
368
  }));
302
369
  const folder = (0, path_1.dirname)(module);
303
370
  Object.assign(data, res, {
304
371
  customHtml: (0, customHtml_1.newCustomHtmlState)()
305
372
  });
306
373
  const customHtmlWatcher = (0, customHtml_1.watchLoadCustomHtml)(data.customHtml, folder);
307
- const plugin = new Plugin(id, folder, data, lodash_1.default.flow(unwatch, customHtmlWatcher.unwatch));
374
+ const plugin = plugins[id] = new Plugin(id, folder, data, customHtmlWatcher.unwatch);
308
375
  if (alreadyRunning)
309
376
  events_1.default.emit('pluginUpdated', Object.assign(lodash_1.default.pick(plugin, 'started'), getPluginInfo(id)));
310
377
  else {
@@ -315,9 +382,23 @@ function loadPlugin(id, path) {
315
382
  }
316
383
  }
317
384
  catch (e) {
318
- console.log("plugin error:", e);
385
+ await markItAvailable();
386
+ e = e.message || String(e);
387
+ console.log(`plugin error: ${id}:`, e);
388
+ setError(id, e);
319
389
  }
320
- });
390
+ finally {
391
+ starting === null || starting === void 0 ? void 0 : starting.resolve();
392
+ starting = undefined;
393
+ }
394
+ }
395
+ }
396
+ function getError(id) {
397
+ return getPluginInfo(id).error;
398
+ }
399
+ function setError(id, error) {
400
+ getPluginInfo(id).error = error;
401
+ events_1.default.emit('pluginUpdated', { id, error });
321
402
  }
322
403
  function deleteModule(id) {
323
404
  var _a;
@@ -344,12 +425,14 @@ function deleteModule(id) {
344
425
  }
345
426
  (0, misc_1.onProcessExit)(() => Promise.allSettled(mapPlugins(pl => pl.unload())));
346
427
  function parsePluginSource(id, source) {
347
- var _a, _b, _c, _d, _e, _f;
428
+ var _a, _b, _c, _d, _e, _f, _g, _h;
348
429
  const pl = { id };
349
430
  pl.description = (0, misc_1.tryJson)((_a = /exports.description *= *(".*")/.exec(source)) === null || _a === void 0 ? void 0 : _a[1]);
350
431
  pl.repo = (_b = /exports.repo *= *"(.*)"/.exec(source)) === null || _b === void 0 ? void 0 : _b[1];
351
432
  pl.version = (_d = Number((_c = /exports.version *= *(\d*\.?\d+)/.exec(source)) === null || _c === void 0 ? void 0 : _c[1])) !== null && _d !== void 0 ? _d : undefined;
352
433
  pl.apiRequired = (_f = (0, misc_1.tryJson)((_e = /exports.apiRequired *= *([ \d.,[\]]+)/.exec(source)) === null || _e === void 0 ? void 0 : _e[1])) !== null && _f !== void 0 ? _f : undefined;
434
+ pl.depend = (_h = (0, misc_1.tryJson)((_g = /exports.depend *= *(\[.*\])/m.exec(source)) === null || _g === void 0 ? void 0 : _g[1])) === null || _h === void 0 ? void 0 : _h.filter((x) => typeof x.repo === 'string' && x.version === undefined || typeof x.version === 'number'
435
+ || console.warn("plugin dependency discarded", x));
353
436
  if (Array.isArray(pl.apiRequired) && (pl.apiRequired.length !== 2 || !pl.apiRequired.every(lodash_1.default.isFinite))) // validate [from,to] form
354
437
  pl.apiRequired = undefined;
355
438
  calculateBadApi(pl);
package/src/serveFile.js CHANGED
@@ -14,6 +14,7 @@ const misc_1 = require("./misc");
14
14
  const lodash_1 = __importDefault(require("lodash"));
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const util_1 = require("util");
17
+ const connections_1 = require("./connections");
17
18
  const allowedReferer = (0, config_1.defineConfig)('allowed_referer', '');
18
19
  function serveFileNode(ctx, node) {
19
20
  var _a;
@@ -27,15 +28,15 @@ function serveFileNode(ctx, node) {
27
28
  if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
28
29
  && !(0, misc_1.matches)(ref, allowed))
29
30
  return ctx.status = const_1.HTTP_FORBIDDEN;
30
- function host() {
31
- const s = ctx.get('host');
32
- return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s === null || s === void 0 ? void 0 : s.split(':')[0];
33
- }
34
31
  }
35
32
  ctx.vfsNode = node; // useful to tell service files from files shared by the user
36
33
  if ('dl' in ctx.query) // please, download
37
34
  ctx.attachment(name);
38
35
  return serveFile(ctx, source || '', mimeString);
36
+ function host() {
37
+ const s = ctx.get('host');
38
+ return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s === null || s === void 0 ? void 0 : s.split(':')[0];
39
+ }
39
40
  }
40
41
  exports.serveFileNode = serveFileNode;
41
42
  const mimeCfg = (0, config_1.defineConfig)('mime', { '*': 'auto' });
@@ -65,8 +66,15 @@ async function serveFile(ctx, source, mime, content) {
65
66
  return ctx.status = const_1.HTTP_NOT_MODIFIED;
66
67
  if (content !== undefined)
67
68
  return ctx.body = content;
68
- const range = getRange(ctx, stats.size);
69
+ const { size } = stats;
70
+ const range = getRange(ctx, size);
69
71
  ctx.body = (0, fs_1.createReadStream)(source, range);
72
+ if (ctx.vfsNode)
73
+ (0, connections_1.updateConnection)(ctx.state.connection, {
74
+ op: 'download',
75
+ opTotal: stats.size,
76
+ opOffset: range && (range.start / size),
77
+ });
70
78
  }
71
79
  catch (e) {
72
80
  return ctx.status = const_1.HTTP_NOT_FOUND;