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
@@ -42,6 +42,7 @@ const customHtml_1 = require("./customHtml");
42
42
  const lodash_1 = __importDefault(require("lodash"));
43
43
  const config_1 = require("./config");
44
44
  const lang_1 = require("./lang");
45
+ const logGui = (0, config_1.defineConfig)('log_gui', false);
45
46
  // in case of dev env we have our static files within the 'dist' folder'
46
47
  const DEV_STATIC = process.env.DEV ? 'dist/' : '';
47
48
  function serveStatic(uri) {
@@ -49,6 +50,8 @@ function serveStatic(uri) {
49
50
  let cache = {};
50
51
  (0, vanilla_1.subscribe)(customHtml_1.customHtmlState, () => cache = {}); // reset cache at every change
51
52
  return async (ctx) => {
53
+ if (!logGui.get())
54
+ ctx.state.dont_log = true;
52
55
  if (ctx.method === 'OPTIONS') {
53
56
  ctx.status = const_1.HTTP_NO_CONTENT;
54
57
  ctx.set({ Allow: 'OPTIONS, GET' });
@@ -104,7 +107,7 @@ async function treatIndex(ctx, filesUri, body) {
104
107
  .replace('<body>', () => `<body>
105
108
  ${!isFrontend ? '' : `
106
109
  <title>${adminApis_1.title.get()}</title>
107
- <link rel="icon" href="${adminApis_1.favicon.get() ? '/favicon.ico' : 'data:;'}" />
110
+ <link rel="shortcut icon" href="${adminApis_1.favicon.get() ? '/favicon.ico' : '#'}" />
108
111
  `}
109
112
  <script>
110
113
  HFS = ${JSON.stringify({
@@ -120,13 +123,6 @@ async function treatIndex(ctx, filesUri, body) {
120
123
  }, null, 4)
121
124
  .replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
122
125
  document.documentElement.setAttribute('ver', '${const_1.VERSION.split('-')[0] /*for style selectors*/}')
123
- function getScriptAttr(k) {
124
- return document.currentScript?.getAttribute(k)
125
- || console.error("this function must be called at the very top of your file")
126
- }
127
- HFS.getPluginKey = () => getScriptAttr('plugin')
128
- HFS.getPluginConfig = () => HFS.plugins[HFS.getPluginKey()]
129
- HFS.getPluginPublic = () => getScriptAttr('src')?.match(/^.*\\//)[0]
130
126
  </script>
131
127
  <style>
132
128
  :root {
@@ -167,8 +163,10 @@ function serveProxied(port, uri) {
167
163
  : adjustBundlerLinks(ctx, uri, data);
168
164
  }
169
165
  }));
170
- return function () {
171
- return proxy.apply(this, arguments);
166
+ return function (ctx, next) {
167
+ if (!logGui.get())
168
+ ctx.state.dont_log = true;
169
+ return proxy(ctx, next);
172
170
  };
173
171
  }
174
172
  function serveGuiFiles(proxyPort, uri) {
package/src/throttler.js CHANGED
@@ -35,18 +35,23 @@ const throttler = async (ctx, next) => {
35
35
  if (!conn)
36
36
  throw 'assert throttler connection';
37
37
  const ts = conn[SymThrStr] = new ThrottledStream_1.ThrottledStream(ipGroup.group, conn[SymThrStr]);
38
+ const offset = ts.getBytesSent();
38
39
  let closed = false;
39
40
  const DELAY = 1000;
40
41
  const update = lodash_1.default.debounce(() => {
41
42
  const ts = conn[SymThrStr];
42
43
  const outSpeed = roundSpeed(ts.getSpeed());
43
- (0, connections_1.updateConnection)(conn, { outSpeed, sent: ts.getBytesSent() });
44
+ (0, connections_1.updateConnection)(conn, {
45
+ outSpeed,
46
+ sent: conn.socket.bytesWritten,
47
+ opProgress: (conn.opOffset || 0) + ts.getBytesSent() / conn.opTotal,
48
+ });
44
49
  /* in case this stream stands still for a while (before the end), we'll have neither 'sent' or 'close' events,
45
50
  * so who will take care to updateConnection? This artificial next-call will ensure just that */
46
51
  clearTimeout(conn[SymTimeout]);
47
52
  if (outSpeed || !closed)
48
53
  conn[SymTimeout] = setTimeout(update, DELAY);
49
- }, DELAY, { maxWait: DELAY });
54
+ }, DELAY, { leading: true, maxWait: DELAY });
50
55
  ts.on('sent', (n) => {
51
56
  exports.totalSent += n;
52
57
  update();
@@ -61,12 +66,12 @@ const throttler = async (ctx, next) => {
61
66
  (_a = ipGroup.destroy) === null || _a === void 0 ? void 0 : _a.call(ipGroup);
62
67
  delete ip2group[ctx.ip];
63
68
  });
64
- const bak = ctx.response.length; // preserve
69
+ const downloadTotal = ctx.response.length;
65
70
  ctx.body = ctx.body.pipe(ts);
66
- if (bak)
67
- ctx.response.length = bak;
71
+ if (downloadTotal) // preserve this info
72
+ ctx.response.length = downloadTotal;
68
73
  ts.once('end', () => // in case of compressed response, we offer calculation of real size
69
- ctx.state.length = ts.getBytesSent());
74
+ ctx.state.length = ts.getBytesSent() - offset);
70
75
  };
71
76
  exports.throttler = throttler;
72
77
  function roundSpeed(n) {
package/src/update.js CHANGED
@@ -15,10 +15,7 @@ const plugins_1 = require("./plugins");
15
15
  const promises_1 = require("fs/promises");
16
16
  const open_1 = __importDefault(require("open"));
17
17
  async function getUpdate() {
18
- const [latest] = await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases?per_page=1');
19
- if (latest.name === const_1.VERSION)
20
- throw "you already have the latest version: " + const_1.VERSION;
21
- return latest;
18
+ return (await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases?per_page=1'))[0];
22
19
  }
23
20
  exports.getUpdate = getUpdate;
24
21
  const LOCAL_UPDATE = 'hfs-update.zip'; // update from file takes precedence over net
package/src/upload.js CHANGED
@@ -3,7 +3,7 @@ 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.uploadWriter = exports.minAvailableMb = exports.deleteUnfinishedUploadsAfter = void 0;
6
+ exports.uploadWriter = exports.getUploadMeta = exports.minAvailableMb = exports.deleteUnfinishedUploadsAfter = void 0;
7
7
  const vfs_1 = require("./vfs");
8
8
  const const_1 = require("./const");
9
9
  const path_1 = require("path");
@@ -14,11 +14,23 @@ const config_1 = require("./config");
14
14
  const util_os_1 = require("./util-os");
15
15
  const connections_1 = require("./connections");
16
16
  const throttler_1 = require("./throttler");
17
- const lodash_1 = __importDefault(require("lodash"));
17
+ const perm_1 = require("./perm");
18
18
  exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)('delete_unfinished_uploads_after', 86400);
19
19
  exports.minAvailableMb = (0, config_1.defineConfig)('min_available_mb', 100);
20
20
  const dontOverwriteUploading = (0, config_1.defineConfig)('dont_overwrite_uploading', false);
21
21
  const waitingToBeDeleted = {};
22
+ const ATTR_UPLOADER = 'uploader';
23
+ function getUploadMeta(path) {
24
+ return (0, misc_1.loadFileAttr)(path, ATTR_UPLOADER);
25
+ }
26
+ exports.getUploadMeta = getUploadMeta;
27
+ function setUploadMeta(path, ctx) {
28
+ return (0, misc_1.storeFileAttr)(path, ATTR_UPLOADER, {
29
+ username: (0, perm_1.getCurrentUsername)(ctx),
30
+ ip: ctx.ip,
31
+ });
32
+ }
33
+ // stay sync because we use this function with formidable()
22
34
  function uploadWriter(base, path, ctx) {
23
35
  if ((0, misc_1.dirTraversal)(path))
24
36
  return fail(const_1.HTTP_FOOL);
@@ -33,21 +45,22 @@ function uploadWriter(base, path, ctx) {
33
45
  const free = (0, util_os_1.getFreeDiskSync)(dir);
34
46
  if (typeof free !== 'number' || isNaN(free))
35
47
  throw '';
36
- if (reqSize > (0, util_os_1.getFreeDiskSync)(dir) - (min || 0))
48
+ if (reqSize > free - (min || 0))
37
49
  return fail(const_1.HTTP_PAYLOAD_TOO_LARGE);
38
50
  }
39
- catch (e) {
51
+ catch (e) { // warn, but let it through
40
52
  console.warn("can't check disk size:", e.message || String(e));
41
53
  }
42
54
  if (ctx.query.skipExisting && fs_1.default.existsSync(fullPath))
43
55
  return fail(const_1.HTTP_CONFLICT);
44
- fs_1.default.mkdirSync(dir, { recursive: true });
56
+ if (fs_1.default.mkdirSync(dir, { recursive: true }))
57
+ setUploadMeta(dir, ctx);
45
58
  const keepName = (0, path_1.basename)(fullPath).slice(-200);
46
59
  let tempName = (0, path_1.join)(dir, 'hfs$upload-' + keepName);
47
60
  const resumable = fs_1.default.existsSync(tempName) && tempName;
48
61
  if (resumable)
49
62
  tempName = (0, path_1.join)(dir, 'hfs$upload2-' + keepName);
50
- const resume = Number(ctx.query.resume);
63
+ let resume = Number(ctx.query.resume);
51
64
  const size = resumable && (0, misc_1.try_)(() => fs_1.default.statSync(resumable).size);
52
65
  if (size === undefined) // stat failed
53
66
  return fail(const_1.HTTP_SERVER_ERROR);
@@ -62,6 +75,8 @@ function uploadWriter(base, path, ctx) {
62
75
  }));
63
76
  }
64
77
  const resuming = resume && resumable;
78
+ if (!resuming)
79
+ resume = 0;
65
80
  const ret = resuming ? fs_1.default.createWriteStream(resumable, { flags: 'r+', start: resume })
66
81
  : fs_1.default.createWriteStream(tempName);
67
82
  if (resuming) {
@@ -70,9 +85,10 @@ function uploadWriter(base, path, ctx) {
70
85
  }
71
86
  cancelDeletion(tempName);
72
87
  trackProgress();
73
- ret.once('close', () => {
88
+ ret.once('close', async () => {
74
89
  if (!ctx.req.aborted) {
75
90
  let dest = fullPath;
91
+ await setUploadMeta(tempName, ctx);
76
92
  if (dontOverwriteUploading.get() && fs_1.default.existsSync(dest)) {
77
93
  const ext = (0, path_1.extname)(dest);
78
94
  const base = dest.slice(0, -ext.length);
@@ -102,14 +118,15 @@ function uploadWriter(base, path, ctx) {
102
118
  if (!conn)
103
119
  return () => { };
104
120
  ctx.state.uploadPath = ctx.path + path;
105
- (0, connections_1.updateConnection)(conn, { ctx });
121
+ const opTotal = reqSize + resume;
122
+ (0, connections_1.updateConnection)(conn, { ctx, op: 'upload', opTotal, opOffset: resume / opTotal });
106
123
  const h = setInterval(() => {
107
124
  const now = Date.now();
108
125
  const got = ret.bytesWritten;
109
126
  const inSpeed = (0, throttler_1.roundSpeed)((got - lastGot) / (now - lastGotTime));
110
127
  lastGot = got;
111
128
  lastGotTime = now;
112
- (0, connections_1.updateConnection)(conn, { inSpeed, got, uploadProgress: lodash_1.default.round(got / reqSize, 3) });
129
+ (0, connections_1.updateConnection)(conn, { inSpeed, got, opProgress: (resume + got) / opTotal });
113
130
  }, 1000);
114
131
  ret.once('close', () => clearInterval(h));
115
132
  }
package/src/util-files.js CHANGED
@@ -4,9 +4,10 @@ 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.isValidFileName = exports.createFileWithPath = exports.prepareFolder = exports.unzip = exports.dirStream = exports.adjustStaticPathForGlob = exports.isWindowsDrive = exports.dirTraversal = exports.watchDir = exports.readFileBusy = exports.isDirectory = void 0;
7
+ exports.loadFileAttr = exports.storeFileAttr = exports.isValidFileName = exports.createFileWithPath = exports.prepareFolder = exports.unzip = exports.dirStream = exports.adjustStaticPathForGlob = exports.isWindowsDrive = exports.dirTraversal = exports.watchDir = exports.readFileBusy = exports.isDirectory = void 0;
8
8
  const promises_1 = __importDefault(require("fs/promises"));
9
9
  const misc_1 = require("./misc");
10
+ const util_1 = require("util");
10
11
  const fs_1 = require("fs");
11
12
  const path_1 = require("path");
12
13
  const fast_glob_1 = __importDefault(require("fast-glob"));
@@ -15,6 +16,8 @@ const util_os_1 = require("./util-os");
15
16
  const stream_1 = require("stream");
16
17
  // @ts-ignore
17
18
  const unzip_stream_1 = __importDefault(require("unzip-stream"));
19
+ // @ts-ignore
20
+ const fs_extended_attributes_1 = __importDefault(require("fs-extended-attributes"));
18
21
  async function isDirectory(path) {
19
22
  try {
20
23
  return (await promises_1.default.stat(path)).isDirectory();
@@ -118,17 +121,19 @@ async function unzip(stream, cb) {
118
121
  let pending = Promise.resolve();
119
122
  return new Promise(resolve => stream.pipe(unzip_stream_1.default.Parse())
120
123
  .on('end', () => pending.then(resolve))
121
- .on('entry', async (entry) => {
124
+ .on('entry', (entry) => pending = pending.then(async () => {
122
125
  const { path, type } = entry;
123
- const dest = cb(path);
126
+ const dest = await (0, misc_1.try_)(() => cb(path), e => {
127
+ console.warn(String(e));
128
+ return false;
129
+ });
124
130
  if (!dest || type !== 'File')
125
131
  return entry.autodrain();
126
- await pending; // don't overlap writings
127
132
  console.debug('unzip', dest);
128
133
  await prepareFolder(dest);
129
134
  const thisFile = entry.pipe((0, fs_1.createWriteStream)(dest));
130
- pending = (0, stream_1.once)(thisFile, 'finish');
131
- }));
135
+ await (0, stream_1.once)(thisFile, 'finish');
136
+ })));
132
137
  }
133
138
  exports.unzip = unzip;
134
139
  async function prepareFolder(path, dirnameIt = true) {
@@ -161,3 +166,17 @@ function isValidFileName(name) {
161
166
  return !/^\.\.?$|[/:*?"<>|\\]/.test(name);
162
167
  }
163
168
  exports.isValidFileName = isValidFileName;
169
+ const FILE_ATTR_PREFIX = 'user.hfs.'; // user. prefix to be linux compatible
170
+ function storeFileAttr(path, k, v) {
171
+ return (0, util_1.promisify)(fs_extended_attributes_1.default.set)(path, FILE_ATTR_PREFIX + k, JSON.stringify(v))
172
+ .then(() => true, (e) => {
173
+ console.error("couldn't store metadata on", path, String(e.message || e));
174
+ return false;
175
+ });
176
+ }
177
+ exports.storeFileAttr = storeFileAttr;
178
+ async function loadFileAttr(path, k) {
179
+ var _a;
180
+ return (_a = (0, misc_1.tryJson)(String(await (0, util_1.promisify)(fs_extended_attributes_1.default.get)(path, FILE_ATTR_PREFIX + k)))) !== null && _a !== void 0 ? _a : undefined; // normalize, as we get null instead of undefined on windows
181
+ }
182
+ exports.loadFileAttr = loadFileAttr;
package/src/vfs.js CHANGED
@@ -267,20 +267,14 @@ function masksCouldGivePermission(masks, perm) {
267
267
  }
268
268
  exports.masksCouldGivePermission = masksCouldGivePermission;
269
269
  function parentMaskApplier(parent) {
270
- const matchers = Object.entries(parent.masks || {}).map(([k, v]) => {
270
+ const matchers = (0, misc_1.onlyTruthy)(Object.entries(parent.masks || {}).map(([k, { maskOnly, ...mods }]) => {
271
271
  k = k.startsWith('**/') ? k.slice(3) : !k.includes('/') ? k : '';
272
- if (!k)
273
- return;
274
- const m = (0, misc_1.makeMatcher)(k);
275
- return [m, v];
276
- });
277
- return (item, virtualBasename) => {
278
- if (virtualBasename === undefined)
279
- virtualBasename = getNodeName(item);
280
- for (const entry of matchers) {
281
- if (!entry)
272
+ return k && { mods, maskOnly, matcher: (0, misc_1.makeMatcher)(k) };
273
+ }));
274
+ return (item, virtualBasename = getNodeName(item)) => {
275
+ for (const { matcher, mods, maskOnly } of matchers) {
276
+ if (maskOnly === 'folders' && !item.isFolder || maskOnly === 'files' && item.isFolder)
282
277
  continue;
283
- const [matcher, mods] = entry;
284
278
  if (!matcher(virtualBasename))
285
279
  continue;
286
280
  if (item.masks)
package/src/zip.js CHANGED
@@ -21,7 +21,7 @@ async function zipStreamFromFolder(node, ctx) {
21
21
  ctx.mime = 'zip';
22
22
  // ctx.query.list is undefined | string | string[]
23
23
  const list = (_a = (0, misc_1.wantArray)(ctx.query.list)[0]) === null || _a === void 0 ? void 0 : _a.split('*'); // we are using * as separator because it cannot be used in a file name and doesn't need url encoding
24
- const name = (list === null || list === void 0 ? void 0 : list.length) === 1 ? (0, path_1.basename)(list[0]) : (0, vfs_1.getNodeName)(node);
24
+ const name = (list === null || list === void 0 ? void 0 : list.length) === 1 ? decodeURIComponent((0, path_1.basename)(list[0])) : (0, vfs_1.getNodeName)(node);
25
25
  ctx.attachment(((0, misc_1.isWindowsDrive)(name) ? name[0] : (name || 'archive')) + '.zip');
26
26
  const filter = (0, misc_1.pattern2filter)(String(ctx.query.search || ''));
27
27
  const walker = !list ? (0, vfs_1.walkNode)(node, ctx, Infinity, '', 'can_read')
@@ -33,7 +33,7 @@ async function zipStreamFromFolder(node, ctx) {
33
33
  if (await (0, vfs_1.nodeIsDirectory)(subNode)) { // a directory needs to walked
34
34
  if ((0, vfs_1.hasPermission)(subNode, 'can_list', ctx)) {
35
35
  yield subNode; // it could be empty
36
- yield* (0, vfs_1.walkNode)(subNode, ctx, Infinity, uri + '/', 'can_read');
36
+ yield* (0, vfs_1.walkNode)(subNode, ctx, Infinity, decodeURI(uri) + '/', 'can_read');
37
37
  }
38
38
  continue;
39
39
  }