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.
Files changed (220) hide show
  1. package/admin/assets/af-RjyQ-neT.js +1 -0
  2. package/admin/assets/am-PptHpglb.js +1 -0
  3. package/admin/assets/ar-Du3syDxa.js +1 -0
  4. package/admin/assets/ar-dz-CiziRh1x.js +1 -0
  5. package/admin/assets/ar-iq-hCKg0CRP.js +1 -0
  6. package/admin/assets/ar-kw-COYePt-p.js +1 -0
  7. package/admin/assets/ar-ly-CY5yETqb.js +1 -0
  8. package/admin/assets/ar-ma-DczXi97r.js +1 -0
  9. package/admin/assets/ar-sa-Bu7sTKzl.js +1 -0
  10. package/admin/assets/ar-tn-BrG0r6qZ.js +1 -0
  11. package/admin/assets/az-DYRCrdk0.js +1 -0
  12. package/admin/assets/be-DObSs5HX.js +1 -0
  13. package/admin/assets/bg-BDBkMe50.js +1 -0
  14. package/admin/assets/bi-CApXFwc6.js +1 -0
  15. package/admin/assets/bm-CjxCV8Lr.js +1 -0
  16. package/admin/assets/bn-DzWZbDi3.js +1 -0
  17. package/admin/assets/bn-bd-ijdPRJbb.js +1 -0
  18. package/admin/assets/bo-BDDfj5XH.js +1 -0
  19. package/admin/assets/br-DRHEjait.js +1 -0
  20. package/admin/assets/bs-B0Zv2Agm.js +1 -0
  21. package/admin/assets/ca-DuZAUqU_.js +1 -0
  22. package/admin/assets/cs-DVkylosR.js +1 -0
  23. package/admin/assets/cv-8Is1Fns9.js +1 -0
  24. package/admin/assets/cy-Bk30Or_y.js +1 -0
  25. package/admin/assets/da-C41DIdZK.js +1 -0
  26. package/admin/assets/de--qvLJgIE.js +1 -0
  27. package/admin/assets/de-at-D5vCBlsw.js +1 -0
  28. package/admin/assets/de-ch-Oh2cO23c.js +1 -0
  29. package/admin/assets/dv-4ZO2KNbw.js +1 -0
  30. package/admin/assets/el-CobOUqyu.js +1 -0
  31. package/admin/assets/en-C9afClpS.js +1 -0
  32. package/admin/assets/en-au-C_QMghEl.js +1 -0
  33. package/admin/assets/en-ca-C80KILwc.js +1 -0
  34. package/admin/assets/en-gb-BwQ0asAI.js +1 -0
  35. package/admin/assets/en-ie-ConCcQkq.js +1 -0
  36. package/admin/assets/en-il-CAOzD4DD.js +1 -0
  37. package/admin/assets/en-in-87gardaO.js +1 -0
  38. package/admin/assets/en-nz-uMTjgkDP.js +1 -0
  39. package/admin/assets/en-sg-BlbiLv4L.js +1 -0
  40. package/admin/assets/en-tt-BXXmBax2.js +1 -0
  41. package/admin/assets/eo-BBL7o-0W.js +1 -0
  42. package/admin/assets/es-DuNej-79.js +1 -0
  43. package/admin/assets/es-do-bZWKFwTE.js +1 -0
  44. package/admin/assets/es-mx-C2Lfb86F.js +1 -0
  45. package/admin/assets/es-pr-wU3Du8m-.js +1 -0
  46. package/admin/assets/es-us-qcU-zRiw.js +1 -0
  47. package/admin/assets/et-B-4PjN_n.js +1 -0
  48. package/admin/assets/eu-D0pNDZ9i.js +1 -0
  49. package/admin/assets/fa-R_zmYBGc.js +1 -0
  50. package/admin/assets/fi-CWYS_0Hg.js +1 -0
  51. package/admin/assets/fo-aJYpcnWS.js +1 -0
  52. package/admin/assets/fr-BHFrWfwB.js +1 -0
  53. package/admin/assets/fr-ca-ihwmO47n.js +1 -0
  54. package/admin/assets/fr-ch-Cb8XbRfE.js +1 -0
  55. package/admin/assets/fy-DT9BUc6t.js +1 -0
  56. package/admin/assets/ga-B8p4naXJ.js +1 -0
  57. package/admin/assets/gd-Dzrl6rPc.js +1 -0
  58. package/admin/assets/gl-bN2tXl9d.js +1 -0
  59. package/admin/assets/gom-latn-DUK8PgIJ.js +1 -0
  60. package/admin/assets/gu-CxAIP8fw.js +1 -0
  61. package/admin/assets/he-Bm9XJVAj.js +1 -0
  62. package/admin/assets/hi-CHik9Qxx.js +1 -0
  63. package/admin/assets/hr-Dn57_dqN.js +1 -0
  64. package/admin/assets/ht-BRS-FrJq.js +1 -0
  65. package/admin/assets/hu-hVaGcZ-U.js +1 -0
  66. package/admin/assets/hy-am-heLxXhpz.js +1 -0
  67. package/admin/assets/id-B6AkO4h8.js +1 -0
  68. package/admin/assets/{index-DTxjaflW.js → index-Buyl8rI8.js} +1 -1
  69. package/admin/assets/index-CFWd-FDo.css +1 -0
  70. package/admin/assets/index-WXxQwyJV.js +889 -0
  71. package/admin/assets/is-BUADPoni.js +1 -0
  72. package/admin/assets/it-Ce3gssOP.js +1 -0
  73. package/admin/assets/it-ch-CJzj2czr.js +1 -0
  74. package/admin/assets/ja-DykMIu-9.js +1 -0
  75. package/admin/assets/jv-Cum6DLwa.js +1 -0
  76. package/admin/assets/ka-foCPVFxw.js +1 -0
  77. package/admin/assets/kk-CMNu1ysC.js +1 -0
  78. package/admin/assets/km-BfMisGaD.js +1 -0
  79. package/admin/assets/kn-R_9S5KJz.js +1 -0
  80. package/admin/assets/ko-DYRND-mM.js +1 -0
  81. package/admin/assets/ku-CNZ_weGy.js +1 -0
  82. package/admin/assets/ky-BhTAWL4I.js +1 -0
  83. package/admin/assets/lb-CxWhQxwF.js +1 -0
  84. package/admin/assets/lo-DBL_XLkE.js +1 -0
  85. package/admin/assets/lt-BakZVm4p.js +1 -0
  86. package/admin/assets/lv-CCn-slXG.js +1 -0
  87. package/admin/assets/me-xPEA4qjE.js +1 -0
  88. package/admin/assets/mi-6cCs2Mzx.js +1 -0
  89. package/admin/assets/mk-CwWbHJPS.js +1 -0
  90. package/admin/assets/ml-Dp7Ab1SV.js +1 -0
  91. package/admin/assets/mn-ClotGWAe.js +1 -0
  92. package/admin/assets/mr-GiM-paTF.js +1 -0
  93. package/admin/assets/ms-BiZ1_eVR.js +1 -0
  94. package/admin/assets/ms-my-oevI0fOH.js +1 -0
  95. package/admin/assets/mt-BaIHeOl1.js +1 -0
  96. package/admin/assets/my-Cg5Fc_1j.js +1 -0
  97. package/admin/assets/nb-ZPODjT6R.js +1 -0
  98. package/admin/assets/ne-C0ti-ZX2.js +1 -0
  99. package/admin/assets/nl-DP2PkkGx.js +1 -0
  100. package/admin/assets/nl-be-VPC0QyaW.js +1 -0
  101. package/admin/assets/nn-NLidKyJd.js +1 -0
  102. package/admin/assets/oc-lnc-Ddj9_PnD.js +1 -0
  103. package/admin/assets/pa-in-DSAWffhb.js +1 -0
  104. package/admin/assets/pl-Yojex5aS.js +1 -0
  105. package/admin/assets/pt-BQMs2la2.js +1 -0
  106. package/admin/assets/pt-br-DVeeNZu8.js +1 -0
  107. package/admin/assets/rn-CKkTrK3f.js +1 -0
  108. package/admin/assets/ro-BbgGULSZ.js +1 -0
  109. package/admin/assets/ru-mAZX3DTa.js +1 -0
  110. package/admin/assets/rw-CYErOxKd.js +1 -0
  111. package/admin/assets/sd-kfPTqkSy.js +1 -0
  112. package/admin/assets/se-D0bN3rDS.js +1 -0
  113. package/admin/assets/{sha512-D936QW8l.js → sha512-99nhg44S.js} +1 -1
  114. package/admin/assets/si-COjb_4Hy.js +1 -0
  115. package/admin/assets/sk-Co95XNOo.js +1 -0
  116. package/admin/assets/sl-DUXa5wTF.js +1 -0
  117. package/admin/assets/sq-B-KU0nWK.js +1 -0
  118. package/admin/assets/sr-Cb_b-zPG.js +1 -0
  119. package/admin/assets/sr-cyrl-csuTDIB6.js +1 -0
  120. package/admin/assets/ss-BDnE-cG6.js +1 -0
  121. package/admin/assets/sv-CzWdOHcE.js +1 -0
  122. package/admin/assets/sv-fi-IK8oi5nB.js +1 -0
  123. package/admin/assets/sw-fPHo5hof.js +1 -0
  124. package/admin/assets/ta-TteN0nyU.js +1 -0
  125. package/admin/assets/te-2gsQb1fF.js +1 -0
  126. package/admin/assets/tet-ClTpDYJv.js +1 -0
  127. package/admin/assets/tg-Bel7Uc6z.js +1 -0
  128. package/admin/assets/th-pDQttM9V.js +1 -0
  129. package/admin/assets/tk-CxoKYZH6.js +1 -0
  130. package/admin/assets/tl-ph-D9htRcOQ.js +1 -0
  131. package/admin/assets/tlh-7UqvDBxU.js +1 -0
  132. package/admin/assets/tr-NjJrq1iC.js +1 -0
  133. package/admin/assets/tzl-CzGaXn8M.js +1 -0
  134. package/admin/assets/tzm-BiuscZFr.js +1 -0
  135. package/admin/assets/tzm-latn-IeWkCgWf.js +1 -0
  136. package/admin/assets/ug-cn-5LK64x5v.js +1 -0
  137. package/admin/assets/uk-CzacUaQe.js +1 -0
  138. package/admin/assets/ur-BRV7z4Gu.js +1 -0
  139. package/admin/assets/uz-C4G77QOI.js +1 -0
  140. package/admin/assets/uz-latn-CQFrzZ6F.js +1 -0
  141. package/admin/assets/vi-DEFXdlJi.js +1 -0
  142. package/admin/assets/x-pseudo-sWj5RKxR.js +1 -0
  143. package/admin/assets/yo-CBDjk96e.js +1 -0
  144. package/admin/assets/zh-CQtX_fkD.js +1 -0
  145. package/admin/assets/zh-cn-D6dF0jKM.js +1 -0
  146. package/admin/assets/zh-hk-DY-Jaqdg.js +1 -0
  147. package/admin/assets/zh-tw-BdmVby0R.js +1 -0
  148. package/admin/index.html +2 -2
  149. package/frontend/assets/index-legacy-BtoTkZho.js +9 -0
  150. package/frontend/assets/{index-legacy-vmpqwZZf.js → index-legacy-CQovmh_0.js} +1 -1
  151. package/frontend/assets/{sha512-legacy-wI89-UHR.js → sha512-legacy-CXU3efCO.js} +1 -1
  152. package/frontend/index.html +1 -1
  153. package/npm-shrinkwrap.json +160 -131
  154. package/package.json +10 -10
  155. package/plugins/antibrute/plugin.js +150 -19
  156. package/plugins/list-uploader/public/main.js +1 -1
  157. package/src/acme.js +11 -7
  158. package/src/api.accounts.js +3 -3
  159. package/src/api.auth.js +3 -1
  160. package/src/api.get_file_list.js +16 -12
  161. package/src/api.monitor.js +47 -43
  162. package/src/api.net.js +4 -3
  163. package/src/api.vfs.js +1 -1
  164. package/src/commands.js +54 -1
  165. package/src/config.js +9 -5
  166. package/src/consoleLog.js +39 -1
  167. package/src/const.js +5 -1
  168. package/src/cross.js +22 -1
  169. package/src/errorPages.js +20 -10
  170. package/src/events.js +1 -0
  171. package/src/fileAttr.js +73 -15
  172. package/src/frontEndApis.js +3 -1
  173. package/src/index.js +2 -1
  174. package/src/langs/hfs-lang-ar.json +2 -1
  175. package/src/langs/hfs-lang-bg.json +2 -1
  176. package/src/langs/hfs-lang-de.json +2 -1
  177. package/src/langs/hfs-lang-el.json +2 -1
  178. package/src/langs/hfs-lang-es.json +2 -1
  179. package/src/langs/hfs-lang-fi.json +2 -1
  180. package/src/langs/hfs-lang-fr.json +2 -1
  181. package/src/langs/hfs-lang-hu.json +2 -1
  182. package/src/langs/hfs-lang-it.json +2 -1
  183. package/src/langs/hfs-lang-ja.json +2 -1
  184. package/src/langs/hfs-lang-ko.json +2 -1
  185. package/src/langs/hfs-lang-lt.json +2 -1
  186. package/src/langs/hfs-lang-ms.json +2 -0
  187. package/src/langs/hfs-lang-nl.json +2 -1
  188. package/src/langs/hfs-lang-pt-br.json +2 -1
  189. package/src/langs/hfs-lang-ro.json +2 -1
  190. package/src/langs/hfs-lang-ru.json +2 -1
  191. package/src/langs/hfs-lang-sr-latn.json +2 -1
  192. package/src/langs/hfs-lang-sr.json +2 -1
  193. package/src/langs/hfs-lang-th.json +2 -1
  194. package/src/langs/hfs-lang-tr.json +2 -1
  195. package/src/langs/hfs-lang-uk.json +2 -1
  196. package/src/langs/hfs-lang-vi.json +2 -1
  197. package/src/langs/hfs-lang-zh-tw.json +2 -1
  198. package/src/langs/hfs-lang-zh.json +2 -1
  199. package/src/listen.js +3 -3
  200. package/src/log.js +27 -2
  201. package/src/middlewares.js +8 -2
  202. package/src/misc.js +14 -0
  203. package/src/nat.js +57 -20
  204. package/src/outboundProxy.js +1 -1
  205. package/src/perm.js +5 -1
  206. package/src/plugins.js +34 -5
  207. package/src/selfCheck.js +1 -1
  208. package/src/serveGuiAndSharedFiles.js +1 -0
  209. package/src/serveGuiFiles.js +1 -0
  210. package/src/update.js +9 -15
  211. package/src/urlList.js +32 -0
  212. package/src/util-files.js +15 -5
  213. package/src/util-http.js +2 -0
  214. package/src/vfs.js +1 -1
  215. package/src/walkDir.js +7 -1
  216. package/src/webdav.js +4 -1
  217. package/src/zip.js +2 -1
  218. package/admin/assets/index-B66w-a0v.css +0 -1
  219. package/admin/assets/index-Df6vYR7s.js +0 -822
  220. 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
- this.set((0, immer_1.produce)(this.get(), v));
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 boolean value means startedWithoutConfig
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
- return original((0, cross_1.formatTime)(ts), ...args); // bundled nodejs doesn't have locales (and apparently uses en-US)
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 = fs.statSync(PKG_PATH).mtime.toISOString();
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
- const msg = cross_1.HTTP_MESSAGES[ctx.status];
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
- if (!lang)
22
- return;
23
- const trans = Object.values(lang)[0]?.translate;
24
- ctx.body = trans?.[msg] ?? msg;
25
- const errorPage = (0, customHtml_1.getSection)(ctx.status === cross_1.HTTP_UNAUTHORIZED ? 'unauthorized' : String(ctx.status));
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 = errorPage.replace('$MESSAGE', String(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
@@ -67,6 +67,7 @@ class BetterEventEmitter {
67
67
  const output = [];
68
68
  let prevented = false;
69
69
  const extra = {
70
+ event,
70
71
  output,
71
72
  preventDefault() { prevented = true; }
72
73
  };
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
- (0, promises_1.access)(FN).then(() => fileAttrDb.open(FN).catch(e => console.error(String(e))), () => { });
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
- if (!fileAttrDb.isOpen())
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(`${path}|${k}`) : null)
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 [fn] = k.split('|');
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
+ }
@@ -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
- }).catch(e => e.code || String(e));
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
- "send_files": "Send {n,plural,one{# file} other{# files}}, {size}",
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
- "send_files": "Изпрати {n,plural,one{# файл} other{# файла}}, {size}",
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
- "send_files": "{n,plural,one{# Datei} other{# Dateien}} senden, {size}",
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
- "send_files": "Αποστολή {n,plural,one{# αρχείο} other{# αρχεία}}, {size}",
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
- "send_files": "Enviar {n,plural,one {# archivo} other{# archivos}}, {size}",
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
- "send_files": "Lähetä {n,plural,one{# tiedosto} other{# tiedostot}}, {size}",
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
- "send_files": "Envoyer {n,plural,one{# fichier} other{# fichiers}}, {size}",
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
- "send_files": "Feltöltés {n,többes szám,egy{# file} egyéb{# file}}, {size}",
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
- "send_files": "Invia {n} file, {size}",
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
- "send_files": "{n,plural,one{# ファイル} other{# ファイル}}を送る {size}",
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
- "send_files": "{n,plural,one{# file} other{# files}}, {size} 보내기",
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
- "send_files": "Siųsti {n,plural,one{# failą} few{# failus} other{# failų}}, {size}",
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
- "send_files": "Verzonden {n,plural,one{# bestand} other{# bestanden}}, {grootte}",
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
- "send_files": "Enviar {n,plural,one{# arquivo} other{# arquivos}}, {size}",
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
- "send_files": "Trimite {n,plural,one{# fișier} other{# fișiere}}, {size}",
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
- "send_files": "Загрузить {n,plural, one{# файл} few{# файла} many{# файлов}}, {size}",
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
- "send_files": "Pošalji {n,plural,one{# datoteka} other{# datoteke}}, {size}",
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
- "send_files": "Отпремите {n} документ(а), {size}",
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
- "send_files": "Send {n,plural,one{# file} other{# files}}, {size}",
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
- "send_files": "{n,plural,one{# file} other{# files}} dosyayı gönder (UPLOAD), {size}",
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?",