hfs 3.1.5 → 3.2.0-beta2

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 (224) hide show
  1. package/admin/assets/af-DWEYq388.js +1 -0
  2. package/admin/assets/am-DgLbAgj6.js +1 -0
  3. package/admin/assets/ar-DgEWkO74.js +1 -0
  4. package/admin/assets/ar-dz-DHq--Sr8.js +1 -0
  5. package/admin/assets/ar-iq-D1r3nsb9.js +1 -0
  6. package/admin/assets/ar-kw-C85fGHwp.js +1 -0
  7. package/admin/assets/ar-ly-Bq6pjGjs.js +1 -0
  8. package/admin/assets/ar-ma-SGvmh6Mj.js +1 -0
  9. package/admin/assets/ar-sa-Bv8hiFi6.js +1 -0
  10. package/admin/assets/ar-tn-Bdvo77v3.js +1 -0
  11. package/admin/assets/az-7fPndoov.js +1 -0
  12. package/admin/assets/be-wjXeIeAK.js +1 -0
  13. package/admin/assets/bg-DZwBvjzH.js +1 -0
  14. package/admin/assets/bi-qCtxvMhO.js +1 -0
  15. package/admin/assets/bm-BVPWvreb.js +1 -0
  16. package/admin/assets/bn-CdWvye7d.js +1 -0
  17. package/admin/assets/bn-bd-B5-3blmz.js +1 -0
  18. package/admin/assets/bo-BEVcgyN2.js +1 -0
  19. package/admin/assets/br-DGQD6fFs.js +1 -0
  20. package/admin/assets/bs-So4MoRua.js +1 -0
  21. package/admin/assets/ca-Bk5ytG4g.js +1 -0
  22. package/admin/assets/cs-C4NU8eW-.js +1 -0
  23. package/admin/assets/cv-Ct-s-zrW.js +1 -0
  24. package/admin/assets/cy-ojtDPj8_.js +1 -0
  25. package/admin/assets/da-CYGin6vm.js +1 -0
  26. package/admin/assets/de-BEbJ01zH.js +1 -0
  27. package/admin/assets/de-at-C7UwE1rJ.js +1 -0
  28. package/admin/assets/de-ch-CsZ7YQc_.js +1 -0
  29. package/admin/assets/dv-DaVliwLd.js +1 -0
  30. package/admin/assets/el-DM_KqKEP.js +1 -0
  31. package/admin/assets/en-DedtOfaf.js +1 -0
  32. package/admin/assets/en-au-52Bzk5D9.js +1 -0
  33. package/admin/assets/en-ca-3pzEPK2N.js +1 -0
  34. package/admin/assets/en-gb-BrwDQS2G.js +1 -0
  35. package/admin/assets/en-ie-BUXSHrkL.js +1 -0
  36. package/admin/assets/en-il-a22drDCn.js +1 -0
  37. package/admin/assets/en-in-BUjecjkp.js +1 -0
  38. package/admin/assets/en-nz-Bbo7tnB_.js +1 -0
  39. package/admin/assets/en-sg-CZVDddmd.js +1 -0
  40. package/admin/assets/en-tt-DmSGwRia.js +1 -0
  41. package/admin/assets/eo-B71nkHZU.js +1 -0
  42. package/admin/assets/es-Dk6VCuuk.js +1 -0
  43. package/admin/assets/es-do-DMErY8ol.js +1 -0
  44. package/admin/assets/es-mx-BMRmqa3u.js +1 -0
  45. package/admin/assets/es-pr-CtBQz48p.js +1 -0
  46. package/admin/assets/es-us-CrDl5pnO.js +1 -0
  47. package/admin/assets/et-CO9OHqio.js +1 -0
  48. package/admin/assets/eu-Bip44atW.js +1 -0
  49. package/admin/assets/fa-CHbJ_dTM.js +1 -0
  50. package/admin/assets/fi-DKfoLmaQ.js +1 -0
  51. package/admin/assets/fo-DG1kOEfw.js +1 -0
  52. package/admin/assets/fr-DV73GZR4.js +1 -0
  53. package/admin/assets/fr-ca-BK-RoZiC.js +1 -0
  54. package/admin/assets/fr-ch-DjqEC5E_.js +1 -0
  55. package/admin/assets/fy-znrRQdeC.js +1 -0
  56. package/admin/assets/ga-BlZeKu0N.js +1 -0
  57. package/admin/assets/gd-BmrycMnC.js +1 -0
  58. package/admin/assets/gl-CuT8e5mi.js +1 -0
  59. package/admin/assets/gom-latn-BSWVd0A6.js +1 -0
  60. package/admin/assets/gu-BHK6LfvD.js +1 -0
  61. package/admin/assets/he-DPoTUevR.js +1 -0
  62. package/admin/assets/hi-BRuLafoW.js +1 -0
  63. package/admin/assets/hr-Bzge-10P.js +1 -0
  64. package/admin/assets/ht-Ck9BCna1.js +1 -0
  65. package/admin/assets/hu-CzqqbYmU.js +1 -0
  66. package/admin/assets/hy-am-C-eV4E8v.js +1 -0
  67. package/admin/assets/id-Dv8GZQvB.js +1 -0
  68. package/admin/assets/{index-DTxjaflW.js → index-BPIX0qPj.js} +1 -1
  69. package/admin/assets/index-CFWd-FDo.css +1 -0
  70. package/admin/assets/index-D3HviM6x.js +889 -0
  71. package/admin/assets/is-CK6VY3M_.js +1 -0
  72. package/admin/assets/it-1gtki4a5.js +1 -0
  73. package/admin/assets/it-ch-C0Mj3-pC.js +1 -0
  74. package/admin/assets/ja-Dl3AfnM1.js +1 -0
  75. package/admin/assets/jv-CznX-tGV.js +1 -0
  76. package/admin/assets/ka-BNjZxCug.js +1 -0
  77. package/admin/assets/kk-80J_xldf.js +1 -0
  78. package/admin/assets/km-CuWChDRB.js +1 -0
  79. package/admin/assets/kn-BZp_PBdl.js +1 -0
  80. package/admin/assets/ko-RirpyUl_.js +1 -0
  81. package/admin/assets/ku-Dz8ACD5w.js +1 -0
  82. package/admin/assets/ky-BN3ylOhj.js +1 -0
  83. package/admin/assets/lb-D7h_YoEn.js +1 -0
  84. package/admin/assets/lo-BrlbTUPD.js +1 -0
  85. package/admin/assets/lt-cXuHFdTa.js +1 -0
  86. package/admin/assets/lv-CjIpv13Q.js +1 -0
  87. package/admin/assets/me-B-jTh39Z.js +1 -0
  88. package/admin/assets/mi-D06xdVmt.js +1 -0
  89. package/admin/assets/mk-iLbuxyOf.js +1 -0
  90. package/admin/assets/ml-2W0y6Zb2.js +1 -0
  91. package/admin/assets/mn-6zbvKjeb.js +1 -0
  92. package/admin/assets/mr-7-jrgLyw.js +1 -0
  93. package/admin/assets/ms-CRH6rEXt.js +1 -0
  94. package/admin/assets/ms-my-BIJmu2S-.js +1 -0
  95. package/admin/assets/mt-CbXmxK-D.js +1 -0
  96. package/admin/assets/my-BvMUsxU8.js +1 -0
  97. package/admin/assets/nb-DvKHgF7L.js +1 -0
  98. package/admin/assets/ne-DiZZ3Lm6.js +1 -0
  99. package/admin/assets/nl-be-Dy7PYRbC.js +1 -0
  100. package/admin/assets/nl-t_2A_VAT.js +1 -0
  101. package/admin/assets/nn-Cw7EwosO.js +1 -0
  102. package/admin/assets/oc-lnc-CuFfB75K.js +1 -0
  103. package/admin/assets/pa-in-DOFyZ-Ft.js +1 -0
  104. package/admin/assets/pl-BD7FyCJj.js +1 -0
  105. package/admin/assets/pt-DSKLLE_u.js +1 -0
  106. package/admin/assets/pt-br-BhL4gb5Z.js +1 -0
  107. package/admin/assets/rn-DoHoZZPd.js +1 -0
  108. package/admin/assets/ro-B0v-_lH0.js +1 -0
  109. package/admin/assets/ru-BMVOk5eA.js +1 -0
  110. package/admin/assets/rw-CWF0w6eL.js +1 -0
  111. package/admin/assets/sd-DO2rrjch.js +1 -0
  112. package/admin/assets/se-CAolO9WQ.js +1 -0
  113. package/admin/assets/{sha512-D936QW8l.js → sha512-ZlUYj4Hr.js} +1 -1
  114. package/admin/assets/si-D03dHfb5.js +1 -0
  115. package/admin/assets/sk-_GcZGaN3.js +1 -0
  116. package/admin/assets/sl-Cb1lUGab.js +1 -0
  117. package/admin/assets/sq-Czzt23Tr.js +1 -0
  118. package/admin/assets/sr-D76dVqKJ.js +1 -0
  119. package/admin/assets/sr-cyrl-CuoFbJjW.js +1 -0
  120. package/admin/assets/ss-g-fGaM29.js +1 -0
  121. package/admin/assets/sv-CZxc8I45.js +1 -0
  122. package/admin/assets/sv-fi-D8REJeLz.js +1 -0
  123. package/admin/assets/sw-B1n3PjWG.js +1 -0
  124. package/admin/assets/ta-K5mexJNT.js +1 -0
  125. package/admin/assets/te-DT6dj5B6.js +1 -0
  126. package/admin/assets/tet-DXwYNm_H.js +1 -0
  127. package/admin/assets/tg-BCcZKcE2.js +1 -0
  128. package/admin/assets/th-CeeeseFX.js +1 -0
  129. package/admin/assets/tk-CJ6KW44d.js +1 -0
  130. package/admin/assets/tl-ph-DzE8lDmm.js +1 -0
  131. package/admin/assets/tlh-ER6KiMxG.js +1 -0
  132. package/admin/assets/tr-D48NGNpr.js +1 -0
  133. package/admin/assets/tzl-5AsmDTYM.js +1 -0
  134. package/admin/assets/tzm-2AzZ1YPf.js +1 -0
  135. package/admin/assets/tzm-latn-Cd5hPQuT.js +1 -0
  136. package/admin/assets/ug-cn-DU-MZ9Vx.js +1 -0
  137. package/admin/assets/uk-hn6vcOkn.js +1 -0
  138. package/admin/assets/ur-jOObtqB6.js +1 -0
  139. package/admin/assets/uz-BlftYfHF.js +1 -0
  140. package/admin/assets/uz-latn-BpuI0ccM.js +1 -0
  141. package/admin/assets/vi-DVo3LusT.js +1 -0
  142. package/admin/assets/x-pseudo-BuVzNhqi.js +1 -0
  143. package/admin/assets/yo-BvWGwb4m.js +1 -0
  144. package/admin/assets/zh-D9-tfba1.js +1 -0
  145. package/admin/assets/zh-cn-CFL5sbIW.js +1 -0
  146. package/admin/assets/zh-hk-DRP8u65r.js +1 -0
  147. package/admin/assets/zh-tw-CtVs1ihI.js +1 -0
  148. package/admin/index.html +2 -2
  149. package/frontend/assets/index-legacy-D3BTBYs5.js +9 -0
  150. package/frontend/assets/{index-legacy-vmpqwZZf.js → index-legacy-DcrWtKxQ.js} +1 -1
  151. package/frontend/assets/{sha512-legacy-wI89-UHR.js → sha512-legacy-DJvEwScE.js} +1 -1
  152. package/frontend/index.html +1 -1
  153. package/npm-shrinkwrap.json +144 -71
  154. package/package.json +9 -9
  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 +17 -13
  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/basicWeb.js +1 -1
  165. package/src/commands.js +54 -1
  166. package/src/comments.js +7 -4
  167. package/src/config.js +9 -5
  168. package/src/consoleLog.js +39 -1
  169. package/src/const.js +4 -0
  170. package/src/cross.js +33 -6
  171. package/src/errorPages.js +20 -10
  172. package/src/events.js +3 -3
  173. package/src/expiringCache.js +8 -7
  174. package/src/fileAttr.js +73 -15
  175. package/src/frontEndApis.js +20 -15
  176. package/src/index.js +2 -1
  177. package/src/langs/hfs-lang-ar.json +2 -1
  178. package/src/langs/hfs-lang-bg.json +2 -1
  179. package/src/langs/hfs-lang-de.json +2 -1
  180. package/src/langs/hfs-lang-el.json +2 -1
  181. package/src/langs/hfs-lang-es.json +2 -1
  182. package/src/langs/hfs-lang-fi.json +2 -1
  183. package/src/langs/hfs-lang-fr.json +2 -1
  184. package/src/langs/hfs-lang-hu.json +2 -1
  185. package/src/langs/hfs-lang-it.json +2 -1
  186. package/src/langs/hfs-lang-ja.json +2 -1
  187. package/src/langs/hfs-lang-ko.json +2 -1
  188. package/src/langs/hfs-lang-lt.json +2 -1
  189. package/src/langs/hfs-lang-ms.json +2 -0
  190. package/src/langs/hfs-lang-nl.json +2 -1
  191. package/src/langs/hfs-lang-pt-br.json +2 -1
  192. package/src/langs/hfs-lang-ro.json +2 -1
  193. package/src/langs/hfs-lang-ru.json +2 -1
  194. package/src/langs/hfs-lang-sr-latn.json +2 -1
  195. package/src/langs/hfs-lang-sr.json +2 -1
  196. package/src/langs/hfs-lang-th.json +2 -1
  197. package/src/langs/hfs-lang-tr.json +2 -1
  198. package/src/langs/hfs-lang-uk.json +2 -1
  199. package/src/langs/hfs-lang-vi.json +2 -1
  200. package/src/langs/hfs-lang-zh-tw.json +2 -1
  201. package/src/langs/hfs-lang-zh.json +2 -1
  202. package/src/listen.js +2 -1
  203. package/src/log.js +27 -2
  204. package/src/middlewares.js +8 -2
  205. package/src/misc.js +14 -0
  206. package/src/nat.js +61 -21
  207. package/src/outboundProxy.js +1 -1
  208. package/src/perm.js +5 -1
  209. package/src/plugins.js +34 -5
  210. package/src/roots.js +1 -1
  211. package/src/selfCheck.js +2 -1
  212. package/src/serveGuiAndSharedFiles.js +18 -7
  213. package/src/serveGuiFiles.js +2 -1
  214. package/src/update.js +10 -18
  215. package/src/urlList.js +32 -0
  216. package/src/util-files.js +24 -9
  217. package/src/util-http.js +4 -0
  218. package/src/vfs.js +21 -9
  219. package/src/walkDir.js +7 -1
  220. package/src/webdav.js +4 -1
  221. package/src/zip.js +3 -2
  222. package/admin/assets/index-B66w-a0v.css +0 -1
  223. package/admin/assets/index-Df6vYR7s.js +0 -822
  224. package/frontend/assets/index-legacy-emBsvICj.js +0 -9
package/src/cross.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.VFS_STORED_KEYS = exports.PERM_KEYS = exports.defaultPerms = exports.WHO_ANY_ACCOUNT = exports.WHO_NO_ONE = exports.WHO_ANYONE = exports.LIST = exports.CFG = exports.THEME_OPTIONS = exports.SORT_BY_OPTIONS = exports.FRONTEND_OPTIONS = exports.MAX_TILE_SIZE = exports.DAY = exports.HOUR = exports.MINUTE = exports.WIKI_URL = exports.REPO_URL = exports.WEBSITE = void 0;
20
+ exports.VFS_STORED_KEYS = exports.PERM_KEYS = exports.defaultPerms = exports.WHO_ADMIN = exports.WHO_ANY_ACCOUNT = exports.WHO_NO_ONE = exports.WHO_ANYONE = exports.LIST = exports.CFG = exports.THEME_OPTIONS = exports.SORT_BY_OPTIONS = exports.FRONTEND_OPTIONS = exports.MAX_TILE_SIZE = exports.DAY = exports.HOUR = exports.MINUTE = exports.WIKI_URL = exports.REPO_URL = exports.WEBSITE = void 0;
21
21
  exports.isWhoObject = isWhoObject;
22
22
  exports.formatBytes = formatBytes;
23
23
  exports.formatSpeed = formatSpeed;
@@ -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_;
@@ -79,6 +81,7 @@ exports.escapeHTML = escapeHTML;
79
81
  exports.promiseBestEffort = promiseBestEffort;
80
82
  exports.pathEncode = pathEncode;
81
83
  exports.pathDecode = pathDecode;
84
+ exports.pathDecodeSegments = pathDecodeSegments;
82
85
  exports.runAt = runAt;
83
86
  exports.makeMatcher = makeMatcher;
84
87
  exports.matches = matches;
@@ -125,18 +128,19 @@ exports.CFG = constMap(['geo_enable', 'geo_allow', 'geo_list', 'geo_allow_unknow
125
128
  'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua', 'log_spam', 'track_ips',
126
129
  'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'force_address', 'split_uploads',
127
130
  'force_lang', 'suspend_plugins', 'base_url', 'size_1024', 'disable_custom_html', 'comments_storage',
128
- 'force_webdav_login', 'webdav_initial_auth', 'outbound_proxy']);
131
+ 'force_webdav_login', 'webdav_initial_auth', 'outbound_proxy', 'mapped_port', 'upnp_enabled', 'show_uploader']);
129
132
  exports.LIST = { add: '+', remove: '-', update: '=', props: 'props', ready: 'ready', error: 'e' };
130
133
  exports.WHO_ANYONE = true;
131
134
  exports.WHO_NO_ONE = false;
132
135
  exports.WHO_ANY_ACCOUNT = '*';
136
+ exports.WHO_ADMIN = 'admin';
133
137
  exports.defaultPerms = {
134
- can_see: 'can_read',
135
138
  can_read: exports.WHO_ANYONE,
139
+ can_see: 'can_read',
136
140
  can_list: 'can_read',
137
- can_upload: exports.WHO_NO_ONE,
138
- can_delete: exports.WHO_NO_ONE,
139
- can_archive: 'can_read'
141
+ can_archive: 'can_read',
142
+ can_upload: exports.WHO_ADMIN,
143
+ can_delete: exports.WHO_ADMIN,
140
144
  };
141
145
  exports.PERM_KEYS = typedKeys(exports.defaultPerms);
142
146
  exports.VFS_STORED_KEYS = ['name', 'source', 'masks', 'default', 'accept', 'rename',
@@ -215,12 +219,31 @@ function stringAfter(sub, all) {
215
219
  const i = all.indexOf(sub);
216
220
  return i < 0 ? '' : all.slice(i + sub.length);
217
221
  }
222
+ function stringBefore(sub, all, returnEmptyWhenSubMissing = true) {
223
+ const i = all.indexOf(sub);
224
+ return i >= 0 ? all.slice(0, i + sub.length - 1) : returnEmptyWhenSubMissing ? '' : all;
225
+ }
218
226
  function truthy(value) {
219
227
  return Boolean(value);
220
228
  }
221
229
  function onlyTruthy(arr) {
222
230
  return arr.filter(truthy);
223
231
  }
232
+ function countUniqueBy(items, keyFn, predicate) {
233
+ // use a Set so unique counting stays linear even on very large live lists
234
+ const seen = new Set();
235
+ let count = 0;
236
+ for (const item of items) {
237
+ if (predicate && !predicate(item))
238
+ continue;
239
+ const key = keyFn(item);
240
+ if (seen.has(key))
241
+ continue;
242
+ seen.add(key);
243
+ count++;
244
+ }
245
+ return count;
246
+ }
224
247
  function setHidden(dest, src) {
225
248
  return Object.defineProperties(dest, newObj(src, value => ({
226
249
  enumerable: false,
@@ -457,6 +480,10 @@ function pathEncode(s, all = false) {
457
480
  function pathDecode(s) {
458
481
  return decodeURI(s).replace(/%23/g, '#');
459
482
  }
483
+ function pathDecodeSegments(s, map = String) {
484
+ // decode segment by segment so reserved escapes are decoded without turning encoded slashes into separators
485
+ return s.split('/').map(x => map(safeDecodeURIComponent(x)).replaceAll('/', '%2F')).join('/');
486
+ }
460
487
  // run at a specific point in time, also solving the limit of setTimeout, which doesn't work with +32bit delays
461
488
  function runAt(ts, cb) {
462
489
  let cancel = 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
@@ -5,8 +5,7 @@ exports.BetterEventEmitter = void 0;
5
5
  const LISTENERS_SUFFIX = '\0listeners';
6
6
  class BetterEventEmitter {
7
7
  listeners = new Map();
8
- preventDefault = Symbol();
9
- stop = this.preventDefault; // legacy pre-0.54 (introduced in 0.53)
8
+ stop = Symbol();
10
9
  on(event, listener, { warnAfter = 10, callNow = false } = {}) {
11
10
  if (typeof event === 'string')
12
11
  event = [event];
@@ -67,6 +66,7 @@ class BetterEventEmitter {
67
66
  const output = [];
68
67
  let prevented = false;
69
68
  const extra = {
69
+ event,
70
70
  output,
71
71
  preventDefault() { prevented = true; }
72
72
  };
@@ -88,7 +88,7 @@ class BetterEventEmitter {
88
88
  const asyncRet = await Promise.all(syncRet);
89
89
  return Object.assign(asyncRet, {
90
90
  isDefaultPrevented: () => syncRet.isDefaultPrevented()
91
- || asyncRet.some((r) => r === this.preventDefault)
91
+ || asyncRet.some((r) => r === this.stop)
92
92
  });
93
93
  }
94
94
  }
@@ -6,24 +6,25 @@ function expiringCache(ttlMs) {
6
6
  throw Error('invalid TTL');
7
7
  const o = new Map();
8
8
  return Object.assign(o, {
9
- // creator can return undefined if the value should not be cached. `invalidate` is useful in case you have some custom logic for it, other than ttl.
9
+ invalidate,
10
+ // creator can return undefined if the value should not be cached
10
11
  try(k, creator) {
11
12
  let ret = o.get(k);
12
13
  if (ret === undefined) { // undefined = missing, as we don't accept this value in our cache
13
- ret = creator(invalidate);
14
+ ret = creator(k);
14
15
  if (ret !== undefined) {
15
16
  o.set(k, ret);
16
17
  Promise.resolve(ret).then(v => {
17
18
  if (v === undefined) // even in a promise, we'll consider undefined as a request to cancel the caching
18
- invalidate();
19
+ invalidate(k);
19
20
  }, () => { }) // avoid js warning
20
- .finally(() => setTimeout(invalidate, ttlMs)); // wait for async (in case) before starting the timer
21
- }
22
- function invalidate() {
23
- o.delete(k);
21
+ .finally(() => setTimeout(() => invalidate(k), ttlMs)); // wait for async (in case) before starting the timer
24
22
  }
25
23
  }
26
24
  return ret;
27
25
  },
28
26
  });
27
+ function invalidate(k) {
28
+ o.delete(k);
29
+ }
29
30
  }
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
+ }
@@ -20,11 +20,13 @@ const promises_1 = require("fs/promises");
20
20
  const path_1 = require("path");
21
21
  const upload_1 = require("./upload");
22
22
  const misc_1 = require("./misc");
23
+ const config_1 = require("./config");
23
24
  const comments_1 = require("./comments");
24
25
  const SendList_1 = require("./SendList");
25
26
  const adminApis_1 = require("./adminApis");
26
27
  const lodash_1 = __importDefault(require("lodash"));
27
28
  const partialFolderSize = {};
29
+ const showUploader = (0, config_1.defineConfig)(misc_1.CFG.show_uploader, misc_1.WHO_ADMIN);
28
30
  exports.frontEndApis = {
29
31
  get_file_list: api_get_file_list_1.get_file_list,
30
32
  ...api_auth_1.authApis,
@@ -43,19 +45,20 @@ exports.frontEndApis = {
43
45
  return new apiMiddleware_1.ApiError(const_1.HTTP_BAD_REQUEST, 'bad uris');
44
46
  const isAdmin = (0, adminApis_1.ctxAdminAccess)(ctx);
45
47
  return {
46
- details: await Promise.all(uris.map(async (uri) => {
47
- if (typeof uri !== 'string')
48
- return false; // false means error
49
- const node = await (0, vfs_1.urlToNode)(uri, ctx);
50
- if (!node || !(0, vfs_1.hasPermission)(node, 'can_see', ctx))
51
- return false;
52
- let upload = node.source && await (0, upload_1.getUploadMeta)(node.source).catch(() => undefined);
53
- if (!upload)
54
- return;
55
- if (!isAdmin)
56
- upload = lodash_1.default.omit(upload, 'ip');
57
- return { upload };
58
- }))
48
+ details: (0, vfs_1.simpleWhoToError)(showUploader.get(), ctx) ? [] // return early because at the moment we only have the uploader
49
+ : await Promise.all(uris.map(async (uri) => {
50
+ if (typeof uri !== 'string')
51
+ return false; // false means error
52
+ const node = await (0, vfs_1.urlToNode)(uri, ctx);
53
+ if (!node || !(0, vfs_1.hasPermission)(node, 'can_see', ctx))
54
+ return false;
55
+ let upload = node.source && await (0, upload_1.getUploadMeta)(node.source).catch(() => undefined);
56
+ if (!upload)
57
+ return;
58
+ if (!isAdmin)
59
+ upload = lodash_1.default.omit(upload, 'ip');
60
+ return { upload };
61
+ }))
59
62
  };
60
63
  },
61
64
  async create_folder({ uri, name }, ctx) {
@@ -126,7 +129,7 @@ exports.frontEndApis = {
126
129
  await (0, comments_1.setCommentFor)(node.source, comment);
127
130
  return {};
128
131
  },
129
- async get_folder_size_partial({ id }, ctx) {
132
+ async get_folder_size_partial({ id }) {
130
133
  (0, misc_1.apiAssertTypes)({ string: { id } });
131
134
  return partialFolderSize[id] || new apiMiddleware_1.ApiError(const_1.HTTP_NOT_FOUND);
132
135
  },
@@ -195,7 +198,8 @@ async function moveFiles(uri_from, uri_to, ctx, override) {
195
198
  throw e; // exdev = different drive
196
199
  await (0, promises_1.copyFile)(src, dest);
197
200
  await (0, promises_1.unlink)(src);
198
- }).catch(e => e.code || String(e));
201
+ }).then(() => (0, misc_1.moveStoredFileAttrs)(src, dest))
202
+ .catch(e => e.code || String(e));
199
203
  }))
200
204
  };
201
205
  }
@@ -216,6 +220,7 @@ async function requestedRename(node, newName, ctx) {
216
220
  try {
217
221
  const destSource = (0, path_1.join)((0, path_1.dirname)(node.source), newName);
218
222
  await (0, promises_1.rename)(node.source, destSource);
223
+ await (0, misc_1.moveStoredFileAttrs)(node.source, destSource);
219
224
  (0, comments_1.getCommentFor)(node.source).then(c => {
220
225
  if (!c)
221
226
  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": "Настави преузимање?",