hfs 0.48.0-alpha2 → 0.48.0-beta6

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.
@@ -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
  }
@@ -38,11 +38,12 @@ const vfs_1 = require("./vfs");
38
38
  const promises_1 = require("fs/promises");
39
39
  const path_1 = require("path");
40
40
  const upload_1 = require("./upload");
41
+ const misc_1 = require("./misc");
41
42
  exports.frontEndApis = {
42
43
  get_file_list: api_file_list_1.get_file_list,
43
44
  ...api_auth,
44
45
  get_notifications({ channel }, ctx) {
45
- apiAssertTypes({ string: { channel } });
46
+ (0, misc_1.apiAssertTypes)({ string: { channel } });
46
47
  const list = new apiMiddleware_1.SendListReadable();
47
48
  list.ready(); // on chrome109 EventSource doesn't emit 'open' until something is sent
48
49
  return list.events(ctx, {
@@ -55,7 +56,7 @@ exports.frontEndApis = {
55
56
  if (typeof (uris === null || uris === void 0 ? void 0 : uris[0]) !== 'string')
56
57
  return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad uris');
57
58
  return {
58
- details: Promise.all(uris.map(async (uri) => {
59
+ details: await Promise.all(uris.map(async (uri) => {
59
60
  if (typeof uri !== 'string')
60
61
  return false; // false means error
61
62
  const node = await (0, vfs_1.urlToNode)(uri, ctx);
@@ -67,7 +68,7 @@ exports.frontEndApis = {
67
68
  };
68
69
  },
69
70
  async create_folder({ uri, name }, ctx) {
70
- apiAssertTypes({ string: { uri, name } });
71
+ (0, misc_1.apiAssertTypes)({ string: { uri, name } });
71
72
  if (!(0, util_files_1.isValidFileName)(name) || (0, util_files_1.dirTraversal)(name))
72
73
  return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad name');
73
74
  const parentNode = await (0, vfs_1.urlToNode)(uri, ctx);
@@ -84,7 +85,7 @@ exports.frontEndApis = {
84
85
  }
85
86
  },
86
87
  async delete({ uri }, ctx) {
87
- apiAssertTypes({ string: { uri } });
88
+ (0, misc_1.apiAssertTypes)({ string: { uri } });
88
89
  const node = await (0, vfs_1.urlToNode)(uri, ctx);
89
90
  if (!node)
90
91
  throw new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND);
@@ -101,7 +102,7 @@ exports.frontEndApis = {
101
102
  }
102
103
  },
103
104
  async rename({ uri, dest }, ctx) {
104
- apiAssertTypes({ string: { uri, dest } });
105
+ (0, misc_1.apiAssertTypes)({ string: { uri, dest } });
105
106
  if (dest.includes('/') || (0, util_files_1.dirTraversal)(dest))
106
107
  throw new apiMiddleware_1.ApiError(const_1.HTTP_FORBIDDEN);
107
108
  const node = await (0, vfs_1.urlToNode)(uri, ctx);
@@ -130,10 +131,3 @@ function notifyClient(ctx, name, data) {
130
131
  }
131
132
  exports.notifyClient = notifyClient;
132
133
  const NOTIFICATION_PREFIX = 'notificationChannel:';
133
- function apiAssertTypes(paramsByType) {
134
- for (const [types, params] of Object.entries(paramsByType))
135
- for (const type of types.split('_'))
136
- for (const [name, val] of Object.entries(params))
137
- if (typeof val !== type)
138
- throw new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad ' + name);
139
- }
package/src/github.js CHANGED
@@ -23,8 +23,17 @@ function downloadProgress(id, status) {
23
23
  downloading[id] = status;
24
24
  events_1.default.emit('pluginDownload_' + id, status);
25
25
  }
26
- async function downloadPlugin(repo, branch = '', overwrite) {
26
+ // determine default branch, possibly without consuming api quota
27
+ async function getGithubDefaultBranch(repo) {
28
+ var _a;
29
+ const res = await (0, misc_1.httpString)(`https://github.com/${repo}/archive/refs/heads/main.zip`, { method: 'HEAD' });
30
+ return res.ok ? 'main'
31
+ : (_a = (await getRepoInfo(repo))) === null || _a === void 0 ? void 0 : _a.default_branch;
32
+ }
33
+ async function downloadPlugin(repo, { branch = '', overwrite = false } = {}) {
27
34
  var _a, _b, _c;
35
+ if (typeof repo !== 'string')
36
+ repo = repo.main;
28
37
  if (downloading[repo])
29
38
  return new apiMiddleware_1.ApiError(const_1.HTTP_CONFLICT, "already downloading");
30
39
  console.log('downloading plugin', repo);
@@ -42,9 +51,7 @@ async function downloadPlugin(repo, branch = '', overwrite) {
42
51
  url = customRepo.web + url;
43
52
  return await go(url, pl === null || pl === void 0 ? void 0 : pl.id, (_c = customRepo.zipRoot) !== null && _c !== void 0 ? _c : DIST_ROOT);
44
53
  }
45
- const rec = await getRepoInfo(repo);
46
- if (!branch)
47
- branch = rec.default_branch;
54
+ branch || (branch = await getGithubDefaultBranch(repo));
48
55
  const short = repo.split('/')[1]; // second part, repo without the owner
49
56
  if (!short)
50
57
  return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, "bad repo");
@@ -75,7 +82,7 @@ async function downloadPlugin(repo, branch = '', overwrite) {
75
82
  });
76
83
  const main = (0, path_1.join)(installPath, MAIN);
77
84
  await (0, promises_1.rename)(main + plugins_1.DISABLING_POSTFIX, main) // we are good now, restore name
78
- .catch(e => { throw e.code !== 'ENOENT' ? e : new apiMiddleware_1.ApiError(const_1.HTTP_FAILED_DEPENDENCY, "missing main file"); });
85
+ .catch(e => { throw e.code !== 'ENOENT' ? e : new apiMiddleware_1.ApiError(const_1.HTTP_NOT_ACCEPTABLE, "missing main file"); });
79
86
  return folder;
80
87
  }
81
88
  }
@@ -109,19 +116,13 @@ async function readOnlinePlugin(repo, branch = '') {
109
116
  throw Error("bad repo.main");
110
117
  return (0, plugins_1.parsePluginSource)(main, res.body); // use 'repo' as 'id' client-side
111
118
  }
112
- const branches = branch ? [branch] : (async function* () {
113
- var _a;
114
- yield 'main'; // getRepoInfo consumes github-api-quota, so give 'main' a shot first, and if it fails we'll ask
115
- yield (_a = (await getRepoInfo(repo))) === null || _a === void 0 ? void 0 : _a.default_branch;
116
- })();
117
- for await (const b of branches) {
118
- const res = await readGithubFile(`${repo}/${b}/${DIST_ROOT}/plugin.js`);
119
- if (!res)
120
- continue;
121
- const pl = (0, plugins_1.parsePluginSource)(repo, res); // use 'repo' as 'id' client-side
122
- pl.branch = b || undefined;
123
- return pl;
124
- }
119
+ branch || (branch = await getGithubDefaultBranch(repo));
120
+ const res = await readGithubFile(`${repo}/${branch}/${DIST_ROOT}/plugin.js`);
121
+ if (!res)
122
+ throw Error("missing plugin.js");
123
+ const pl = (0, plugins_1.parsePluginSource)(repo, res); // use 'repo' as 'id' client-side
124
+ pl.branch = branch;
125
+ return pl;
125
126
  }
126
127
  exports.readOnlinePlugin = readOnlinePlugin;
127
128
  function getFolder2repo() {
@@ -148,19 +149,19 @@ async function apiGithub(uri) {
148
149
  : e;
149
150
  }
150
151
  }
151
- async function* searchPlugins(text = '') {
152
- var _a, _b;
153
- const projectInfo = await getProjectInfo();
154
- const res = await apiGithub('search/repositories?q=topic:hfs-plugin+' + encodeURI(text));
155
- for (const it of res.items) {
152
+ async function searchPlugins(text = '') {
153
+ const projectInfo = await (0, exports.getProjectInfo)();
154
+ const list = await apiGithub('search/repositories?q=topic:hfs-plugin+' + encodeURI(text));
155
+ return new misc_1.AsapStream(list.items.map(async (it) => {
156
+ var _a, _b;
156
157
  const repo = it.full_name;
157
158
  if ((_a = projectInfo === null || projectInfo === void 0 ? void 0 : projectInfo.plugins_blacklist) === null || _a === void 0 ? void 0 : _a.includes(repo))
158
- continue;
159
+ return;
159
160
  let pl = await readOnlinePlugin(repo, it.default_branch);
160
161
  if (!(pl === null || pl === void 0 ? void 0 : pl.apiRequired))
161
- continue; // mandatory field
162
+ return; // mandatory field
162
163
  if (pl.badApi) { // we try other branches (starting with 'api')
163
- const res = await apiGithub('repos/' + it.full_name + '/branches');
164
+ const res = await apiGithub('repos/' + repo + '/branches');
164
165
  const branches = res.map((x) => x === null || x === void 0 ? void 0 : x.name)
165
166
  .filter((x) => typeof x === 'string' && x.startsWith('api'))
166
167
  .sort().reverse();
@@ -175,30 +176,17 @@ async function* searchPlugins(text = '') {
175
176
  }
176
177
  }
177
178
  if (!pl || pl.badApi)
178
- continue;
179
+ return;
179
180
  Object.assign(pl, {
180
181
  downloading: downloading[repo],
181
182
  license: (_b = it.license) === null || _b === void 0 ? void 0 : _b.spdx_id,
182
183
  }, lodash_1.default.pick(it, ['pushed_at', 'stargazers_count', 'default_branch']));
183
- yield pl;
184
- }
184
+ return pl;
185
+ }));
185
186
  }
186
187
  exports.searchPlugins = searchPlugins;
187
188
  // centralized hosted information, to be used as little as possible
188
- let cache;
189
189
  const FN = 'central.json';
190
- let latest = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', FN), 'utf8')); // initially built-in is our latest
191
- function getProjectInfo() {
192
- return cache || (cache = readGithubFile(const_1.HFS_REPO + '/main/' + FN)
193
- .catch(() => latest) // fall back to latest
194
- .then(x => {
195
- if (!x)
196
- throw x; // go catch
197
- setTimeout(() => cache = undefined, const_1.DAY); // invalidate cache
198
- return latest = JSON.parse(x);
199
- }).catch(() => {
200
- setTimeout(() => cache = undefined, 10000); // invalidate cache sooner on errors
201
- return latest;
202
- }));
203
- }
204
- exports.getProjectInfo = getProjectInfo;
190
+ let builtIn = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', FN), 'utf8'));
191
+ exports.getProjectInfo = (0, misc_1.debounceAsync)(() => readGithubFile(`${const_1.HFS_REPO}/${const_1.HFS_REPO_BRANCH}/${FN}`).then(JSON.parse, () => builtIn), // fall back to latest
192
+ 0, { retain: misc_1.DAY, retainFailure: 60000 });
package/src/index.js CHANGED
@@ -25,11 +25,13 @@ const assert_1 = require("assert");
25
25
  const lodash_1 = __importDefault(require("lodash"));
26
26
  const misc_1 = require("./misc");
27
27
  const koa_session_1 = __importDefault(require("koa-session"));
28
+ const api_net_1 = require("./api.net");
28
29
  (0, assert_1.ok)(lodash_1.default.intersection(Object.keys(frontEndApis_1.frontEndApis), Object.keys(adminApis_1.adminApis)).length === 0); // they share same endpoints, don't clash
29
30
  process.title = 'HFS ' + const_1.VERSION;
30
31
  const keys = ((_a = process.env.COOKIE_SIGN_KEYS) === null || _a === void 0 ? void 0 : _a.split(',')) || [(0, misc_1.randomId)(30)];
31
32
  exports.app = new koa_1.default({ keys });
32
33
  exports.app.use(middlewares_1.someSecurity)
34
+ .use(api_net_1.acmeMiddleware)
33
35
  .use((0, koa_session_1.default)({ key: 'hfs_$id', signed: true, rolling: true, sameSite: 'lax' }, exports.app))
34
36
  .use(middlewares_1.prepareState)
35
37
  .use(middlewares_1.gzipper)
@@ -17,4 +17,5 @@ const hfs_lang_es_json_1 = __importDefault(require("./hfs-lang-es.json"));
17
17
  const hfs_lang_nl_json_1 = __importDefault(require("./hfs-lang-nl.json"));
18
18
  const hfs_lang_el_json_1 = __importDefault(require("./hfs-lang-el.json"));
19
19
  const hfs_lang_de_json_1 = __importDefault(require("./hfs-lang-de.json"));
20
- exports.default = { it: hfs_lang_it_json_1.default, zh: hfs_lang_zh_json_1.default, ru: hfs_lang_ru_json_1.default, sr: hfs_lang_sr_json_1.default, ko: hfs_lang_ko_json_1.default, ms: hfs_lang_ms_json_1.default, 'zh-tw': hfs_lang_zh_tw_json_1.default, fr: hfs_lang_fr_json_1.default, pt: hfs_lang_pt_json_1.default, vi: hfs_lang_vi_json_1.default, es: hfs_lang_es_json_1.default, nl: hfs_lang_nl_json_1.default, el: hfs_lang_el_json_1.default, de: hfs_lang_de_json_1.default };
20
+ const hfs_lang_fi_json_1 = __importDefault(require("./hfs-lang-fi.json"));
21
+ exports.default = { it: hfs_lang_it_json_1.default, zh: hfs_lang_zh_json_1.default, ru: hfs_lang_ru_json_1.default, sr: hfs_lang_sr_json_1.default, ko: hfs_lang_ko_json_1.default, ms: hfs_lang_ms_json_1.default, 'zh-tw': hfs_lang_zh_tw_json_1.default, fr: hfs_lang_fr_json_1.default, pt: hfs_lang_pt_json_1.default, vi: hfs_lang_vi_json_1.default, es: hfs_lang_es_json_1.default, nl: hfs_lang_nl_json_1.default, el: hfs_lang_el_json_1.default, de: hfs_lang_de_json_1.default, fi: hfs_lang_fi_json_1.default };
@@ -0,0 +1,131 @@
1
+ {
2
+ "author": "Pultsari",
3
+ "version": 1.0,
4
+ "hfs_version": "0.47.0",
5
+ "translate": {
6
+ "Select": "Valitse",
7
+ "n_files": "{n,plural,one{# tiedosto} other{# tiedostoa}}",
8
+ "n_folders": "{n,plural,one{# kansio} other{# kansiota}}",
9
+ "filter_count": "{n,plural, one{# suodatettu} other{# suodatettu}}",
10
+ "select_count": "{n,plural, one{# valittu} other{# valittu}}",
11
+ "filter_placeholder": "Kirjoita tähän suodattaaksesi listasta",
12
+ "Select some files": "Valitse joitakin tiedostoja tai kansioita",
13
+ "zip_checkboxes": "Käytä valintaruutuja valitaksesi ladattavat tiedostot ja kansiot, sitten voit ladata pakatun zip tiedoston",
14
+ "zip_tooltip_selected": "Lataa valitut tiedostot ja kansiot pakatussa zip tiedostossa",
15
+ "zip_tooltip_whole": "Lataa ja pakkaa kaikki (suodattamattomana) yhtenä zip tiedostona. Jos valitset vain joitakin tiedostoja tai kansioita, pakataan ja ladataan vain ne.",
16
+ "zip_confirm_search": "Haluatko ladata kaikki haun tulokset pakattuna zip tiedostona?",
17
+ "zip_confirm_folder": "Haluatko ladata aivan kaiken pakattuna zip tiedostona?",
18
+ "select_tooltip": "Valinnat koskevat pakattua zip tiedostoa ja tiedostojen poistamista (kun sallittu), sen lisäksi voit suodattaa listaa.",
19
+ "delete_hint": "Poistaaksesi tiedostoja tai kansioita, klikkaa ensin Valitse yläpalkista",
20
+ "delete_confirm": "Poista {n,plural, one{# valittu tiedosto tai kansio} other{# valittua tiedostoa tai / ja kansiota}}?",
21
+ "delete_completed": "{n} tiedostoa tai / ja kansiota poistettu",
22
+ "delete_failed": ", {n,plural, one{# epäonnistui} other{# epäonnistui}}",
23
+ "delete_select": "Valitse tiedostoja tai / ja kansioita poistettavaksi",
24
+ "Delete": "Poista",
25
+ "Options": "Asetukset",
26
+ "Search": "Haku",
27
+ "Zip": "Zip",
28
+ "search_msg": "Hae tästä kansiosta ja alikansioista",
29
+ "Searching": "Haetaan",
30
+ "Searched": "Haettu",
31
+ "Clear search": "Tyhjennä haku",
32
+ "Interrupted": "Keskeytetty",
33
+ "stopped_before": "pysäytetty ennen",
34
+ "empty_list": "Tyhjä",
35
+ "filter_none": "Ei tuloksia",
36
+ "Admin-panel": "Hallintapaneeli",
37
+ "Login": "Kirjaudu",
38
+ "Username": "Käyttäjätunnus",
39
+ "Password": "Salasana",
40
+ "login_untrusted": "Kirjautuminen keskeytetty:: palvelimen identiteettiin ei voida luottaa",
41
+ "login_bad_credentials": "Käyttäjätunnus tai salasana virheellinen.",
42
+ "login_bad_cookies": "Evästeet eivät toimi - kirjautuminen epäonnistui",
43
+ "User panel": "Hallintapaneeli",
44
+ "Change password": "Vaihda salasana",
45
+ "enter_pass": "Kirjoita uusi salasana",
46
+ "enter_pass2": "Kirjoita uusi salasana uudelleen",
47
+ "pass2_mismatch": "Toinen kirjoittamasi salasana ei vastannut ensimmäistä. Salasanan vaihtaminen keskeytetty.",
48
+ "password_changed": "Salasana vaihdettu",
49
+ "Logout": "Kirjaudu ulos",
50
+ "connection error": "Yhteysvirhe",
51
+ "Full timestamp:": "Täysi aikaleima:",
52
+ "Search was interrupted": "Haku keskeytetty",
53
+ "Stop list": "Pysäytä listaus",
54
+ "upload_starting": "Aloitetaan lataaminen",
55
+ "wrong_account": "Tilillä {u} ei ole pääsyä, kokeile toista",
56
+ "no_upload_here": "Ei lähetysoikeutta tähän kansioon",
57
+ "Create folder": "Tee uusi kansio",
58
+ "Pick files": "Valitse lähetettävät tiedostot",
59
+ "Pick folder": "Valitse lähetettävä kansio",
60
+ "send_files": "Lähetä {n,plural,one{# tiedosto} other{# tiedostot}}, {size}",
61
+ "Clear": "Tyhjennä",
62
+ "failed_upload": "Lähetys epäonnistui {name}",
63
+ "confirm_resume": "Jatketaanko lähetystä?",
64
+ "file too large": "Tiedoston koko on liian suuri",
65
+ "Enter folder name": "Kirjoita kansion nimi",
66
+ "Successfully created": "Kansio luotu onnistuneesti",
67
+ "enter_folder": "Siirry kansioon",
68
+ "folder_exists": "Kansio samalla nimellä on jo olemassa",
69
+ "Sort by:": "Lajittele: {by}",
70
+ "name": "nimen mukaan",
71
+ "extension": "tiedostopäätteen mukaan",
72
+ "size": "koon mukaan",
73
+ "time": "ajan mukaan",
74
+ "Invert order": "Käänteinen järjestys",
75
+ "Folders first": "Kansiot ensin",
76
+ "Numeric names": "Numeroidut nimet",
77
+ "theme:": "Teema:",
78
+ "auto": "auto",
79
+ "light": "vaalea",
80
+ "dark": "tumma",
81
+ "parent folder": "Ylempi kansio",
82
+ "home": "Koti",
83
+ "Continue": "Jatka",
84
+ "Confirm": "Vahvista",
85
+ "Don't": "Älä",
86
+ "Warning": "Varoitus",
87
+ "Error": "Virhe",
88
+ "Info": "Tiedoksi",
89
+ "Unauthorized": "Ei lupaa",
90
+ "Forbidden": "Kielletty",
91
+ "Not found": "Ei löydy",
92
+ "Server error": "Palvelinvirhe",
93
+ "Upload": "Lähetä",
94
+ "upload_concluded": "Lähetys päättyi:",
95
+ "upload_finished": "{n} tiedostoa tai kansiota lähetetty onnistuneesti ({size})",
96
+ "upload_errors": "{n} epäonnistui",
97
+ "upload_file_rejected": "Joitakin tiedostoja ei hyväksytty",
98
+ "File menu": "Tiedostovalikko",
99
+ "Folder menu": "Kansiovalikko",
100
+ "Name": "Nimi",
101
+ "file_open": "Avaa",
102
+ "Download": "Lataa",
103
+ "Missing permission": "Oikeudet puuuttuvat",
104
+ "Reload": "Päivitä",
105
+ "Get list": "Näytä listana",
106
+ "Skip existing files": "Ohita jo olemassa olevat tiedostot",
107
+ "Size": "Koko",
108
+ "Timestamp": "Aikaleima",
109
+ "Show": "Näytä",
110
+ "Loading failed": "Lataaminen epäonnistui",
111
+ "Rename": "Nimeä uudelleen",
112
+ "Tiles mode:": "Kuvakkeiden koko:",
113
+ "off": "pienin",
114
+ "Operation successful": "Suoritettu onnistuneesti",
115
+ "Uploader": "Lähettäjä",
116
+ "Download counter": "Latauslaskuri",
117
+ "Switch zoom mode": "Vaihda zoomaus tilaa",
118
+ "Full screen": "Koko näyttö",
119
+ "File Show help": "Ohjeita",
120
+ "showHelpMain": "Voit käyttää näppäimistöä joihinkin toimintoihin:",
121
+ "showHelp_←/→": "←/→",
122
+ "showHelp_↑/↓": "↑/↓",
123
+ "showHelp_space": "välilyönti",
124
+ "showHelp_←/→_body": "siirry edelliseen/seuraavaan tiedostoon",
125
+ "showHelp_↑/↓_body": "vieritä korkeita kuvia",
126
+ "showHelp_space_body": "valitse",
127
+ "showHelp_D_body": "lataa",
128
+ "showHelp_Z_body": "vaihda zoomauksen tilaa",
129
+ "showHelp_F_body": "koko näyttö"
130
+ }
131
+ }
@@ -119,6 +119,8 @@
119
119
  "showHelp_D_body": "download",
120
120
  "showHelp_Z_body": "cambia modalità zoom",
121
121
  "showHelp_F_body": "pieno schermo",
122
+ "Destination": "Destinazione",
123
+ "in_queue": "{n} in coda",
122
124
  "": "PLUGINS SECTION",
123
125
  "": "PLUGIN thumbnails",
124
126
  "Enable tiles mode": "Modalità griglia",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "author": "Mefistofell, SanokKule",
3
- "version": 2.0,
4
- "hfs_version": "0.47.0",
3
+ "version": 2.1,
4
+ "hfs_version": "0.48.0",
5
5
  "translate": {
6
6
  "Select": "Выбор",
7
7
  "n_files": "{n,plural, one{1 файл} few{# файла} other{# файлов}}",
@@ -15,7 +15,7 @@
15
15
  "zip_tooltip_whole": "Выбрать файлы для скачивания в zip-архиве. Если файлы не выбраны, то будет скачана вся папка",
16
16
  "zip_confirm_search": "Скачать все результаты поиска в одном zip-архиве",
17
17
  "zip_confirm_folder": "Скачать всю папку как zip-архив?",
18
- "select_tooltip": "Выбор применяется к \"Zip\" и \"Удалить\" (если доступно), но также список можно отфильтровать",
18
+ "select_tooltip": "Позволяет выбрать файлы для скачивания или удаления, а так же использовать фильтры для быстрого поиска",
19
19
  "delete_hint": "Для удаления файлов сначала нажмите Выбор",
20
20
  "delete_confirm": "Удалить {n,plural, =1{элемент} one{# элемент} few{# элемента} many{# элементов} other{# элементов}}?",
21
21
  "delete_completed": "{n,plural, one{Удалён} other{Удалено}} {n,plural, one{# элемент} few{# элемента} many{# элементов} other{# элементов}}",
@@ -116,7 +116,7 @@
116
116
  "Download counter": "Счётчик загрузок",
117
117
  "Switch zoom mode": "Переключить увеличение",
118
118
  "Full screen": "Полный экран",
119
- "File Show help": "Помощь меню просмотра файла",
119
+ "File Show help": "Помощь",
120
120
  "showHelpMain": "Для некоторых действий можно использовать клавиатуру:",
121
121
  "showHelp_←/→": "←/→",
122
122
  "showHelp_↑/↓": "↑/↓",
@@ -126,6 +126,12 @@
126
126
  "showHelp_space_body": "выбрать файл",
127
127
  "showHelp_D_body": "скачать",
128
128
  "showHelp_Z_body": "переключение увеличения",
129
- "showHelp_F_body": "полноэкранный режим"
129
+ "showHelp_F_body": "полноэкранный режим",
130
+ "Destination": "Назначение",
131
+ "in_queue": "{n} в очереди",
132
+ "": "PLUGINS SECTION",
133
+ "": "PLUGIN thumbnails",
134
+ "Enable tiles mode": "Просмотр в режиме плиток",
135
+ "thumbnails_switchBack": "Изменить вид можно в настройках"
130
136
  }
131
137
  }
package/src/listen.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.getUrls = exports.getIps = exports.getServerStatus = exports.httpsPortCfg = exports.openAdmin = exports.portCfg = exports.getHttpsWorkingPort = void 0;
30
+ exports.getUrls = exports.getIps = exports.getServerStatus = exports.stopServer = exports.startServer = exports.httpsPortCfg = exports.privateKey = exports.cert = exports.getCertObject = exports.openAdmin = exports.portCfg = exports.getHttpsWorkingPort = void 0;
31
31
  const http = __importStar(require("http"));
32
32
  const config_1 = require("./config");
33
33
  const index_1 = require("./index");
@@ -51,12 +51,13 @@ function getHttpsWorkingPort() {
51
51
  return (httpsSrv === null || httpsSrv === void 0 ? void 0 : httpsSrv.listening) && ((_a = httpsSrv.address()) === null || _a === void 0 ? void 0 : _a.port);
52
52
  }
53
53
  exports.getHttpsWorkingPort = getHttpsWorkingPort;
54
+ const commonOptions = { requestTimeout: 0 };
54
55
  exports.portCfg = (0, config_1.defineConfig)('port', 80);
55
56
  exports.portCfg.sub(async (port) => {
56
57
  while (!index_1.app)
57
58
  await (0, misc_1.wait)(100);
58
59
  stopServer(httpSrv).then();
59
- httpSrv = Object.assign(http.createServer(index_1.app.callback()), { name: 'http' });
60
+ httpSrv = Object.assign(http.createServer(commonOptions, index_1.app.callback()), { name: 'http' });
60
61
  port = await startServer(httpSrv, { port });
61
62
  if (!port)
62
63
  return;
@@ -82,6 +83,12 @@ function openAdmin() {
82
83
  console.log("openAdmin failed");
83
84
  }
84
85
  exports.openAdmin = openAdmin;
86
+ function getCertObject() {
87
+ const o = new crypto_1.X509Certificate(httpsOptions.cert);
88
+ const some = lodash_1.default.pick(o, ['subject', 'issuer', 'validFrom', 'validTo']);
89
+ return (0, misc_1.objSameKeys)(some, v => (v === null || v === void 0 ? void 0 : v.includes('=')) ? Object.fromEntries(v.split('\n').map(x => x.split('='))) : v);
90
+ }
91
+ exports.getCertObject = getCertObject;
85
92
  const considerHttps = (0, misc_1.debounceAsync)(async () => {
86
93
  var _a, _b, _c;
87
94
  stopServer(httpsSrv).then();
@@ -89,10 +96,10 @@ const considerHttps = (0, misc_1.debounceAsync)(async () => {
89
96
  try {
90
97
  while (!index_1.app)
91
98
  await (0, misc_1.wait)(100);
92
- httpsSrv = Object.assign(https.createServer(port === PORT_DISABLED ? {} : { key: httpsOptions.private_key, cert: httpsOptions.cert }, index_1.app.callback()), { name: 'https' });
99
+ httpsSrv = Object.assign(https.createServer(port === PORT_DISABLED ? {} : { ...commonOptions, key: httpsOptions.private_key, cert: httpsOptions.cert }, index_1.app.callback()), { name: 'https' });
93
100
  if (port >= 0) {
94
- const cert = new crypto_1.X509Certificate(httpsOptions.cert);
95
- const cn = (_a = cert.subject.split('CN=')[1]) === null || _a === void 0 ? void 0 : _a.split('\n')[0];
101
+ const cert = getCertObject();
102
+ const cn = (_a = cert.subject) === null || _a === void 0 ? void 0 : _a.CN;
96
103
  if (cn)
97
104
  console.log("certificate loaded for", cn);
98
105
  const now = new Date();
@@ -119,9 +126,9 @@ const considerHttps = (0, misc_1.debounceAsync)(async () => {
119
126
  httpsSrv.on('connection', connections_1.newConnection);
120
127
  printUrls(httpsSrv.name);
121
128
  });
122
- const cert = (0, config_1.defineConfig)('cert', '');
123
- const privateKey = (0, config_1.defineConfig)('private_key', '');
124
- const httpsNeeds = [cert, privateKey];
129
+ exports.cert = (0, config_1.defineConfig)('cert', '');
130
+ exports.privateKey = (0, config_1.defineConfig)('private_key', '');
131
+ const httpsNeeds = [exports.cert, exports.privateKey];
125
132
  const httpsOptions = { cert: '', private_key: '' };
126
133
  for (const cfg of httpsNeeds) {
127
134
  let unwatch;
@@ -170,7 +177,7 @@ function startServer(srv, { port, host }) {
170
177
  }
171
178
  function listen(host) {
172
179
  return new Promise(async (resolve, reject) => {
173
- srv === null || srv === void 0 ? void 0 : srv.listen({ port, host }, () => {
180
+ srv === null || srv === void 0 ? void 0 : srv.on('error', onError).listen({ port, host }, () => {
174
181
  const ad = srv.address();
175
182
  if (!ad)
176
183
  return reject('no address');
@@ -178,8 +185,12 @@ function startServer(srv, { port, host }) {
178
185
  srv.close();
179
186
  return reject('type of socket not supported');
180
187
  }
188
+ srv.removeListener('error', onError); // necessary in case someone calls stop/start many times
181
189
  resolve(ad.port);
182
- }).on('error', async (e) => {
190
+ });
191
+ async function onError(e) {
192
+ if (!srv)
193
+ return;
183
194
  srv.error = String(e);
184
195
  srv.busy = undefined;
185
196
  const { code } = e;
@@ -191,10 +202,11 @@ function startServer(srv, { port, host }) {
191
202
  const k = (srv === httpSrv ? exports.portCfg : exports.httpsPortCfg).key();
192
203
  console.log(` >> try specifying a different port, enter this command: config ${k} 8011`);
193
204
  resolve(0);
194
- });
205
+ }
195
206
  });
196
207
  }
197
208
  }
209
+ exports.startServer = startServer;
198
210
  function stopServer(srv) {
199
211
  return new Promise(resolve => {
200
212
  if (!(srv === null || srv === void 0 ? void 0 : srv.listening))
@@ -209,19 +221,22 @@ function stopServer(srv) {
209
221
  });
210
222
  });
211
223
  }
224
+ exports.stopServer = stopServer;
212
225
  async function getServerStatus() {
213
226
  return {
214
227
  http: await serverStatus(httpSrv, exports.portCfg.get()),
215
228
  https: await serverStatus(httpsSrv, exports.httpsPortCfg.get()),
216
229
  };
217
- async function serverStatus(h, configuredPort) {
230
+ async function serverStatus(srv, configuredPort) {
218
231
  var _a;
219
- const busy = await (h === null || h === void 0 ? void 0 : h.busy);
232
+ const busy = await (srv === null || srv === void 0 ? void 0 : srv.busy);
220
233
  await (0, misc_1.wait)(0); // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
221
234
  return {
222
- ...lodash_1.default.pick(h, ['listening', 'error']),
235
+ ...lodash_1.default.pick(srv, ['listening', 'error']),
223
236
  busy,
224
- port: ((_a = h === null || h === void 0 ? void 0 : h.address()) === null || _a === void 0 ? void 0 : _a.port) || configuredPort,
237
+ port: ((_a = srv === null || srv === void 0 ? void 0 : srv.address()) === null || _a === void 0 ? void 0 : _a.port) || configuredPort,
238
+ configuredPort,
239
+ srv,
225
240
  };
226
241
  }
227
242
  }
@@ -232,7 +247,7 @@ async function getIps() {
232
247
  && v4first((0, misc_1.onlyTruthy)(nets.map(net => !net.internal && net.address)))[0] // for each interface we consider only 1 address
233
248
  )).flat();
234
249
  const e = await api_net_1.externalIp;
235
- if (e)
250
+ if (e && !ips.includes(e))
236
251
  ips.unshift(e);
237
252
  return v4first(ips)
238
253
  .filter((x, i, a) => a.length > 1 || !x.startsWith('169.254')); // 169.254 = dhcp failure on the interface, but keep it if it's our only one
package/src/log.js CHANGED
@@ -33,7 +33,6 @@ const config_1 = require("./config");
33
33
  const fs_1 = require("fs");
34
34
  const util = __importStar(require("util"));
35
35
  const promises_1 = require("fs/promises");
36
- const const_1 = require("./const");
37
36
  const lodash_1 = __importDefault(require("lodash"));
38
37
  const util_files_1 = require("./util-files");
39
38
  const perm_1 = require("./perm");
@@ -104,9 +103,9 @@ const logMw = async (ctx, next) => {
104
103
  if (rotate && last) { // rotation enabled and a file exists?
105
104
  const passed = Number(now) - Number(last)
106
105
  - 3600000; // be pessimistic and count a possible DST change
107
- if (rotate === 'm' && (passed >= 31 * const_1.DAY || now.getMonth() !== last.getMonth())
108
- || 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
109
- || rotate === 'w' && (passed >= 7 * const_1.DAY || now.getDay() < last.getDay())) {
106
+ if (rotate === 'm' && (passed >= 31 * misc_1.DAY || now.getMonth() !== last.getMonth())
107
+ || rotate === 'd' && (passed >= misc_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
108
+ || rotate === 'w' && (passed >= 7 * misc_1.DAY || now.getDay() < last.getDay())) {
110
109
  stream.end();
111
110
  const postfix = last.getFullYear() + '-' + doubleDigit(last.getMonth() + 1) + '-' + doubleDigit(last.getDate());
112
111
  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.
@@ -126,7 +125,7 @@ const logMw = async (ctx, next) => {
126
125
  const user = (0, perm_1.getCurrentUsername)(ctx);
127
126
  const length = (_c = ctx.state.length) !== null && _c !== void 0 ? _c : ctx.length;
128
127
  const uri = ctx.originalUrl;
129
- const extra = ctx.state.includesLastByte && ctx.vfsNode && { dl: 1 }
128
+ const extra = ctx.state.includesLastByte && ctx.vfsNode && ctx.res.finished && { dl: 1 }
130
129
  || ctx.state.uploadPath && { ul: ctx.state.uploadPath, size: ctx.state.uploadSize }
131
130
  || undefined;
132
131
  events_1.default.emit(logger.name, Object.assign(lodash_1.default.pick(ctx, ['ip', 'method', 'status']), { length, user, ts: now, uri, extra }));
@@ -32,7 +32,7 @@ const config_1 = require("./config");
32
32
  const lang_1 = require("./lang");
33
33
  const forceHttps = (0, config_1.defineConfig)('force_https', true);
34
34
  const ignoreProxies = (0, config_1.defineConfig)('ignore_proxies', false);
35
- exports.sessionDuration = (0, config_1.defineConfig)('session_duration', Number(process.env.SESSION_DURATION) || const_1.DAY / 1000, v => v * 1000);
35
+ exports.sessionDuration = (0, config_1.defineConfig)('session_duration', Number(process.env.SESSION_DURATION) || misc_1.DAY / 1000, v => v * 1000);
36
36
  exports.gzipper = (0, koa_compress_1.default)({
37
37
  threshold: 2048,
38
38
  gzip: { flush: zlib_1.constants.Z_SYNC_FLUSH },
@@ -213,7 +213,7 @@ const someSecurity = async (ctx, next) => {
213
213
  exports.someSecurity = someSecurity;
214
214
  // limited to http proxies
215
215
  function getProxyDetected() {
216
- if ((proxyDetected === null || proxyDetected === void 0 ? void 0 : proxyDetected.state.when) < Date.now() - const_1.DAY)
216
+ if ((proxyDetected === null || proxyDetected === void 0 ? void 0 : proxyDetected.state.when) < Date.now() - misc_1.DAY)
217
217
  proxyDetected = undefined;
218
218
  return !ignoreProxies.get() && proxyDetected
219
219
  && { from: proxyDetected.ip, for: proxyDetected.get('X-Forwarded-For') };