hfs 3.1.5 → 3.2.0-alpha1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/assets/af-RjyQ-neT.js +1 -0
- package/admin/assets/am-PptHpglb.js +1 -0
- package/admin/assets/ar-Du3syDxa.js +1 -0
- package/admin/assets/ar-dz-CiziRh1x.js +1 -0
- package/admin/assets/ar-iq-hCKg0CRP.js +1 -0
- package/admin/assets/ar-kw-COYePt-p.js +1 -0
- package/admin/assets/ar-ly-CY5yETqb.js +1 -0
- package/admin/assets/ar-ma-DczXi97r.js +1 -0
- package/admin/assets/ar-sa-Bu7sTKzl.js +1 -0
- package/admin/assets/ar-tn-BrG0r6qZ.js +1 -0
- package/admin/assets/az-DYRCrdk0.js +1 -0
- package/admin/assets/be-DObSs5HX.js +1 -0
- package/admin/assets/bg-BDBkMe50.js +1 -0
- package/admin/assets/bi-CApXFwc6.js +1 -0
- package/admin/assets/bm-CjxCV8Lr.js +1 -0
- package/admin/assets/bn-DzWZbDi3.js +1 -0
- package/admin/assets/bn-bd-ijdPRJbb.js +1 -0
- package/admin/assets/bo-BDDfj5XH.js +1 -0
- package/admin/assets/br-DRHEjait.js +1 -0
- package/admin/assets/bs-B0Zv2Agm.js +1 -0
- package/admin/assets/ca-DuZAUqU_.js +1 -0
- package/admin/assets/cs-DVkylosR.js +1 -0
- package/admin/assets/cv-8Is1Fns9.js +1 -0
- package/admin/assets/cy-Bk30Or_y.js +1 -0
- package/admin/assets/da-C41DIdZK.js +1 -0
- package/admin/assets/de--qvLJgIE.js +1 -0
- package/admin/assets/de-at-D5vCBlsw.js +1 -0
- package/admin/assets/de-ch-Oh2cO23c.js +1 -0
- package/admin/assets/dv-4ZO2KNbw.js +1 -0
- package/admin/assets/el-CobOUqyu.js +1 -0
- package/admin/assets/en-C9afClpS.js +1 -0
- package/admin/assets/en-au-C_QMghEl.js +1 -0
- package/admin/assets/en-ca-C80KILwc.js +1 -0
- package/admin/assets/en-gb-BwQ0asAI.js +1 -0
- package/admin/assets/en-ie-ConCcQkq.js +1 -0
- package/admin/assets/en-il-CAOzD4DD.js +1 -0
- package/admin/assets/en-in-87gardaO.js +1 -0
- package/admin/assets/en-nz-uMTjgkDP.js +1 -0
- package/admin/assets/en-sg-BlbiLv4L.js +1 -0
- package/admin/assets/en-tt-BXXmBax2.js +1 -0
- package/admin/assets/eo-BBL7o-0W.js +1 -0
- package/admin/assets/es-DuNej-79.js +1 -0
- package/admin/assets/es-do-bZWKFwTE.js +1 -0
- package/admin/assets/es-mx-C2Lfb86F.js +1 -0
- package/admin/assets/es-pr-wU3Du8m-.js +1 -0
- package/admin/assets/es-us-qcU-zRiw.js +1 -0
- package/admin/assets/et-B-4PjN_n.js +1 -0
- package/admin/assets/eu-D0pNDZ9i.js +1 -0
- package/admin/assets/fa-R_zmYBGc.js +1 -0
- package/admin/assets/fi-CWYS_0Hg.js +1 -0
- package/admin/assets/fo-aJYpcnWS.js +1 -0
- package/admin/assets/fr-BHFrWfwB.js +1 -0
- package/admin/assets/fr-ca-ihwmO47n.js +1 -0
- package/admin/assets/fr-ch-Cb8XbRfE.js +1 -0
- package/admin/assets/fy-DT9BUc6t.js +1 -0
- package/admin/assets/ga-B8p4naXJ.js +1 -0
- package/admin/assets/gd-Dzrl6rPc.js +1 -0
- package/admin/assets/gl-bN2tXl9d.js +1 -0
- package/admin/assets/gom-latn-DUK8PgIJ.js +1 -0
- package/admin/assets/gu-CxAIP8fw.js +1 -0
- package/admin/assets/he-Bm9XJVAj.js +1 -0
- package/admin/assets/hi-CHik9Qxx.js +1 -0
- package/admin/assets/hr-Dn57_dqN.js +1 -0
- package/admin/assets/ht-BRS-FrJq.js +1 -0
- package/admin/assets/hu-hVaGcZ-U.js +1 -0
- package/admin/assets/hy-am-heLxXhpz.js +1 -0
- package/admin/assets/id-B6AkO4h8.js +1 -0
- package/admin/assets/{index-DTxjaflW.js → index-Buyl8rI8.js} +1 -1
- package/admin/assets/index-CFWd-FDo.css +1 -0
- package/admin/assets/index-WXxQwyJV.js +889 -0
- package/admin/assets/is-BUADPoni.js +1 -0
- package/admin/assets/it-Ce3gssOP.js +1 -0
- package/admin/assets/it-ch-CJzj2czr.js +1 -0
- package/admin/assets/ja-DykMIu-9.js +1 -0
- package/admin/assets/jv-Cum6DLwa.js +1 -0
- package/admin/assets/ka-foCPVFxw.js +1 -0
- package/admin/assets/kk-CMNu1ysC.js +1 -0
- package/admin/assets/km-BfMisGaD.js +1 -0
- package/admin/assets/kn-R_9S5KJz.js +1 -0
- package/admin/assets/ko-DYRND-mM.js +1 -0
- package/admin/assets/ku-CNZ_weGy.js +1 -0
- package/admin/assets/ky-BhTAWL4I.js +1 -0
- package/admin/assets/lb-CxWhQxwF.js +1 -0
- package/admin/assets/lo-DBL_XLkE.js +1 -0
- package/admin/assets/lt-BakZVm4p.js +1 -0
- package/admin/assets/lv-CCn-slXG.js +1 -0
- package/admin/assets/me-xPEA4qjE.js +1 -0
- package/admin/assets/mi-6cCs2Mzx.js +1 -0
- package/admin/assets/mk-CwWbHJPS.js +1 -0
- package/admin/assets/ml-Dp7Ab1SV.js +1 -0
- package/admin/assets/mn-ClotGWAe.js +1 -0
- package/admin/assets/mr-GiM-paTF.js +1 -0
- package/admin/assets/ms-BiZ1_eVR.js +1 -0
- package/admin/assets/ms-my-oevI0fOH.js +1 -0
- package/admin/assets/mt-BaIHeOl1.js +1 -0
- package/admin/assets/my-Cg5Fc_1j.js +1 -0
- package/admin/assets/nb-ZPODjT6R.js +1 -0
- package/admin/assets/ne-C0ti-ZX2.js +1 -0
- package/admin/assets/nl-DP2PkkGx.js +1 -0
- package/admin/assets/nl-be-VPC0QyaW.js +1 -0
- package/admin/assets/nn-NLidKyJd.js +1 -0
- package/admin/assets/oc-lnc-Ddj9_PnD.js +1 -0
- package/admin/assets/pa-in-DSAWffhb.js +1 -0
- package/admin/assets/pl-Yojex5aS.js +1 -0
- package/admin/assets/pt-BQMs2la2.js +1 -0
- package/admin/assets/pt-br-DVeeNZu8.js +1 -0
- package/admin/assets/rn-CKkTrK3f.js +1 -0
- package/admin/assets/ro-BbgGULSZ.js +1 -0
- package/admin/assets/ru-mAZX3DTa.js +1 -0
- package/admin/assets/rw-CYErOxKd.js +1 -0
- package/admin/assets/sd-kfPTqkSy.js +1 -0
- package/admin/assets/se-D0bN3rDS.js +1 -0
- package/admin/assets/{sha512-D936QW8l.js → sha512-99nhg44S.js} +1 -1
- package/admin/assets/si-COjb_4Hy.js +1 -0
- package/admin/assets/sk-Co95XNOo.js +1 -0
- package/admin/assets/sl-DUXa5wTF.js +1 -0
- package/admin/assets/sq-B-KU0nWK.js +1 -0
- package/admin/assets/sr-Cb_b-zPG.js +1 -0
- package/admin/assets/sr-cyrl-csuTDIB6.js +1 -0
- package/admin/assets/ss-BDnE-cG6.js +1 -0
- package/admin/assets/sv-CzWdOHcE.js +1 -0
- package/admin/assets/sv-fi-IK8oi5nB.js +1 -0
- package/admin/assets/sw-fPHo5hof.js +1 -0
- package/admin/assets/ta-TteN0nyU.js +1 -0
- package/admin/assets/te-2gsQb1fF.js +1 -0
- package/admin/assets/tet-ClTpDYJv.js +1 -0
- package/admin/assets/tg-Bel7Uc6z.js +1 -0
- package/admin/assets/th-pDQttM9V.js +1 -0
- package/admin/assets/tk-CxoKYZH6.js +1 -0
- package/admin/assets/tl-ph-D9htRcOQ.js +1 -0
- package/admin/assets/tlh-7UqvDBxU.js +1 -0
- package/admin/assets/tr-NjJrq1iC.js +1 -0
- package/admin/assets/tzl-CzGaXn8M.js +1 -0
- package/admin/assets/tzm-BiuscZFr.js +1 -0
- package/admin/assets/tzm-latn-IeWkCgWf.js +1 -0
- package/admin/assets/ug-cn-5LK64x5v.js +1 -0
- package/admin/assets/uk-CzacUaQe.js +1 -0
- package/admin/assets/ur-BRV7z4Gu.js +1 -0
- package/admin/assets/uz-C4G77QOI.js +1 -0
- package/admin/assets/uz-latn-CQFrzZ6F.js +1 -0
- package/admin/assets/vi-DEFXdlJi.js +1 -0
- package/admin/assets/x-pseudo-sWj5RKxR.js +1 -0
- package/admin/assets/yo-CBDjk96e.js +1 -0
- package/admin/assets/zh-CQtX_fkD.js +1 -0
- package/admin/assets/zh-cn-D6dF0jKM.js +1 -0
- package/admin/assets/zh-hk-DY-Jaqdg.js +1 -0
- package/admin/assets/zh-tw-BdmVby0R.js +1 -0
- package/admin/index.html +2 -2
- package/frontend/assets/index-legacy-BtoTkZho.js +9 -0
- package/frontend/assets/{index-legacy-vmpqwZZf.js → index-legacy-CQovmh_0.js} +1 -1
- package/frontend/assets/{sha512-legacy-wI89-UHR.js → sha512-legacy-CXU3efCO.js} +1 -1
- package/frontend/index.html +1 -1
- package/npm-shrinkwrap.json +160 -131
- package/package.json +10 -10
- package/plugins/antibrute/plugin.js +150 -19
- package/plugins/list-uploader/public/main.js +1 -1
- package/src/acme.js +11 -7
- package/src/api.accounts.js +3 -3
- package/src/api.auth.js +3 -1
- package/src/api.get_file_list.js +16 -12
- package/src/api.monitor.js +47 -43
- package/src/api.net.js +4 -3
- package/src/api.vfs.js +1 -1
- package/src/commands.js +54 -1
- package/src/config.js +9 -5
- package/src/consoleLog.js +39 -1
- package/src/const.js +5 -1
- package/src/cross.js +22 -1
- package/src/errorPages.js +20 -10
- package/src/events.js +1 -0
- package/src/fileAttr.js +73 -15
- package/src/frontEndApis.js +3 -1
- package/src/index.js +2 -1
- package/src/langs/hfs-lang-ar.json +2 -1
- package/src/langs/hfs-lang-bg.json +2 -1
- package/src/langs/hfs-lang-de.json +2 -1
- package/src/langs/hfs-lang-el.json +2 -1
- package/src/langs/hfs-lang-es.json +2 -1
- package/src/langs/hfs-lang-fi.json +2 -1
- package/src/langs/hfs-lang-fr.json +2 -1
- package/src/langs/hfs-lang-hu.json +2 -1
- package/src/langs/hfs-lang-it.json +2 -1
- package/src/langs/hfs-lang-ja.json +2 -1
- package/src/langs/hfs-lang-ko.json +2 -1
- package/src/langs/hfs-lang-lt.json +2 -1
- package/src/langs/hfs-lang-ms.json +2 -0
- package/src/langs/hfs-lang-nl.json +2 -1
- package/src/langs/hfs-lang-pt-br.json +2 -1
- package/src/langs/hfs-lang-ro.json +2 -1
- package/src/langs/hfs-lang-ru.json +2 -1
- package/src/langs/hfs-lang-sr-latn.json +2 -1
- package/src/langs/hfs-lang-sr.json +2 -1
- package/src/langs/hfs-lang-th.json +2 -1
- package/src/langs/hfs-lang-tr.json +2 -1
- package/src/langs/hfs-lang-uk.json +2 -1
- package/src/langs/hfs-lang-vi.json +2 -1
- package/src/langs/hfs-lang-zh-tw.json +2 -1
- package/src/langs/hfs-lang-zh.json +2 -1
- package/src/listen.js +3 -3
- package/src/log.js +27 -2
- package/src/middlewares.js +8 -2
- package/src/misc.js +14 -0
- package/src/nat.js +57 -20
- package/src/outboundProxy.js +1 -1
- package/src/perm.js +5 -1
- package/src/plugins.js +34 -5
- package/src/selfCheck.js +1 -1
- package/src/serveGuiAndSharedFiles.js +1 -0
- package/src/serveGuiFiles.js +1 -0
- package/src/update.js +9 -15
- package/src/urlList.js +32 -0
- package/src/util-files.js +15 -5
- package/src/util-http.js +2 -0
- package/src/vfs.js +1 -1
- package/src/walkDir.js +7 -1
- package/src/webdav.js +4 -1
- package/src/zip.js +2 -1
- package/admin/assets/index-B66w-a0v.css +0 -1
- package/admin/assets/index-Df6vYR7s.js +0 -822
- package/frontend/assets/index-legacy-emBsvICj.js +0 -9
package/src/config.js
CHANGED
|
@@ -21,10 +21,8 @@ const fs_1 = require("fs");
|
|
|
21
21
|
const path_1 = require("path");
|
|
22
22
|
const events_1 = __importDefault(require("./events"));
|
|
23
23
|
const promises_1 = require("fs/promises");
|
|
24
|
-
const immer_1 = require("immer");
|
|
25
24
|
const argv_1 = require("./argv");
|
|
26
25
|
const util_files_1 = require("./util-files");
|
|
27
|
-
(0, immer_1.setAutoFreeze)(false); // we still want to mess with objects later (eg: account.belongs)
|
|
28
26
|
// keep definition of config properties
|
|
29
27
|
const configProps = {};
|
|
30
28
|
let started = false; // this will tell the difference for subscribeConfig()s that are called before or after config is loaded
|
|
@@ -74,6 +72,10 @@ function defineConfig(k, defaultValue, compiler) {
|
|
|
74
72
|
get() {
|
|
75
73
|
return getConfig(k);
|
|
76
74
|
},
|
|
75
|
+
async getWhenReady() {
|
|
76
|
+
await exports.configReady;
|
|
77
|
+
return this.get();
|
|
78
|
+
},
|
|
77
79
|
sub(cb) {
|
|
78
80
|
if (started) // initial event already passed, we'll make the first call
|
|
79
81
|
cb(getConfig(k), { k, was: defaultValue, defaultValue, version: configVersion.compiled(), object });
|
|
@@ -90,8 +92,10 @@ function defineConfig(k, defaultValue, compiler) {
|
|
|
90
92
|
}, { warnAfter: 1000 }); // e.g. each plugin watch enable_plugins
|
|
91
93
|
},
|
|
92
94
|
set(v) {
|
|
93
|
-
if (typeof v === 'function')
|
|
94
|
-
|
|
95
|
+
if (typeof v === 'function') {
|
|
96
|
+
const draft = structuredClone(this.get());
|
|
97
|
+
this.set(v(draft) ?? draft); // use return value if provided
|
|
98
|
+
}
|
|
95
99
|
else
|
|
96
100
|
setConfig1(k, v);
|
|
97
101
|
},
|
|
@@ -220,7 +224,7 @@ function subMultipleConfigs(cb, configs) {
|
|
|
220
224
|
};
|
|
221
225
|
}
|
|
222
226
|
exports.showHelp = argv_1.argv.help;
|
|
223
|
-
exports.configReady = events_1.default.once('configReady'); // the
|
|
227
|
+
exports.configReady = events_1.default.once('configReady').then(x => x[0]); // the value is startedWithoutConfig. The .then also avoids exposing the cancel-subscription function.
|
|
224
228
|
exports.configReady.then(() => {
|
|
225
229
|
if (!exports.showHelp)
|
|
226
230
|
return;
|
package/src/consoleLog.js
CHANGED
|
@@ -10,7 +10,16 @@ const cross_1 = require("./cross");
|
|
|
10
10
|
const fs_1 = require("fs");
|
|
11
11
|
const argv_1 = require("./argv");
|
|
12
12
|
exports.consoleLog = [];
|
|
13
|
+
const originalConsoleLog = console.log;
|
|
13
14
|
const f = argv_1.argv.consoleFile ? (0, fs_1.createWriteStream)(argv_1.argv.consoleFile, { flags: 'a', encoding: 'utf8' }) : null;
|
|
15
|
+
let terminalOutputBroken = false;
|
|
16
|
+
for (const stream of [process.stdout, process.stderr])
|
|
17
|
+
stream.on('error', err => {
|
|
18
|
+
if (!isBrokenTerminalOutput(err))
|
|
19
|
+
throw err;
|
|
20
|
+
// after the terminal/pipe is gone, further console writes would just re-emit the same process-level error
|
|
21
|
+
terminalOutputBroken = true;
|
|
22
|
+
});
|
|
14
23
|
for (const k of ['log', 'warn', 'error', 'debug']) {
|
|
15
24
|
const original = console[k];
|
|
16
25
|
console[k] = (...args) => {
|
|
@@ -28,7 +37,31 @@ for (const k of ['log', 'warn', 'error', 'debug']) {
|
|
|
28
37
|
if (k !== 'log')
|
|
29
38
|
args.unshift('!');
|
|
30
39
|
}
|
|
31
|
-
|
|
40
|
+
if (!terminalOutputBroken) {
|
|
41
|
+
try {
|
|
42
|
+
return original((0, cross_1.formatTime)(ts), ...args);
|
|
43
|
+
} // bundled nodejs doesn't have locales (and apparently uses en-US)
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (!isBrokenTerminalOutput(err))
|
|
46
|
+
throw err;
|
|
47
|
+
terminalOutputBroken = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
Object.assign(console[k], { original });
|
|
52
|
+
}
|
|
53
|
+
const over = console.log;
|
|
54
|
+
for (const k of ['table']) {
|
|
55
|
+
const original = console[k];
|
|
56
|
+
console[k] = (...args) => {
|
|
57
|
+
console.log = originalConsoleLog;
|
|
58
|
+
// @ts-ignore
|
|
59
|
+
try {
|
|
60
|
+
return original(...args);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
console.log = over;
|
|
64
|
+
}
|
|
32
65
|
};
|
|
33
66
|
}
|
|
34
67
|
function safeJoin(a) {
|
|
@@ -55,6 +88,11 @@ function safeJoin(a) {
|
|
|
55
88
|
}).join(' ');
|
|
56
89
|
}
|
|
57
90
|
}
|
|
91
|
+
function isBrokenTerminalOutput(err) {
|
|
92
|
+
const code = err?.code;
|
|
93
|
+
return code === 'EPIPE' || code === 'EIO'
|
|
94
|
+
|| code === 'ERR_STREAM_DESTROYED' || code === 'ERR_STREAM_WRITE_AFTER_END';
|
|
95
|
+
}
|
|
58
96
|
function consoleHint(msg) {
|
|
59
97
|
console.log("HINT: " + msg);
|
|
60
98
|
}
|
package/src/const.js
CHANGED
|
@@ -68,7 +68,7 @@ exports.DEV = process.env.DEV ? 'DEV' : '';
|
|
|
68
68
|
exports.ORIGINAL_CWD = process.cwd();
|
|
69
69
|
exports.HFS_STARTED = new Date();
|
|
70
70
|
const PKG_PATH = (0, path_1.join)(__dirname, '..', 'package.json');
|
|
71
|
-
exports.BUILD_TIMESTAMP =
|
|
71
|
+
exports.BUILD_TIMESTAMP = "2026-05-09T20:07:35.794Z";
|
|
72
72
|
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
|
|
73
73
|
exports.VERSION = pkg.version;
|
|
74
74
|
exports.RUNNING_BETA = exports.VERSION.includes('-');
|
|
@@ -78,6 +78,10 @@ exports.IS_BINARY = !/node|bun/.test((0, path_1.basename)(process.execPath)); //
|
|
|
78
78
|
exports.APP_PATH = (0, path_1.dirname)(exports.IS_BINARY ? process.execPath : __dirname); // __dirname's parent can be compared with cwd
|
|
79
79
|
exports.MIME_AUTO = 'auto';
|
|
80
80
|
exports.CONFIG_FILE = 'config.yaml';
|
|
81
|
+
if (argv_1.argv.version) {
|
|
82
|
+
process.stdout.write(exports.VERSION + '\n');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
81
85
|
// we want this to be the first stuff to be printed, then we print it in this module, that is executed at the beginning
|
|
82
86
|
if (exports.DEV) {
|
|
83
87
|
console.clear();
|
package/src/cross.js
CHANGED
|
@@ -34,8 +34,10 @@ exports.removeStarting = removeStarting;
|
|
|
34
34
|
exports.strinsert = strinsert;
|
|
35
35
|
exports.splitAt = splitAt;
|
|
36
36
|
exports.stringAfter = stringAfter;
|
|
37
|
+
exports.stringBefore = stringBefore;
|
|
37
38
|
exports.truthy = truthy;
|
|
38
39
|
exports.onlyTruthy = onlyTruthy;
|
|
40
|
+
exports.countUniqueBy = countUniqueBy;
|
|
39
41
|
exports.setHidden = setHidden;
|
|
40
42
|
exports.try_ = try_;
|
|
41
43
|
exports.with_ = with_;
|
|
@@ -125,7 +127,7 @@ exports.CFG = constMap(['geo_enable', 'geo_allow', 'geo_list', 'geo_allow_unknow
|
|
|
125
127
|
'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua', 'log_spam', 'track_ips',
|
|
126
128
|
'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'force_address', 'split_uploads',
|
|
127
129
|
'force_lang', 'suspend_plugins', 'base_url', 'size_1024', 'disable_custom_html', 'comments_storage',
|
|
128
|
-
'force_webdav_login', 'webdav_initial_auth', 'outbound_proxy']);
|
|
130
|
+
'force_webdav_login', 'webdav_initial_auth', 'outbound_proxy', 'mapped_port', 'upnp_enabled']);
|
|
129
131
|
exports.LIST = { add: '+', remove: '-', update: '=', props: 'props', ready: 'ready', error: 'e' };
|
|
130
132
|
exports.WHO_ANYONE = true;
|
|
131
133
|
exports.WHO_NO_ONE = false;
|
|
@@ -215,12 +217,31 @@ function stringAfter(sub, all) {
|
|
|
215
217
|
const i = all.indexOf(sub);
|
|
216
218
|
return i < 0 ? '' : all.slice(i + sub.length);
|
|
217
219
|
}
|
|
220
|
+
function stringBefore(sub, all, returnEmptyWhenSubMissing = true) {
|
|
221
|
+
const i = all.indexOf(sub);
|
|
222
|
+
return i >= 0 ? all.slice(0, i + sub.length - 1) : returnEmptyWhenSubMissing ? '' : all;
|
|
223
|
+
}
|
|
218
224
|
function truthy(value) {
|
|
219
225
|
return Boolean(value);
|
|
220
226
|
}
|
|
221
227
|
function onlyTruthy(arr) {
|
|
222
228
|
return arr.filter(truthy);
|
|
223
229
|
}
|
|
230
|
+
function countUniqueBy(items, keyFn, predicate) {
|
|
231
|
+
// use a Set so unique counting stays linear even on very large live lists
|
|
232
|
+
const seen = new Set();
|
|
233
|
+
let count = 0;
|
|
234
|
+
for (const item of items) {
|
|
235
|
+
if (predicate && !predicate(item))
|
|
236
|
+
continue;
|
|
237
|
+
const key = keyFn(item);
|
|
238
|
+
if (seen.has(key))
|
|
239
|
+
continue;
|
|
240
|
+
seen.add(key);
|
|
241
|
+
count++;
|
|
242
|
+
}
|
|
243
|
+
return count;
|
|
244
|
+
}
|
|
224
245
|
function setHidden(dest, src) {
|
|
225
246
|
return Object.defineProperties(dest, newObj(src, value => ({
|
|
226
247
|
enumerable: false,
|
package/src/errorPages.js
CHANGED
|
@@ -14,18 +14,28 @@ async function sendErrorPage(ctx, code = ctx.status) {
|
|
|
14
14
|
ctx.type = 'text';
|
|
15
15
|
ctx.set('content-disposition', ''); // reset ctx.attachment (or forceDownload)
|
|
16
16
|
ctx.status = code;
|
|
17
|
-
|
|
17
|
+
let msg = cross_1.HTTP_MESSAGES[ctx.status] || '';
|
|
18
18
|
if (!msg)
|
|
19
19
|
return;
|
|
20
20
|
const lang = await (0, lang_1.getLangData)(ctx);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (!errorPage)
|
|
27
|
-
return;
|
|
28
|
-
if (errorPage.includes('<'))
|
|
21
|
+
const trans = lang ? Object.values(lang)[0]?.translate : undefined;
|
|
22
|
+
msg = trans?.[msg] ?? msg;
|
|
23
|
+
const page = (0, customHtml_1.getSection)(ctx.status === cross_1.HTTP_UNAUTHORIZED ? 'unauthorized' : String(ctx.status))
|
|
24
|
+
|| SIMPLE_PAGE || '';
|
|
25
|
+
if (page.includes('<'))
|
|
29
26
|
ctx.type = 'html';
|
|
30
|
-
ctx.body =
|
|
27
|
+
ctx.body = (0, cross_1.replace)(page, { MESSAGE: msg, HOME: trans?.home ?? 'home', URL: ctx.state.revProxyPath || '/' }, '$');
|
|
31
28
|
}
|
|
29
|
+
const SIMPLE_PAGE = `<!DOCTYPE html>
|
|
30
|
+
<html><head>
|
|
31
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
32
|
+
<title>$MESSAGE</title>
|
|
33
|
+
<style>
|
|
34
|
+
body{margin-top:30vh; text-align:center; font-family:sans-serif;}
|
|
35
|
+
a{color:#68a}
|
|
36
|
+
@media (prefers-color-scheme:dark){body{background:#111; color:#999;}}
|
|
37
|
+
</style>
|
|
38
|
+
</head><body>
|
|
39
|
+
<h1>$MESSAGE</h1>
|
|
40
|
+
<h2><a href="$URL">$HOME</a></h2>
|
|
41
|
+
</body></html>`;
|
package/src/events.js
CHANGED
package/src/fileAttr.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileAttrDb = void 0;
|
|
3
4
|
exports.storeFileAttr = storeFileAttr;
|
|
4
5
|
exports.loadFileAttr = loadFileAttr;
|
|
5
6
|
exports.purgeFileAttr = purgeFileAttr;
|
|
7
|
+
exports.moveStoredFileAttrs = moveStoredFileAttrs;
|
|
8
|
+
exports.deleteStoredFileAttrs = deleteStoredFileAttrs;
|
|
6
9
|
const kvstorage_1 = require("@rejetto/kvstorage");
|
|
7
10
|
const util_1 = require("util");
|
|
8
11
|
const promises_1 = require("fs/promises");
|
|
@@ -11,15 +14,17 @@ const first_1 = require("./first");
|
|
|
11
14
|
const promises_2 = require("node:fs/promises");
|
|
12
15
|
const util_files_1 = require("./util-files");
|
|
13
16
|
const const_1 = require("./const");
|
|
17
|
+
const path_1 = require("path");
|
|
14
18
|
const fsx = (0, cross_1.try_)(() => {
|
|
15
19
|
const lib = require('fs-x-attributes');
|
|
16
20
|
return { set: (0, util_1.promisify)(lib.set), get: (0, util_1.promisify)(lib.get) };
|
|
17
21
|
}, () => console.warn('fs-x-attributes not available'));
|
|
18
|
-
const fileAttrDb = new kvstorage_1.KvStorage({ defaultPutDelay: 1000, maxPutDelay: 5000 });
|
|
19
|
-
(0, first_1.onProcessExit)(() => fileAttrDb.close());
|
|
20
22
|
const FN = 'file-attr.kv';
|
|
21
|
-
|
|
23
|
+
exports.fileAttrDb = new kvstorage_1.KvStorage({ defaultPutDelay: 1000, maxPutDelay: 5000 });
|
|
24
|
+
(0, first_1.onProcessExit)(() => exports.fileAttrDb.close());
|
|
25
|
+
exports.fileAttrDb.open(FN).catch(e => console.error(String(e)));
|
|
22
26
|
const FILE_ATTR_PREFIX = 'user.hfs.'; // user. prefix to be linux compatible
|
|
27
|
+
const FILE_ATTR_KEY_SEPARATOR = '|';
|
|
23
28
|
/* @param v must be JSON-able or undefined */
|
|
24
29
|
async function storeFileAttr(path, k, v) {
|
|
25
30
|
const s = await (0, util_files_1.statWithTimeout)(path).catch(() => null);
|
|
@@ -30,29 +35,82 @@ async function storeFileAttr(path, k, v) {
|
|
|
30
35
|
return true;
|
|
31
36
|
}
|
|
32
37
|
// fallback to our kv-storage
|
|
33
|
-
|
|
34
|
-
if (!s && !v)
|
|
35
|
-
return; // file was probably deleted, and we were asked to remove a possible attribute, but there's no fileAttrDb, so we are done, don't create the db file for nothing
|
|
36
|
-
else
|
|
37
|
-
await fileAttrDb.open(FN);
|
|
38
|
-
// pipe should be a safe separator
|
|
39
|
-
return await fileAttrDb.put(`${path}|${k}`, v)?.catch((e) => {
|
|
38
|
+
return await exports.fileAttrDb.put(fileAttrKey(path, k), v)?.catch((e) => {
|
|
40
39
|
console.error("Couldn't store metadata on", path, String(e.message || e));
|
|
41
40
|
return false;
|
|
42
41
|
}) ?? true; // if put is undefined, the value was already there
|
|
43
42
|
}
|
|
44
43
|
async function loadFileAttr(path, k) {
|
|
45
44
|
return await fsx?.get(path, FILE_ATTR_PREFIX + k)
|
|
46
|
-
.then((x) => x === '' ? undefined : (0, cross_1.tryJson)(String(x)), () => fileAttrDb.isOpen() ? fileAttrDb.get(
|
|
45
|
+
.then((x) => x === '' ? undefined : (0, cross_1.tryJson)(String(x)), () => exports.fileAttrDb.isOpen() ? exports.fileAttrDb.get(fileAttrKey(path, k)).catch(console.error) : null)
|
|
47
46
|
?? undefined; // normalize, as we get null instead of undefined on windows
|
|
48
47
|
}
|
|
49
48
|
async function purgeFileAttr() {
|
|
50
49
|
let n = 0;
|
|
51
|
-
await Promise.all(Array.from(fileAttrDb.keys()).map(k => {
|
|
52
|
-
const
|
|
53
|
-
return fn && (0, promises_1.access)(fn).catch(() => n++ && void fileAttrDb.del(k));
|
|
50
|
+
await Promise.all(Array.from(exports.fileAttrDb.keys()).map(k => {
|
|
51
|
+
const fn = splitFileAttrKey(k)?.filePath;
|
|
52
|
+
return fn && (0, promises_1.access)(fn).catch(() => n++ && void exports.fileAttrDb.del(k));
|
|
54
53
|
}));
|
|
55
54
|
if (n)
|
|
56
|
-
await fileAttrDb.rewrite();
|
|
55
|
+
await exports.fileAttrDb.rewrite();
|
|
57
56
|
console.log(`Removed ${n} entrie(s)`);
|
|
58
57
|
}
|
|
58
|
+
async function moveStoredFileAttrs(fromPath, toPath) {
|
|
59
|
+
try {
|
|
60
|
+
if (fromPath === toPath || !exports.fileAttrDb.isOpen())
|
|
61
|
+
return;
|
|
62
|
+
const entries = storedFileAttrEntries();
|
|
63
|
+
const affectedEntries = entries.filter(x => isSameOrInside(fromPath, x.filePath));
|
|
64
|
+
if (!affectedEntries.length)
|
|
65
|
+
return;
|
|
66
|
+
const affectedWithValues = await Promise.all(affectedEntries.map(async (x) => ({
|
|
67
|
+
...x,
|
|
68
|
+
value: await exports.fileAttrDb.get(x.key)
|
|
69
|
+
})));
|
|
70
|
+
const oldDestinationKeys = entries.filter(x => isSameOrInside(toPath, x.filePath)).map(x => x.key);
|
|
71
|
+
// destination attrs must be cleared first because a replaced file may not have all attrs owned by the source
|
|
72
|
+
await Promise.all(oldDestinationKeys.map(k => exports.fileAttrDb.del(k)));
|
|
73
|
+
await Promise.all(affectedWithValues.map(async ({ key, filePath, attr, value }) => {
|
|
74
|
+
const rel = (0, path_1.relative)(fromPath, filePath);
|
|
75
|
+
// physical path keys the fallback DB, so filesystem moves must carry descendant entries explicitly
|
|
76
|
+
await exports.fileAttrDb.put(fileAttrKey((0, path_1.join)(toPath, rel), attr), value);
|
|
77
|
+
await exports.fileAttrDb.del(key);
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
// metadata sync runs after the filesystem mutation, so it must not report the completed file operation as failed
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error("Couldn't move metadata in file-attr DB", fromPath, toPath, String(e.message || e));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function deleteStoredFileAttrs(path) {
|
|
86
|
+
try {
|
|
87
|
+
if (!exports.fileAttrDb.isOpen())
|
|
88
|
+
return;
|
|
89
|
+
const keys = storedFileAttrEntries().filter(x => isSameOrInside(path, x.filePath)).map(x => x.key);
|
|
90
|
+
await Promise.all(keys.map(k => exports.fileAttrDb.del(k)));
|
|
91
|
+
}
|
|
92
|
+
// metadata cleanup runs after deletion, so surfacing this would leave clients seeing a false delete failure
|
|
93
|
+
catch (e) {
|
|
94
|
+
console.error("Couldn't delete metadata from file-attr DB", path, String(e.message || e));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function storedFileAttrEntries() {
|
|
98
|
+
return (0, cross_1.onlyTruthy)(Array.from(exports.fileAttrDb.keys()).map(splitFileAttrKey));
|
|
99
|
+
}
|
|
100
|
+
function splitFileAttrKey(key) {
|
|
101
|
+
const i = key.lastIndexOf(FILE_ATTR_KEY_SEPARATOR);
|
|
102
|
+
if (i < 0)
|
|
103
|
+
return;
|
|
104
|
+
return {
|
|
105
|
+
key,
|
|
106
|
+
filePath: key.slice(0, i),
|
|
107
|
+
attr: key.slice(i + 1)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function fileAttrKey(path, attr) {
|
|
111
|
+
return path + FILE_ATTR_KEY_SEPARATOR + attr;
|
|
112
|
+
}
|
|
113
|
+
function isSameOrInside(parent, path) {
|
|
114
|
+
const rel = (0, path_1.relative)(parent, path);
|
|
115
|
+
return rel === '' || Boolean(rel) && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel);
|
|
116
|
+
}
|
package/src/frontEndApis.js
CHANGED
|
@@ -195,7 +195,8 @@ async function moveFiles(uri_from, uri_to, ctx, override) {
|
|
|
195
195
|
throw e; // exdev = different drive
|
|
196
196
|
await (0, promises_1.copyFile)(src, dest);
|
|
197
197
|
await (0, promises_1.unlink)(src);
|
|
198
|
-
}).
|
|
198
|
+
}).then(() => (0, misc_1.moveStoredFileAttrs)(src, dest))
|
|
199
|
+
.catch(e => e.code || String(e));
|
|
199
200
|
}))
|
|
200
201
|
};
|
|
201
202
|
}
|
|
@@ -216,6 +217,7 @@ async function requestedRename(node, newName, ctx) {
|
|
|
216
217
|
try {
|
|
217
218
|
const destSource = (0, path_1.join)((0, path_1.dirname)(node.source), newName);
|
|
218
219
|
await (0, promises_1.rename)(node.source, destSource);
|
|
220
|
+
await (0, misc_1.moveStoredFileAttrs)(node.source, destSource);
|
|
219
221
|
(0, comments_1.getCommentFor)(node.source).then(c => {
|
|
220
222
|
if (!c)
|
|
221
223
|
return;
|
package/src/index.js
CHANGED
|
@@ -39,13 +39,13 @@ if (new config_1.Version(process.versions.node).olderThan('18.15.0')) {
|
|
|
39
39
|
process.exit(2);
|
|
40
40
|
}
|
|
41
41
|
process.title = 'HFS ' + const_1.VERSION;
|
|
42
|
+
misc_1.httpStream.defaultUA = 'HFS';
|
|
42
43
|
const keys = process.env.COOKIE_SIGN_KEYS?.split(',')
|
|
43
44
|
|| [(0, misc_1.randomId)(30)]; // randomness at start gives some extra security, btu also invalidates existing sessions
|
|
44
45
|
exports.app = new koa_1.default({ keys });
|
|
45
46
|
exports.app.use(middlewares_1.sessionMiddleware)
|
|
46
47
|
.use(selfCheck_1.selfCheckMiddleware)
|
|
47
48
|
.use(acme_1.acmeMiddleware)
|
|
48
|
-
.use(middlewares_1.someSecurity)
|
|
49
49
|
.use(middlewares_1.prepareState)
|
|
50
50
|
.use(geo_1.geoFilter)
|
|
51
51
|
.use(ips_1.trackIpsMw)
|
|
@@ -54,6 +54,7 @@ exports.app.use(middlewares_1.sessionMiddleware)
|
|
|
54
54
|
.use(middlewares_1.headRequests)
|
|
55
55
|
.use(roots_1.rootsMiddleware)
|
|
56
56
|
.use(log_1.logMw)
|
|
57
|
+
.use(middlewares_1.someSecurity)
|
|
57
58
|
.use(throttler_1.throttler)
|
|
58
59
|
.use(plugins_1.pluginsMiddleware)
|
|
59
60
|
.use((0, koa_mount_1.default)(const_1.API_URI, (0, apiMiddleware_1.apiMiddleware)({ ...frontEndApis_1.frontEndApis, ...adminApis_1.adminApis })))
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "إنشاء مجلد",
|
|
58
58
|
"Pick files": "اختر ملفات",
|
|
59
59
|
"Pick folder": "اختر مجلدًا",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# file} other{# files}}, {size}، جاهز للرفع",
|
|
61
|
+
"Send": "إرسال",
|
|
61
62
|
"Clear": "مسح",
|
|
62
63
|
"failed_upload": "تعذر رفع {name}",
|
|
63
64
|
"confirm_resume": "استئناف الرفع؟",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"Create folder": "Създай папка",
|
|
57
57
|
"Pick files": "Избери файлове",
|
|
58
58
|
"Pick folder": "Избери папка",
|
|
59
|
-
"
|
|
59
|
+
"ready_to_upload": "{n,plural,one{# файл} other{# файла}}, {size}, готови за качване",
|
|
60
|
+
"Send": "Изпрати",
|
|
60
61
|
"Clear": "Изчисти",
|
|
61
62
|
"failed_upload": "Неуспешно качване на {name}",
|
|
62
63
|
"file too large": "файлът е твърде голям",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Ordner erstellen",
|
|
58
58
|
"Pick files": "Dateien auswählen",
|
|
59
59
|
"Pick folder": "Ordner auswählen",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# Datei} other{# Dateien}}, {size}, bereit zum Hochladen",
|
|
61
|
+
"Send": "Senden",
|
|
61
62
|
"Clear": "Löschen",
|
|
62
63
|
"failed_upload": "Konnte {name} nicht hochladen.",
|
|
63
64
|
"confirm_resume": "Upload fortsetzen?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Δημιουργία φακέλου",
|
|
58
58
|
"Pick files": "Επιλογή αρχείων",
|
|
59
59
|
"Pick folder": "Επιλογή φακέλου",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# αρχείο} other{# αρχεία}}, {size}, έτοιμα για αποστολή",
|
|
61
|
+
"Send": "Αποστολή",
|
|
61
62
|
"Clear": "Καθαρισμός",
|
|
62
63
|
"failed_upload": "Αποτυχία μεταφόρτωσης του {name}",
|
|
63
64
|
"confirm_resume": "Συνέχεια της μεταφόρτωσης;",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Crear carpeta",
|
|
58
58
|
"Pick files": "Seleccionar archivo",
|
|
59
59
|
"Pick folder": "Seleccionar carpeta",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# archivo} other{# archivos}}, {size}, listo para subir",
|
|
61
|
+
"Send": "Enviar",
|
|
61
62
|
"Clear": "Limpiar",
|
|
62
63
|
"failed_upload": "No se puede subir {name}",
|
|
63
64
|
"confirm_resume": "Continuar subida?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Tee uusi kansio",
|
|
58
58
|
"Pick files": "Valitse lähetettävät tiedostot",
|
|
59
59
|
"Pick folder": "Valitse lähetettävä kansio",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# tiedosto} other{# tiedostot}}, {size}, valmis ladattavaksi",
|
|
61
|
+
"Send": "Lähetä",
|
|
61
62
|
"Clear": "Tyhjennä",
|
|
62
63
|
"failed_upload": "Lähetys epäonnistui {name}",
|
|
63
64
|
"confirm_resume": "Jatketaanko lähetystä?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Créez un dossier",
|
|
58
58
|
"Pick files": "Choisissez les fichiers",
|
|
59
59
|
"Pick folder": "Choisissez un dossier",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# fichier} other{# fichiers}}, {size}, prêt à téléverser",
|
|
61
|
+
"Send": "Envoyer",
|
|
61
62
|
"Clear": "Effacer",
|
|
62
63
|
"failed_upload": "Impossible de transférer {name}",
|
|
63
64
|
"confirm_resume": "Reprendre le télé-versement?",
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"Create folder": "Mappa létrehozása",
|
|
59
59
|
"Pick files": "Fájlok kiválasztása",
|
|
60
60
|
"Pick folder": "Mappa kiválasztása",
|
|
61
|
-
"
|
|
61
|
+
"ready_to_upload": "{n,plural,one{# fájl} other{# fájl}}, {size}, feltöltésre kész",
|
|
62
|
+
"Send": "Küldés",
|
|
62
63
|
"Clear": "Mégsem",
|
|
63
64
|
"failed_upload": "A(z) {name} feltöltése sikertelen",
|
|
64
65
|
"confirm_resume": "Folytatja a feltöltést?",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"Create folder": "Crea cartella",
|
|
56
56
|
"Pick files": "Scegli file",
|
|
57
57
|
"Pick folder": "Scegli una cartella",
|
|
58
|
-
"
|
|
58
|
+
"ready_to_upload": "{n} file, {size}, pronto per il caricamento",
|
|
59
|
+
"Send": "Invia",
|
|
59
60
|
"Clear": "Azzera",
|
|
60
61
|
"failed_upload": "Upload fallito per {name}",
|
|
61
62
|
"file too large": "file troppo grande",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "新しいフォルダ",
|
|
58
58
|
"Pick files": "ファイルを選択",
|
|
59
59
|
"Pick folder": "フォルダを選択",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# ファイル} other{# ファイル}}、{size}、アップロード準備完了",
|
|
61
|
+
"Send": "送信",
|
|
61
62
|
"Clear": "クリア",
|
|
62
63
|
"failed_upload": "{name}をアップロードできませんでした",
|
|
63
64
|
"confirm_resume": "アップロードを再開しますか?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "폴더 생성",
|
|
58
58
|
"Pick files": "파일 선택",
|
|
59
59
|
"Pick folder": "폴더 선택",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# file} other{# files}}, {size}, 업로드 준비 완료",
|
|
61
|
+
"Send": "보내기",
|
|
61
62
|
"Clear": "지우기",
|
|
62
63
|
"failed_upload": "{name} 업로드에 실패했습니다.",
|
|
63
64
|
"confirm_resume": "업로드를 재시도 하시겠습니까?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Sukurti aplanką",
|
|
58
58
|
"Pick files": "Pasirinkti failus",
|
|
59
59
|
"Pick folder": "Pasirinkti aplanką",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# failas} few{# failai} other{# failų}}, {size}, paruošta įkelti",
|
|
61
|
+
"Send": "Siųsti",
|
|
61
62
|
"Clear": "Išvalyti",
|
|
62
63
|
"failed_upload": "Nepavyko įkelti {name}",
|
|
63
64
|
"file too large": "failas per didelis",
|
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
"upload_finished": "{n} selesai ({size})",
|
|
60
60
|
"upload_errors": "{n} gagal",
|
|
61
61
|
"upload_file_rejected": "Beberapa fail tidak diterima",
|
|
62
|
+
"ready_to_upload": "{n,plural,one{# fail} other{# fail}}, {size}, sedia untuk dimuat naik",
|
|
63
|
+
"Send": "Hantar",
|
|
62
64
|
"download counter": "pengira muat turun",
|
|
63
65
|
"File menu": "Menu fail",
|
|
64
66
|
"Folder menu": "Menu folder",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Creeer map",
|
|
58
58
|
"Pick files": "Kies bestand",
|
|
59
59
|
"Pick folder": "Kies map",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# bestand} other{# bestanden}}, {size}, klaar om te uploaden",
|
|
61
|
+
"Send": "Verzenden",
|
|
61
62
|
"Clear": "Clear",
|
|
62
63
|
"failed_upload": "Kon het niet uploaden {name}",
|
|
63
64
|
"file too large": "bestand te groot",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Criar pasta",
|
|
58
58
|
"Pick files": "Escolher arquivos",
|
|
59
59
|
"Pick folder": "Escolher pastas",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# arquivo} other{# arquivos}}, {size}, pronto para enviar",
|
|
61
|
+
"Send": "Enviar",
|
|
61
62
|
"Clear": "Limpar",
|
|
62
63
|
"failed_upload": "Não foi possivel enviar o arquivo {name}",
|
|
63
64
|
"file too large": "arquivo é muito grande",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Creează folder",
|
|
58
58
|
"Pick files": "Selectează fișiere",
|
|
59
59
|
"Pick folder": "Selectează folder",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# fișier} other{# fișiere}}, {size}, gata de încărcat",
|
|
61
|
+
"Send": "Trimite",
|
|
61
62
|
"Clear": "Șterge",
|
|
62
63
|
"failed_upload": "Nu s-a putut încărca {name}",
|
|
63
64
|
"confirm_resume": "Continuă încărcarea?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Создать папку",
|
|
58
58
|
"Pick files": "Выбрать файлы",
|
|
59
59
|
"Pick folder": "Выбрать папку",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural, one{# файл} few{# файла} many{# файлов}}, {size}, готово к загрузке",
|
|
61
|
+
"Send": "Загрузить",
|
|
61
62
|
"Clear": "Очистить",
|
|
62
63
|
"failed_upload": "Не удалась загрузить {name}",
|
|
63
64
|
"confirm_resume": "Возобновить загрузку?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Napravi fasciklu",
|
|
58
58
|
"Pick files": "Izaberite datoteke",
|
|
59
59
|
"Pick folder": "Izaberite fasciklu",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# datoteka} other{# datoteke}}, {size}, spremno za otpremanje",
|
|
61
|
+
"Send": "Pošalji",
|
|
61
62
|
"Clear": "Očisti",
|
|
62
63
|
"failed_upload": "Nije moguće otpremiti {name}",
|
|
63
64
|
"file too large": "datoteka je prevelika",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"Create folder": "Креирај фолдер",
|
|
60
60
|
"Pick files": " Изаберите документ(а)",
|
|
61
61
|
"Pick folder": "Изаберите фолдер(е)",
|
|
62
|
-
"
|
|
62
|
+
"ready_to_upload": "{n} документ(а), {size}, спремно за отпремање",
|
|
63
|
+
"Send": "Пошаљи",
|
|
63
64
|
"Clear": "Обриши",
|
|
64
65
|
"failed_upload": "Отпремање није успело {name}",
|
|
65
66
|
"confirm_resume": "Настави преузимање?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "สร้างโฟลเดอร์",
|
|
58
58
|
"Pick files": "เลือกไฟล์",
|
|
59
59
|
"Pick folder": "เลือกโฟลเดอร์",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# ไฟล์} other{# ไฟล์}}, {size}, พร้อมอัปโหลด",
|
|
61
|
+
"Send": "ส่ง",
|
|
61
62
|
"Clear": "ล้าง",
|
|
62
63
|
"failed_upload": "ไม่สามารถอัปโหลด {name}",
|
|
63
64
|
"confirm_resume": "ดำเนินการอัปโหลดต่อหรือไม่?",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"Create folder": "Klasör Oluştur",
|
|
58
58
|
"Pick files": "Dosya(lar)ı Seç",
|
|
59
59
|
"Pick folder": "Klasör Seç",
|
|
60
|
-
"
|
|
60
|
+
"ready_to_upload": "{n,plural,one{# dosya} other{# dosya}}, {size}, yüklemeye hazır",
|
|
61
|
+
"Send": "Gönder",
|
|
61
62
|
"Clear": "Temizle",
|
|
62
63
|
"failed_upload": "{name} UPLOAD edilemedi (veri aktarım sorunu)",
|
|
63
64
|
"confirm_resume": "UPLOAD kaldığı yerden devam etsin mi?",
|