hfs 3.2.2 → 3.3.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 (268) hide show
  1. package/admin/assets/AccountsPage-DB7ljNHT.js +3 -0
  2. package/admin/assets/ArrayField-Qj2iHD7I.js +2 -0
  3. package/admin/assets/ConfigFilePage-TErmt0PK.js +4 -0
  4. package/admin/assets/CustomHtmlPage-CVmk-_JS.js +2 -0
  5. package/admin/assets/DataTable-DmnQXyFO.js +119 -0
  6. package/admin/assets/DateTimeField-BUWd2w9-.js +1 -0
  7. package/admin/assets/EventManager-9vwKg8as.js +1 -0
  8. package/admin/assets/FileField-DoBaSTxF.js +117 -0
  9. package/admin/assets/InternetPage-Biuer1tb.js +8 -0
  10. package/admin/assets/LangPage-Vd24DNK5.js +2 -0
  11. package/admin/assets/LogsPage-BdvttEqM.js +3 -0
  12. package/admin/assets/MenuButton-CHLgqU5W.js +1 -0
  13. package/admin/assets/MonitorPage-DlXkZUmT.js +2 -0
  14. package/admin/assets/OptionsPage-Zi8FmrjE.js +3 -0
  15. package/admin/assets/PluginsPage-BzwEIFt5.js +1 -0
  16. package/admin/assets/Tabs-CVipqhB9.js +1 -0
  17. package/admin/assets/TextEditor-BeS-mR2I.css +1 -0
  18. package/admin/assets/TextEditor-clbVugYM.js +34 -0
  19. package/admin/assets/VfsPathField-YUCV9PfL.js +1 -0
  20. package/admin/assets/{af-BDoqNBMg.js → af-B1Gi_bpo.js} +1 -1
  21. package/admin/assets/agentIcons-DW9QViyx.js +1 -0
  22. package/admin/assets/{am-DXBvKJ3X.js → am-B-dZnhsM.js} +1 -1
  23. package/admin/assets/{ar-BJIoxKTh.js → ar-DsQGp6kZ.js} +1 -1
  24. package/admin/assets/{ar-dz-C9l3uc_3.js → ar-dz-CxlYiKtP.js} +1 -1
  25. package/admin/assets/{ar-iq-DKH8u2lX.js → ar-iq-y4_Puo4B.js} +1 -1
  26. package/admin/assets/{ar-kw-D3gJalQp.js → ar-kw-DTOZ0VC7.js} +1 -1
  27. package/admin/assets/{ar-ly-BZJzWFeR.js → ar-ly-lPV9gApv.js} +1 -1
  28. package/admin/assets/{ar-ma-DabiOCr0.js → ar-ma-DZqLSpeB.js} +1 -1
  29. package/admin/assets/{ar-sa-C7suzSCY.js → ar-sa-v-5vsIOR.js} +1 -1
  30. package/admin/assets/{ar-tn-BvBucEx0.js → ar-tn-a4q8lhXy.js} +1 -1
  31. package/admin/assets/{az-CPUppH2M.js → az-Cj4yq-j-.js} +1 -1
  32. package/admin/assets/baseUrl-Do5CYuBZ.js +2 -0
  33. package/admin/assets/{be-vzvhX2dl.js → be-B-3QlFM8.js} +1 -1
  34. package/admin/assets/{bg-09Xbj_Y3.js → bg-A8-93xdK.js} +1 -1
  35. package/admin/assets/{bi-CvhiuiE9.js → bi-bwwEWGG2.js} +1 -1
  36. package/admin/assets/{bm-DtJAZAkZ.js → bm-DpLmtYXC.js} +1 -1
  37. package/admin/assets/{bn-Dnzh2x6I.js → bn-DPL_BgLw.js} +1 -1
  38. package/admin/assets/{bn-bd-xEy4797g.js → bn-bd-DukmY_R5.js} +1 -1
  39. package/admin/assets/{bo-vuPmptG3.js → bo-68KDEKGI.js} +1 -1
  40. package/admin/assets/{br-C9xH4OD3.js → br-gA86uwqY.js} +1 -1
  41. package/admin/assets/{bs-BB_uCUp1.js → bs-SfthSp42.js} +1 -1
  42. package/admin/assets/{ca-D7LKaf98.js → ca-8qUtZKFe.js} +1 -1
  43. package/admin/assets/{cs-DMFTNdNC.js → cs-CTHcqPCX.js} +1 -1
  44. package/admin/assets/{cv-DNUjJwnO.js → cv-BhBW7hcO.js} +1 -1
  45. package/admin/assets/{cy-qmAOTPwC.js → cy-BYTS4PNY.js} +1 -1
  46. package/admin/assets/{da-DDX54iM9.js → da-BiwfOrRd.js} +1 -1
  47. package/admin/assets/{de-BBmlsfbM.js → de-BFqyDxvo.js} +1 -1
  48. package/admin/assets/{de-at-CuMZqYfu.js → de-at-C7zUXXVL.js} +1 -1
  49. package/admin/assets/{de-ch-s8OXKb-5.js → de-ch-BVHosAzt.js} +1 -1
  50. package/admin/assets/dist-DC2A6cD2.js +1 -0
  51. package/admin/assets/{dv-Cjs3QDBN.js → dv-dvig99nB.js} +1 -1
  52. package/admin/assets/{el-BsAOrUDP.js → el-BtJcYjoT.js} +1 -1
  53. package/admin/assets/en-B26lXzkx.js +1 -0
  54. package/admin/assets/{en-au-8tMHM2OT.js → en-au-CWzJ565h.js} +1 -1
  55. package/admin/assets/{en-ca-c9jM4M_8.js → en-ca-nou5bZ6q.js} +1 -1
  56. package/admin/assets/{en-in-D2ULzXMp.js → en-gb-kl3giCnh.js} +1 -1
  57. package/admin/assets/{en-ie-D4_e3kV6.js → en-ie-C55e3apX.js} +1 -1
  58. package/admin/assets/{en-il-BoaZOven.js → en-il-HYabE6j7.js} +1 -1
  59. package/admin/assets/{en-tt-Cn72OR4N.js → en-in-Bri93cNM.js} +1 -1
  60. package/admin/assets/{en-nz-grrmB3dg.js → en-nz-BqhpzeuX.js} +1 -1
  61. package/admin/assets/{en-sg-Cmqxpoir.js → en-sg-q8xv-C8V.js} +1 -1
  62. package/admin/assets/{en-gb-unFIXx9f.js → en-tt-BqzQXPA5.js} +1 -1
  63. package/admin/assets/{eo-Cqxmc0TT.js → eo-5Y962E4S.js} +1 -1
  64. package/admin/assets/{es-CkjKdFZ3.js → es-DOQ7swrW.js} +1 -1
  65. package/admin/assets/{es-do-CTcTshup.js → es-do-Diob7juh.js} +1 -1
  66. package/admin/assets/{es-mx-DMXgMMxs.js → es-mx-miy7RFDh.js} +1 -1
  67. package/admin/assets/{es-pr-DNCV91L3.js → es-pr-CZa5Iw2n.js} +1 -1
  68. package/admin/assets/{es-us-C7o2mlIH.js → es-us-BX03CBQf.js} +1 -1
  69. package/admin/assets/{esm-DvUbRuYx.js → esm-Dtaf0rWK.js} +1 -1
  70. package/admin/assets/{et-DaPpH-gK.js → et-DGWOmMlC.js} +1 -1
  71. package/admin/assets/{eu-DfKyFruu.js → eu-D1Qj8RMh.js} +1 -1
  72. package/admin/assets/{fa-xWrBjePb.js → fa--TfwKnl9.js} +1 -1
  73. package/admin/assets/{fi-BOWDMsDS.js → fi-Dcbkv925.js} +1 -1
  74. package/admin/assets/{fo-B3FupW8R.js → fo-BPGX_WF4.js} +1 -1
  75. package/admin/assets/{fr-DZBDBkwp.js → fr-CNsDHzNh.js} +1 -1
  76. package/admin/assets/{fr-ca-CRePPJJ5.js → fr-ca-BsNs3KIJ.js} +1 -1
  77. package/admin/assets/{fr-ch-pFKvYU8D.js → fr-ch-BBmdtvlB.js} +1 -1
  78. package/admin/assets/{fy-0k7RzHUH.js → fy-q3UE-mqp.js} +1 -1
  79. package/admin/assets/{ga-D31rB2ys.js → ga-C-DOgnYh.js} +1 -1
  80. package/admin/assets/{gd-CbgY-FFE.js → gd-mLMfry_l.js} +1 -1
  81. package/admin/assets/{gl-DrtvS9gt.js → gl-CO8Bj9I_.js} +1 -1
  82. package/admin/assets/{gom-latn-Dkke3GyE.js → gom-latn-myglMmHV.js} +1 -1
  83. package/admin/assets/{gu-BlEipYhR.js → gu-Bdz1OpMU.js} +1 -1
  84. package/admin/assets/{he-BrHT6OLB.js → he-C4lqk2AA.js} +1 -1
  85. package/admin/assets/{hi-Bx0zlAgr.js → hi-h0Byc_UN.js} +1 -1
  86. package/admin/assets/{hr-CoFYGbr6.js → hr-CZV7Za1n.js} +1 -1
  87. package/admin/assets/{ht-Df5xKo4a.js → ht-Dg2blNvl.js} +1 -1
  88. package/admin/assets/{hu-BFTN3q6-.js → hu-DbOVhd9K.js} +1 -1
  89. package/admin/assets/{hy-am-DuZTJQed.js → hy-am-E48Q7kWE.js} +1 -1
  90. package/admin/assets/icons-3rupj9Xr.js +1 -0
  91. package/admin/assets/{id-DBZ5nPkf.js → id-Cd4yWevW.js} +1 -1
  92. package/admin/assets/{index-eeRX25da.css → index-11n5z0Ez.css} +1 -1
  93. package/admin/assets/index-Dsq-sVu9.js +477 -0
  94. package/admin/assets/{is-CmeHm47B.js → is-BJZYSfrA.js} +1 -1
  95. package/admin/assets/{it-Cv3fjOA9.js → it-BRaqqFz4.js} +1 -1
  96. package/admin/assets/{it-ch-DCTyHA3T.js → it-ch-BkLqxaff.js} +1 -1
  97. package/admin/assets/{ja-DHNVXAM5.js → ja-X_I-25cD.js} +1 -1
  98. package/admin/assets/{jv-BdNIxhWv.js → jv-BBBx3R3v.js} +1 -1
  99. package/admin/assets/{ka-D-joD0QB.js → ka-D9Yrujqk.js} +1 -1
  100. package/admin/assets/{kk-8wRNNjxz.js → kk-Bz450_o8.js} +1 -1
  101. package/admin/assets/{km-dUWOg1uK.js → km-CKOAqvHM.js} +1 -1
  102. package/admin/assets/{kn-CwQbzel0.js → kn-k2xTBR7a.js} +1 -1
  103. package/admin/assets/{ko-D_gn9Sfw.js → ko-BCvoCYib.js} +1 -1
  104. package/admin/assets/{ku-Dh3MRML3.js → ku-D6Rloctr.js} +1 -1
  105. package/admin/assets/{ky-CWYuRvil.js → ky-C54qnEzk.js} +1 -1
  106. package/admin/assets/{lb-Cc2xsSIx.js → lb-y3rkrWB2.js} +1 -1
  107. package/admin/assets/{lo-CkwNFDBz.js → lo-nA_dPnOU.js} +1 -1
  108. package/admin/assets/{lt-HWyFDLXu.js → lt-hjPmCCML.js} +1 -1
  109. package/admin/assets/{lv-C7Zx3OLO.js → lv-Bh4qmUa_.js} +1 -1
  110. package/admin/assets/{me-DH3AqnfF.js → me-e32suC_n.js} +1 -1
  111. package/admin/assets/{mi-DMmUry6m.js → mi-Dwbsuyr9.js} +1 -1
  112. package/admin/assets/{mk-D6MNZrzX.js → mk-DjUXwKRn.js} +1 -1
  113. package/admin/assets/{ml-B_ur8Zqf.js → ml-CWjFAViF.js} +1 -1
  114. package/admin/assets/{mn-DeL8Ry7x.js → mn-BU4qTvEl.js} +1 -1
  115. package/admin/assets/{mr-C01qRXFL.js → mr-Bvnduzfb.js} +1 -1
  116. package/admin/assets/{ms-DFUZJzOm.js → ms-XuKVAni7.js} +1 -1
  117. package/admin/assets/{ms-my-CUYHkebs.js → ms-my-CDbnYp0g.js} +1 -1
  118. package/admin/assets/{mt-CE8t5jSj.js → mt-PDuxJf8J.js} +1 -1
  119. package/admin/assets/{my-Df96A8jZ.js → my-7N6SHnsD.js} +1 -1
  120. package/admin/assets/{nb-D0837ewu.js → nb-Cu3KUtTR.js} +1 -1
  121. package/admin/assets/{ne-BdK5TR0R.js → ne-CYW0uI2a.js} +1 -1
  122. package/admin/assets/{nl-oYKmSma6.js → nl-DL7cw699.js} +1 -1
  123. package/admin/assets/{nl-be-BfVC0cur.js → nl-be-B3NSF-2w.js} +1 -1
  124. package/admin/assets/{nn-IEUPnyc8.js → nn-BHITVkGB.js} +1 -1
  125. package/admin/assets/{oc-lnc-g_wSIROr.js → oc-lnc-dp3c4Bty.js} +1 -1
  126. package/admin/assets/{pa-in-gWhcLmZE.js → pa-in-BxgIlP1T.js} +1 -1
  127. package/admin/assets/{pl-H3-LoxVn.js → pl-GrG6yjkB.js} +1 -1
  128. package/admin/assets/{pt-br-B8UcNR6_.js → pt-br-BxTgeF95.js} +1 -1
  129. package/admin/assets/{pt-hiX8RqVw.js → pt-cVbj6hL6.js} +1 -1
  130. package/admin/assets/{rn-BKupViLr.js → rn-DiVJ2_Jy.js} +1 -1
  131. package/admin/assets/{ro-DmhgIvml.js → ro-D1WeRyXR.js} +1 -1
  132. package/admin/assets/rolldown-runtime-CacB07dV.js +1 -0
  133. package/admin/assets/routing-Cjyek7O_.js +1 -0
  134. package/admin/assets/{ru-CcpO-b8g.js → ru-DZCu4V9W.js} +1 -1
  135. package/admin/assets/{rw-DWX_5ZK0.js → rw-DfFpVc_g.js} +1 -1
  136. package/admin/assets/{sd-Dat8YgLw.js → sd-yDyBYxtQ.js} +1 -1
  137. package/admin/assets/{se-nTBYCwx3.js → se-EJKibocp.js} +1 -1
  138. package/admin/assets/sha512-DJtGRwVu.js +1 -0
  139. package/admin/assets/{si-DG88w5El.js → si-BCBV00rO.js} +1 -1
  140. package/admin/assets/{sk-s5FxW36J.js → sk-B2VvG_mo.js} +1 -1
  141. package/admin/assets/{sl-B-HyGrpA.js → sl-BTYmt65M.js} +1 -1
  142. package/admin/assets/{sq-DPFtfEC8.js → sq-BaLGKAHu.js} +1 -1
  143. package/admin/assets/{sr-bBPg1zz4.js → sr-DarE-95b.js} +1 -1
  144. package/admin/assets/{sr-cyrl-cXkYHMcP.js → sr-cyrl-CrwXFtkw.js} +1 -1
  145. package/admin/assets/{ss-7nSqXJN7.js → ss-CbBe1RUs.js} +1 -1
  146. package/admin/assets/{sv-B8mG1QK4.js → sv-Baw7Je9j.js} +1 -1
  147. package/admin/assets/{sv-fi-CotGRXSH.js → sv-fi-CVHORXkw.js} +1 -1
  148. package/admin/assets/{sw-fAxU-Uck.js → sw-k1H_OzIi.js} +1 -1
  149. package/admin/assets/{ta-Dl0Z1pan.js → ta-Bc4ZaZT9.js} +1 -1
  150. package/admin/assets/{te-DmcKasFF.js → te-ClciACZi.js} +1 -1
  151. package/admin/assets/{tet-D6mmvYpQ.js → tet-B75w1oSG.js} +1 -1
  152. package/admin/assets/{tg-39Hf2-oO.js → tg-CuH2ARRk.js} +1 -1
  153. package/admin/assets/{th-BSmTJSFN.js → th-LvExAoJ6.js} +1 -1
  154. package/admin/assets/{tk-m85E5igC.js → tk-2CFwTMKo.js} +1 -1
  155. package/admin/assets/{tl-ph-BlYxVvNt.js → tl-ph-CNJ_lViV.js} +1 -1
  156. package/admin/assets/{tlh-D1POnIeE.js → tlh-9UbKQkHo.js} +1 -1
  157. package/admin/assets/{tr-BurtfrLy.js → tr-Cu4P4OMe.js} +1 -1
  158. package/admin/assets/{tzl-BvyD0lOf.js → tzl-D97WsZW-.js} +1 -1
  159. package/admin/assets/{tzm-DZHS0dK6.js → tzm-CURkYgMY.js} +1 -1
  160. package/admin/assets/{tzm-latn-yNW7CNCH.js → tzm-latn-BRGsWK-9.js} +1 -1
  161. package/admin/assets/{ug-cn-rxs2pf5Y.js → ug-cn-Cd4exKOc.js} +1 -1
  162. package/admin/assets/{uk-CgZM-NnA.js → uk-DEXM3PIH.js} +1 -1
  163. package/admin/assets/{ur-YxIR8yYD.js → ur-DpUycj07.js} +1 -1
  164. package/admin/assets/{uz-CE9QsQMG.js → uz-CznjzufA.js} +1 -1
  165. package/admin/assets/{uz-latn-DiVdSW6G.js → uz-latn-Baga58FN.js} +1 -1
  166. package/admin/assets/{vi-DKARV6qf.js → vi-BrZWcL2C.js} +1 -1
  167. package/admin/assets/{x-pseudo-BuAdOrXQ.js → x-pseudo-BY6J4I7_.js} +1 -1
  168. package/admin/assets/{yo-BcB85DhC.js → yo-Dkeaym0x.js} +1 -1
  169. package/admin/assets/{zh-DLmxfxzl.js → zh-C2fOZUKR.js} +1 -1
  170. package/admin/assets/{zh-cn-ILRADZJi.js → zh-cn-CiA7hMZm.js} +1 -1
  171. package/admin/assets/{zh-hk-fT7fQk9_.js → zh-hk-EkoniWh0.js} +1 -1
  172. package/admin/assets/{zh-tw-NqRXGP6v.js → zh-tw-KDgos-kB.js} +1 -1
  173. package/admin/index.html +3 -2
  174. package/frontend/assets/{dist-legacy-BPToXNXu.js → dist-legacy-BNFW04Ss.js} +1 -1
  175. package/frontend/assets/index-legacy-2akkos6t.js +1 -0
  176. package/frontend/assets/{lib-legacy-CBk-_G90.js → lib-legacy-CO5spnaN.js} +1 -1
  177. package/frontend/assets/{sha512-legacy-CWrR2rkQ.js → sha512-legacy-BuJAJxVV.js} +1 -1
  178. package/frontend/index.html +1 -1
  179. package/npm-shrinkwrap.json +6 -6
  180. package/package.json +1 -1
  181. package/src/AsapStream.js +18 -14
  182. package/src/SendList.js +42 -16
  183. package/src/ThrottledStream.js +3 -3
  184. package/src/acme.js +23 -10
  185. package/src/adminApis.js +7 -6
  186. package/src/api.accounts.js +11 -7
  187. package/src/api.auth.js +8 -5
  188. package/src/api.cert.js +1 -1
  189. package/src/api.get_file_list.js +14 -11
  190. package/src/api.lang.js +3 -2
  191. package/src/api.monitor.js +1 -1
  192. package/src/api.net.js +3 -1
  193. package/src/api.plugins.js +2 -0
  194. package/src/api.vfs.js +9 -12
  195. package/src/apiMiddleware.js +5 -1
  196. package/src/basicWeb.js +3 -2
  197. package/src/block.js +1 -1
  198. package/src/commands.js +6 -5
  199. package/src/comments.js +29 -13
  200. package/src/config.js +7 -4
  201. package/src/consoleLog.js +14 -12
  202. package/src/const.js +1 -1
  203. package/src/cross-const.js +27 -1
  204. package/src/cross.js +37 -34
  205. package/src/ddns.js +1 -4
  206. package/src/events.js +1 -4
  207. package/src/fileAttr.js +1 -1
  208. package/src/frontEndApis.js +7 -3
  209. package/src/geo.js +8 -2
  210. package/src/github.js +10 -9
  211. package/src/index.js +2 -1
  212. package/src/ips.js +24 -3
  213. package/src/langs/hfs-lang-ar.json +1 -0
  214. package/src/langs/hfs-lang-bg.json +1 -0
  215. package/src/langs/hfs-lang-de.json +1 -0
  216. package/src/langs/hfs-lang-el.json +1 -0
  217. package/src/langs/hfs-lang-es.json +1 -0
  218. package/src/langs/hfs-lang-fi.json +1 -0
  219. package/src/langs/hfs-lang-fr.json +1 -0
  220. package/src/langs/hfs-lang-hu.json +1 -0
  221. package/src/langs/hfs-lang-it.json +1 -0
  222. package/src/langs/hfs-lang-ja.json +1 -0
  223. package/src/langs/hfs-lang-ko.json +1 -0
  224. package/src/langs/hfs-lang-lt.json +1 -0
  225. package/src/langs/hfs-lang-ms.json +1 -0
  226. package/src/langs/hfs-lang-nl.json +1 -0
  227. package/src/langs/hfs-lang-pl.json +1 -0
  228. package/src/langs/hfs-lang-pt-br.json +1 -0
  229. package/src/langs/hfs-lang-ro.json +1 -0
  230. package/src/langs/hfs-lang-ru.json +1 -0
  231. package/src/langs/hfs-lang-sr-latn.json +1 -0
  232. package/src/langs/hfs-lang-sr.json +1 -0
  233. package/src/langs/hfs-lang-th.json +1 -0
  234. package/src/langs/hfs-lang-tr.json +1 -0
  235. package/src/langs/hfs-lang-uk.json +1 -0
  236. package/src/langs/hfs-lang-vi.json +1 -0
  237. package/src/langs/hfs-lang-zh-tw.json +1 -0
  238. package/src/langs/hfs-lang-zh.json +1 -0
  239. package/src/listen.js +17 -13
  240. package/src/log.js +11 -2
  241. package/src/middlewares.js +4 -4
  242. package/src/misc.js +3 -3
  243. package/src/multipartUpload.js +1 -2
  244. package/src/nat.js +1 -1
  245. package/src/outboundProxy.js +7 -0
  246. package/src/perm.js +5 -3
  247. package/src/plugins.js +60 -37
  248. package/src/selfCheck.js +4 -3
  249. package/src/serveFile.js +3 -3
  250. package/src/serveGuiAndSharedFiles.js +7 -6
  251. package/src/serveGuiFiles.js +15 -2
  252. package/src/serverCode.js +27 -0
  253. package/src/throttler.js +3 -2
  254. package/src/update.js +9 -3
  255. package/src/upload.js +29 -11
  256. package/src/uploadOwners.js +95 -0
  257. package/src/util-files.js +4 -2
  258. package/src/util-http.js +3 -1
  259. package/src/vfs.js +67 -30
  260. package/src/walkDir.js +24 -11
  261. package/src/watchLoad.js +13 -2
  262. package/src/webdav.js +33 -31
  263. package/src/zip.js +1 -1
  264. package/admin/assets/dist-CHP3cW_n.js +0 -1
  265. package/admin/assets/en-DQ460ET7.js +0 -1
  266. package/admin/assets/index-BGIe1xaR.js +0 -760
  267. package/admin/assets/sha512-CZz40GwW.js +0 -1
  268. package/frontend/assets/index-legacy-CmusDwvl.js +0 -1
package/src/throttler.js CHANGED
@@ -15,11 +15,11 @@ const lodash_1 = __importDefault(require("lodash"));
15
15
  const events_1 = __importDefault(require("./events"));
16
16
  const persistence_1 = require("./persistence");
17
17
  const mainThrottleGroup = new ThrottledStream_1.ThrottleGroup(Infinity);
18
- (0, config_1.defineConfig)('max_kbps', Infinity).sub(v => mainThrottleGroup.updateLimit(v));
18
+ (0, config_1.defineConfig)(misc_1.CFG.max_kbps, Infinity).sub(v => mainThrottleGroup.updateLimit(v));
19
19
  const ip2group = {};
20
20
  const SymThrStr = Symbol('stream');
21
21
  const SymTimeout = Symbol('timeout');
22
- const maxKbpsPerIp = (0, config_1.defineConfig)('max_kbps_per_ip', Infinity);
22
+ const maxKbpsPerIp = (0, config_1.defineConfig)(misc_1.CFG.max_kbps_per_ip, Infinity);
23
23
  maxKbpsPerIp.sub(v => {
24
24
  for (const [ip, { group }] of Object.entries(ip2group))
25
25
  if (ip) // empty-string = unlimited group
@@ -71,6 +71,7 @@ const throttler = async (ctx, next) => {
71
71
  delete ip2group[ctx.ip];
72
72
  });
73
73
  ctx.state.originalStream = body;
74
+ body.on('error', e => ts.destroy(e)); // pipe doesn't forward source errors, so make Koa's response stream report them
74
75
  ctx.body = body.pipe(ts);
75
76
  if (downloadTotal !== undefined) // undefined will break SSE
76
77
  ctx.response.length = downloadTotal; // preserve this info
package/src/update.js CHANGED
@@ -28,8 +28,8 @@ const persistence_1 = require("./persistence");
28
28
  const lodash_1 = __importDefault(require("lodash"));
29
29
  const argv_1 = require("./argv");
30
30
  const promises_2 = require("stream/promises");
31
- const updateToBeta = (0, config_1.defineConfig)('update_to_beta', false);
32
- const autoCheckUpdate = (0, config_1.defineConfig)('auto_check_update', true);
31
+ const updateToBeta = (0, config_1.defineConfig)(misc_1.CFG.update_to_beta, false);
32
+ const autoCheckUpdate = (0, config_1.defineConfig)(misc_1.CFG.auto_check_update, true);
33
33
  const lastCheckUpdate = persistence_1.storedMap.singleSync('lastCheckUpdate', 0);
34
34
  const AUTO_CHECK_EVERY = misc_1.DAY;
35
35
  exports.autoCheckUpdateResult = persistence_1.storedMap.singleSync('autoCheckUpdateResult', undefined);
@@ -162,7 +162,13 @@ async function update(tagOrUrl = '') {
162
162
  } while ((0, fs_1.existsSync)((0, path_1.join)(binPath, newBinFile)));
163
163
  plugins_1.pluginsWatcher.pause();
164
164
  try {
165
- await (0, misc_1.unzip)((0, fs_1.createReadStream)(LOCAL_UPDATE), path => (0, path_1.join)(binPath, path === binFile ? newBinFile : path));
165
+ try {
166
+ await (0, misc_1.unzip)((0, fs_1.createReadStream)(LOCAL_UPDATE), path => (0, path_1.join)(binPath, path === binFile ? newBinFile : path));
167
+ }
168
+ catch (e) {
169
+ await (0, promises_1.rename)(LOCAL_UPDATE, 'hfs-update-bad.zip'); // quarantine failed archives so they don't take precedence over future downloads
170
+ throw e;
171
+ }
166
172
  const newBin = (0, path_1.join)(binPath, newBinFile);
167
173
  if (!(0, fs_1.existsSync)(newBin)) {
168
174
  if (url) // the file was downloaded, and the UI would show the "update from local file" button until we remove it
package/src/upload.js CHANGED
@@ -23,10 +23,15 @@ const events_1 = __importDefault(require("./events"));
23
23
  const promises_1 = require("fs/promises");
24
24
  const expiringCache_1 = require("./expiringCache");
25
25
  const first_1 = require("./first");
26
- exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)('delete_unfinished_uploads_after', 86_400);
27
- exports.minAvailableMb = (0, config_1.defineConfig)('min_available_mb', 100);
28
- exports.dontOverwriteUploading = (0, config_1.defineConfig)('dont_overwrite_uploading', true);
26
+ const uploadOwners_1 = require("./uploadOwners");
27
+ exports.deleteUnfinishedUploadsAfter = (0, config_1.defineConfig)(misc_1.CFG.delete_unfinished_uploads_after, 86_400);
28
+ exports.minAvailableMb = (0, config_1.defineConfig)(misc_1.CFG.min_available_mb, 100);
29
+ exports.dontOverwriteUploading = (0, config_1.defineConfig)(misc_1.CFG.dont_overwrite_uploading, true);
29
30
  const waitingToBeDeleted = {};
31
+ function cancelDeletion(path) {
32
+ clearTimeout(waitingToBeDeleted[path]?.timeout);
33
+ delete waitingToBeDeleted[path];
34
+ }
30
35
  (0, first_1.onProcessExit)(() => {
31
36
  if (!Object.keys(waitingToBeDeleted).length)
32
37
  return;
@@ -48,7 +53,7 @@ function setUploadMeta(path, ctx) {
48
53
  });
49
54
  }
50
55
  function getUploadTempFor(fullPath) {
51
- return (0, path_1.join)((0, path_1.dirname)(fullPath), 'hfs$upload-' + (0, path_1.basename)(fullPath).slice(-200));
56
+ return (0, path_1.join)((0, path_1.dirname)(fullPath), const_1.UPLOAD_TEMP_PREFIX + (0, path_1.basename)(fullPath).slice(-200));
52
57
  }
53
58
  const diskSpaceCache = (0, expiringCache_1.expiringCache)(3_000); // invalidate shortly
54
59
  const uploadingFiles = new Map();
@@ -105,6 +110,7 @@ function uploadWriter(base, baseUri, filename, ctx) {
105
110
  setUploadMeta(dir, ctx);
106
111
  // use temporary name while uploading
107
112
  const tempName = getUploadTempFor(fullPath);
113
+ const tempUri = (0, misc_1.enforceFinal)('/', baseUri) + (0, misc_1.pathEncode)((0, path_1.basename)(tempName));
108
114
  // try to catch errors early (sync): this is avoiding on chrome139 when uploading a big file (1GB) to get miss the error code and have to make a `simulate`
109
115
  try {
110
116
  fs_1.default.accessSync(tempName, fs_1.default.constants.W_OK);
@@ -130,8 +136,11 @@ function uploadWriter(base, baseUri, filename, ctx) {
130
136
  return fail(const_1.HTTP_PRECONDITION_FAILED);
131
137
  }
132
138
  // append if resuming
133
- if (!resume && stats)
139
+ if (!resume && stats) {
140
+ // a new upload discards the old resumable temp, so its owner grant must not survive
141
+ (0, uploadOwners_1.deleteUploadOwner)(tempUri);
134
142
  fs_1.default.unlinkSync(tempName);
143
+ }
135
144
  const writeStream = (0, misc_1.createStreamLimiter)(isNaN(contentLength) ? Infinity : contentLength);
136
145
  const fullSize = stillToWrite + resume;
137
146
  // allow plugins to mess with the write-stream, because the read-stream can be complicated in case of multipart
@@ -152,6 +161,9 @@ function uploadWriter(base, baseUri, filename, ctx) {
152
161
  const tracked = { ctx, got: 0, size: stillToWrite };
153
162
  uploadingFiles.set(fullPath, tracked);
154
163
  console.debug('Upload started');
164
+ let tempOwnerSet = false;
165
+ // ownership must be visible before a follow-up request can act on the unfinished file
166
+ ctx.req.once('aborted', setTempOwner);
155
167
  // the file stream doesn't have an event for data being written, so we use 'data' of its feeder, which happens before, so we postpone a bit, trying to have a fresher number
156
168
  writeStream.on('data', () => setTimeout(() => tracked.got = bytesGot()));
157
169
  const lockMiddleware = (0, misc_1.pendingPromise)(); // expose outside, to let know when all operations stopped
@@ -171,7 +183,8 @@ function uploadWriter(base, baseUri, filename, ctx) {
171
183
  return (0, promises_1.rm)(tempName).catch(() => { });
172
184
  if (ctx.isAborted()) { // in the very unlikely case the connection is interrupted between last-byte and here, we still consider it unfinished, as the client had no way to know, and will resume, but it would get an error if we finish the process
173
185
  const sec = exports.deleteUnfinishedUploadsAfter.get();
174
- return lodash_1.default.isNumber(sec) && delayedDelete(tempName, sec);
186
+ setTempOwner();
187
+ return lodash_1.default.isNumber(sec) && delayedDelete(tempName, sec, tempUri);
175
188
  }
176
189
  if (isPartial) // we are supposed to leave the unfinished upload as it is, with its temp name
177
190
  return ctx.status = const_1.HTTP_NO_CONTENT; // lockMiddleware contains an empty string, so we must take care of the status
@@ -194,6 +207,7 @@ function uploadWriter(base, baseUri, filename, ctx) {
194
207
  () => (0, promises_1.rename)(tempName, dest).then(() => true, e => e?.code !== 'EBUSY' && Promise.reject(e)), { timeout: 10_000 });
195
208
  if (!done)
196
209
  throw 'EBUSY';
210
+ (0, uploadOwners_1.deleteUploadOwner)(tempUri); // the temp URI no longer exists after rename; final ownership is recorded below
197
211
  if (mtime) // so we use it to touch the file
198
212
  await (0, promises_1.utimes)(dest, Date.now() / 1000, mtime / 1000);
199
213
  cancelDeletion(tempName); // not necessary, as deletion's failure is silent, but still
@@ -202,6 +216,7 @@ function uploadWriter(base, baseUri, filename, ctx) {
202
216
  if (ctx.query.comment)
203
217
  void (0, comments_1.setCommentFor)(dest, String(ctx.query.comment));
204
218
  obj.uri = (0, misc_1.enforceFinal)('/', baseUri) + (0, misc_1.pathEncode)((0, path_1.basename)(dest));
219
+ await (0, uploadOwners_1.setUploadOwner)(obj.uri, ctx);
205
220
  events_1.default.emit('uploadFinished', obj);
206
221
  console.debug("Upload finished", dest);
207
222
  if (resEvent)
@@ -245,6 +260,12 @@ function uploadWriter(base, baseUri, filename, ctx) {
245
260
  function bytesGot() {
246
261
  return fileStream.bytesWritten + fileStream.writableLength;
247
262
  }
263
+ function setTempOwner() {
264
+ if (tempOwnerSet)
265
+ return;
266
+ tempOwnerSet = true;
267
+ void (0, uploadOwners_1.setUploadOwner)(tempUri, ctx);
268
+ }
248
269
  }
249
270
  catch (e) {
250
271
  releaseFile();
@@ -259,21 +280,18 @@ function uploadWriter(base, baseUri, filename, ctx) {
259
280
  overwriteRequestedButForbidden = true;
260
281
  return false;
261
282
  }
262
- function delayedDelete(path, secs) {
283
+ function delayedDelete(path, secs, tempUri) {
263
284
  clearTimeout(waitingToBeDeleted[path]?.timeout);
264
285
  return waitingToBeDeleted[path] = {
265
286
  mtime,
266
287
  expires: Date.now() + secs * 1000,
267
288
  timeout: setTimeout(() => {
268
289
  delete waitingToBeDeleted[path];
290
+ (0, uploadOwners_1.deleteUploadOwner)(tempUri);
269
291
  (0, promises_1.rm)(path).catch(() => { });
270
292
  }, secs * 1000)
271
293
  };
272
294
  }
273
- function cancelDeletion(path) {
274
- clearTimeout(waitingToBeDeleted[path]?.timeout);
275
- delete waitingToBeDeleted[path];
276
- }
277
295
  function releaseFile() {
278
296
  uploadingFiles.delete(fullPath);
279
297
  }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.uploadOwners = exports.ownUploadDeleteHours = void 0;
7
+ exports.setUploadOwner = setUploadOwner;
8
+ exports.moveUploadOwner = moveUploadOwner;
9
+ exports.deleteUploadOwner = deleteUploadOwner;
10
+ exports.getSessionId = getSessionId;
11
+ const kvstorage_1 = require("@rejetto/kvstorage");
12
+ const misc_1 = require("./misc");
13
+ const first_1 = require("./first");
14
+ const config_1 = require("./config");
15
+ const auth_1 = require("./auth");
16
+ const events_1 = __importDefault(require("./events"));
17
+ exports.ownUploadDeleteHours = (0, config_1.defineConfig)(misc_1.CFG.own_upload_delete_hours, 24);
18
+ exports.uploadOwners = new kvstorage_1.KvStorage({
19
+ rewriteLater: true,
20
+ });
21
+ exports.uploadOwners.open('upload-owners.kv').catch(e => console.error("Upload owners won't work correctly", e));
22
+ (0, first_1.onProcessExit)(() => exports.uploadOwners.close());
23
+ setInterval(() => {
24
+ if (!exports.uploadOwners.isOpen())
25
+ return;
26
+ const keys = Array.from(exports.uploadOwners.keys());
27
+ for (const k of keys) {
28
+ const owner = exports.uploadOwners.getSync(k);
29
+ if (owner && isExpired(owner))
30
+ void exports.uploadOwners.del(k);
31
+ }
32
+ }, misc_1.MINUTE);
33
+ events_1.default.on('checkVfsPermission', ({ node, perm, ctx }) => {
34
+ if (perm !== 'can_delete' || !node.source)
35
+ return;
36
+ const { vfsPath } = node;
37
+ if (!vfsPath || !exports.ownUploadDeleteHours.get() || !exports.uploadOwners.isOpen())
38
+ return;
39
+ const owner = exports.uploadOwners.getSync(cleanVfsPath(vfsPath));
40
+ if (!owner)
41
+ return;
42
+ if (isExpired(owner)) {
43
+ deleteUploadOwner(vfsPath);
44
+ return;
45
+ }
46
+ const username = (0, auth_1.getCurrentUsername)(ctx);
47
+ const { sessionId } = owner;
48
+ if (sessionId && sessionId === ctx.session?.sessionId || username && owner.username === username)
49
+ return 0;
50
+ });
51
+ function setUploadOwner(vfsPath, ctx) {
52
+ if (!exports.uploadOwners.isOpen() || !exports.ownUploadDeleteHours.get())
53
+ return;
54
+ const username = (0, auth_1.getCurrentUsername)(ctx) || undefined;
55
+ return exports.uploadOwners.put(cleanVfsPath(vfsPath), {
56
+ username,
57
+ sessionId: username ? undefined : getSessionId(ctx),
58
+ ip: ctx.ip,
59
+ created: new Date(),
60
+ })?.catch(e => {
61
+ console.error("Couldn't store upload owner for", vfsPath, String(e.message || e));
62
+ });
63
+ }
64
+ async function moveUploadOwner(fromPath, toPath) {
65
+ if (!exports.uploadOwners.isOpen())
66
+ return;
67
+ const from = cleanVfsPath(fromPath);
68
+ const affected = Array.from(exports.uploadOwners.keys()).filter(k => isSameOrInside(from, k));
69
+ if (!affected.length)
70
+ return;
71
+ const to = cleanVfsPath(toPath);
72
+ const owners = affected.map(k => ({ k, owner: exports.uploadOwners.getSync(k) }));
73
+ // ownership is keyed by VFS path, so HFS moves must carry descendant upload records too
74
+ await Promise.all(owners.map(({ k, owner }) => owner && exports.uploadOwners.put(to + k.slice(from.length), owner)));
75
+ await Promise.all(affected.map(k => exports.uploadOwners.del(k)));
76
+ }
77
+ function deleteUploadOwner(vfsPath) {
78
+ if (exports.uploadOwners.isOpen())
79
+ // deleting a folder must clear ownership for uploaded files below it too
80
+ for (const k of Array.from(exports.uploadOwners.keys()).filter(k => isSameOrInside(cleanVfsPath(vfsPath), k)))
81
+ void exports.uploadOwners.del(k);
82
+ }
83
+ function isExpired(owner) {
84
+ const hours = exports.ownUploadDeleteHours.get();
85
+ return !hours || Number(owner.created) + hours * misc_1.HOUR <= Date.now();
86
+ }
87
+ function getSessionId(ctx) {
88
+ return ctx.session.sessionId ||= (0, misc_1.randomId)(30);
89
+ }
90
+ function cleanVfsPath(path) {
91
+ return '/' + path.replace(/^\/+|\/+$/g, '');
92
+ }
93
+ function isSameOrInside(parent, path) {
94
+ return path === parent || path.startsWith(parent + '/');
95
+ }
package/src/util-files.js CHANGED
@@ -31,7 +31,7 @@ const const_1 = require("./const");
31
31
  const promises_2 = require("stream/promises");
32
32
  const stat_1 = require("./stat");
33
33
  const unzipper_1 = __importDefault(require("unzipper"));
34
- const fileTimeout = (0, config_1.defineConfig)('file_timeout', 3, x => x * 1000);
34
+ const fileTimeout = (0, config_1.defineConfig)(cross_1.CFG.file_timeout, 3, x => x * 1000);
35
35
  // a smart (and a bit arbitrary) way to decide if we need the stat-workers functionality. Without it, we may be a bit faster. We'll see with experience if we need a dedicated configuration.
36
36
  const disableStatWorkers = Number(process.env.UV_THREADPOOL_SIZE) >= 10;
37
37
  // some paths (mostly offline networked ones) can take 20+ seconds to fail
@@ -106,7 +106,9 @@ function escapeGlobPath(path) {
106
106
  async function unzip(stream, cb) {
107
107
  const extracted = new Map();
108
108
  let chain = Promise.resolve();
109
- return new Promise((resolve, reject) => stream.pipe(unzipper_1.default.Parse())
109
+ const parser = unzipper_1.default.Parse();
110
+ stream.once('error', e => parser.destroy(e)); // pipe doesn't forward source errors, so make the parser reject and release its resources
111
+ return new Promise((resolve, reject) => stream.pipe(parser)
110
112
  .on('close', () => chain.then(resolve, reject))
111
113
  .on('error', reject)
112
114
  .on('entry', (entry) => chain = chain.then(async () => {
package/src/util-http.js CHANGED
@@ -64,6 +64,7 @@ async function httpWithBody(url, options) {
64
64
  function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...options } = {}, redirected = []) {
65
65
  const controller = new AbortController();
66
66
  options.signal ??= controller.signal;
67
+ options.localAddress ??= httpStream.defaultLocalAddress;
67
68
  return Object.assign(new Promise(async (resolve, reject) => {
68
69
  proxy ??= httpStream.defaultProxy;
69
70
  options.headers ??= {};
@@ -140,7 +141,7 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
140
141
  if (options.timeout) // node only emits the timeout event, so destroy the request to unblock callers waiting for the body
141
142
  req.setTimeout(options.timeout, () => req.destroy(Object.assign(Error('timeout'), { code: 'ETIMEDOUT' })));
142
143
  if (body && body instanceof node_stream_1.Readable)
143
- body.pipe(req).on('end', () => req.end());
144
+ body.on('error', e => req.destroy(e)).pipe(req);
144
145
  else
145
146
  req.end(body);
146
147
  function connect() {
@@ -149,6 +150,7 @@ function httpStream(url, { body, proxy, jar, noRedirect, httpThrow = true, ...op
149
150
  (proxyParsed.protocol === 'https:' ? node_https_1.default : node_http_1.default).request({
150
151
  ...proxyParsed,
151
152
  auth: undefined, // void proxyParsed.auth
153
+ localAddress: options.localAddress, // the proxy socket is the actual outbound connection when tunneling HTTPS
152
154
  method: 'CONNECT',
153
155
  path,
154
156
  headers: { Host: path, ...proxyAuth }
package/src/vfs.js CHANGED
@@ -8,6 +8,7 @@ exports.saveVfs = exports.vfs = void 0;
8
8
  exports.permsFromParent = permsFromParent;
9
9
  exports.isSameFilenameAs = isSameFilenameAs;
10
10
  exports.normalizeFilename = normalizeFilename;
11
+ exports.getFreeVfsName = getFreeVfsName;
11
12
  exports.applyParentToChild = applyParentToChild;
12
13
  exports.urlToNode = urlToNode;
13
14
  exports.nodeStats = nodeStats;
@@ -37,7 +38,12 @@ const comments_1 = require("./comments");
37
38
  const walkDir_1 = require("./walkDir");
38
39
  const node_stream_1 = require("node:stream");
39
40
  const adminApis_1 = require("./adminApis");
40
- const showHiddenFiles = (0, config_1.defineConfig)('show_hidden_files', false);
41
+ const showHiddenFiles = (0, config_1.defineConfig)(misc_1.CFG.show_hidden_files, false);
42
+ function setVfsPath(node, vfsPath, parent) {
43
+ return (0, misc_1.setHidden)(node, {
44
+ vfsPath: (0, misc_1.enforceFinal)('/', parent?.vfsPath) + (0, misc_1.pathEncode)(vfsPath)
45
+ });
46
+ }
41
47
  function permsFromParent(parent, child) {
42
48
  const ret = {};
43
49
  for (const k of misc_1.PERM_KEYS) {
@@ -80,21 +86,29 @@ function isSameFilenameAs(name) {
80
86
  function normalizeFilename(x) {
81
87
  return (const_1.IS_WINDOWS || const_1.IS_MAC ? x.toLocaleLowerCase() : x).normalize();
82
88
  }
89
+ function getFreeVfsName(siblings, name) {
90
+ const ext = (0, path_1.extname)(name);
91
+ const noExt = ext ? name.slice(0, -ext.length) : name;
92
+ let idx = 2;
93
+ while (siblings?.find(isSameFilenameAs(name)))
94
+ name = `${noExt} ${idx++}${ext}`;
95
+ return name;
96
+ }
83
97
  async function applyParentToChild(child, parent, name) {
84
- const ret = {
98
+ name ||= child ? getNodeName(child) : '';
99
+ const ret = setVfsPath({
85
100
  original: child, // this can be overridden by passing an 'original' in `child`
86
101
  ...child,
87
102
  isFolder: child?.isFolder ?? (child?.children?.length > 0 || undefined), // isFolder is hidden in original node, so we must copy it explicitly
88
103
  isTemp: true,
89
104
  parent,
90
- };
91
- name ||= child ? getNodeName(child) : '';
105
+ }, name, parent);
92
106
  inheritMasks(ret, parent, name);
93
107
  await parentMaskApplier(parent)(ret, name);
94
108
  inheritFromParent(ret);
95
109
  return ret;
96
110
  }
97
- async function urlToNode(url, ctx, parent = exports.vfs, allowMissing // true means missing path segments still resolve to temporary nodes with a computed source path
111
+ async function urlToNode(url, ctx, parent = exports.vfs.compiled(), allowMissing // true means missing path segments still resolve to temporary nodes with a computed source path
98
112
  ) {
99
113
  let initialSlashes = 0;
100
114
  while (url[initialSlashes] === '/')
@@ -112,6 +126,7 @@ async function urlToNode(url, ctx, parent = exports.vfs, allowMissing // true me
112
126
  const ret = await getNodeByName(name, parent, assumeFolder);
113
127
  if (!ret)
114
128
  return;
129
+ setVfsPath(ret, name, parent);
115
130
  if (rest || ret?.original)
116
131
  return urlToNode(rest, ctx, ret, allowMissing);
117
132
  if (ret.source)
@@ -164,36 +179,49 @@ async function getNodeByName(name, parent, assumeMissingToBeFolder = false) {
164
179
  return ret;
165
180
  }
166
181
  }
167
- const smartUncFolderDetection = (0, config_1.defineConfig)('smart_unc_folder_detection', false);
182
+ const smartUncFolderDetection = (0, config_1.defineConfig)(misc_1.CFG.smart_unc_folder_detection, false);
168
183
  async function setIsFolder(node) {
169
184
  if (!node.source)
170
185
  return;
171
- const isFolder = /[\\/]$/.test(node.source)
186
+ const isFolder = (0, misc_1.hasFinalSlash)(node.source)
172
187
  || smartUncFolderDetection.get() && (0, misc_1.getUncHost)(node.source) && !(0, path_1.basename)(node.source).includes('.') // no dot = folder; not very reliable but fast for unreachable unc hosts, and it's an opt-in
173
188
  || await nodeStats(node).then(x => x?.isDirectory(), () => undefined);
174
189
  (0, misc_1.setHidden)(node, { isFolder });
190
+ if (isFolder)
191
+ persistFolderMarker(node);
175
192
  return isFolder;
176
193
  }
177
- exports.vfs = {};
178
- (0, config_1.defineConfig)('vfs', exports.vfs).sub(async (x) => {
179
- await reviewVfs(x);
194
+ // compiled is the stable mutable tree, while get() may return a fresh clone of the default
195
+ exports.vfs = (0, config_1.defineConfig)(misc_1.CFG.vfs, {}, x => setVfsPath(structuredClone(x && typeof x === 'object' ? x : {}), '')); // ensure the type is right
196
+ exports.vfs.sub(async () => {
197
+ await reviewVfs();
180
198
  console.log('VFS ready');
181
199
  });
182
- async function reviewVfs(data = exports.vfs) {
200
+ async function reviewVfs() {
183
201
  await (async function recur(node) {
184
202
  if (node.source && !node.children?.length && node.isFolder === undefined)
185
203
  await setIsFolder(node);
186
- if (node.children)
187
- await Promise.allSettled(node.children.map(recur));
188
- })(data);
189
- exports.vfs = data;
204
+ if (!node.children)
205
+ return;
206
+ // we rename your node in case you got 2 nodes with the same name
207
+ const usedNames = new Set();
208
+ for (const child of node.children) {
209
+ const name = getNodeName(child);
210
+ const normalized = normalizeFilename(name);
211
+ if (usedNames.has(normalized))
212
+ child.name = getFreeVfsName(node.children, name);
213
+ usedNames.add(normalizeFilename(getNodeName(child)));
214
+ }
215
+ await Promise.allSettled(node.children.map(recur));
216
+ })(exports.vfs.compiled());
190
217
  }
191
218
  exports.saveVfs = (0, misc_1.debounceAsync)(async () => {
192
- await reviewVfs();
193
- await (0, config_1.setConfig)({ vfs: exports.vfs }, true);
219
+ await reviewVfs(); // refresh runtime-derived folder flags before saving mutated VFS state
220
+ exports.vfs.set(exports.vfs.compiled()); // sync the mutable runtime tree back to config state before persisting it
221
+ (0, config_1.saveConfigAsap)();
194
222
  });
195
223
  function isRoot(node) {
196
- return node === exports.vfs;
224
+ return node === exports.vfs.compiled();
197
225
  }
198
226
  function getNodeName(node) {
199
227
  if (isRoot(node))
@@ -221,12 +249,24 @@ function nodeIsFolder(node) {
221
249
  function reconsider() {
222
250
  // a networked source may be offline at startup, and become online later: recalculate in the background
223
251
  nodeStats(node).then(s => {
224
- if (s)
225
- (0, misc_1.setHidden)(node.original || node, { isFolder: s.isDirectory() });
252
+ if (s) {
253
+ const isFolder = s.isDirectory();
254
+ (0, misc_1.setHidden)(node.original || node, { isFolder });
255
+ if (isFolder)
256
+ persistFolderMarker(node);
257
+ }
226
258
  }, () => { });
227
259
  return undefined;
228
260
  }
229
261
  }
262
+ // we mark folder paths with a final slash – the UI already does so, but the config may be modified
263
+ function persistFolderMarker(node) {
264
+ const stored = node.original || node; // temporary VFS nodes must update their stored original for saveVfs to persist the marker
265
+ if (!stored.source || (0, misc_1.hasFinalSlash)(stored.source))
266
+ return;
267
+ stored.source += path_1.sep;
268
+ void (0, exports.saveVfs)();
269
+ }
230
270
  async function getDefaultFile(node, ctx) {
231
271
  return node.default && nodeIsFolder(node) && await urlToNode(node.default, ctx, node) || undefined;
232
272
  }
@@ -266,12 +306,9 @@ function statusCodeForMissingPerm(node, perm, ctx, assign = true) {
266
306
  } while (1);
267
307
  if ((0, misc_1.isWhoObject)(who) || isWhoVfsPerms(who))
268
308
  throw Error(`permission type-guard: ${JSON.stringify(who)}`);
269
- const eventName = 'checkVfsPermission';
270
- if (events_1.default.anyListener(eventName)) {
271
- const first = lodash_1.default.max(events_1.default.emit(eventName, { who, node, perm, ctx }));
272
- if (first !== undefined)
273
- return first;
274
- }
309
+ const first = lodash_1.default.max(events_1.default.emit('checkVfsPermission', { who, node, perm, ctx }));
310
+ if (first !== undefined)
311
+ return first;
275
312
  return simpleWhoToError(who, ctx)
276
313
  ?? (0, misc_1.throw_)(Error(`invalid permission: ${perm}=${(0, misc_1.try_)(() => JSON.stringify(who))}`));
277
314
  }
@@ -306,10 +343,10 @@ async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requi
306
343
  const nodeName = getNodeName(child);
307
344
  const name = prefixPath + nodeName;
308
345
  taken?.add(normalizeFilename(name));
309
- const item = { ...child, original: child, name, parent };
346
+ const item = setVfsPath({ ...child, original: child, name, parent }, name, parent);
310
347
  if (await cantSee(item))
311
348
  return;
312
- if (item.source && !item.children?.length) // real items must be accessible, unless there's more to it
349
+ if (item.source && !item.children?.length && !item.see_without_probing) // real items must be accessible, unless probing was explicitly disabled
313
350
  try {
314
351
  await promises_1.default.access(item.source);
315
352
  }
@@ -349,7 +386,7 @@ async function* walkNode(parent, { ctx, depth = Infinity, prefixPath = '', requi
349
386
  const name = prefixPath + (renamed || path);
350
387
  if (taken?.has(normalizeFilename(name))) // taken by vfs node above
351
388
  return false; // false just in case it's a folder
352
- const item = { name, isFolder, source: (0, path_1.join)(source, path), parent, stats: entry.stats };
389
+ const item = setVfsPath({ name, isFolder, source: (0, path_1.join)(source, path), parent, stats: entry.stats }, name, parent);
353
390
  // masks containing '/' must be matched against the relative path while keeping walkDir recursion enabled
354
391
  await pathMaskApplier(item, renamed || path);
355
392
  if (await cantSee(item)) // can't see: don't produce and don't recur
@@ -482,7 +519,7 @@ events_1.default.on('accountRenamed', ({ from, to }) => {
482
519
  if (n.masks)
483
520
  Object.values(n.masks).forEach(renameInNode);
484
521
  n.children?.forEach(renameInNode);
485
- })(exports.vfs);
522
+ })(exports.vfs.compiled());
486
523
  (0, exports.saveVfs)();
487
524
  function renameInPerm(a) {
488
525
  if ((0, misc_1.isWhoObject)(a)) {
package/src/walkDir.js CHANGED
@@ -23,12 +23,12 @@ function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false,
23
23
  return new Promise(async (resolve, reject) => {
24
24
  if (!await (0, util_files_1.isDirectory)(path))
25
25
  return reject(Error('ENOTDIR'));
26
- dirQ.add(() => readDir('', depth)
26
+ dirQ.add(() => readDir('', depth, [path])
27
27
  .then(res => {
28
28
  Promise.resolve(res?.branchDone).then(resolve);
29
29
  }, reject));
30
30
  });
31
- async function readDir(relativePath, depth) {
31
+ async function readDir(relativePath, depth, ancestorPaths) {
32
32
  if (stopped)
33
33
  return;
34
34
  const base = (0, path_1.join)(path, relativePath);
@@ -39,6 +39,8 @@ function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false,
39
39
  const pluginReceiver = lodash_1.default.isFunction(res) && res || null;
40
40
  const pluginIterator = lodash_1.default.isFunction(res?.[Symbol.asyncIterator] || res?.[Symbol.iterator]) && res;
41
41
  if (const_1.IS_WINDOWS && !pluginIterator) { // use native apis to read the 'hidden' attribute
42
+ // fswin callbacks cannot await, so track their work before closing the branch
43
+ const entriesWorking = [];
42
44
  const direntMethods = {
43
45
  isDir: false,
44
46
  isFile() { return !this.isDir; },
@@ -46,14 +48,16 @@ function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false,
46
48
  isBlockDevice() { return false; },
47
49
  isCharacterDevice() { return false; },
48
50
  };
49
- await new Promise(res => fswin_1.default.find(base + '\\*', (event, f) => {
50
- if (event !== 'FOUND')
51
- return res();
51
+ await new Promise((resolve, reject) => fswin_1.default.find(base + '\\*', (event, f) => {
52
+ if (event !== 'FOUND') {
53
+ Promise.all(entriesWorking).then(() => resolve());
54
+ return;
55
+ }
52
56
  if (stopped)
53
57
  return true; // stop signal
54
58
  if (!hidden && f.IS_HIDDEN)
55
59
  return;
56
- work(Object.assign(Object.create(direntMethods), {
60
+ entriesWorking.push(work(Object.assign(Object.create(direntMethods), {
57
61
  isDir: f.IS_DIRECTORY,
58
62
  name: f.LONG_NAME,
59
63
  stats: {
@@ -63,7 +67,7 @@ function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false,
63
67
  isFile: () => !f.IS_DIRECTORY,
64
68
  isDirectory: () => f.IS_DIRECTORY,
65
69
  }
66
- }));
70
+ }), Boolean(f.REPARSE_POINT_TAG)).catch(reject));
67
71
  }, true));
68
72
  }
69
73
  else
@@ -72,7 +76,9 @@ function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false,
72
76
  break;
73
77
  if (!hidden && !const_1.IS_WINDOWS && entry.name[0] === '.')
74
78
  continue;
75
- const stats = entry.isSymbolicLink?.() && await (0, util_files_1.statWithTimeout)((0, path_1.join)(base, entry.name)).catch(() => null);
79
+ // stat follows links, so preserve their identity for cycle detection
80
+ const isSymlink = entry.isSymbolicLink?.();
81
+ const stats = isSymlink && await (0, util_files_1.statWithTimeout)((0, path_1.join)(base, entry.name)).catch(() => null);
76
82
  if (stats === null)
77
83
  continue;
78
84
  if (stats)
@@ -80,7 +86,7 @@ function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false,
80
86
  const expanded = entry;
81
87
  if (stats)
82
88
  expanded.stats = stats;
83
- await work(expanded);
89
+ await work(expanded, isSymlink);
84
90
  }
85
91
  pluginReceiver?.(!stopped);
86
92
  const branchDone = Promise.allSettled(subDirsDone);
@@ -90,7 +96,7 @@ function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false,
90
96
  closingQ.push(relativePath); // ok, we'll ask next one to carry this info
91
97
  // don't return the promise directly, as this job ends here, but communicate to caller the promise for the whole branch
92
98
  return { branchDone, n };
93
- async function work(entry) {
99
+ async function work(entry, isSymlink = false) {
94
100
  entry.path = (relativePath && relativePath + '/') + entry.name;
95
101
  pluginReceiver?.(entry);
96
102
  if (last && closingQ.length) // pending entries
@@ -104,9 +110,16 @@ function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false,
104
110
  n++;
105
111
  if (!depth || !entry.isDirectory())
106
112
  return;
113
+ const entryPath = (0, path_1.join)(base, entry.name);
114
+ if (isSymlink) {
115
+ // compare only with ancestors so separate links to the same folder still work
116
+ const [target, ...ancestors] = await Promise.all([entryPath, ...ancestorPaths].map(x => (0, promises_1.realpath)(x).catch(() => '')));
117
+ if (!target || ancestors.includes(target))
118
+ return;
119
+ }
107
120
  const branchDone = (0, cross_1.pendingPromise)(); // per-job
108
121
  subDirsDone.push(branchDone);
109
- const job = () => readDir(entry.path, depth - 1) // recur
122
+ const job = () => readDir(entry.path, depth - 1, [...ancestorPaths, entryPath]) // recur
110
123
  .then(x => x, () => { }) // mute errors
111
124
  .then(res => {
112
125
  if (!res?.n)
package/src/watchLoad.js CHANGED
@@ -12,8 +12,9 @@ const debounceAsync_1 = require("./debounceAsync");
12
12
  const events_1 = require("./events");
13
13
  function watchLoad(path, parser, { failedOnFirstAttempt, immediateFirst } = {}) {
14
14
  let doing = false;
15
+ let cancelled = false;
15
16
  let watcher;
16
- const debounced = (0, debounceAsync_1.debounceAsync)(load, { wait: 500, maxWait: 1000, reuseRunning: true });
17
+ const debounced = (0, debounceAsync_1.debounceAsync)(load, { wait: 500, maxWait: 1000, reuseRunning: true, cancelable: true });
17
18
  let retry;
18
19
  let last;
19
20
  const emitter = new events_1.BetterEventEmitter();
@@ -27,6 +28,8 @@ function watchLoad(path, parser, { failedOnFirstAttempt, immediateFirst } = {})
27
28
  install(true);
28
29
  return { unwatch, save, emitter, getText: () => last, getPath: () => path };
29
30
  function install(first = false) {
31
+ if (cancelled)
32
+ return;
30
33
  try {
31
34
  watcher = (0, fs_1.watch)(path, () => {
32
35
  if (!save.isWorking())
@@ -43,6 +46,12 @@ function watchLoad(path, parser, { failedOnFirstAttempt, immediateFirst } = {})
43
46
  }
44
47
  }
45
48
  function unwatch() {
49
+ // a pending or running load must not resurrect a watcher cancelled by its owner
50
+ cancelled = true;
51
+ debounced.cancel();
52
+ closeWatcher();
53
+ }
54
+ function closeWatcher() {
46
55
  watcher?.close();
47
56
  clearTimeout(retry);
48
57
  watcher = undefined;
@@ -61,12 +70,14 @@ function watchLoad(path, parser, { failedOnFirstAttempt, immediateFirst } = {})
61
70
  return '';
62
71
  throw e;
63
72
  });
73
+ if (cancelled)
74
+ return;
64
75
  if (text === last)
65
76
  return;
66
77
  last = text;
67
78
  emitter.emit('change', last);
68
79
  console.debug('Loaded', path);
69
- unwatch();
80
+ closeWatcher();
70
81
  install(); // reinstall, as the original file could have been renamed. We watch by the name.
71
82
  await parser(text);
72
83
  }